From 88fbb9ae77548257c885ab194ef6ef43a66ffbe9 Mon Sep 17 00:00:00 2001 From: Michael Collado <40346148+collado-mike@users.noreply.github.com> Date: Thu, 25 Jul 2024 21:36:35 -0700 Subject: [PATCH 01/27] Merge Polaris catalog code (#1) * Initial commit Co-authored-by: Aihua Xu Co-authored-by: Alvin Chen Co-authored-by: Benoit Dageville Co-authored-by: Dennis Huo Co-authored-by: Evan Gilbert Co-authored-by: Evgeny Zubatov Co-authored-by: Jonas-Taha El Sesiy Co-authored-by: Maninder Parmar Co-authored-by: Michael Collado Co-authored-by: Sean Lee Co-authored-by: Shannon Chen Co-authored-by: Tyler Jones Co-authored-by: Vivo Xu * Add brief description to README * Update package naming to separate snowflake code and oss core, extensions, and service impl * Set up new gradle project structure * Add gradle wrapper * Moved docker files to project root and renamed files and classes to Polaris * Update CI scripts to use new layout * Add missing gradlew file * Updates to READMEs and move manual* scripts to snowflake repository root * Fix SparkIntegrationTest after merge * Fix regtest in polaris application Fix json error messages to return clearer validation causes (#272) Extended gradle format in root project to apply to oss (#273) Improve error message for invalid json and distinguish from invalid values (#274) Update repository references to managed-polaris Removed references and made aws resources configurable Fix references to snowflake reg test resources Update README with instructions on running cloud-specific regtests Copy recommended gradle .gitignore contents Update github actions Add @polaris-catalog/polaris team to codeowners * Merge branch 'managed-polaris' into mcollado-polaris-import Co-authored-by: Dennis Huo * Merged changes into polaris-catalog/polaris-dev Co-authored-by: Dennis Huo Co-authored-by: Evgeny Zubatov * Squashed commit of the following: Co-authored-by: Benoit Dageville Co-authored-by: Dennis Huo Co-authored-by: Eric Maynard Co-authored-by: Evgeny Zubatov Co-authored-by: Michael Collado Co-authored-by: Shannon Chen commit bd256f544c069ff15a7a96ab7f2abc650a2e9812 Author: Shannon Chen Date: Tue Jul 23 23:43:38 2024 +0000 Remove s3gov s3china enums and validate roleArn. Removing the enums because the iceberg spec does not have s3gov or s3china prefix for the url, those are snowflake style supported prefix. commit 855dbb702bdc4fc80ca852b8bf563979e08d63d2 Author: Michael Collado Date: Tue Jul 23 10:02:35 2024 -0700 Fix credential vending for view creation (#19) Correctly sets vended credentials for view creation commit 0429e6356cd71b3908600b6c5c17f82493f1d37d Author: Eric Maynard Date: Tue Jul 23 09:49:20 2024 -0700 This PR implements a basic CLI for Polaris, supporting simple commands like: ``` polaris catalogs list polaris catalogs create --type --storage-type s3 --default-base-location s3://my-bucket --role-arn ${ARN} polaris principals update emaynard --property foo=bar --property e=mc2 polaris privileges --catalog my_cat --catalog-role my_role namespace grant --namespace a.b.c TABLE_READ_DATA polaris privileges --catalog my_cat --catalog-role my_role table revoke --namespace a.b.c --table t1 TABLE_READ_DATA ``` commit 01d4c294e6f8b3e77bf205af00ea2e1dbef0d362 Author: Evgeny Zubatov Date: Mon Jul 22 11:12:29 2024 -0700 Service Bootstrap (Part 2): we are removing bootstrap code in init methods and updates to In-Memory store (#8) Changing bootstrap logic, moving bootstrap code to a separate method and only use it during service bootstrapping and first time initialization. So moving forward we will not call bootstrap during SessionManager init code as it used to be, as this will be destructive if service gets restarted. For InMemory Store we have special handling and doing bootstrap on a very first initialization of SessionManager for a given realm. And it makes sense as we can't use our custom dropwizard Bootstrap command for bootstrapping in-memory store (as in-memory store is only valid and available during server process lifetime) commit 2c7f3c43c557e521d7177a4d7dd44157147f0a0c Author: Dennis Huo Date: Fri Jul 19 23:33:05 2024 +0000 Defense-in-depth - make FileIO impl come from StorageConfigurationInfo (#15) Description Rather than specifying ResolvingFileIO, we can be more explicit about the FileIO impl we're allowing. Also only allow custom specification of FileIO in test environments using a feature config flag. Even if there are valid FileIO customizations customers could specify, we have only really vetted the enumerated list of impls, so for example we don't want a customer to be able to force Polaris to try to use Hadoop's S3AFileSystem for S3 files even if it "might" work. This in conjunction with omitting `FILE` from SUPPORTED_CATALOG_STORAGE_TYPES for managed environments (https://github.com/snowflakedb/polaris-k8-config/pull/116/files) ensures we won't have a FileIO impl that's capable of reading unexpected files. commit 498861114994b0508efdbdd2167918be5517f4cb Merge: cf07ac0 c100175 Author: Michael Collado Date: Fri Jul 19 13:41:02 2024 -0700 Merge branch 'main' into mcollado-update-aws-region commit cf07ac099644b96f93026b209c9938243c1cce18 Author: Michael Collado Date: Fri Jul 19 13:38:22 2024 -0700 Stop setting AWS_REGION env and use client.region spark config in tests commit c10017521145e138ae5cdd903d7d51b4bee9e82c Merge: b1de84a d2df00f Author: Eric Maynard Date: Fri Jul 19 12:43:15 2024 -0700 Merge pull request #12 from snowflakedb/confirm-warehouse-non-null commit b1de84ad47f6bdf5be4318d4664767dfc33bb5a0 Merge: 504dcc0 1f79e19 Author: Michael Collado Date: Fri Jul 19 09:25:07 2024 -0700 Merge branch 'main' into mcollado-view-tests commit d4c58a6a19756078309229c1de4dbf5f737dbdd0 Author: Shannon Chen Date: Thu Jul 18 02:58:52 2024 -0700 cross region support commit 504dcc05bb33e686f5765e5b2d91aa4dcfe2e5d1 Author: Michael Collado Date: Fri Jul 19 00:00:57 2024 -0700 fix regtest failures commit b7ed5d27e2d71708977cc6fe7eac3ab10e8d9836 Author: Michael Collado Date: Thu Jul 18 21:52:46 2024 -0700 Add reg tests to verify view support * Squashed commit of the following: commit 4fb3b6c19a8a8a4961b777ad32dbe1b87d5efe94 Author: Evgeny Zubatov Date: Thu Jul 25 14:02:30 2024 -0700 Adding annotation and enforcing size limits for Principal, Role, Catalog and Catalog Role names. Also blocking "SYSTEM$" prefix from being used in names. Adding case-insensitive regex rule to block "SYSTEM$" commit 2fcc2898ea038c074fed075cdc7ff62e4884e76a Author: Alvin Chen Date: Thu Jul 25 11:28:00 2024 -0700 Replace Dropwizard Metrics with Micrometer (#18) Since the current Dropwizard Metric library 4.x doesn't support adding custom labels to metrics, we cannot define per-account metrics in order As a result, we're migrating to Micrometer metrics to support custom tagging and align with the metric implementations Major changes by component - `PolarisMetricRegistry` - defines caching for timers and error counters as well as abstracts away the creation of two separate metrics, one with and one without the `account` tag - `TimedApplicationEventListener` - an implementation of the Jersey ApplicationEventListener to listen on requests invoking methods with `@TimedApi` annotation, and handles logic of timing resource/counting errors on success/failure cases respectively - `IcebergMappedException` - removed the original logic for counting errors since the code is now centralized in the above two classes ## Test Manual tested by calling the /metrics endpoint. Following is the result of one successful and one failure invoke of the /oauth endpoint. Note that the timer produces a `summary` and a `gauge`, and doesn't get incremented on failure cases. ``` % curl http://localhost:8182/metrics # HELP polaris_OAuth2Api_getToken_error_total # TYPE polaris_OAuth2Api_getToken_error_total counter polaris_OAuth2Api_getToken_error_total{HTTP_RESPONSE_CODE="401"} 1.0 # HELP polaris_OAuth2Api_getToken_error_realm_total # TYPE polaris_OAuth2Api_getToken_error_realm_total counter polaris_OAuth2Api_getToken_error_realm_total{HTTP_RESPONSE_CODE="401",REALM_ID="testpolaris"} 1.0 # HELP polaris_OAuth2Api_getToken_realm_seconds # TYPE polaris_OAuth2Api_getToken_realm_seconds summary polaris_OAuth2Api_getToken_realm_seconds_count{REALM_ID="testpolaris"} 1 polaris_OAuth2Api_getToken_realm_seconds_sum{REALM_ID="testpolaris"} 0.384 # HELP polaris_OAuth2Api_getToken_realm_seconds_max # TYPE polaris_OAuth2Api_getToken_realm_seconds_max gauge polaris_OAuth2Api_getToken_realm_seconds_max{REALM_ID="testpolaris"} 0.384 # HELP polaris_OAuth2Api_getToken_seconds # TYPE polaris_OAuth2Api_getToken_seconds summary polaris_OAuth2Api_getToken_seconds_count 1 polaris_OAuth2Api_getToken_seconds_sum 0.384 # HELP polaris_OAuth2Api_getToken_seconds_max # TYPE polaris_OAuth2Api_getToken_seconds_max gauge polaris_OAuth2Api_getToken_seconds_max 0.384 # HELP polaris_persistence_loadEntity_realm_seconds # TYPE polaris_persistence_loadEntity_realm_seconds summary polaris_persistence_loadEntity_realm_seconds_count{REALM_ID="testpolaris"} 1 polaris_persistence_loadEntity_realm_seconds_sum{REALM_ID="testpolaris"} 0.041 # HELP polaris_persistence_loadEntity_realm_seconds_max # TYPE polaris_persistence_loadEntity_realm_seconds_max gauge polaris_persistence_loadEntity_realm_seconds_max{REALM_ID="testpolaris"} 0.041 # HELP polaris_persistence_loadEntity_seconds # TYPE polaris_persistence_loadEntity_seconds summary polaris_persistence_loadEntity_seconds_count 1 polaris_persistence_loadEntity_seconds_sum 0.041 # HELP polaris_persistence_loadEntity_seconds_max # TYPE polaris_persistence_loadEntity_seconds_max gauge polaris_persistence_loadEntity_seconds_max 0.041 ``` commit 5abee21b07be00f5f3b18faabe61fb88ecec37e0 Author: Shannon Chen Date: Thu Jul 25 17:14:09 2024 +0000 select view hangs in remote polaris because iceberg SDK could not initialize the s3client since it is missing credentials. It works locally because the SDK S3client initialization work if your local environment have AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY set, and our dev environment does have these two variables set, so it was not using vending scopedcreds. This PR does below things: 1. add scoped creds to the fileIO when select view 2. stops retry for more cases, the `select view` hangs because it keeps retying commit 85d41bcbab30c9fc3fad56dea83f80e8146ee79c Author: Eric Maynard Date: Wed Jul 24 16:33:57 2024 -0700 In this PR, I've regenerated the Python clients from the spec by following the steps [here](https://github.com/snowflakedb/managed-polaris/tree/main/polaris/regtests#python-tests). I ran: ``` docker run --rm \ -v ${PWD}:/local openapitools/openapi-generator-cli generate \ -i /local/spec/polaris-management-service.yml \ -g python \ -o /local/regtests/client/python --additional-properties=packageName=polaris.management --additional-properties=apiNamePrefix=polaris docker run --rm \ -v ${PWD}:/local openapitools/openapi-generator-cli generate \ -i /local/spec/rest-catalog-open-api.yaml \ -g python \ -o /local/regtests/client/python --additional-properties=packageName=polaris.catalog --additional-properties=apiNameSuffix="" --additional-properties=apiNamePrefix=Iceberg ``` commit 485d99c89abd7b7c3690f45d96a5043a47032ba3 Author: Eric Maynard Date: Wed Jul 24 11:27:21 2024 -0700 This PR introduces quickstart documentation and adds a basic structure for OSS docs. commit 4310980aecf81cc23bbf583cfb6c360ca738a788 Author: Shannon Chen Date: Wed Jul 24 17:38:14 2024 +0000 Stop retry 403 Access Denied error (#22) commit 95acd5b3e7983b89d47a915c62ac5bb247730313 Author: Benoit Dageville <59930187+sfc-gh-bdagevil@users.noreply.github.com> Date: Tue Jul 23 22:15:34 2024 -0700 * Fix readme statement and snowflake reference in PolarisDefaultDiagServiceImpl --------- Co-authored-by: Daniel Myers Co-authored-by: Anna Filippova <7892219+annafil@users.noreply.github.com> Co-authored-by: Michael Collado Co-authored-by: Aihua Xu Co-authored-by: Alvin Chen Co-authored-by: Benoit Dageville Co-authored-by: Dennis Huo Co-authored-by: Evan Gilbert Co-authored-by: Evgeny Zubatov Co-authored-by: Jonas-Taha El Sesiy Co-authored-by: Maninder Parmar Co-authored-by: Sean Lee Co-authored-by: Shannon Chen Co-authored-by: Tyler Jones Co-authored-by: Vivo Xu --- .dockerignore | 5 + .github/CODEOWNERS | 1 + .github/dependabot.yml | 7 + .github/pull_request_template.md | 1 + .github/workflows/gradle.yml | 60 + .github/workflows/regtest.yml | 23 + .github/workflows/semgrep.yml | 12 + .github/workflows/stale.yml | 18 + .gitignore | 29 + .openapi-generator-ignore | 7 + Dockerfile | 22 + README.md | 154 +- build.gradle | 96 + docker-compose-jupyter.yml | 41 + docker-compose.yml | 67 + docs/entities.md | 67 + docs/iceberg-rest/index.html | 1307 +++ .../quickstart/privilege-illustration-1.png | Bin 0 -> 61323 bytes .../quickstart/privilege-illustration-2.png | Bin 0 -> 88965 bytes docs/index.html | 381 + docs/polaris-management/index.html | 861 ++ docs/quickstart.md | 311 + .../persistence/eclipselink/build.gradle | 9 + ...pseLinkPolarisMetaStoreManagerFactory.java | 36 + ...olarisEclipseLinkMetaStoreSessionImpl.java | 678 ++ .../eclipselink/PolarisEclipseLinkStore.java | 397 + .../PolarisEclipseLinkMetaStoreTest.java | 41 + gradle.properties | 2 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43462 bytes gradle/wrapper/gradle-wrapper.properties | 7 + gradlew | 249 + kind-registry.sh | 64 + notebooks/Dockerfile | 10 + notebooks/SparkPolaris.ipynb | 807 ++ polaris | 21 + polaris-core/build.gradle | 131 + .../io/polaris/core/PolarisCallContext.java | 58 + .../io/polaris/core/PolarisConfiguration.java | 9 + .../core/PolarisConfigurationStore.java | 41 + .../core/PolarisDefaultDiagServiceImpl.java | 112 + .../io/polaris/core/PolarisDiagnostics.java | 96 + .../auth/AuthenticatedPolarisPrincipal.java | 53 + .../auth/PolarisAuthorizableOperation.java | 223 + .../polaris/core/auth/PolarisAuthorizer.java | 615 ++ .../core/catalog/PolarisCatalogHelpers.java | 77 + .../io/polaris/core/context/CallContext.java | 141 + .../io/polaris/core/context/RealmContext.java | 10 + .../io/polaris/core/entity/AsyncTaskType.java | 30 + .../io/polaris/core/entity/CatalogEntity.java | 268 + .../core/entity/CatalogRoleEntity.java | 50 + .../polaris/core/entity/NamespaceEntity.java | 63 + .../core/entity/PolarisBaseEntity.java | 341 + .../entity/PolarisChangeTrackingVersions.java | 45 + .../core/entity/PolarisEntitiesActiveKey.java | 66 + .../io/polaris/core/entity/PolarisEntity.java | 403 + .../entity/PolarisEntityActiveRecord.java | 124 + .../core/entity/PolarisEntityConstants.java | 98 + .../core/entity/PolarisEntityCore.java | 174 + .../polaris/core/entity/PolarisEntityId.java | 48 + .../core/entity/PolarisEntitySubType.java | 100 + .../core/entity/PolarisEntityType.java | 121 + .../core/entity/PolarisGrantRecord.java | 142 + .../core/entity/PolarisPrincipalSecrets.java | 104 + .../polaris/core/entity/PolarisPrivilege.java | 201 + .../core/entity/PolarisTaskConstants.java | 13 + .../polaris/core/entity/PrincipalEntity.java | 67 + .../core/entity/PrincipalRoleEntity.java | 54 + .../polaris/core/entity/TableLikeEntity.java | 81 + .../io/polaris/core/entity/TaskEntity.java | 87 + .../core/monitor/PolarisMetricRegistry.java | 64 + .../LocalPolarisMetaStoreManagerFactory.java | 199 + .../persistence/MetaStoreManagerFactory.java | 31 + .../persistence/PolarisEntityManager.java | 141 + .../persistence/PolarisEntityResolver.java | 288 + .../persistence/PolarisMetaStoreManager.java | 1471 +++ .../PolarisMetaStoreManagerImpl.java | 2398 +++++ .../persistence/PolarisMetaStoreSession.java | 509 + .../persistence/PolarisObjectMapperUtil.java | 170 + .../PolarisResolvedPathWrapper.java | 66 + .../PolarisTreeMapMetaStoreSessionImpl.java | 553 + .../core/persistence/PolarisTreeMapStore.java | 540 + .../persistence/ResolvedPolarisEntity.java | 63 + .../RetryOnConcurrencyException.java | 20 + .../core/persistence/cache/EntityCache.java | 452 + .../cache/EntityCacheByNameKey.java | 97 + .../persistence/cache/EntityCacheEntry.java | 108 + .../cache/EntityCacheLookupResult.java | 27 + .../persistence/cache/EntityCacheMode.java | 13 + .../core/persistence/models/ModelEntity.java | 290 + .../persistence/models/ModelEntityActive.java | 136 + .../models/ModelEntityChangeTracking.java | 61 + .../models/ModelEntityDropped.java | 146 + .../persistence/models/ModelGrantRecord.java | 131 + .../models/ModelPrincipalSecrets.java | 116 + .../persistence/models/ModelSequenceId.java | 21 + .../resolver/PolarisResolutionManifest.java | 394 + .../PolarisResolutionManifestCatalogView.java | 20 + .../core/persistence/resolver/Resolver.java | 968 ++ .../resolver/ResolverEntityName.java | 49 + .../persistence/resolver/ResolverPath.java | 70 + .../resolver/ResolverPrincipalRole.java | 8 + .../persistence/resolver/ResolverStatus.java | 88 + .../storage/FileStorageConfigurationInfo.java | 38 + .../storage/InMemoryStorageIntegration.java | 128 + .../storage/PolarisCredentialProperty.java | 43 + .../core/storage/PolarisStorageActions.java | 20 + .../PolarisStorageConfigurationInfo.java | 147 + .../storage/PolarisStorageIntegration.java | 131 + .../PolarisStorageIntegrationProvider.java | 14 + .../aws/AwsCredentialsStorageIntegration.java | 171 + .../aws/AwsStorageConfigurationInfo.java | 103 + .../aws/PolarisS3FileIOClientFactory.java | 51 + .../AzureCredentialsStorageIntegration.java | 270 + .../core/storage/azure/AzureLocation.java | 77 + .../azure/AzureStorageConfigurationInfo.java | 79 + .../storage/cache/StorageCredentialCache.java | 153 + .../cache/StorageCredentialCacheEntry.java | 61 + .../cache/StorageCredentialCacheKey.java | 124 + .../gcp/GcpCredentialsStorageIntegration.java | 201 + .../gcp/GcpStorageConfigurationInfo.java | 53 + .../core/persistence/EntityCacheTest.java | 448 + .../PolarisObjectMapperUtilTest.java | 59 + .../PolarisTreeMapMetaStoreManagerTest.java | 24 + .../core/persistence/ResolverTest.java | 923 ++ .../InMemoryStorageIntegrationTest.java | 189 + .../cache/StorageCredentialCacheTest.java | 418 + .../AwsCredentialsStorageIntegrationTest.java | 449 + ...AzureCredentialStorageIntegrationTest.java | 386 + .../storage/azure/AzureLocationTest.java | 31 + .../GcpCredentialsStorageIntegrationTest.java | 414 + .../PolarisMetaStoreManagerTest.java | 473 + .../PolarisTestMetaStoreManager.java | 2381 +++++ polaris-server.yml | 150 + polaris-service/build.gradle | 191 + .../service/BootstrapRealmsCommand.java | 45 + .../service/IcebergExceptionMapper.java | 85 + ...IcebergJerseyViolationExceptionMapper.java | 31 + .../IcebergJsonProcessingExceptionMapper.java | 55 + .../polaris/service/PolarisApplication.java | 322 + .../polaris/service/PolarisHealthCheck.java | 11 + .../TimedApplicationEventListener.java | 72 + .../service/admin/PolarisAdminService.java | 1686 ++++ .../service/admin/PolarisServiceImpl.java | 605 ++ .../auth/BasePolarisAuthenticator.java | 102 + .../io/polaris/service/auth/DecodedToken.java | 11 + .../service/auth/DefaultOAuth2ApiService.java | 80 + .../auth/DefaultPolarisAuthenticator.java | 33 + .../auth/DiscoverableAuthenticator.java | 20 + .../io/polaris/service/auth/JWTBroker.java | 149 + .../polaris/service/auth/JWTRSAKeyPair.java | 25 + .../service/auth/JWTRSAKeyPairFactory.java | 28 + .../service/auth/JWTSymmetricKeyBroker.java | 23 + .../service/auth/JWTSymmetricKeyFactory.java | 57 + .../io/polaris/service/auth/KeyProvider.java | 13 + .../service/auth/LocalRSAKeyProvider.java | 64 + .../service/auth/OAuthTokenErrorResponse.java | 57 + .../io/polaris/service/auth/OAuthUtils.java | 59 + .../io/polaris/service/auth/PemUtils.java | 75 + ...InlineBearerTokenPolarisAuthenticator.java | 72 + .../service/auth/TestOAuth2ApiService.java | 100 + .../io/polaris/service/auth/TokenBroker.java | 50 + .../service/auth/TokenBrokerFactory.java | 13 + .../auth/TokenInfoExchangeResponse.java | 132 + .../service/auth/TokenRequestValidator.java | 64 + .../polaris/service/auth/TokenResponse.java | 41 + .../service/catalog/BasePolarisCatalog.java | 1557 +++ .../catalog/IcebergCatalogAdapter.java | 463 + .../catalog/PolarisCatalogHandlerWrapper.java | 1045 ++ .../catalog/SupportsCredentialDelegation.java | 21 + .../catalog/SupportsNotifications.java | 9 + .../config/ConfigurationStoreAware.java | 9 + .../service/config/CorsConfiguration.java | 77 + .../config/DefaultConfigurationStore.java | 19 + .../config/HasEntityManagerFactory.java | 5 + .../service/config/OAuth2ApiService.java | 11 + .../config/PolarisApplicationConfig.java | 144 + .../config/RealmEntityManagerFactory.java | 46 + .../polaris/service/config/Serializers.java | 228 + .../config/TaskHandlerConfiguration.java | 34 + .../context/CallContextCatalogFactory.java | 9 + .../service/context/CallContextResolver.java | 19 + .../context/DefaultContextResolver.java | 153 + .../PolarisCallContextCatalogFactory.java | 71 + .../service/context/RealmContextResolver.java | 17 + .../SqlliteCallContextCatalogFactory.java | 89 + .../logging/PolarisJsonLayoutFactory.java | 224 + ...nMemoryPolarisMetaStoreManagerFactory.java | 70 + .../io/polaris/service/resource/TimedApi.java | 22 + ...PolarisStorageIntegrationProviderImpl.java | 94 + .../task/ManifestFileCleanupTaskHandler.java | 206 + .../service/task/TableCleanupTaskHandler.java | 153 + .../io/polaris/service/task/TaskExecutor.java | 11 + .../service/task/TaskExecutorImpl.java | 123 + .../service/task/TaskFileIOSupplier.java | 47 + .../io/polaris/service/task/TaskHandler.java | 9 + .../io/polaris/service/task/TaskUtils.java | 38 + .../service/tracing/HeadersMapAccessor.java | 39 + .../service/tracing/OpenTelemetryAware.java | 8 + .../service/tracing/TracingFilter.java | 82 + .../service/types/CommitTableRequest.java | 5 + .../service/types/CommitViewRequest.java | 5 + .../service/types/NotificationRequest.java | 76 + .../service/types/NotificationType.java | 74 + .../types/TableUpdateNotification.java | 181 + .../io/polaris/service/types/TokenType.java | 48 + .../main/resources/META-INF/persistence.xml | 27 + .../io.dropwizard.jackson.Discoverable | 6 + ...ng.common.layout.DiscoverableLayoutFactory | 1 + ...s.core.persistence.MetaStoreManagerFactory | 4 + ...io.polaris.service.auth.TokenBrokerFactory | 3 + ...io.polaris.service.config.OAuth2ApiService | 2 + ...olaris.service.context.CallContextResolver | 1 + ...laris.service.context.RealmContextResolver | 1 + .../src/main/resources/log4j.properties | 5 + .../PolarisApplicationIntegrationTest.java | 609 ++ .../admin/PolarisAdminServiceAuthzTest.java | 1027 ++ .../service/admin/PolarisAuthzTestBase.java | 496 + .../PolarisServiceImplIntegrationTest.java | 1770 ++++ .../service/auth/JWTRSAKeyPairTest.java | 144 + .../auth/JWTSymmetricKeyGeneratorTest.java | 79 + .../auth/TokenRequestValidatorTest.java | 73 + .../io/polaris/service/auth/TokenUtils.java | 53 + .../catalog/BasePolarisCatalogTest.java | 954 ++ .../catalog/BasePolarisCatalogViewTest.java | 129 + ...PolarisCatalogHandlerWrapperAuthzTest.java | 1671 ++++ .../PolarisPassthroughResolutionView.java | 129 + .../PolarisRestCatalogIntegrationTest.java | 577 ++ ...PolarisRestCatalogViewIntegrationTest.java | 273 + .../catalog/PolarisSparkIntegrationTest.java | 341 + .../service/entity/CatalogEntityTest.java | 217 + .../ManifestFileCleanupTaskHandlerTest.java | 213 + .../task/TableCleanupTaskHandlerTest.java | 352 + .../polaris/service/task/TaskTestUtils.java | 88 + .../io/polaris/service/task/TestSnapshot.java | 115 + .../test/PolarisConnectionExtension.java | 233 + .../test/SnowmanCredentialsExtension.java | 209 + .../test/resources/META-INF/persistence.xml | 27 + ...ris.service.auth.DiscoverableAuthenticator | 1 + .../polaris-server-integrationtest.yml | 150 + regtests/.dockerignore | 5 + regtests/.gitignore | 4 + regtests/Dockerfile | 28 + regtests/README.md | 134 + .../python/.github/workflows/python.yml | 38 + regtests/client/python/.gitignore | 66 + regtests/client/python/.gitlab-ci.yml | 31 + .../client/python/.openapi-generator-ignore | 23 + .../client/python/.openapi-generator/FILES | 249 + .../client/python/.openapi-generator/VERSION | 1 + regtests/client/python/.travis.yml | 17 + regtests/client/python/README.md | 264 + regtests/client/python/cli/__init__.py | 0 .../client/python/cli/command/__init__.py | 107 + .../python/cli/command/catalog_roles.py | 78 + .../client/python/cli/command/catalogs.py | 169 + .../python/cli/command/principal_roles.py | 79 + .../client/python/cli/command/principals.py | 67 + .../client/python/cli/command/privileges.py | 107 + regtests/client/python/cli/constants.py | 185 + .../client/python/cli/options/__init__.py | 0 .../client/python/cli/options/option_tree.py | 189 + regtests/client/python/cli/options/parser.py | 178 + regtests/client/python/cli/polaris_cli.py | 58 + .../client/python/docs/AddGrantRequest.md | 29 + .../python/docs/AddPartitionSpecUpdate.md | 30 + .../client/python/docs/AddSchemaUpdate.md | 31 + .../client/python/docs/AddSnapshotUpdate.md | 30 + .../client/python/docs/AddSortOrderUpdate.md | 30 + .../python/docs/AddViewVersionUpdate.md | 30 + .../client/python/docs/AndOrExpression.md | 31 + regtests/client/python/docs/AssertCreate.md | 30 + .../python/docs/AssertCurrentSchemaId.md | 31 + .../python/docs/AssertDefaultSortOrderId.md | 31 + .../client/python/docs/AssertDefaultSpecId.md | 31 + .../python/docs/AssertLastAssignedFieldId.md | 31 + .../docs/AssertLastAssignedPartitionId.md | 31 + .../client/python/docs/AssertRefSnapshotId.md | 32 + .../client/python/docs/AssertTableUUID.md | 31 + regtests/client/python/docs/AssertViewUUID.md | 31 + .../client/python/docs/AssignUUIDUpdate.md | 31 + .../python/docs/AwsStorageConfigInfo.md | 32 + .../python/docs/AzureStorageConfigInfo.md | 32 + regtests/client/python/docs/BaseUpdate.md | 29 + regtests/client/python/docs/BlobMetadata.md | 33 + regtests/client/python/docs/Catalog.md | 36 + regtests/client/python/docs/CatalogConfig.md | 31 + regtests/client/python/docs/CatalogGrant.md | 29 + .../client/python/docs/CatalogPrivilege.md | 58 + .../client/python/docs/CatalogProperties.md | 29 + regtests/client/python/docs/CatalogRole.md | 33 + regtests/client/python/docs/CatalogRoles.md | 29 + regtests/client/python/docs/Catalogs.md | 30 + regtests/client/python/docs/CommitReport.md | 34 + .../client/python/docs/CommitTableRequest.md | 31 + .../client/python/docs/CommitTableResponse.md | 30 + .../python/docs/CommitTransactionRequest.md | 29 + .../client/python/docs/CommitViewRequest.md | 31 + regtests/client/python/docs/ContentFile.md | 38 + regtests/client/python/docs/CountMap.md | 30 + regtests/client/python/docs/CounterResult.md | 30 + .../python/docs/CreateCatalogRequest.md | 30 + .../python/docs/CreateCatalogRoleRequest.md | 29 + .../python/docs/CreateNamespaceRequest.md | 30 + .../python/docs/CreateNamespaceResponse.md | 30 + .../python/docs/CreatePrincipalRequest.md | 30 + .../python/docs/CreatePrincipalRoleRequest.md | 29 + .../client/python/docs/CreateTableRequest.md | 35 + .../client/python/docs/CreateViewRequest.md | 33 + regtests/client/python/docs/DataFile.md | 35 + .../client/python/docs/EqualityDeleteFile.md | 30 + regtests/client/python/docs/ErrorModel.md | 33 + regtests/client/python/docs/Expression.md | 35 + .../client/python/docs/ExternalCatalog.md | 30 + regtests/client/python/docs/FileFormat.md | 14 + .../python/docs/FileStorageConfigInfo.md | 29 + .../python/docs/GcpStorageConfigInfo.md | 30 + .../python/docs/GetNamespaceResponse.md | 30 + .../python/docs/GrantCatalogRoleRequest.md | 29 + .../python/docs/GrantPrincipalRoleRequest.md | 29 + regtests/client/python/docs/GrantResource.md | 29 + regtests/client/python/docs/GrantResources.md | 29 + .../client/python/docs/IcebergCatalogAPI.md | 2240 +++++ .../python/docs/IcebergConfigurationAPI.md | 96 + .../python/docs/IcebergErrorResponse.md | 30 + .../client/python/docs/IcebergOAuth2API.md | 107 + .../python/docs/ListNamespacesResponse.md | 30 + .../client/python/docs/ListTablesResponse.md | 30 + regtests/client/python/docs/ListType.md | 32 + .../client/python/docs/LiteralExpression.md | 31 + .../client/python/docs/LoadTableResult.md | 32 + regtests/client/python/docs/LoadViewResult.md | 32 + regtests/client/python/docs/MapType.md | 34 + .../client/python/docs/MetadataLogInner.md | 30 + regtests/client/python/docs/MetricResult.md | 33 + regtests/client/python/docs/ModelSchema.md | 32 + regtests/client/python/docs/NamespaceGrant.md | 30 + .../client/python/docs/NamespacePrivilege.md | 54 + regtests/client/python/docs/NotExpression.md | 30 + .../client/python/docs/NotificationRequest.md | 30 + .../client/python/docs/NotificationType.md | 16 + regtests/client/python/docs/NullOrder.md | 12 + regtests/client/python/docs/OAuthError.md | 31 + .../client/python/docs/OAuthTokenResponse.md | 34 + regtests/client/python/docs/PartitionField.md | 32 + regtests/client/python/docs/PartitionSpec.md | 30 + .../python/docs/PartitionStatisticsFile.md | 31 + regtests/client/python/docs/PolarisCatalog.md | 29 + .../client/python/docs/PolarisDefaultApi.md | 2474 +++++ .../client/python/docs/PositionDeleteFile.md | 29 + .../client/python/docs/PrimitiveTypeValue.md | 28 + regtests/client/python/docs/Principal.md | 35 + regtests/client/python/docs/PrincipalRole.md | 33 + regtests/client/python/docs/PrincipalRoles.md | 29 + .../python/docs/PrincipalWithCredentials.md | 31 + .../PrincipalWithCredentialsCredentials.md | 30 + regtests/client/python/docs/Principals.md | 30 + .../python/docs/RegisterTableRequest.md | 30 + .../docs/RemovePartitionStatisticsUpdate.md | 30 + .../python/docs/RemovePropertiesUpdate.md | 30 + .../python/docs/RemoveSnapshotRefUpdate.md | 30 + .../python/docs/RemoveSnapshotsUpdate.md | 30 + .../python/docs/RemoveStatisticsUpdate.md | 30 + .../client/python/docs/RenameTableRequest.md | 30 + .../python/docs/ReportMetricsRequest.md | 39 + .../client/python/docs/RevokeGrantRequest.md | 29 + .../python/docs/SQLViewRepresentation.md | 31 + regtests/client/python/docs/ScanReport.md | 36 + .../python/docs/SetCurrentSchemaUpdate.md | 30 + .../docs/SetCurrentViewVersionUpdate.md | 30 + .../python/docs/SetDefaultSortOrderUpdate.md | 30 + .../python/docs/SetDefaultSpecUpdate.md | 30 + regtests/client/python/docs/SetExpression.md | 31 + .../client/python/docs/SetLocationUpdate.md | 30 + .../docs/SetPartitionStatisticsUpdate.md | 30 + .../client/python/docs/SetPropertiesUpdate.md | 30 + .../python/docs/SetSnapshotRefUpdate.md | 35 + .../client/python/docs/SetStatisticsUpdate.md | 31 + regtests/client/python/docs/Snapshot.md | 35 + .../client/python/docs/SnapshotLogInner.md | 30 + .../client/python/docs/SnapshotReference.md | 33 + .../client/python/docs/SnapshotSummary.md | 29 + regtests/client/python/docs/SortDirection.md | 12 + regtests/client/python/docs/SortField.md | 32 + regtests/client/python/docs/SortOrder.md | 30 + regtests/client/python/docs/StatisticsFile.md | 33 + .../client/python/docs/StorageConfigInfo.md | 31 + regtests/client/python/docs/StructField.md | 33 + regtests/client/python/docs/StructType.md | 30 + regtests/client/python/docs/TableGrant.md | 31 + .../client/python/docs/TableIdentifier.md | 30 + regtests/client/python/docs/TableMetadata.md | 49 + regtests/client/python/docs/TablePrivilege.md | 26 + .../client/python/docs/TableRequirement.md | 29 + regtests/client/python/docs/TableUpdate.md | 49 + .../python/docs/TableUpdateNotification.md | 33 + regtests/client/python/docs/Term.md | 31 + regtests/client/python/docs/TimerResult.md | 31 + regtests/client/python/docs/TokenType.md | 21 + regtests/client/python/docs/TransformTerm.md | 31 + regtests/client/python/docs/Type.md | 38 + .../client/python/docs/UnaryExpression.md | 31 + .../python/docs/UpdateCatalogRequest.md | 32 + .../python/docs/UpdateCatalogRoleRequest.md | 31 + .../docs/UpdateNamespacePropertiesRequest.md | 30 + .../docs/UpdateNamespacePropertiesResponse.md | 31 + .../python/docs/UpdatePrincipalRequest.md | 31 + .../python/docs/UpdatePrincipalRoleRequest.md | 31 + .../python/docs/UpgradeFormatVersionUpdate.md | 30 + regtests/client/python/docs/ValueMap.md | 30 + regtests/client/python/docs/ViewGrant.md | 31 + .../client/python/docs/ViewHistoryEntry.md | 30 + regtests/client/python/docs/ViewMetadata.md | 36 + regtests/client/python/docs/ViewPrivilege.md | 22 + .../client/python/docs/ViewRepresentation.md | 31 + .../client/python/docs/ViewRequirement.md | 29 + regtests/client/python/docs/ViewUpdate.md | 37 + regtests/client/python/docs/ViewVersion.md | 35 + regtests/client/python/git_push.sh | 57 + regtests/client/python/poetry.lock | 594 ++ regtests/client/python/polaris/__init__.py | 0 .../client/python/polaris/catalog/__init__.py | 145 + .../python/polaris/catalog/api/__init__.py | 7 + .../catalog/api/iceberg_catalog_api.py | 7806 +++++++++++++++ .../catalog/api/iceberg_configuration_api.py | 319 + .../catalog/api/iceberg_o_auth2_api.py | 441 + .../python/polaris/catalog/api_client.py | 788 ++ .../python/polaris/catalog/api_response.py | 21 + .../python/polaris/catalog/configuration.py | 501 + .../python/polaris/catalog/exceptions.py | 199 + .../python/polaris/catalog/models/__init__.py | 126 + .../models/add_partition_spec_update.py | 97 + .../catalog/models/add_schema_update.py | 98 + .../catalog/models/add_snapshot_update.py | 97 + .../catalog/models/add_sort_order_update.py | 97 + .../catalog/models/add_view_version_update.py | 97 + .../catalog/models/and_or_expression.py | 100 + .../polaris/catalog/models/assert_create.py | 95 + .../models/assert_current_schema_id.py | 96 + .../models/assert_default_sort_order_id.py | 96 + .../catalog/models/assert_default_spec_id.py | 96 + .../models/assert_last_assigned_field_id.py | 96 + .../assert_last_assigned_partition_id.py | 96 + .../catalog/models/assert_ref_snapshot_id.py | 97 + .../catalog/models/assert_table_uuid.py | 96 + .../catalog/models/assert_view_uuid.py | 96 + .../catalog/models/assign_uuid_update.py | 96 + .../polaris/catalog/models/base_update.py | 167 + .../polaris/catalog/models/blob_metadata.py | 95 + .../polaris/catalog/models/catalog_config.py | 89 + .../polaris/catalog/models/commit_report.py | 110 + .../catalog/models/commit_table_request.py | 111 + .../catalog/models/commit_table_response.py | 93 + .../models/commit_transaction_request.py | 95 + .../catalog/models/commit_view_request.py | 111 + .../polaris/catalog/models/content_file.py | 131 + .../polaris/catalog/models/count_map.py | 89 + .../polaris/catalog/models/counter_result.py | 89 + .../models/create_namespace_request.py | 89 + .../models/create_namespace_response.py | 89 + .../catalog/models/create_table_request.py | 111 + .../catalog/models/create_view_request.py | 103 + .../polaris/catalog/models/data_file.py | 121 + .../catalog/models/equality_delete_file.py | 114 + .../polaris/catalog/models/error_model.py | 94 + .../polaris/catalog/models/expression.py | 181 + .../polaris/catalog/models/file_format.py | 38 + .../catalog/models/get_namespace_response.py | 94 + .../catalog/models/iceberg_error_response.py | 91 + .../models/list_namespaces_response.py | 94 + .../catalog/models/list_tables_response.py | 102 + .../polaris/catalog/models/list_type.py | 106 + .../catalog/models/literal_expression.py | 95 + .../catalog/models/load_table_result.py | 95 + .../catalog/models/load_view_result.py | 95 + .../python/polaris/catalog/models/map_type.py | 113 + .../catalog/models/metadata_log_inner.py | 89 + .../polaris/catalog/models/metric_result.py | 134 + .../polaris/catalog/models/model_schema.py | 110 + .../polaris/catalog/models/not_expression.py | 95 + .../catalog/models/notification_request.py | 94 + .../catalog/models/notification_type.py | 39 + .../polaris/catalog/models/null_order.py | 37 + .../polaris/catalog/models/o_auth_error.py | 98 + .../catalog/models/o_auth_token_response.py | 105 + .../polaris/catalog/models/partition_field.py | 93 + .../polaris/catalog/models/partition_spec.py | 99 + .../models/partition_statistics_file.py | 91 + .../catalog/models/position_delete_file.py | 113 + .../catalog/models/primitive_type_value.py | 383 + .../catalog/models/register_table_request.py | 89 + .../remove_partition_statistics_update.py | 96 + .../models/remove_properties_update.py | 96 + .../models/remove_snapshot_ref_update.py | 96 + .../catalog/models/remove_snapshots_update.py | 96 + .../models/remove_statistics_update.py | 96 + .../catalog/models/rename_table_request.py | 96 + .../catalog/models/report_metrics_request.py | 134 + .../polaris/catalog/models/scan_report.py | 118 + .../models/set_current_schema_update.py | 96 + .../models/set_current_view_version_update.py | 96 + .../models/set_default_sort_order_update.py | 96 + .../catalog/models/set_default_spec_update.py | 96 + .../polaris/catalog/models/set_expression.py | 95 + .../catalog/models/set_location_update.py | 96 + .../models/set_partition_statistics_update.py | 97 + .../catalog/models/set_properties_update.py | 96 + .../catalog/models/set_snapshot_ref_update.py | 113 + .../catalog/models/set_statistics_update.py | 98 + .../python/polaris/catalog/models/snapshot.py | 103 + .../catalog/models/snapshot_log_inner.py | 89 + .../catalog/models/snapshot_reference.py | 102 + .../catalog/models/snapshot_summary.py | 107 + .../polaris/catalog/models/sort_direction.py | 37 + .../polaris/catalog/models/sort_field.py | 95 + .../polaris/catalog/models/sort_order.py | 99 + .../catalog/models/sql_view_representation.py | 91 + .../polaris/catalog/models/statistics_file.py | 103 + .../polaris/catalog/models/struct_field.py | 101 + .../polaris/catalog/models/struct_type.py | 106 + .../catalog/models/table_identifier.py | 89 + .../polaris/catalog/models/table_metadata.py | 205 + .../catalog/models/table_requirement.py | 128 + .../polaris/catalog/models/table_update.py | 362 + .../models/table_update_notification.py | 99 + .../python/polaris/catalog/models/term.py | 140 + .../polaris/catalog/models/timer_result.py | 91 + .../polaris/catalog/models/token_type.py | 41 + .../polaris/catalog/models/transform_term.py | 98 + .../python/polaris/catalog/models/type.py | 170 + .../catalog/models/unary_expression.py | 95 + .../update_namespace_properties_request.py | 89 + .../update_namespace_properties_response.py | 96 + .../models/upgrade_format_version_update.py | 96 + .../polaris/catalog/models/value_map.py | 97 + .../catalog/models/view_history_entry.py | 89 + .../polaris/catalog/models/view_metadata.py | 126 + .../catalog/models/view_representation.py | 123 + .../catalog/models/view_requirement.py | 107 + .../polaris/catalog/models/view_update.py | 227 + .../polaris/catalog/models/view_version.py | 107 + .../client/python/polaris/catalog/py.typed | 0 .../client/python/polaris/catalog/rest.py | 257 + .../python/polaris/management/__init__.py | 73 + .../python/polaris/management/api/__init__.py | 5 + .../management/api/polaris_default_api.py | 8883 +++++++++++++++++ .../python/polaris/management/api_client.py | 788 ++ .../python/polaris/management/api_response.py | 21 + .../polaris/management/configuration.py | 468 + .../python/polaris/management/exceptions.py | 199 + .../polaris/management/models/__init__.py | 56 + .../management/models/add_grant_request.py | 91 + .../models/aws_storage_config_info.py | 93 + .../models/azure_storage_config_info.py | 91 + .../polaris/management/models/catalog.py | 131 + .../management/models/catalog_grant.py | 90 + .../management/models/catalog_privilege.py | 60 + .../management/models/catalog_properties.py | 100 + .../polaris/management/models/catalog_role.py | 95 + .../management/models/catalog_roles.py | 95 + .../polaris/management/models/catalogs.py | 95 + .../models/create_catalog_request.py | 91 + .../models/create_catalog_role_request.py | 91 + .../models/create_principal_request.py | 93 + .../models/create_principal_role_request.py | 91 + .../management/models/external_catalog.py | 103 + .../models/file_storage_config_info.py | 88 + .../models/gcp_storage_config_info.py | 89 + .../models/grant_catalog_role_request.py | 91 + .../models/grant_principal_role_request.py | 91 + .../management/models/grant_resource.py | 123 + .../management/models/grant_resources.py | 95 + .../management/models/namespace_grant.py | 92 + .../management/models/namespace_privilege.py | 58 + .../management/models/polaris_catalog.py | 101 + .../polaris/management/models/principal.py | 97 + .../management/models/principal_role.py | 95 + .../management/models/principal_roles.py | 95 + .../models/principal_with_credentials.py | 97 + .../principal_with_credentials_credentials.py | 89 + .../polaris/management/models/principals.py | 95 + .../management/models/revoke_grant_request.py | 91 + .../management/models/storage_config_info.py | 124 + .../polaris/management/models/table_grant.py | 94 + .../management/models/table_privilege.py | 44 + .../models/update_catalog_request.py | 95 + .../models/update_catalog_role_request.py | 89 + .../models/update_principal_request.py | 89 + .../models/update_principal_role_request.py | 89 + .../polaris/management/models/view_grant.py | 94 + .../management/models/view_privilege.py | 42 + .../client/python/polaris/management/py.typed | 0 .../client/python/polaris/management/rest.py | 257 + regtests/client/python/pyproject.toml | 72 + regtests/client/python/requirements.txt | 5 + regtests/client/python/setup.cfg | 2 + regtests/client/python/setup.py | 49 + regtests/client/python/test-requirements.txt | 5 + regtests/client/python/test/__init__.py | 0 .../python/test/test_add_grant_request.py | 52 + .../test/test_add_partition_spec_update.py | 70 + .../python/test/test_add_schema_update.py | 55 + .../python/test/test_add_snapshot_update.py | 72 + .../python/test/test_add_sort_order_update.py | 70 + .../test/test_add_view_version_update.py | 76 + .../python/test/test_and_or_expression.py | 56 + .../client/python/test/test_assert_create.py | 52 + .../test/test_assert_current_schema_id.py | 54 + .../test/test_assert_default_sort_order_id.py | 54 + .../test/test_assert_default_spec_id.py | 54 + .../test_assert_last_assigned_field_id.py | 54 + .../test_assert_last_assigned_partition_id.py | 54 + .../test/test_assert_ref_snapshot_id.py | 56 + .../python/test/test_assert_table_uuid.py | 54 + .../python/test/test_assert_view_uuid.py | 54 + .../python/test/test_assign_uuid_update.py | 54 + .../test/test_aws_storage_config_info.py | 54 + .../test/test_azure_storage_config_info.py | 54 + .../client/python/test/test_base_update.py | 52 + .../client/python/test/test_blob_metadata.py | 63 + regtests/client/python/test/test_catalog.py | 64 + .../client/python/test/test_catalog_config.py | 62 + .../client/python/test/test_catalog_grant.py | 52 + .../python/test/test_catalog_privilege.py | 33 + .../python/test/test_catalog_properties.py | 52 + .../client/python/test/test_catalog_role.py | 58 + .../client/python/test/test_catalog_roles.py | 70 + regtests/client/python/test/test_catalogs.py | 80 + .../client/python/test/test_cli_parsing.py | 419 + .../client/python/test/test_commit_report.py | 63 + .../python/test/test_commit_table_request.py | 67 + .../python/test/test_commit_table_response.py | 236 + .../test/test_commit_transaction_request.py | 76 + .../python/test/test_commit_view_request.py | 63 + .../client/python/test/test_content_file.py | 68 + regtests/client/python/test/test_count_map.py | 56 + .../client/python/test/test_counter_result.py | 54 + .../test/test_create_catalog_request.py | 76 + .../test/test_create_catalog_role_request.py | 58 + .../test/test_create_namespace_request.py | 53 + .../test/test_create_namespace_response.py | 53 + .../test/test_create_principal_request.py | 60 + .../test_create_principal_role_request.py | 58 + .../python/test/test_create_table_request.py | 77 + .../python/test/test_create_view_request.py | 85 + regtests/client/python/test/test_data_file.py | 58 + .../python/test/test_equality_delete_file.py | 55 + .../client/python/test/test_error_model.py | 59 + .../client/python/test/test_expression.py | 68 + .../python/test/test_external_catalog.py | 52 + .../client/python/test/test_file_format.py | 33 + .../test/test_file_storage_config_info.py | 50 + .../test/test_gcp_storage_config_info.py | 51 + .../test/test_get_namespace_response.py | 53 + .../test/test_grant_catalog_role_request.py | 58 + .../test/test_grant_principal_role_request.py | 58 + .../client/python/test/test_grant_resource.py | 52 + .../python/test/test_grant_resources.py | 58 + .../python/test/test_iceberg_catalog_api.py | 199 + .../test/test_iceberg_configuration_api.py | 38 + .../test/test_iceberg_error_response.py | 64 + .../python/test/test_iceberg_o_auth2_api.py | 38 + .../test/test_list_namespaces_response.py | 54 + .../python/test/test_list_tables_response.py | 56 + regtests/client/python/test/test_list_type.py | 58 + .../python/test/test_literal_expression.py | 56 + .../python/test/test_load_table_result.py | 238 + .../python/test/test_load_view_result.py | 115 + regtests/client/python/test/test_map_type.py | 62 + .../python/test/test_metadata_log_inner.py | 54 + .../client/python/test/test_metric_result.py | 60 + .../client/python/test/test_model_schema.py | 72 + .../python/test/test_namespace_grant.py | 58 + .../python/test/test_namespace_privilege.py | 33 + .../client/python/test/test_not_expression.py | 54 + .../python/test/test_notification_request.py | 149 + .../python/test/test_notification_type.py | 33 + .../client/python/test/test_null_order.py | 33 + .../client/python/test/test_o_auth_error.py | 54 + .../python/test/test_o_auth_token_response.py | 58 + .../python/test/test_partition_field.py | 57 + .../client/python/test/test_partition_spec.py | 65 + .../test/test_partition_statistics_file.py | 56 + .../python/test/test_polaris_catalog.py | 50 + .../python/test/test_polaris_default_api.py | 223 + .../python/test/test_position_delete_file.py | 52 + .../python/test/test_primitive_type_value.py | 50 + regtests/client/python/test/test_principal.py | 61 + .../client/python/test/test_principal_role.py | 58 + .../python/test/test_principal_roles.py | 70 + .../test/test_principal_with_credentials.py | 76 + ..._principal_with_credentials_credentials.py | 52 + .../client/python/test/test_principals.py | 74 + .../test/test_register_table_request.py | 54 + ...test_remove_partition_statistics_update.py | 54 + .../test/test_remove_properties_update.py | 58 + .../test/test_remove_snapshot_ref_update.py | 54 + .../test/test_remove_snapshots_update.py | 58 + .../test/test_remove_statistics_update.py | 54 + .../python/test/test_rename_table_request.py | 62 + .../test/test_report_metrics_request.py | 81 + .../python/test/test_revoke_grant_request.py | 52 + .../client/python/test/test_scan_report.py | 75 + .../test/test_set_current_schema_update.py | 54 + .../test_set_current_view_version_update.py | 54 + .../test_set_default_sort_order_update.py | 54 + .../test/test_set_default_spec_update.py | 54 + .../client/python/test/test_set_expression.py | 60 + .../python/test/test_set_location_update.py | 54 + .../test_set_partition_statistics_update.py | 60 + .../python/test/test_set_properties_update.py | 58 + .../test/test_set_snapshot_ref_update.py | 61 + .../python/test/test_set_statistics_update.py | 84 + regtests/client/python/test/test_snapshot.py | 65 + .../python/test/test_snapshot_log_inner.py | 54 + .../python/test/test_snapshot_reference.py | 57 + .../python/test/test_snapshot_summary.py | 52 + .../client/python/test/test_sort_direction.py | 33 + .../client/python/test/test_sort_field.py | 58 + .../client/python/test/test_sort_order.py | 66 + .../test/test_sql_view_representation.py | 56 + .../python/test/test_statistics_file.py | 78 + .../python/test/test_storage_config_info.py | 53 + .../client/python/test/test_struct_field.py | 59 + .../client/python/test/test_struct_type.py | 68 + .../client/python/test/test_table_grant.py | 60 + .../python/test/test_table_identifier.py | 54 + .../client/python/test/test_table_metadata.py | 144 + .../python/test/test_table_privilege.py | 33 + .../python/test/test_table_requirement.py | 52 + .../client/python/test/test_table_update.py | 178 + .../test/test_table_update_notification.py | 150 + regtests/client/python/test/test_term.py | 56 + .../client/python/test/test_timer_result.py | 56 + .../client/python/test/test_token_type.py | 33 + .../client/python/test/test_transform_term.py | 56 + regtests/client/python/test/test_type.py | 84 + .../python/test/test_unary_expression.py | 56 + .../test/test_update_catalog_request.py | 57 + .../test/test_update_catalog_role_request.py | 54 + ...est_update_namespace_properties_request.py | 52 + ...st_update_namespace_properties_response.py | 65 + .../test/test_update_principal_request.py | 54 + .../test_update_principal_role_request.py | 54 + .../test_upgrade_format_version_update.py | 54 + regtests/client/python/test/test_value_map.py | 56 + .../client/python/test/test_view_grant.py | 60 + .../python/test/test_view_history_entry.py | 54 + .../client/python/test/test_view_metadata.py | 105 + .../client/python/test/test_view_privilege.py | 33 + .../python/test/test_view_representation.py | 56 + .../python/test/test_view_requirement.py | 52 + .../client/python/test/test_view_update.py | 97 + .../client/python/test/test_view_version.py | 71 + regtests/client/python/tox.ini | 9 + regtests/credentials/.keep | 0 regtests/output/.keep | 0 regtests/pyspark-setup.sh | 13 + regtests/run.sh | 131 + regtests/run_spark_sql.sh | 60 + regtests/setup.sh | 105 + regtests/t_hello_world/ref/hello_world.sh.ref | 1 + regtests/t_hello_world/src/hello_world.sh | 3 + regtests/t_oauth/test_oauth2_tokens.py | 52 + regtests/t_pyspark/src/conftest.py | 128 + regtests/t_pyspark/src/iceberg_spark.py | 108 + .../src/test_spark_sql_s3_with_privileges.py | 600 ++ .../ref/spark_sql_azure_blob.sh.ref | 35 + .../ref/spark_sql_azure_dfs.sh.ref | 35 + .../t_spark_sql/ref/spark_sql_basic.sh.ref | 39 + regtests/t_spark_sql/ref/spark_sql_gcp.sh.ref | 35 + regtests/t_spark_sql/ref/spark_sql_s3.sh.ref | 35 + .../ref/spark_sql_s3_cross_region.sh.ref | 35 + .../t_spark_sql/ref/spark_sql_views.sh.ref | 52 + .../t_spark_sql/src/spark_sql_azure_blob.sh | 51 + .../t_spark_sql/src/spark_sql_azure_dfs.sh | 51 + regtests/t_spark_sql/src/spark_sql_basic.sh | 53 + regtests/t_spark_sql/src/spark_sql_gcp.sh | 51 + regtests/t_spark_sql/src/spark_sql_s3.sh | 56 + .../src/spark_sql_s3_cross_region.sh | 58 + regtests/t_spark_sql/src/spark_sql_views.sh | 63 + server-templates/api.mustache | 100 + server-templates/apiService.mustache | 42 + server-templates/apiServiceImpl.mustache | 43 + server-templates/bodyParams.mustache | 4 + server-templates/formParams.mustache | 4 + server-templates/headerParams.mustache | 4 + server-templates/pojo.mustache | 219 + server-templates/queryParams.mustache | 18 + settings.gradle | 8 + setup.sh | 19 + spec/polaris-management-service.yml | 1327 +++ spec/rest-catalog-open-api.yaml | 4148 ++++++++ 792 files changed, 115301 insertions(+), 2 deletions(-) create mode 100644 .dockerignore create mode 100644 .github/CODEOWNERS create mode 100644 .github/dependabot.yml create mode 100644 .github/pull_request_template.md create mode 100644 .github/workflows/gradle.yml create mode 100644 .github/workflows/regtest.yml create mode 100644 .github/workflows/semgrep.yml create mode 100644 .github/workflows/stale.yml create mode 100644 .gitignore create mode 100644 .openapi-generator-ignore create mode 100644 Dockerfile create mode 100644 build.gradle create mode 100644 docker-compose-jupyter.yml create mode 100644 docker-compose.yml create mode 100644 docs/entities.md create mode 100644 docs/iceberg-rest/index.html create mode 100644 docs/img/quickstart/privilege-illustration-1.png create mode 100644 docs/img/quickstart/privilege-illustration-2.png create mode 100644 docs/index.html create mode 100644 docs/polaris-management/index.html create mode 100644 docs/quickstart.md create mode 100644 extension/persistence/eclipselink/build.gradle create mode 100644 extension/persistence/eclipselink/src/main/java/io/polaris/extension/persistence/impl/eclipselink/EclipseLinkPolarisMetaStoreManagerFactory.java create mode 100644 extension/persistence/eclipselink/src/main/java/io/polaris/extension/persistence/impl/eclipselink/PolarisEclipseLinkMetaStoreSessionImpl.java create mode 100644 extension/persistence/eclipselink/src/main/java/io/polaris/extension/persistence/impl/eclipselink/PolarisEclipseLinkStore.java create mode 100644 extension/persistence/eclipselink/src/test/java/com/snowflake/polaris/persistence/impl/eclipselink/PolarisEclipseLinkMetaStoreTest.java create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100755 kind-registry.sh create mode 100644 notebooks/Dockerfile create mode 100644 notebooks/SparkPolaris.ipynb create mode 100755 polaris create mode 100644 polaris-core/build.gradle create mode 100644 polaris-core/src/main/java/io/polaris/core/PolarisCallContext.java create mode 100644 polaris-core/src/main/java/io/polaris/core/PolarisConfiguration.java create mode 100644 polaris-core/src/main/java/io/polaris/core/PolarisConfigurationStore.java create mode 100644 polaris-core/src/main/java/io/polaris/core/PolarisDefaultDiagServiceImpl.java create mode 100644 polaris-core/src/main/java/io/polaris/core/PolarisDiagnostics.java create mode 100644 polaris-core/src/main/java/io/polaris/core/auth/AuthenticatedPolarisPrincipal.java create mode 100644 polaris-core/src/main/java/io/polaris/core/auth/PolarisAuthorizableOperation.java create mode 100644 polaris-core/src/main/java/io/polaris/core/auth/PolarisAuthorizer.java create mode 100644 polaris-core/src/main/java/io/polaris/core/catalog/PolarisCatalogHelpers.java create mode 100644 polaris-core/src/main/java/io/polaris/core/context/CallContext.java create mode 100644 polaris-core/src/main/java/io/polaris/core/context/RealmContext.java create mode 100644 polaris-core/src/main/java/io/polaris/core/entity/AsyncTaskType.java create mode 100644 polaris-core/src/main/java/io/polaris/core/entity/CatalogEntity.java create mode 100644 polaris-core/src/main/java/io/polaris/core/entity/CatalogRoleEntity.java create mode 100644 polaris-core/src/main/java/io/polaris/core/entity/NamespaceEntity.java create mode 100644 polaris-core/src/main/java/io/polaris/core/entity/PolarisBaseEntity.java create mode 100644 polaris-core/src/main/java/io/polaris/core/entity/PolarisChangeTrackingVersions.java create mode 100644 polaris-core/src/main/java/io/polaris/core/entity/PolarisEntitiesActiveKey.java create mode 100644 polaris-core/src/main/java/io/polaris/core/entity/PolarisEntity.java create mode 100644 polaris-core/src/main/java/io/polaris/core/entity/PolarisEntityActiveRecord.java create mode 100644 polaris-core/src/main/java/io/polaris/core/entity/PolarisEntityConstants.java create mode 100644 polaris-core/src/main/java/io/polaris/core/entity/PolarisEntityCore.java create mode 100644 polaris-core/src/main/java/io/polaris/core/entity/PolarisEntityId.java create mode 100644 polaris-core/src/main/java/io/polaris/core/entity/PolarisEntitySubType.java create mode 100644 polaris-core/src/main/java/io/polaris/core/entity/PolarisEntityType.java create mode 100644 polaris-core/src/main/java/io/polaris/core/entity/PolarisGrantRecord.java create mode 100644 polaris-core/src/main/java/io/polaris/core/entity/PolarisPrincipalSecrets.java create mode 100644 polaris-core/src/main/java/io/polaris/core/entity/PolarisPrivilege.java create mode 100644 polaris-core/src/main/java/io/polaris/core/entity/PolarisTaskConstants.java create mode 100644 polaris-core/src/main/java/io/polaris/core/entity/PrincipalEntity.java create mode 100644 polaris-core/src/main/java/io/polaris/core/entity/PrincipalRoleEntity.java create mode 100644 polaris-core/src/main/java/io/polaris/core/entity/TableLikeEntity.java create mode 100644 polaris-core/src/main/java/io/polaris/core/entity/TaskEntity.java create mode 100644 polaris-core/src/main/java/io/polaris/core/monitor/PolarisMetricRegistry.java create mode 100644 polaris-core/src/main/java/io/polaris/core/persistence/LocalPolarisMetaStoreManagerFactory.java create mode 100644 polaris-core/src/main/java/io/polaris/core/persistence/MetaStoreManagerFactory.java create mode 100644 polaris-core/src/main/java/io/polaris/core/persistence/PolarisEntityManager.java create mode 100644 polaris-core/src/main/java/io/polaris/core/persistence/PolarisEntityResolver.java create mode 100644 polaris-core/src/main/java/io/polaris/core/persistence/PolarisMetaStoreManager.java create mode 100644 polaris-core/src/main/java/io/polaris/core/persistence/PolarisMetaStoreManagerImpl.java create mode 100644 polaris-core/src/main/java/io/polaris/core/persistence/PolarisMetaStoreSession.java create mode 100644 polaris-core/src/main/java/io/polaris/core/persistence/PolarisObjectMapperUtil.java create mode 100644 polaris-core/src/main/java/io/polaris/core/persistence/PolarisResolvedPathWrapper.java create mode 100644 polaris-core/src/main/java/io/polaris/core/persistence/PolarisTreeMapMetaStoreSessionImpl.java create mode 100644 polaris-core/src/main/java/io/polaris/core/persistence/PolarisTreeMapStore.java create mode 100644 polaris-core/src/main/java/io/polaris/core/persistence/ResolvedPolarisEntity.java create mode 100644 polaris-core/src/main/java/io/polaris/core/persistence/RetryOnConcurrencyException.java create mode 100644 polaris-core/src/main/java/io/polaris/core/persistence/cache/EntityCache.java create mode 100644 polaris-core/src/main/java/io/polaris/core/persistence/cache/EntityCacheByNameKey.java create mode 100644 polaris-core/src/main/java/io/polaris/core/persistence/cache/EntityCacheEntry.java create mode 100644 polaris-core/src/main/java/io/polaris/core/persistence/cache/EntityCacheLookupResult.java create mode 100644 polaris-core/src/main/java/io/polaris/core/persistence/cache/EntityCacheMode.java create mode 100644 polaris-core/src/main/java/io/polaris/core/persistence/models/ModelEntity.java create mode 100644 polaris-core/src/main/java/io/polaris/core/persistence/models/ModelEntityActive.java create mode 100644 polaris-core/src/main/java/io/polaris/core/persistence/models/ModelEntityChangeTracking.java create mode 100644 polaris-core/src/main/java/io/polaris/core/persistence/models/ModelEntityDropped.java create mode 100644 polaris-core/src/main/java/io/polaris/core/persistence/models/ModelGrantRecord.java create mode 100644 polaris-core/src/main/java/io/polaris/core/persistence/models/ModelPrincipalSecrets.java create mode 100644 polaris-core/src/main/java/io/polaris/core/persistence/models/ModelSequenceId.java create mode 100644 polaris-core/src/main/java/io/polaris/core/persistence/resolver/PolarisResolutionManifest.java create mode 100644 polaris-core/src/main/java/io/polaris/core/persistence/resolver/PolarisResolutionManifestCatalogView.java create mode 100644 polaris-core/src/main/java/io/polaris/core/persistence/resolver/Resolver.java create mode 100644 polaris-core/src/main/java/io/polaris/core/persistence/resolver/ResolverEntityName.java create mode 100644 polaris-core/src/main/java/io/polaris/core/persistence/resolver/ResolverPath.java create mode 100644 polaris-core/src/main/java/io/polaris/core/persistence/resolver/ResolverPrincipalRole.java create mode 100644 polaris-core/src/main/java/io/polaris/core/persistence/resolver/ResolverStatus.java create mode 100644 polaris-core/src/main/java/io/polaris/core/storage/FileStorageConfigurationInfo.java create mode 100644 polaris-core/src/main/java/io/polaris/core/storage/InMemoryStorageIntegration.java create mode 100644 polaris-core/src/main/java/io/polaris/core/storage/PolarisCredentialProperty.java create mode 100644 polaris-core/src/main/java/io/polaris/core/storage/PolarisStorageActions.java create mode 100644 polaris-core/src/main/java/io/polaris/core/storage/PolarisStorageConfigurationInfo.java create mode 100644 polaris-core/src/main/java/io/polaris/core/storage/PolarisStorageIntegration.java create mode 100644 polaris-core/src/main/java/io/polaris/core/storage/PolarisStorageIntegrationProvider.java create mode 100644 polaris-core/src/main/java/io/polaris/core/storage/aws/AwsCredentialsStorageIntegration.java create mode 100644 polaris-core/src/main/java/io/polaris/core/storage/aws/AwsStorageConfigurationInfo.java create mode 100644 polaris-core/src/main/java/io/polaris/core/storage/aws/PolarisS3FileIOClientFactory.java create mode 100644 polaris-core/src/main/java/io/polaris/core/storage/azure/AzureCredentialsStorageIntegration.java create mode 100644 polaris-core/src/main/java/io/polaris/core/storage/azure/AzureLocation.java create mode 100644 polaris-core/src/main/java/io/polaris/core/storage/azure/AzureStorageConfigurationInfo.java create mode 100644 polaris-core/src/main/java/io/polaris/core/storage/cache/StorageCredentialCache.java create mode 100644 polaris-core/src/main/java/io/polaris/core/storage/cache/StorageCredentialCacheEntry.java create mode 100644 polaris-core/src/main/java/io/polaris/core/storage/cache/StorageCredentialCacheKey.java create mode 100644 polaris-core/src/main/java/io/polaris/core/storage/gcp/GcpCredentialsStorageIntegration.java create mode 100644 polaris-core/src/main/java/io/polaris/core/storage/gcp/GcpStorageConfigurationInfo.java create mode 100644 polaris-core/src/test/java/io/polaris/core/persistence/EntityCacheTest.java create mode 100644 polaris-core/src/test/java/io/polaris/core/persistence/PolarisObjectMapperUtilTest.java create mode 100644 polaris-core/src/test/java/io/polaris/core/persistence/PolarisTreeMapMetaStoreManagerTest.java create mode 100644 polaris-core/src/test/java/io/polaris/core/persistence/ResolverTest.java create mode 100644 polaris-core/src/test/java/io/polaris/core/storage/InMemoryStorageIntegrationTest.java create mode 100644 polaris-core/src/test/java/io/polaris/core/storage/cache/StorageCredentialCacheTest.java create mode 100644 polaris-core/src/test/java/io/polaris/service/storage/aws/AwsCredentialsStorageIntegrationTest.java create mode 100644 polaris-core/src/test/java/io/polaris/service/storage/azure/AzureCredentialStorageIntegrationTest.java create mode 100644 polaris-core/src/test/java/io/polaris/service/storage/azure/AzureLocationTest.java create mode 100644 polaris-core/src/test/java/io/polaris/service/storage/gcp/GcpCredentialsStorageIntegrationTest.java create mode 100644 polaris-core/src/testFixtures/java/io/polaris/core/persistence/PolarisMetaStoreManagerTest.java create mode 100644 polaris-core/src/testFixtures/java/io/polaris/core/persistence/PolarisTestMetaStoreManager.java create mode 100644 polaris-server.yml create mode 100644 polaris-service/build.gradle create mode 100644 polaris-service/src/main/java/io/polaris/service/BootstrapRealmsCommand.java create mode 100644 polaris-service/src/main/java/io/polaris/service/IcebergExceptionMapper.java create mode 100644 polaris-service/src/main/java/io/polaris/service/IcebergJerseyViolationExceptionMapper.java create mode 100644 polaris-service/src/main/java/io/polaris/service/IcebergJsonProcessingExceptionMapper.java create mode 100644 polaris-service/src/main/java/io/polaris/service/PolarisApplication.java create mode 100644 polaris-service/src/main/java/io/polaris/service/PolarisHealthCheck.java create mode 100644 polaris-service/src/main/java/io/polaris/service/TimedApplicationEventListener.java create mode 100644 polaris-service/src/main/java/io/polaris/service/admin/PolarisAdminService.java create mode 100644 polaris-service/src/main/java/io/polaris/service/admin/PolarisServiceImpl.java create mode 100644 polaris-service/src/main/java/io/polaris/service/auth/BasePolarisAuthenticator.java create mode 100644 polaris-service/src/main/java/io/polaris/service/auth/DecodedToken.java create mode 100644 polaris-service/src/main/java/io/polaris/service/auth/DefaultOAuth2ApiService.java create mode 100644 polaris-service/src/main/java/io/polaris/service/auth/DefaultPolarisAuthenticator.java create mode 100644 polaris-service/src/main/java/io/polaris/service/auth/DiscoverableAuthenticator.java create mode 100644 polaris-service/src/main/java/io/polaris/service/auth/JWTBroker.java create mode 100644 polaris-service/src/main/java/io/polaris/service/auth/JWTRSAKeyPair.java create mode 100644 polaris-service/src/main/java/io/polaris/service/auth/JWTRSAKeyPairFactory.java create mode 100644 polaris-service/src/main/java/io/polaris/service/auth/JWTSymmetricKeyBroker.java create mode 100644 polaris-service/src/main/java/io/polaris/service/auth/JWTSymmetricKeyFactory.java create mode 100644 polaris-service/src/main/java/io/polaris/service/auth/KeyProvider.java create mode 100644 polaris-service/src/main/java/io/polaris/service/auth/LocalRSAKeyProvider.java create mode 100644 polaris-service/src/main/java/io/polaris/service/auth/OAuthTokenErrorResponse.java create mode 100644 polaris-service/src/main/java/io/polaris/service/auth/OAuthUtils.java create mode 100644 polaris-service/src/main/java/io/polaris/service/auth/PemUtils.java create mode 100644 polaris-service/src/main/java/io/polaris/service/auth/TestInlineBearerTokenPolarisAuthenticator.java create mode 100644 polaris-service/src/main/java/io/polaris/service/auth/TestOAuth2ApiService.java create mode 100644 polaris-service/src/main/java/io/polaris/service/auth/TokenBroker.java create mode 100644 polaris-service/src/main/java/io/polaris/service/auth/TokenBrokerFactory.java create mode 100644 polaris-service/src/main/java/io/polaris/service/auth/TokenInfoExchangeResponse.java create mode 100644 polaris-service/src/main/java/io/polaris/service/auth/TokenRequestValidator.java create mode 100644 polaris-service/src/main/java/io/polaris/service/auth/TokenResponse.java create mode 100644 polaris-service/src/main/java/io/polaris/service/catalog/BasePolarisCatalog.java create mode 100644 polaris-service/src/main/java/io/polaris/service/catalog/IcebergCatalogAdapter.java create mode 100644 polaris-service/src/main/java/io/polaris/service/catalog/PolarisCatalogHandlerWrapper.java create mode 100644 polaris-service/src/main/java/io/polaris/service/catalog/SupportsCredentialDelegation.java create mode 100644 polaris-service/src/main/java/io/polaris/service/catalog/SupportsNotifications.java create mode 100644 polaris-service/src/main/java/io/polaris/service/config/ConfigurationStoreAware.java create mode 100644 polaris-service/src/main/java/io/polaris/service/config/CorsConfiguration.java create mode 100644 polaris-service/src/main/java/io/polaris/service/config/DefaultConfigurationStore.java create mode 100644 polaris-service/src/main/java/io/polaris/service/config/HasEntityManagerFactory.java create mode 100644 polaris-service/src/main/java/io/polaris/service/config/OAuth2ApiService.java create mode 100644 polaris-service/src/main/java/io/polaris/service/config/PolarisApplicationConfig.java create mode 100644 polaris-service/src/main/java/io/polaris/service/config/RealmEntityManagerFactory.java create mode 100644 polaris-service/src/main/java/io/polaris/service/config/Serializers.java create mode 100644 polaris-service/src/main/java/io/polaris/service/config/TaskHandlerConfiguration.java create mode 100644 polaris-service/src/main/java/io/polaris/service/context/CallContextCatalogFactory.java create mode 100644 polaris-service/src/main/java/io/polaris/service/context/CallContextResolver.java create mode 100644 polaris-service/src/main/java/io/polaris/service/context/DefaultContextResolver.java create mode 100644 polaris-service/src/main/java/io/polaris/service/context/PolarisCallContextCatalogFactory.java create mode 100644 polaris-service/src/main/java/io/polaris/service/context/RealmContextResolver.java create mode 100644 polaris-service/src/main/java/io/polaris/service/context/SqlliteCallContextCatalogFactory.java create mode 100644 polaris-service/src/main/java/io/polaris/service/logging/PolarisJsonLayoutFactory.java create mode 100644 polaris-service/src/main/java/io/polaris/service/persistence/InMemoryPolarisMetaStoreManagerFactory.java create mode 100644 polaris-service/src/main/java/io/polaris/service/resource/TimedApi.java create mode 100644 polaris-service/src/main/java/io/polaris/service/storage/PolarisStorageIntegrationProviderImpl.java create mode 100644 polaris-service/src/main/java/io/polaris/service/task/ManifestFileCleanupTaskHandler.java create mode 100644 polaris-service/src/main/java/io/polaris/service/task/TableCleanupTaskHandler.java create mode 100644 polaris-service/src/main/java/io/polaris/service/task/TaskExecutor.java create mode 100644 polaris-service/src/main/java/io/polaris/service/task/TaskExecutorImpl.java create mode 100644 polaris-service/src/main/java/io/polaris/service/task/TaskFileIOSupplier.java create mode 100644 polaris-service/src/main/java/io/polaris/service/task/TaskHandler.java create mode 100644 polaris-service/src/main/java/io/polaris/service/task/TaskUtils.java create mode 100644 polaris-service/src/main/java/io/polaris/service/tracing/HeadersMapAccessor.java create mode 100644 polaris-service/src/main/java/io/polaris/service/tracing/OpenTelemetryAware.java create mode 100644 polaris-service/src/main/java/io/polaris/service/tracing/TracingFilter.java create mode 100644 polaris-service/src/main/java/io/polaris/service/types/CommitTableRequest.java create mode 100644 polaris-service/src/main/java/io/polaris/service/types/CommitViewRequest.java create mode 100644 polaris-service/src/main/java/io/polaris/service/types/NotificationRequest.java create mode 100644 polaris-service/src/main/java/io/polaris/service/types/NotificationType.java create mode 100644 polaris-service/src/main/java/io/polaris/service/types/TableUpdateNotification.java create mode 100644 polaris-service/src/main/java/io/polaris/service/types/TokenType.java create mode 100644 polaris-service/src/main/resources/META-INF/persistence.xml create mode 100644 polaris-service/src/main/resources/META-INF/services/io.dropwizard.jackson.Discoverable create mode 100644 polaris-service/src/main/resources/META-INF/services/io.dropwizard.logging.common.layout.DiscoverableLayoutFactory create mode 100644 polaris-service/src/main/resources/META-INF/services/io.polaris.core.persistence.MetaStoreManagerFactory create mode 100644 polaris-service/src/main/resources/META-INF/services/io.polaris.service.auth.TokenBrokerFactory create mode 100644 polaris-service/src/main/resources/META-INF/services/io.polaris.service.config.OAuth2ApiService create mode 100644 polaris-service/src/main/resources/META-INF/services/io.polaris.service.context.CallContextResolver create mode 100644 polaris-service/src/main/resources/META-INF/services/io.polaris.service.context.RealmContextResolver create mode 100644 polaris-service/src/main/resources/log4j.properties create mode 100644 polaris-service/src/test/java/io/polaris/service/PolarisApplicationIntegrationTest.java create mode 100644 polaris-service/src/test/java/io/polaris/service/admin/PolarisAdminServiceAuthzTest.java create mode 100644 polaris-service/src/test/java/io/polaris/service/admin/PolarisAuthzTestBase.java create mode 100644 polaris-service/src/test/java/io/polaris/service/admin/PolarisServiceImplIntegrationTest.java create mode 100644 polaris-service/src/test/java/io/polaris/service/auth/JWTRSAKeyPairTest.java create mode 100644 polaris-service/src/test/java/io/polaris/service/auth/JWTSymmetricKeyGeneratorTest.java create mode 100644 polaris-service/src/test/java/io/polaris/service/auth/TokenRequestValidatorTest.java create mode 100644 polaris-service/src/test/java/io/polaris/service/auth/TokenUtils.java create mode 100644 polaris-service/src/test/java/io/polaris/service/catalog/BasePolarisCatalogTest.java create mode 100644 polaris-service/src/test/java/io/polaris/service/catalog/BasePolarisCatalogViewTest.java create mode 100644 polaris-service/src/test/java/io/polaris/service/catalog/PolarisCatalogHandlerWrapperAuthzTest.java create mode 100644 polaris-service/src/test/java/io/polaris/service/catalog/PolarisPassthroughResolutionView.java create mode 100644 polaris-service/src/test/java/io/polaris/service/catalog/PolarisRestCatalogIntegrationTest.java create mode 100644 polaris-service/src/test/java/io/polaris/service/catalog/PolarisRestCatalogViewIntegrationTest.java create mode 100644 polaris-service/src/test/java/io/polaris/service/catalog/PolarisSparkIntegrationTest.java create mode 100644 polaris-service/src/test/java/io/polaris/service/entity/CatalogEntityTest.java create mode 100644 polaris-service/src/test/java/io/polaris/service/task/ManifestFileCleanupTaskHandlerTest.java create mode 100644 polaris-service/src/test/java/io/polaris/service/task/TableCleanupTaskHandlerTest.java create mode 100644 polaris-service/src/test/java/io/polaris/service/task/TaskTestUtils.java create mode 100644 polaris-service/src/test/java/io/polaris/service/task/TestSnapshot.java create mode 100644 polaris-service/src/test/java/io/polaris/service/test/PolarisConnectionExtension.java create mode 100644 polaris-service/src/test/java/io/polaris/service/test/SnowmanCredentialsExtension.java create mode 100644 polaris-service/src/test/resources/META-INF/persistence.xml create mode 100644 polaris-service/src/test/resources/META-INF/services/io.polaris.service.auth.DiscoverableAuthenticator create mode 100644 polaris-service/src/test/resources/polaris-server-integrationtest.yml create mode 100644 regtests/.dockerignore create mode 100644 regtests/.gitignore create mode 100644 regtests/Dockerfile create mode 100644 regtests/README.md create mode 100644 regtests/client/python/.github/workflows/python.yml create mode 100644 regtests/client/python/.gitignore create mode 100644 regtests/client/python/.gitlab-ci.yml create mode 100644 regtests/client/python/.openapi-generator-ignore create mode 100644 regtests/client/python/.openapi-generator/FILES create mode 100644 regtests/client/python/.openapi-generator/VERSION create mode 100644 regtests/client/python/.travis.yml create mode 100644 regtests/client/python/README.md create mode 100644 regtests/client/python/cli/__init__.py create mode 100644 regtests/client/python/cli/command/__init__.py create mode 100644 regtests/client/python/cli/command/catalog_roles.py create mode 100644 regtests/client/python/cli/command/catalogs.py create mode 100644 regtests/client/python/cli/command/principal_roles.py create mode 100644 regtests/client/python/cli/command/principals.py create mode 100644 regtests/client/python/cli/command/privileges.py create mode 100644 regtests/client/python/cli/constants.py create mode 100644 regtests/client/python/cli/options/__init__.py create mode 100644 regtests/client/python/cli/options/option_tree.py create mode 100644 regtests/client/python/cli/options/parser.py create mode 100644 regtests/client/python/cli/polaris_cli.py create mode 100644 regtests/client/python/docs/AddGrantRequest.md create mode 100644 regtests/client/python/docs/AddPartitionSpecUpdate.md create mode 100644 regtests/client/python/docs/AddSchemaUpdate.md create mode 100644 regtests/client/python/docs/AddSnapshotUpdate.md create mode 100644 regtests/client/python/docs/AddSortOrderUpdate.md create mode 100644 regtests/client/python/docs/AddViewVersionUpdate.md create mode 100644 regtests/client/python/docs/AndOrExpression.md create mode 100644 regtests/client/python/docs/AssertCreate.md create mode 100644 regtests/client/python/docs/AssertCurrentSchemaId.md create mode 100644 regtests/client/python/docs/AssertDefaultSortOrderId.md create mode 100644 regtests/client/python/docs/AssertDefaultSpecId.md create mode 100644 regtests/client/python/docs/AssertLastAssignedFieldId.md create mode 100644 regtests/client/python/docs/AssertLastAssignedPartitionId.md create mode 100644 regtests/client/python/docs/AssertRefSnapshotId.md create mode 100644 regtests/client/python/docs/AssertTableUUID.md create mode 100644 regtests/client/python/docs/AssertViewUUID.md create mode 100644 regtests/client/python/docs/AssignUUIDUpdate.md create mode 100644 regtests/client/python/docs/AwsStorageConfigInfo.md create mode 100644 regtests/client/python/docs/AzureStorageConfigInfo.md create mode 100644 regtests/client/python/docs/BaseUpdate.md create mode 100644 regtests/client/python/docs/BlobMetadata.md create mode 100644 regtests/client/python/docs/Catalog.md create mode 100644 regtests/client/python/docs/CatalogConfig.md create mode 100644 regtests/client/python/docs/CatalogGrant.md create mode 100644 regtests/client/python/docs/CatalogPrivilege.md create mode 100644 regtests/client/python/docs/CatalogProperties.md create mode 100644 regtests/client/python/docs/CatalogRole.md create mode 100644 regtests/client/python/docs/CatalogRoles.md create mode 100644 regtests/client/python/docs/Catalogs.md create mode 100644 regtests/client/python/docs/CommitReport.md create mode 100644 regtests/client/python/docs/CommitTableRequest.md create mode 100644 regtests/client/python/docs/CommitTableResponse.md create mode 100644 regtests/client/python/docs/CommitTransactionRequest.md create mode 100644 regtests/client/python/docs/CommitViewRequest.md create mode 100644 regtests/client/python/docs/ContentFile.md create mode 100644 regtests/client/python/docs/CountMap.md create mode 100644 regtests/client/python/docs/CounterResult.md create mode 100644 regtests/client/python/docs/CreateCatalogRequest.md create mode 100644 regtests/client/python/docs/CreateCatalogRoleRequest.md create mode 100644 regtests/client/python/docs/CreateNamespaceRequest.md create mode 100644 regtests/client/python/docs/CreateNamespaceResponse.md create mode 100644 regtests/client/python/docs/CreatePrincipalRequest.md create mode 100644 regtests/client/python/docs/CreatePrincipalRoleRequest.md create mode 100644 regtests/client/python/docs/CreateTableRequest.md create mode 100644 regtests/client/python/docs/CreateViewRequest.md create mode 100644 regtests/client/python/docs/DataFile.md create mode 100644 regtests/client/python/docs/EqualityDeleteFile.md create mode 100644 regtests/client/python/docs/ErrorModel.md create mode 100644 regtests/client/python/docs/Expression.md create mode 100644 regtests/client/python/docs/ExternalCatalog.md create mode 100644 regtests/client/python/docs/FileFormat.md create mode 100644 regtests/client/python/docs/FileStorageConfigInfo.md create mode 100644 regtests/client/python/docs/GcpStorageConfigInfo.md create mode 100644 regtests/client/python/docs/GetNamespaceResponse.md create mode 100644 regtests/client/python/docs/GrantCatalogRoleRequest.md create mode 100644 regtests/client/python/docs/GrantPrincipalRoleRequest.md create mode 100644 regtests/client/python/docs/GrantResource.md create mode 100644 regtests/client/python/docs/GrantResources.md create mode 100644 regtests/client/python/docs/IcebergCatalogAPI.md create mode 100644 regtests/client/python/docs/IcebergConfigurationAPI.md create mode 100644 regtests/client/python/docs/IcebergErrorResponse.md create mode 100644 regtests/client/python/docs/IcebergOAuth2API.md create mode 100644 regtests/client/python/docs/ListNamespacesResponse.md create mode 100644 regtests/client/python/docs/ListTablesResponse.md create mode 100644 regtests/client/python/docs/ListType.md create mode 100644 regtests/client/python/docs/LiteralExpression.md create mode 100644 regtests/client/python/docs/LoadTableResult.md create mode 100644 regtests/client/python/docs/LoadViewResult.md create mode 100644 regtests/client/python/docs/MapType.md create mode 100644 regtests/client/python/docs/MetadataLogInner.md create mode 100644 regtests/client/python/docs/MetricResult.md create mode 100644 regtests/client/python/docs/ModelSchema.md create mode 100644 regtests/client/python/docs/NamespaceGrant.md create mode 100644 regtests/client/python/docs/NamespacePrivilege.md create mode 100644 regtests/client/python/docs/NotExpression.md create mode 100644 regtests/client/python/docs/NotificationRequest.md create mode 100644 regtests/client/python/docs/NotificationType.md create mode 100644 regtests/client/python/docs/NullOrder.md create mode 100644 regtests/client/python/docs/OAuthError.md create mode 100644 regtests/client/python/docs/OAuthTokenResponse.md create mode 100644 regtests/client/python/docs/PartitionField.md create mode 100644 regtests/client/python/docs/PartitionSpec.md create mode 100644 regtests/client/python/docs/PartitionStatisticsFile.md create mode 100644 regtests/client/python/docs/PolarisCatalog.md create mode 100644 regtests/client/python/docs/PolarisDefaultApi.md create mode 100644 regtests/client/python/docs/PositionDeleteFile.md create mode 100644 regtests/client/python/docs/PrimitiveTypeValue.md create mode 100644 regtests/client/python/docs/Principal.md create mode 100644 regtests/client/python/docs/PrincipalRole.md create mode 100644 regtests/client/python/docs/PrincipalRoles.md create mode 100644 regtests/client/python/docs/PrincipalWithCredentials.md create mode 100644 regtests/client/python/docs/PrincipalWithCredentialsCredentials.md create mode 100644 regtests/client/python/docs/Principals.md create mode 100644 regtests/client/python/docs/RegisterTableRequest.md create mode 100644 regtests/client/python/docs/RemovePartitionStatisticsUpdate.md create mode 100644 regtests/client/python/docs/RemovePropertiesUpdate.md create mode 100644 regtests/client/python/docs/RemoveSnapshotRefUpdate.md create mode 100644 regtests/client/python/docs/RemoveSnapshotsUpdate.md create mode 100644 regtests/client/python/docs/RemoveStatisticsUpdate.md create mode 100644 regtests/client/python/docs/RenameTableRequest.md create mode 100644 regtests/client/python/docs/ReportMetricsRequest.md create mode 100644 regtests/client/python/docs/RevokeGrantRequest.md create mode 100644 regtests/client/python/docs/SQLViewRepresentation.md create mode 100644 regtests/client/python/docs/ScanReport.md create mode 100644 regtests/client/python/docs/SetCurrentSchemaUpdate.md create mode 100644 regtests/client/python/docs/SetCurrentViewVersionUpdate.md create mode 100644 regtests/client/python/docs/SetDefaultSortOrderUpdate.md create mode 100644 regtests/client/python/docs/SetDefaultSpecUpdate.md create mode 100644 regtests/client/python/docs/SetExpression.md create mode 100644 regtests/client/python/docs/SetLocationUpdate.md create mode 100644 regtests/client/python/docs/SetPartitionStatisticsUpdate.md create mode 100644 regtests/client/python/docs/SetPropertiesUpdate.md create mode 100644 regtests/client/python/docs/SetSnapshotRefUpdate.md create mode 100644 regtests/client/python/docs/SetStatisticsUpdate.md create mode 100644 regtests/client/python/docs/Snapshot.md create mode 100644 regtests/client/python/docs/SnapshotLogInner.md create mode 100644 regtests/client/python/docs/SnapshotReference.md create mode 100644 regtests/client/python/docs/SnapshotSummary.md create mode 100644 regtests/client/python/docs/SortDirection.md create mode 100644 regtests/client/python/docs/SortField.md create mode 100644 regtests/client/python/docs/SortOrder.md create mode 100644 regtests/client/python/docs/StatisticsFile.md create mode 100644 regtests/client/python/docs/StorageConfigInfo.md create mode 100644 regtests/client/python/docs/StructField.md create mode 100644 regtests/client/python/docs/StructType.md create mode 100644 regtests/client/python/docs/TableGrant.md create mode 100644 regtests/client/python/docs/TableIdentifier.md create mode 100644 regtests/client/python/docs/TableMetadata.md create mode 100644 regtests/client/python/docs/TablePrivilege.md create mode 100644 regtests/client/python/docs/TableRequirement.md create mode 100644 regtests/client/python/docs/TableUpdate.md create mode 100644 regtests/client/python/docs/TableUpdateNotification.md create mode 100644 regtests/client/python/docs/Term.md create mode 100644 regtests/client/python/docs/TimerResult.md create mode 100644 regtests/client/python/docs/TokenType.md create mode 100644 regtests/client/python/docs/TransformTerm.md create mode 100644 regtests/client/python/docs/Type.md create mode 100644 regtests/client/python/docs/UnaryExpression.md create mode 100644 regtests/client/python/docs/UpdateCatalogRequest.md create mode 100644 regtests/client/python/docs/UpdateCatalogRoleRequest.md create mode 100644 regtests/client/python/docs/UpdateNamespacePropertiesRequest.md create mode 100644 regtests/client/python/docs/UpdateNamespacePropertiesResponse.md create mode 100644 regtests/client/python/docs/UpdatePrincipalRequest.md create mode 100644 regtests/client/python/docs/UpdatePrincipalRoleRequest.md create mode 100644 regtests/client/python/docs/UpgradeFormatVersionUpdate.md create mode 100644 regtests/client/python/docs/ValueMap.md create mode 100644 regtests/client/python/docs/ViewGrant.md create mode 100644 regtests/client/python/docs/ViewHistoryEntry.md create mode 100644 regtests/client/python/docs/ViewMetadata.md create mode 100644 regtests/client/python/docs/ViewPrivilege.md create mode 100644 regtests/client/python/docs/ViewRepresentation.md create mode 100644 regtests/client/python/docs/ViewRequirement.md create mode 100644 regtests/client/python/docs/ViewUpdate.md create mode 100644 regtests/client/python/docs/ViewVersion.md create mode 100644 regtests/client/python/git_push.sh create mode 100644 regtests/client/python/poetry.lock create mode 100644 regtests/client/python/polaris/__init__.py create mode 100644 regtests/client/python/polaris/catalog/__init__.py create mode 100644 regtests/client/python/polaris/catalog/api/__init__.py create mode 100644 regtests/client/python/polaris/catalog/api/iceberg_catalog_api.py create mode 100644 regtests/client/python/polaris/catalog/api/iceberg_configuration_api.py create mode 100644 regtests/client/python/polaris/catalog/api/iceberg_o_auth2_api.py create mode 100644 regtests/client/python/polaris/catalog/api_client.py create mode 100644 regtests/client/python/polaris/catalog/api_response.py create mode 100644 regtests/client/python/polaris/catalog/configuration.py create mode 100644 regtests/client/python/polaris/catalog/exceptions.py create mode 100644 regtests/client/python/polaris/catalog/models/__init__.py create mode 100644 regtests/client/python/polaris/catalog/models/add_partition_spec_update.py create mode 100644 regtests/client/python/polaris/catalog/models/add_schema_update.py create mode 100644 regtests/client/python/polaris/catalog/models/add_snapshot_update.py create mode 100644 regtests/client/python/polaris/catalog/models/add_sort_order_update.py create mode 100644 regtests/client/python/polaris/catalog/models/add_view_version_update.py create mode 100644 regtests/client/python/polaris/catalog/models/and_or_expression.py create mode 100644 regtests/client/python/polaris/catalog/models/assert_create.py create mode 100644 regtests/client/python/polaris/catalog/models/assert_current_schema_id.py create mode 100644 regtests/client/python/polaris/catalog/models/assert_default_sort_order_id.py create mode 100644 regtests/client/python/polaris/catalog/models/assert_default_spec_id.py create mode 100644 regtests/client/python/polaris/catalog/models/assert_last_assigned_field_id.py create mode 100644 regtests/client/python/polaris/catalog/models/assert_last_assigned_partition_id.py create mode 100644 regtests/client/python/polaris/catalog/models/assert_ref_snapshot_id.py create mode 100644 regtests/client/python/polaris/catalog/models/assert_table_uuid.py create mode 100644 regtests/client/python/polaris/catalog/models/assert_view_uuid.py create mode 100644 regtests/client/python/polaris/catalog/models/assign_uuid_update.py create mode 100644 regtests/client/python/polaris/catalog/models/base_update.py create mode 100644 regtests/client/python/polaris/catalog/models/blob_metadata.py create mode 100644 regtests/client/python/polaris/catalog/models/catalog_config.py create mode 100644 regtests/client/python/polaris/catalog/models/commit_report.py create mode 100644 regtests/client/python/polaris/catalog/models/commit_table_request.py create mode 100644 regtests/client/python/polaris/catalog/models/commit_table_response.py create mode 100644 regtests/client/python/polaris/catalog/models/commit_transaction_request.py create mode 100644 regtests/client/python/polaris/catalog/models/commit_view_request.py create mode 100644 regtests/client/python/polaris/catalog/models/content_file.py create mode 100644 regtests/client/python/polaris/catalog/models/count_map.py create mode 100644 regtests/client/python/polaris/catalog/models/counter_result.py create mode 100644 regtests/client/python/polaris/catalog/models/create_namespace_request.py create mode 100644 regtests/client/python/polaris/catalog/models/create_namespace_response.py create mode 100644 regtests/client/python/polaris/catalog/models/create_table_request.py create mode 100644 regtests/client/python/polaris/catalog/models/create_view_request.py create mode 100644 regtests/client/python/polaris/catalog/models/data_file.py create mode 100644 regtests/client/python/polaris/catalog/models/equality_delete_file.py create mode 100644 regtests/client/python/polaris/catalog/models/error_model.py create mode 100644 regtests/client/python/polaris/catalog/models/expression.py create mode 100644 regtests/client/python/polaris/catalog/models/file_format.py create mode 100644 regtests/client/python/polaris/catalog/models/get_namespace_response.py create mode 100644 regtests/client/python/polaris/catalog/models/iceberg_error_response.py create mode 100644 regtests/client/python/polaris/catalog/models/list_namespaces_response.py create mode 100644 regtests/client/python/polaris/catalog/models/list_tables_response.py create mode 100644 regtests/client/python/polaris/catalog/models/list_type.py create mode 100644 regtests/client/python/polaris/catalog/models/literal_expression.py create mode 100644 regtests/client/python/polaris/catalog/models/load_table_result.py create mode 100644 regtests/client/python/polaris/catalog/models/load_view_result.py create mode 100644 regtests/client/python/polaris/catalog/models/map_type.py create mode 100644 regtests/client/python/polaris/catalog/models/metadata_log_inner.py create mode 100644 regtests/client/python/polaris/catalog/models/metric_result.py create mode 100644 regtests/client/python/polaris/catalog/models/model_schema.py create mode 100644 regtests/client/python/polaris/catalog/models/not_expression.py create mode 100644 regtests/client/python/polaris/catalog/models/notification_request.py create mode 100644 regtests/client/python/polaris/catalog/models/notification_type.py create mode 100644 regtests/client/python/polaris/catalog/models/null_order.py create mode 100644 regtests/client/python/polaris/catalog/models/o_auth_error.py create mode 100644 regtests/client/python/polaris/catalog/models/o_auth_token_response.py create mode 100644 regtests/client/python/polaris/catalog/models/partition_field.py create mode 100644 regtests/client/python/polaris/catalog/models/partition_spec.py create mode 100644 regtests/client/python/polaris/catalog/models/partition_statistics_file.py create mode 100644 regtests/client/python/polaris/catalog/models/position_delete_file.py create mode 100644 regtests/client/python/polaris/catalog/models/primitive_type_value.py create mode 100644 regtests/client/python/polaris/catalog/models/register_table_request.py create mode 100644 regtests/client/python/polaris/catalog/models/remove_partition_statistics_update.py create mode 100644 regtests/client/python/polaris/catalog/models/remove_properties_update.py create mode 100644 regtests/client/python/polaris/catalog/models/remove_snapshot_ref_update.py create mode 100644 regtests/client/python/polaris/catalog/models/remove_snapshots_update.py create mode 100644 regtests/client/python/polaris/catalog/models/remove_statistics_update.py create mode 100644 regtests/client/python/polaris/catalog/models/rename_table_request.py create mode 100644 regtests/client/python/polaris/catalog/models/report_metrics_request.py create mode 100644 regtests/client/python/polaris/catalog/models/scan_report.py create mode 100644 regtests/client/python/polaris/catalog/models/set_current_schema_update.py create mode 100644 regtests/client/python/polaris/catalog/models/set_current_view_version_update.py create mode 100644 regtests/client/python/polaris/catalog/models/set_default_sort_order_update.py create mode 100644 regtests/client/python/polaris/catalog/models/set_default_spec_update.py create mode 100644 regtests/client/python/polaris/catalog/models/set_expression.py create mode 100644 regtests/client/python/polaris/catalog/models/set_location_update.py create mode 100644 regtests/client/python/polaris/catalog/models/set_partition_statistics_update.py create mode 100644 regtests/client/python/polaris/catalog/models/set_properties_update.py create mode 100644 regtests/client/python/polaris/catalog/models/set_snapshot_ref_update.py create mode 100644 regtests/client/python/polaris/catalog/models/set_statistics_update.py create mode 100644 regtests/client/python/polaris/catalog/models/snapshot.py create mode 100644 regtests/client/python/polaris/catalog/models/snapshot_log_inner.py create mode 100644 regtests/client/python/polaris/catalog/models/snapshot_reference.py create mode 100644 regtests/client/python/polaris/catalog/models/snapshot_summary.py create mode 100644 regtests/client/python/polaris/catalog/models/sort_direction.py create mode 100644 regtests/client/python/polaris/catalog/models/sort_field.py create mode 100644 regtests/client/python/polaris/catalog/models/sort_order.py create mode 100644 regtests/client/python/polaris/catalog/models/sql_view_representation.py create mode 100644 regtests/client/python/polaris/catalog/models/statistics_file.py create mode 100644 regtests/client/python/polaris/catalog/models/struct_field.py create mode 100644 regtests/client/python/polaris/catalog/models/struct_type.py create mode 100644 regtests/client/python/polaris/catalog/models/table_identifier.py create mode 100644 regtests/client/python/polaris/catalog/models/table_metadata.py create mode 100644 regtests/client/python/polaris/catalog/models/table_requirement.py create mode 100644 regtests/client/python/polaris/catalog/models/table_update.py create mode 100644 regtests/client/python/polaris/catalog/models/table_update_notification.py create mode 100644 regtests/client/python/polaris/catalog/models/term.py create mode 100644 regtests/client/python/polaris/catalog/models/timer_result.py create mode 100644 regtests/client/python/polaris/catalog/models/token_type.py create mode 100644 regtests/client/python/polaris/catalog/models/transform_term.py create mode 100644 regtests/client/python/polaris/catalog/models/type.py create mode 100644 regtests/client/python/polaris/catalog/models/unary_expression.py create mode 100644 regtests/client/python/polaris/catalog/models/update_namespace_properties_request.py create mode 100644 regtests/client/python/polaris/catalog/models/update_namespace_properties_response.py create mode 100644 regtests/client/python/polaris/catalog/models/upgrade_format_version_update.py create mode 100644 regtests/client/python/polaris/catalog/models/value_map.py create mode 100644 regtests/client/python/polaris/catalog/models/view_history_entry.py create mode 100644 regtests/client/python/polaris/catalog/models/view_metadata.py create mode 100644 regtests/client/python/polaris/catalog/models/view_representation.py create mode 100644 regtests/client/python/polaris/catalog/models/view_requirement.py create mode 100644 regtests/client/python/polaris/catalog/models/view_update.py create mode 100644 regtests/client/python/polaris/catalog/models/view_version.py create mode 100644 regtests/client/python/polaris/catalog/py.typed create mode 100644 regtests/client/python/polaris/catalog/rest.py create mode 100644 regtests/client/python/polaris/management/__init__.py create mode 100644 regtests/client/python/polaris/management/api/__init__.py create mode 100644 regtests/client/python/polaris/management/api/polaris_default_api.py create mode 100644 regtests/client/python/polaris/management/api_client.py create mode 100644 regtests/client/python/polaris/management/api_response.py create mode 100644 regtests/client/python/polaris/management/configuration.py create mode 100644 regtests/client/python/polaris/management/exceptions.py create mode 100644 regtests/client/python/polaris/management/models/__init__.py create mode 100644 regtests/client/python/polaris/management/models/add_grant_request.py create mode 100644 regtests/client/python/polaris/management/models/aws_storage_config_info.py create mode 100644 regtests/client/python/polaris/management/models/azure_storage_config_info.py create mode 100644 regtests/client/python/polaris/management/models/catalog.py create mode 100644 regtests/client/python/polaris/management/models/catalog_grant.py create mode 100644 regtests/client/python/polaris/management/models/catalog_privilege.py create mode 100644 regtests/client/python/polaris/management/models/catalog_properties.py create mode 100644 regtests/client/python/polaris/management/models/catalog_role.py create mode 100644 regtests/client/python/polaris/management/models/catalog_roles.py create mode 100644 regtests/client/python/polaris/management/models/catalogs.py create mode 100644 regtests/client/python/polaris/management/models/create_catalog_request.py create mode 100644 regtests/client/python/polaris/management/models/create_catalog_role_request.py create mode 100644 regtests/client/python/polaris/management/models/create_principal_request.py create mode 100644 regtests/client/python/polaris/management/models/create_principal_role_request.py create mode 100644 regtests/client/python/polaris/management/models/external_catalog.py create mode 100644 regtests/client/python/polaris/management/models/file_storage_config_info.py create mode 100644 regtests/client/python/polaris/management/models/gcp_storage_config_info.py create mode 100644 regtests/client/python/polaris/management/models/grant_catalog_role_request.py create mode 100644 regtests/client/python/polaris/management/models/grant_principal_role_request.py create mode 100644 regtests/client/python/polaris/management/models/grant_resource.py create mode 100644 regtests/client/python/polaris/management/models/grant_resources.py create mode 100644 regtests/client/python/polaris/management/models/namespace_grant.py create mode 100644 regtests/client/python/polaris/management/models/namespace_privilege.py create mode 100644 regtests/client/python/polaris/management/models/polaris_catalog.py create mode 100644 regtests/client/python/polaris/management/models/principal.py create mode 100644 regtests/client/python/polaris/management/models/principal_role.py create mode 100644 regtests/client/python/polaris/management/models/principal_roles.py create mode 100644 regtests/client/python/polaris/management/models/principal_with_credentials.py create mode 100644 regtests/client/python/polaris/management/models/principal_with_credentials_credentials.py create mode 100644 regtests/client/python/polaris/management/models/principals.py create mode 100644 regtests/client/python/polaris/management/models/revoke_grant_request.py create mode 100644 regtests/client/python/polaris/management/models/storage_config_info.py create mode 100644 regtests/client/python/polaris/management/models/table_grant.py create mode 100644 regtests/client/python/polaris/management/models/table_privilege.py create mode 100644 regtests/client/python/polaris/management/models/update_catalog_request.py create mode 100644 regtests/client/python/polaris/management/models/update_catalog_role_request.py create mode 100644 regtests/client/python/polaris/management/models/update_principal_request.py create mode 100644 regtests/client/python/polaris/management/models/update_principal_role_request.py create mode 100644 regtests/client/python/polaris/management/models/view_grant.py create mode 100644 regtests/client/python/polaris/management/models/view_privilege.py create mode 100644 regtests/client/python/polaris/management/py.typed create mode 100644 regtests/client/python/polaris/management/rest.py create mode 100644 regtests/client/python/pyproject.toml create mode 100644 regtests/client/python/requirements.txt create mode 100644 regtests/client/python/setup.cfg create mode 100644 regtests/client/python/setup.py create mode 100644 regtests/client/python/test-requirements.txt create mode 100644 regtests/client/python/test/__init__.py create mode 100644 regtests/client/python/test/test_add_grant_request.py create mode 100644 regtests/client/python/test/test_add_partition_spec_update.py create mode 100644 regtests/client/python/test/test_add_schema_update.py create mode 100644 regtests/client/python/test/test_add_snapshot_update.py create mode 100644 regtests/client/python/test/test_add_sort_order_update.py create mode 100644 regtests/client/python/test/test_add_view_version_update.py create mode 100644 regtests/client/python/test/test_and_or_expression.py create mode 100644 regtests/client/python/test/test_assert_create.py create mode 100644 regtests/client/python/test/test_assert_current_schema_id.py create mode 100644 regtests/client/python/test/test_assert_default_sort_order_id.py create mode 100644 regtests/client/python/test/test_assert_default_spec_id.py create mode 100644 regtests/client/python/test/test_assert_last_assigned_field_id.py create mode 100644 regtests/client/python/test/test_assert_last_assigned_partition_id.py create mode 100644 regtests/client/python/test/test_assert_ref_snapshot_id.py create mode 100644 regtests/client/python/test/test_assert_table_uuid.py create mode 100644 regtests/client/python/test/test_assert_view_uuid.py create mode 100644 regtests/client/python/test/test_assign_uuid_update.py create mode 100644 regtests/client/python/test/test_aws_storage_config_info.py create mode 100644 regtests/client/python/test/test_azure_storage_config_info.py create mode 100644 regtests/client/python/test/test_base_update.py create mode 100644 regtests/client/python/test/test_blob_metadata.py create mode 100644 regtests/client/python/test/test_catalog.py create mode 100644 regtests/client/python/test/test_catalog_config.py create mode 100644 regtests/client/python/test/test_catalog_grant.py create mode 100644 regtests/client/python/test/test_catalog_privilege.py create mode 100644 regtests/client/python/test/test_catalog_properties.py create mode 100644 regtests/client/python/test/test_catalog_role.py create mode 100644 regtests/client/python/test/test_catalog_roles.py create mode 100644 regtests/client/python/test/test_catalogs.py create mode 100644 regtests/client/python/test/test_cli_parsing.py create mode 100644 regtests/client/python/test/test_commit_report.py create mode 100644 regtests/client/python/test/test_commit_table_request.py create mode 100644 regtests/client/python/test/test_commit_table_response.py create mode 100644 regtests/client/python/test/test_commit_transaction_request.py create mode 100644 regtests/client/python/test/test_commit_view_request.py create mode 100644 regtests/client/python/test/test_content_file.py create mode 100644 regtests/client/python/test/test_count_map.py create mode 100644 regtests/client/python/test/test_counter_result.py create mode 100644 regtests/client/python/test/test_create_catalog_request.py create mode 100644 regtests/client/python/test/test_create_catalog_role_request.py create mode 100644 regtests/client/python/test/test_create_namespace_request.py create mode 100644 regtests/client/python/test/test_create_namespace_response.py create mode 100644 regtests/client/python/test/test_create_principal_request.py create mode 100644 regtests/client/python/test/test_create_principal_role_request.py create mode 100644 regtests/client/python/test/test_create_table_request.py create mode 100644 regtests/client/python/test/test_create_view_request.py create mode 100644 regtests/client/python/test/test_data_file.py create mode 100644 regtests/client/python/test/test_equality_delete_file.py create mode 100644 regtests/client/python/test/test_error_model.py create mode 100644 regtests/client/python/test/test_expression.py create mode 100644 regtests/client/python/test/test_external_catalog.py create mode 100644 regtests/client/python/test/test_file_format.py create mode 100644 regtests/client/python/test/test_file_storage_config_info.py create mode 100644 regtests/client/python/test/test_gcp_storage_config_info.py create mode 100644 regtests/client/python/test/test_get_namespace_response.py create mode 100644 regtests/client/python/test/test_grant_catalog_role_request.py create mode 100644 regtests/client/python/test/test_grant_principal_role_request.py create mode 100644 regtests/client/python/test/test_grant_resource.py create mode 100644 regtests/client/python/test/test_grant_resources.py create mode 100644 regtests/client/python/test/test_iceberg_catalog_api.py create mode 100644 regtests/client/python/test/test_iceberg_configuration_api.py create mode 100644 regtests/client/python/test/test_iceberg_error_response.py create mode 100644 regtests/client/python/test/test_iceberg_o_auth2_api.py create mode 100644 regtests/client/python/test/test_list_namespaces_response.py create mode 100644 regtests/client/python/test/test_list_tables_response.py create mode 100644 regtests/client/python/test/test_list_type.py create mode 100644 regtests/client/python/test/test_literal_expression.py create mode 100644 regtests/client/python/test/test_load_table_result.py create mode 100644 regtests/client/python/test/test_load_view_result.py create mode 100644 regtests/client/python/test/test_map_type.py create mode 100644 regtests/client/python/test/test_metadata_log_inner.py create mode 100644 regtests/client/python/test/test_metric_result.py create mode 100644 regtests/client/python/test/test_model_schema.py create mode 100644 regtests/client/python/test/test_namespace_grant.py create mode 100644 regtests/client/python/test/test_namespace_privilege.py create mode 100644 regtests/client/python/test/test_not_expression.py create mode 100644 regtests/client/python/test/test_notification_request.py create mode 100644 regtests/client/python/test/test_notification_type.py create mode 100644 regtests/client/python/test/test_null_order.py create mode 100644 regtests/client/python/test/test_o_auth_error.py create mode 100644 regtests/client/python/test/test_o_auth_token_response.py create mode 100644 regtests/client/python/test/test_partition_field.py create mode 100644 regtests/client/python/test/test_partition_spec.py create mode 100644 regtests/client/python/test/test_partition_statistics_file.py create mode 100644 regtests/client/python/test/test_polaris_catalog.py create mode 100644 regtests/client/python/test/test_polaris_default_api.py create mode 100644 regtests/client/python/test/test_position_delete_file.py create mode 100644 regtests/client/python/test/test_primitive_type_value.py create mode 100644 regtests/client/python/test/test_principal.py create mode 100644 regtests/client/python/test/test_principal_role.py create mode 100644 regtests/client/python/test/test_principal_roles.py create mode 100644 regtests/client/python/test/test_principal_with_credentials.py create mode 100644 regtests/client/python/test/test_principal_with_credentials_credentials.py create mode 100644 regtests/client/python/test/test_principals.py create mode 100644 regtests/client/python/test/test_register_table_request.py create mode 100644 regtests/client/python/test/test_remove_partition_statistics_update.py create mode 100644 regtests/client/python/test/test_remove_properties_update.py create mode 100644 regtests/client/python/test/test_remove_snapshot_ref_update.py create mode 100644 regtests/client/python/test/test_remove_snapshots_update.py create mode 100644 regtests/client/python/test/test_remove_statistics_update.py create mode 100644 regtests/client/python/test/test_rename_table_request.py create mode 100644 regtests/client/python/test/test_report_metrics_request.py create mode 100644 regtests/client/python/test/test_revoke_grant_request.py create mode 100644 regtests/client/python/test/test_scan_report.py create mode 100644 regtests/client/python/test/test_set_current_schema_update.py create mode 100644 regtests/client/python/test/test_set_current_view_version_update.py create mode 100644 regtests/client/python/test/test_set_default_sort_order_update.py create mode 100644 regtests/client/python/test/test_set_default_spec_update.py create mode 100644 regtests/client/python/test/test_set_expression.py create mode 100644 regtests/client/python/test/test_set_location_update.py create mode 100644 regtests/client/python/test/test_set_partition_statistics_update.py create mode 100644 regtests/client/python/test/test_set_properties_update.py create mode 100644 regtests/client/python/test/test_set_snapshot_ref_update.py create mode 100644 regtests/client/python/test/test_set_statistics_update.py create mode 100644 regtests/client/python/test/test_snapshot.py create mode 100644 regtests/client/python/test/test_snapshot_log_inner.py create mode 100644 regtests/client/python/test/test_snapshot_reference.py create mode 100644 regtests/client/python/test/test_snapshot_summary.py create mode 100644 regtests/client/python/test/test_sort_direction.py create mode 100644 regtests/client/python/test/test_sort_field.py create mode 100644 regtests/client/python/test/test_sort_order.py create mode 100644 regtests/client/python/test/test_sql_view_representation.py create mode 100644 regtests/client/python/test/test_statistics_file.py create mode 100644 regtests/client/python/test/test_storage_config_info.py create mode 100644 regtests/client/python/test/test_struct_field.py create mode 100644 regtests/client/python/test/test_struct_type.py create mode 100644 regtests/client/python/test/test_table_grant.py create mode 100644 regtests/client/python/test/test_table_identifier.py create mode 100644 regtests/client/python/test/test_table_metadata.py create mode 100644 regtests/client/python/test/test_table_privilege.py create mode 100644 regtests/client/python/test/test_table_requirement.py create mode 100644 regtests/client/python/test/test_table_update.py create mode 100644 regtests/client/python/test/test_table_update_notification.py create mode 100644 regtests/client/python/test/test_term.py create mode 100644 regtests/client/python/test/test_timer_result.py create mode 100644 regtests/client/python/test/test_token_type.py create mode 100644 regtests/client/python/test/test_transform_term.py create mode 100644 regtests/client/python/test/test_type.py create mode 100644 regtests/client/python/test/test_unary_expression.py create mode 100644 regtests/client/python/test/test_update_catalog_request.py create mode 100644 regtests/client/python/test/test_update_catalog_role_request.py create mode 100644 regtests/client/python/test/test_update_namespace_properties_request.py create mode 100644 regtests/client/python/test/test_update_namespace_properties_response.py create mode 100644 regtests/client/python/test/test_update_principal_request.py create mode 100644 regtests/client/python/test/test_update_principal_role_request.py create mode 100644 regtests/client/python/test/test_upgrade_format_version_update.py create mode 100644 regtests/client/python/test/test_value_map.py create mode 100644 regtests/client/python/test/test_view_grant.py create mode 100644 regtests/client/python/test/test_view_history_entry.py create mode 100644 regtests/client/python/test/test_view_metadata.py create mode 100644 regtests/client/python/test/test_view_privilege.py create mode 100644 regtests/client/python/test/test_view_representation.py create mode 100644 regtests/client/python/test/test_view_requirement.py create mode 100644 regtests/client/python/test/test_view_update.py create mode 100644 regtests/client/python/test/test_view_version.py create mode 100644 regtests/client/python/tox.ini create mode 100644 regtests/credentials/.keep create mode 100644 regtests/output/.keep create mode 100755 regtests/pyspark-setup.sh create mode 100755 regtests/run.sh create mode 100755 regtests/run_spark_sql.sh create mode 100755 regtests/setup.sh create mode 100755 regtests/t_hello_world/ref/hello_world.sh.ref create mode 100755 regtests/t_hello_world/src/hello_world.sh create mode 100644 regtests/t_oauth/test_oauth2_tokens.py create mode 100644 regtests/t_pyspark/src/conftest.py create mode 100644 regtests/t_pyspark/src/iceberg_spark.py create mode 100644 regtests/t_pyspark/src/test_spark_sql_s3_with_privileges.py create mode 100755 regtests/t_spark_sql/ref/spark_sql_azure_blob.sh.ref create mode 100755 regtests/t_spark_sql/ref/spark_sql_azure_dfs.sh.ref create mode 100755 regtests/t_spark_sql/ref/spark_sql_basic.sh.ref create mode 100755 regtests/t_spark_sql/ref/spark_sql_gcp.sh.ref create mode 100755 regtests/t_spark_sql/ref/spark_sql_s3.sh.ref create mode 100644 regtests/t_spark_sql/ref/spark_sql_s3_cross_region.sh.ref create mode 100755 regtests/t_spark_sql/ref/spark_sql_views.sh.ref create mode 100755 regtests/t_spark_sql/src/spark_sql_azure_blob.sh create mode 100755 regtests/t_spark_sql/src/spark_sql_azure_dfs.sh create mode 100755 regtests/t_spark_sql/src/spark_sql_basic.sh create mode 100755 regtests/t_spark_sql/src/spark_sql_gcp.sh create mode 100755 regtests/t_spark_sql/src/spark_sql_s3.sh create mode 100644 regtests/t_spark_sql/src/spark_sql_s3_cross_region.sh create mode 100755 regtests/t_spark_sql/src/spark_sql_views.sh create mode 100644 server-templates/api.mustache create mode 100644 server-templates/apiService.mustache create mode 100644 server-templates/apiServiceImpl.mustache create mode 100644 server-templates/bodyParams.mustache create mode 100644 server-templates/formParams.mustache create mode 100644 server-templates/headerParams.mustache create mode 100644 server-templates/pojo.mustache create mode 100644 server-templates/queryParams.mustache create mode 100644 settings.gradle create mode 100755 setup.sh create mode 100644 spec/polaris-management-service.yml create mode 100644 spec/rest-catalog-open-api.yaml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000..d56f0ecb92 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +polaris-service/logs +polaris-service/build +polaris-core/build +build +.idea diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000000..0200ca247d --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +@polaris-catalog/polaris \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..23c4cb3b50 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,7 @@ +--- +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000000..0c23671569 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1 @@ + diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml new file mode 100644 index 0000000000..64bd8be24c --- /dev/null +++ b/.github/workflows/gradle.yml @@ -0,0 +1,60 @@ +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. +# This workflow will build a Java project with Gradle and cache/restore any dependencies to improve the workflow execution time +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-gradle + +name: Java CI with Gradle + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + build: + + runs-on: ubuntu-latest + permissions: + contents: read + + steps: + - uses: actions/checkout@v4 + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'temurin' + + # Configure Gradle for optimal use in GiHub Actions, including caching of downloaded dependencies. + # See: https://github.com/gradle/actions/blob/main/setup-gradle/README.md + - name: Setup Gradle + uses: gradle/actions/setup-gradle@dbbdc275be76ac10734476cc723d82dfe7ec6eda # v3.4.2 + + - name: Check formatting + run: ./gradlew check + + - name: Build with Gradle Wrapper + run: ./gradlew test + + - name: Archive test results + uses: actions/upload-artifact@v4 + if: always() + with: + name: upload-test-artifacts + path: | + polaris-core/build/test-results/test + polaris-service/build/test-results/test + + # NOTE: The Gradle Wrapper is the default and recommended way to run Gradle (https://docs.gradle.org/current/userguide/gradle_wrapper.html). + # If your project does not have the Gradle Wrapper configured, you can use the following configuration to run Gradle with a specified version. + # + # - name: Setup Gradle + # uses: gradle/actions/setup-gradle@dbbdc275be76ac10734476cc723d82dfe7ec6eda # v3.4.2 + # with: + # gradle-version: '8.6' + # + # - name: Build with Gradle 8.6 + # run: gradle build diff --git a/.github/workflows/regtest.yml b/.github/workflows/regtest.yml new file mode 100644 index 0000000000..0d65a9bd33 --- /dev/null +++ b/.github/workflows/regtest.yml @@ -0,0 +1,23 @@ +name: Regression Tests +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + regtest: + + runs-on: ubuntu-latest + permissions: + contents: read + + steps: + - uses: actions/checkout@v4 + - name: fix permissions + run: mkdir -p regtests/output && chmod 777 regtests/output && chmod 777 regtests/t_*/ref/* + - name: Regression Test + env: + AWS_ACCESS_KEY_ID: ${{secrets.AWS_ACCESS_KEY_ID}} + AWS_SECRET_ACCESS_KEY: ${{secrets.AWS_SECRET_ACCESS_KEY}} + run: docker compose up --build --exit-code-from regtest diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml new file mode 100644 index 0000000000..f1d7286d3b --- /dev/null +++ b/.github/workflows/semgrep.yml @@ -0,0 +1,12 @@ +--- +name: Run semgrep checks +on: + pull_request: + branches: [main] +permissions: + contents: read +jobs: + run-semgrep-reusable-workflow: + uses: snowflakedb/reusable-workflows/.github/workflows/semgrep-v2.yml@main + secrets: + token: ${{ secrets.SEMGREP_APP_TOKEN }} diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 0000000000..e1b106fc65 --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,18 @@ +--- +jobs: + stale: + runs-on: ubuntu-22.04 + steps: + - uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e + with: + days-before-close: 5 + days-before-stale: 30 + stale-issue-message: "This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days." + stale-pr-message: "This PR is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days." +name: "Close stale issues and PRs" +on: + schedule: + - cron: "30 1 * * *" +permissions: + issues: read + pull-requests: write diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..d18db69378 --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +polaris-service/logs/ +regtests/derby.log +regtests/metastore_db +regtests/output/ +notebooks/.ipynb_checkpoints/ +.gradle +**/build/ +!src/**/build/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +# Avoid ignore Gradle wrappper properties +!gradle-wrapper.properties + +# Cache of project +.gradletasknamecache + +# Eclipse Gradle plugin generated files +# Eclipse Core +.project +# JDT-specific (Eclipse Java Development Tools) +.classpath +.env +.java-version +**/*.iml diff --git a/.openapi-generator-ignore b/.openapi-generator-ignore new file mode 100644 index 0000000000..e866efcbdb --- /dev/null +++ b/.openapi-generator-ignore @@ -0,0 +1,7 @@ +src/main/webapp/** +build.gradle +pom.xml +README.md +settings.gradle +.openapi-generator-ignore +src/main/java/org/** \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000..2b595656b2 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,22 @@ +# Base Image +FROM gradle:8.6-jdk21 as build + +# Copy the REST catalog into the container +COPY . /app + +# Set the working directory in the container, nuke any existing builds +WORKDIR /app +RUN rm -rf build + +# Build the rest catalog +RUN gradle --no-daemon --info shadowJar + +FROM openjdk:21 +WORKDIR /app +COPY --from=build /app/polaris-service/build/libs/polaris-service-1.0.0-all.jar /app +COPY --from=build /app/polaris-server.yml /app + +EXPOSE 8181 + +# Run the resulting java binary +CMD ["java", "-jar", "/app/polaris-service-1.0.0-all.jar", "server", "polaris-server.yml"] diff --git a/README.md b/README.md index 4a11919b88..bd2f49977c 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,158 @@ ## Status -Polaris Catalog will be open sourced under an Apache 2.0 license in the next 90 days. In the meantime: +Polaris Catalog is open source under an Apache 2.0 license. -- 👀 Watch this repo if you would like to be notified when the Polaris code goes live. - ⭐ Star this repo if you’d like to bookmark and come back to it! - 📖 Read the announcement blog post for more details! + +## API Docs + +API docs are hosted via Github Pages at https://polaris-catalog.github.io/polaris. All updates to the main branch +update the hosted docs. + +The Polaris management API docs are found [here](docs%2Fpolaris-management%2Findex.html) + +The open source Iceberg REST API docs are at [index.html](docs%2Ficeberg-rest%2Findex.html) + +Docs are generated using Redocly. They can be regenerated by running the following commands +from the project root directory + +```bash +docker run -p 8080:80 -v ${PWD}:/spec redocly/cli build-docs spec/polaris-management-service.yml --output=docs/polaris-management/index.html +docker run -p 8080:80 -v ${PWD}:/spec redocly/cli build-docs spec/rest-catalog-open-api.yaml --output=docs/iceberg-rest/index.html +``` + +# Setup + +## Requirements / Setup + +- Java JDK >= 21 . If on a Mac you can use [jenv](https://www.jenv.be/) to set the appropriate SDK. +- Gradle 8.6 - This is included in the project and can be run using `./gradlew` in the project root. +- Docker - If you want to run the project in a containerized environment. + +Command-Line getting started +------------------- +Polaris is a multi-module project with three modules: + +- `polaris-core` - The main Polaris entity definitions and core business logic +- `polaris-server` - The Polaris REST API server +- `polaris-eclipselink` - The Eclipselink implementation of the MetaStoreManager interface + +Build the binary (first time may require installing new JDK version). This build will run IntegrationTests by default. + +``` +./gradlew build +``` + +Run the Polaris server locally on localhost:8181 + +``` +./gradlew runApp +``` + +While the Polaris server is running, run regression tests, or end-to-end tests in another terminal + +``` +./regtests/run.sh +``` + +Docker Instructions +------------------- + +Build the image: + +``` +docker build -t localhost:5001/polaris:latest . +``` + +Run it in a standalone mode. This runs a single container that binds the container's port `8181` to localhosts `8181`: + +``` +docker run -p 8181:8181 localhost:5001/polaris:latest +``` + +# Running the tests + +## Unit and Integration tests + +Unit and integration tests are run using gradle. To run all tests, use the following command: + +```bash +./gradlew test +``` + +## Regression tests + +Regression tests, or functional tests, are stored in the `regtests` directory. They can be executed in a docker +environment by using the `docker-compose.yml` file in the project root. + +```bash +docker compose up --build --exit-code-from regtest +``` + +They can also be executed outside of docker by following the setup instructions in +the [README](regtests/README.md) + +# Kubernetes Instructions +----------------------- + +You can run Polaris as a mini-deployment locally. This will create two pods that bind themselves to port `8181`: + +``` +./setup.sh +``` + +You can check the pod and deployment status like so: + +``` +kubectl get pods +kubectl get deployment +``` + +If things aren't working as expected you can troubleshoot like so: + +``` +kubectl describe deployment polaris-deployment +``` + +## Creating a Catalog manually + +Before connecting with Spark, you'll need to create a catalog. To create a catalog, generate a token for the root +principal: + +```bash +curl -i -X POST \ + http://localhost:8181/api/catalog/v1/oauth/tokens \ + -d 'grant_type=client_credentials&client_id==&client_secret==&scope=PRINCIPAL_ROLE:ALL' +``` + +The response output will contain an access token: + +```json +{ + "access_token": "ver:1-hint:1036-ETMsDgAAAY/GPANareallyverylongstringthatissecret", + "token_type": "bearer", + "expires_in": 3600 +} +``` + +Set the contents of the `access_token` field as the `PRINCIPAL_TOKEN` variable. Then use curl to invoke the +createCatalog +api: + +```bash +$ export PRINCIPAL_TOKEN=ver:1-hint:1036-ETMsDgAAAY/GPANareallyverylongstringthatissecret + +$ curl -i -X PUT -H "Authorization: Bearer $PRINCIPAL_TOKEN" -H 'Accept: application/json' -H 'Content-Type: application/json' \ + http://${POLARIS_HOST:-localhost}:8181/api/v1/catalogs \ + -d '{"name": "snowflake", "id": 100, "type": "INTERNAL", "readOnly": false}' +``` + +This creates a catalog called `snowflake`. From here, you can use Spark to create namespaces, tables, etc. + +You must run the following as the first query in your spark-sql shell to actually use Polaris: + +``` +use polaris; +``` diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000000..b92c2cbc99 --- /dev/null +++ b/build.gradle @@ -0,0 +1,96 @@ +buildscript { + repositories { + maven { + url 'https://plugins.gradle.org/m2/' + } + } + dependencies { + classpath 'com.diffplug.spotless:spotless-plugin-gradle:6.25.0' + } +} + +plugins { + id 'idea' +} + +allprojects { + repositories { + mavenLocal() + mavenCentral() + } + idea { + module { + downloadJavadoc = true + downloadSources = true + } + } +} + +subprojects { + apply plugin: 'jacoco' + apply plugin: 'java' + apply plugin: 'com.diffplug.spotless' + apply plugin: 'jacoco-report-aggregation' + apply plugin: 'groovy' + ext { + jacksonVersion = '2.17.1' + icebergVersion = '1.5.0' + hadoopVersion = '3.3.6' + dropwizardVersion = '4.0.7' + assertJVersion = '3.25.3' + } + + tasks.withType(JavaCompile) { + options.compilerArgs << '-Xlint:unchecked' + options.compilerArgs << '-Xlint:deprecation' + } + + project(':polaris-service') { + apply plugin: 'application' + } + + project(':polaris-core') { + apply plugin: 'java-library' + } + + dependencies { + implementation "com.fasterxml.jackson.core:jackson-annotations:${jacksonVersion}" + implementation 'com.google.guava:guava:33.0.0-jre' + implementation "org.jetbrains:annotations:24.0.0" + implementation 'org.slf4j:slf4j-api:2.0.12' + compileOnly "com.github.spotbugs:spotbugs-annotations:4.8.5" + + testImplementation 'org.junit.jupiter:junit-jupiter:5.7.1' + testImplementation 'org.assertj:assertj-core:3.25.3' + testImplementation "org.mockito:mockito-core:5.11.0" + + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + } + + task format { + dependsOn 'spotlessApply' + } + + test { + useJUnitPlatform() + } + + spotless { + def disallowWildcardImports = { + String text = it + def regex = ~/import .*\.\*;/ + def m = regex.matcher(text) + if (m.find()) { + throw new AssertionError("Wildcard imports disallowed - ${m.findAll()}") + } + } + java { + target 'src/main/java/**/*.java', 'src/testFixtures/java/**/*.java', 'src/test/java/**/*.java' + googleJavaFormat() + indentWithSpaces(2) + removeUnusedImports() + endWithNewline() + custom 'disallowWildcardImports', disallowWildcardImports + } + } +} diff --git a/docker-compose-jupyter.yml b/docker-compose-jupyter.yml new file mode 100644 index 0000000000..24da194183 --- /dev/null +++ b/docker-compose-jupyter.yml @@ -0,0 +1,41 @@ +services: + polaris: + build: + context: ../iceberg-rest-server + network: host + ports: + - "8181:8181" + - "8182" + environment: + AWS_REGION: us-west-2 + AWS_ACCESS_KEY_ID: $AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY: $AWS_SECRET_ACCESS_KEY + + healthcheck: + test: ["CMD", "curl", "http://localhost:8182/healthcheck"] + interval: 10s + timeout: 10s + retries: 5 + jupyter: + build: + context: .. + dockerfile: ../notebooks/Dockerfile + network: host + ports: + - "8888:8888" + depends_on: + polaris: + condition: service_healthy + environment: + AWS_REGION: us-west-2 + POLARIS_HOST: polaris + volumes: + - notebooks:/home/jovyan/notebooks + +volumes: + notebooks: + driver: local + driver_opts: + o: bind + type: none + device: ./notebooks diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000000..c15c6ddbbe --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,67 @@ +services: + polaris: + build: + context: . + network: host + ports: + - "8181:8181" + - "8182" + environment: + AWS_REGION: us-west-2 + AWS_ACCESS_KEY_ID: $AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY: $AWS_SECRET_ACCESS_KEY + GOOGLE_APPLICATION_CREDENTIALS: $GOOGLE_APPLICATION_CREDENTIALS + AZURE_TENANT_ID: $AZURE_TENANT_ID + AZURE_CLIENT_ID: $AZURE_CLIENT_ID + AZURE_CLIENT_SECRET: $AZURE_CLIENT_SECRET + volumes: + - credentials:/tmp/credentials/ + + healthcheck: + test: ["CMD", "curl", "http://localhost:8182/healthcheck"] + interval: 10s + timeout: 10s + retries: 5 + regtest: + build: + context: regtests + network: host + args: + POLARIS_HOST: polaris + depends_on: + polaris: + condition: service_healthy + environment: + AWS_TEST_ENABLED: $AWS_TEST_ENABLED + AWS_STORAGE_BUCKET: $AWS_STORAGE_BUCKET + AWS_ROLE_ARN: $AWS_ROLE_ARN + AWS_TEST_BASE: $AWS_TEST_BASE + GCS_TEST_ENABLED: $GCS_TEST_ENABLED + GCS_TEST_BASE: $GCS_TEST_BASE + GOOGLE_APPLICATION_CREDENTIALS: $GOOGLE_APPLICATION_CREDENTIALS + AZURE_TEST_ENABLED: $AZURE_TEST_ENABLED + AZURE_TENANT_ID: $AZURE_TENANT_ID + AZURE_DFS_TEST_BASE: $AZURE_DFS_TEST_BASE + AZURE_BLOB_TEST_BASE: $AZURE_BLOB_TEST_BASE + AZURE_CLIENT_ID: $AZURE_CLIENT_ID + AZURE_CLIENT_SECRET: $AZURE_CLIENT_SECRET + AWS_CROSS_REGION_TEST_ENABLED: $AWS_CROSS_REGION_TEST_ENABLED + AWS_CROSS_REGION_BUCKET: $AWS_CROSS_REGION_BUCKET + AWS_ROLE_FOR_CROSS_REGION_BUCKET: $AWS_ROLE_FOR_CROSS_REGION_BUCKET + volumes: + - local_output:/tmp/polaris-regtests/ + - credentials:/tmp/credentials/ + +volumes: + local_output: + driver: local + driver_opts: + o: bind + type: none + device: ./regtests/output + credentials: + driver: local + driver_opts: + o: bind + type: none + device: ./regtests/credentials diff --git a/docs/entities.md b/docs/entities.md new file mode 100644 index 0000000000..8d4d4968e0 --- /dev/null +++ b/docs/entities.md @@ -0,0 +1,67 @@ +# Polaris Entities + +This page documents various entities that can be managed in Polaris. + +## Catalog + +A catalog is a top-level entity in Polaris that may contain other entities like [namespaces](#namespace) and [tables](#table). These map directly to [Apache Iceberg catalogs](https://iceberg.apache.org/concepts/catalog/). + +For information on managing catalogs with the REST API or for more information on what data can be associated with a catalog, see [the API docs](../regtests/client/python/docs/CreateCatalogRequest.md). + +### Storage Type + +All catalogs in Polaris are associated with a _storage type_. Valid Storage Types are `S3`, `Azure`, and `GCS`. The `FILE` type is also additionally available for testing. Each of these types relates to a different storage provider where data within the catalog may reside. Depending on the storage type, various other configurations may be set for a catalog including credentials to be used when accessing data inside the catalog. + +For details on how to use Storage Types in the REST API, see [the API docs](../regtests/client/python/docs/StorageConfigInfo.md). + +## Namespace + +A namespace is a logical entity that resides within a [catalog](#catalog) and can contain other entities such as [tables](#table) or [views](#view). Some other systems may refer to namespaces as _schemas_ or _databases_. + +In Polaris, namespaces can be nested up to 16 levels. For example, `a.b.c.d.e.f.g` is a valid namespace. `b` is said to reside within `a`, and so on. + +For information on managing namespaces with the REST API or for more information on what data can be associated with a namespace, see [the API docs](../regtests/client/python/docs/CreateNamespaceRequest.md). + + +## Table + +Polaris tables are entites that map to [Apache Iceberg tables](https://iceberg.apache.org/docs/nightly/configuration/). + +For information on managing tables with the REST API or for more information on what data can be associated with a table, see [the API docs](../regtests/client/python/docs/CreateTableRequest.md). + +## View + +Polaris views are entites that map to [Apache Iceberg views](https://iceberg.apache.org/view-spec/). + +For information on managing views with the REST API or for more information on what data can be associated with a view, see [the API docs](../regtests/client/python/docs/CreateViewRequest.md). + +## Principal + +Polaris principals are unique identities that can be used to represent users or services. Each principal may have one or more [principal roles](#principal-role) assigned to it for the purpose of accessing catalogs and the entities within them. + +For information on managing principals with the REST API or for more information on what data can be associated with a principal, see [the API docs](../regtests/client/python/docs/CreatePrincipalRequest.md). + +## Principal Role + +Polaris principal roles are labels that may be granted to [principals](#principal). Each principal may have one or more principal roles, and the same principal role may be granted to multiple principals. Principal roles may be assigned based on the persona or responsibilities of a given principal, or on how that principal will need to access different entities within Polaris. + +For information on managing principal roles with the REST API or for more information on what data can be associated with a principal role, see [the API docs](../regtests/client/python/docs/CreatePrincipalRoleRequest.md). + + +## Catalog Role + +Polaris catalog roles are labels that may be granted to [catalogs](#catalog). Each catalog may have one or more catalog roles, and the same catalog role may be granted to multiple catalogs. Catalog roles may be assigned based on the nature of data that will reside in a catalog, or by the groups of users and services that might need to access that data. + +Each catalog role may have multiple [privileges](#privilege) granted to it, and each catalog role can be granted to one or more [principal roles](#principal-role). This is the mechanism by which principals are granted access to entities inside a catalog such as namespaces and tables. + +## Privilege + +Polaris privileges are granted to [catalog roles](#catalog-role) in order to grant principals with a given principal role some degree of access to catalogs with a given catalog role. When a privilege is granted to a catalog role, any principal roles granted that catalog role receive the privilege. In turn, any principals who are granted that principal role receive it. + +A privilege can be scoped to any entity inside a catalog, including the catalog itself. + +For a list of supported privileges for each privilege class, see the API docs: +* [Table Privileges](../regtests/client/python/docs/TablePrivilege.md) +* [View Privileges](../regtests/client/python/docs/ViewPrivilege.md) +* [Namespace Privileges](../regtests/client/python/docs/NamespacePrivilege.md) +* [Catalog Privileges](../regtests/client/python/docs/CatalogPrivilege.md) diff --git a/docs/iceberg-rest/index.html b/docs/iceberg-rest/index.html new file mode 100644 index 0000000000..1ae8c54d5c --- /dev/null +++ b/docs/iceberg-rest/index.html @@ -0,0 +1,1307 @@ + + + + + + Apache Iceberg REST Catalog API + + + + + + + + + +

Apache Iceberg REST Catalog API (0.0.1)

Download OpenAPI specification:Download

License: Apache 2.0

Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2.

+

Configuration API

List all catalog configuration settings

All REST clients should first call this route to get catalog configuration properties from the server to configure the catalog and its HTTP client. Configuration from the server consists of two sets of key/value pairs.

+
    +
  • defaults - properties that should be used as default configuration; applied before client configuration
  • +
  • overrides - properties that should be used to override client configuration; applied after defaults and client configuration
  • +
+

Catalog configuration is constructed by setting the defaults, then client- provided configuration, and finally overrides. The final property set is then used to configure the catalog.

+

For example, a default configuration property might set the size of the client pool, which can be replaced with a client-specific setting. An override might be used to set the warehouse location, which is stored on the server rather than in client configuration.

+

Common catalog configuration settings are documented at https://iceberg.apache.org/docs/latest/configuration/#catalog-properties

+
Authorizations:
OAuth2BearerAuth
query Parameters
warehouse
string

Warehouse location or identifier to request from the service

+

Responses

Response samples

Content type
application/json
{
  • "overrides": {
    },
  • "defaults": {
    }
}

OAuth2 API

Get a token using an OAuth2 flow

Exchange credentials for a token using the OAuth2 client credentials flow or token exchange.

+

This endpoint is used for three purposes -

+
    +
  1. To exchange client credentials (client ID and secret) for an access token This uses the client credentials flow.
  2. +
  3. To exchange a client token and an identity token for a more specific access token This uses the token exchange flow.
  4. +
  5. To exchange an access token for one with the same claims and a refreshed expiration period This uses the token exchange flow.
  6. +
+

For example, a catalog client may be configured with client credentials from the OAuth2 Authorization flow. This client would exchange its client ID and secret for an access token using the client credentials request with this endpoint (1). Subsequent requests would then use that access token.

+

Some clients may also handle sessions that have additional user context. These clients would use the token exchange flow to exchange a user token (the "subject" token) from the session for a more specific access token for that user, using the catalog's access token as the "actor" token (2). The user ID token is the "subject" token and can be any token type allowed by the OAuth2 token exchange flow, including a unsecured JWT token with a sub claim. This request should use the catalog's bearer token in the "Authorization" header.

+

Clients may also use the token exchange flow to refresh a token that is about to expire by sending a token exchange request (3). The request's "subject" token should be the expiring token. This request should use the subject token in the "Authorization" header.

+
Authorizations:
BearerAuth
Request Body schema: application/x-www-form-urlencoded
required
Any of
grant_type
required
string
Value: "client_credentials"
scope
string
client_id
required
string

Client ID

+

This can be sent in the request body, but OAuth2 recommends sending it in a Basic Authorization header.

+
client_secret
required
string

Client secret

+

This can be sent in the request body, but OAuth2 recommends sending it in a Basic Authorization header.

+

Responses

Response samples

Content type
application/json
{
  • "access_token": "string",
  • "token_type": "bearer",
  • "expires_in": 0,
  • "issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
  • "refresh_token": "string",
  • "scope": "string"
}

Catalog API

List namespaces, optionally providing a parent namespace to list underneath

List all namespaces at a certain level, optionally starting from a given parent namespace. If table accounting.tax.paid.info exists, using 'SELECT NAMESPACE IN accounting' would translate into GET /namespaces?parent=accounting and must return a namespace, ["accounting", "tax"] only. Using 'SELECT NAMESPACE IN accounting.tax' would translate into GET /namespaces?parent=accounting%1Ftax and must return a namespace, ["accounting", "tax", "paid"]. If parent is not provided, all top-level namespaces should be listed.

+
Authorizations:
OAuth2BearerAuth
path Parameters
prefix
required
string

An optional prefix in the path

+
query Parameters
pageToken
string or null (PageToken)

An opaque token that allows clients to make use of pagination for list APIs (e.g. ListTables). Clients may initiate the first paginated request by sending an empty query parameter pageToken to the server. +Servers that support pagination should identify the pageToken parameter and return a next-page-token in the response if there are more results available. After the initial request, the value of next-page-token from each response must be used as the pageToken parameter value for the next request. The server must return null value for the next-page-token in the last response. +Servers that support pagination must return all results in a single response with the value of next-page-token set to null if the query parameter pageToken is not set in the request. +Servers that do not support pagination should ignore the pageToken parameter and return all results in a single response. The next-page-token must be omitted from the response. +Clients must interpret either null or missing response value of next-page-token as the end of the listing results.

+
pageSize
integer >= 1

For servers that support pagination, this signals an upper bound of the number of results that a client will receive. For servers that do not support pagination, clients may receive results larger than the indicated pageSize.

+
parent
string
Example: parent=accounting%1Ftax

An optional namespace, underneath which to list namespaces. If not provided or empty, all top-level namespaces should be listed. If parent is a multipart namespace, the parts must be separated by the unit separator (0x1F) byte.

+

Responses

Response samples

Content type
application/json
Example
{
  • "namespaces": [
    ]
}

Create a namespace

Create a namespace, with an optional set of properties. The server might also add properties, such as last_modified_time etc.

+
Authorizations:
OAuth2BearerAuth
path Parameters
prefix
required
string

An optional prefix in the path

+
Request Body schema: application/json
required
namespace
required
Array of strings (Namespace)

Reference to one or more levels of a namespace

+
object
Default: {}

Configured string to string map of properties for the namespace

+

Responses

Request samples

Content type
application/json
{
  • "namespace": [
    ],
  • "properties": {
    }
}

Response samples

Content type
application/json
{
  • "namespace": [
    ],
  • "properties": {
    }
}

Load the metadata properties for a namespace

Return all stored metadata properties for a given namespace

+
Authorizations:
OAuth2BearerAuth
path Parameters
prefix
required
string

An optional prefix in the path

+
namespace
required
string
Examples:
  • accounting -
  • accounting%1Ftax -

A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (0x1F) byte.

+

Responses

Response samples

Content type
application/json
{
  • "namespace": [
    ],
  • "properties": {
    }
}

Check if a namespace exists

Check if a namespace exists. The response does not contain a body.

+
Authorizations:
OAuth2BearerAuth
path Parameters
prefix
required
string

An optional prefix in the path

+
namespace
required
string
Examples:
  • accounting -
  • accounting%1Ftax -

A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (0x1F) byte.

+

Responses

Response samples

Content type
application/json
{
  • "error": {
    }
}

Drop a namespace from the catalog. Namespace must be empty.

Authorizations:
OAuth2BearerAuth
path Parameters
prefix
required
string

An optional prefix in the path

+
namespace
required
string
Examples:
  • accounting -
  • accounting%1Ftax -

A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (0x1F) byte.

+

Responses

Response samples

Content type
application/json
{
  • "error": {
    }
}

Set or remove properties on a namespace

Set and/or remove properties on a namespace. The request body specifies a list of properties to remove and a map of key value pairs to update. +Properties that are not in the request are not modified or removed by this call. +Server implementations are not required to support namespace properties.

+
Authorizations:
OAuth2BearerAuth
path Parameters
prefix
required
string

An optional prefix in the path

+
namespace
required
string
Examples:
  • accounting -
  • accounting%1Ftax -

A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (0x1F) byte.

+
Request Body schema: application/json
required
removals
Array of strings unique
object

Responses

Request samples

Content type
application/json
{
  • "removals": [
    ],
  • "updates": {
    }
}

Response samples

Content type
application/json
{
  • "updated": [
    ],
  • "removed": [
    ],
  • "missing": [
    ]
}

List all table identifiers underneath a given namespace

Return all table identifiers under this namespace

+
Authorizations:
OAuth2BearerAuth
path Parameters
prefix
required
string

An optional prefix in the path

+
namespace
required
string
Examples:
  • accounting -
  • accounting%1Ftax -

A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (0x1F) byte.

+
query Parameters
pageToken
string or null (PageToken)

An opaque token that allows clients to make use of pagination for list APIs (e.g. ListTables). Clients may initiate the first paginated request by sending an empty query parameter pageToken to the server. +Servers that support pagination should identify the pageToken parameter and return a next-page-token in the response if there are more results available. After the initial request, the value of next-page-token from each response must be used as the pageToken parameter value for the next request. The server must return null value for the next-page-token in the last response. +Servers that support pagination must return all results in a single response with the value of next-page-token set to null if the query parameter pageToken is not set in the request. +Servers that do not support pagination should ignore the pageToken parameter and return all results in a single response. The next-page-token must be omitted from the response. +Clients must interpret either null or missing response value of next-page-token as the end of the listing results.

+
pageSize
integer >= 1

For servers that support pagination, this signals an upper bound of the number of results that a client will receive. For servers that do not support pagination, clients may receive results larger than the indicated pageSize.

+

Responses

Response samples

Content type
application/json
Example
{
  • "identifiers": [
    ]
}

Create a table in the given namespace

Create a table or start a create transaction, like atomic CTAS.

+

If stage-create is false, the table is created immediately.

+

If stage-create is true, the table is not created, but table metadata is initialized and returned. The service should prepare as needed for a commit to the table commit endpoint to complete the create transaction. The client uses the returned metadata to begin a transaction. To commit the transaction, the client sends all create and subsequent changes to the table commit route. Changes from the table create operation include changes like AddSchemaUpdate and SetCurrentSchemaUpdate that set the initial table state.

+
Authorizations:
OAuth2BearerAuth
path Parameters
prefix
required
string

An optional prefix in the path

+
namespace
required
string
Examples:
  • accounting -
  • accounting%1Ftax -

A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (0x1F) byte.

+
header Parameters
X-Iceberg-Access-Delegation
string
Enum: "vended-credentials" "remote-signing"
Example: vended-credentials,remote-signing

Optional signal to the server that the client supports delegated access via a comma-separated list of access mechanisms. The server may choose to supply access via any or none of the requested mechanisms.

+

Specific properties and handling for vended-credentials is documented in the LoadTableResult schema section of this spec document.

+

The protocol and specification for remote-signing is documented in the s3-signer-open-api.yaml OpenApi spec in the aws module.

+
Request Body schema: application/json
required
name
required
string
location
string
required
object (Schema)
object (PartitionSpec)
object (SortOrder)
stage-create
boolean
object

Responses

Request samples

Content type
application/json
{
  • "name": "string",
  • "location": "string",
  • "schema": {
    },
  • "partition-spec": {
    },
  • "write-order": {
    },
  • "stage-create": true,
  • "properties": {
    }
}

Response samples

Content type
application/json
{
  • "metadata-location": "string",
  • "metadata": {
    },
  • "config": {
    }
}

Register a table in the given namespace using given metadata file location

Register a table using given metadata file location.

+
Authorizations:
OAuth2BearerAuth
path Parameters
prefix
required
string

An optional prefix in the path

+
namespace
required
string
Examples:
  • accounting -
  • accounting%1Ftax -

A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (0x1F) byte.

+
Request Body schema: application/json
required
name
required
string
metadata-location
required
string

Responses

Request samples

Content type
application/json
{
  • "name": "string",
  • "metadata-location": "string"
}

Response samples

Content type
application/json
{
  • "metadata-location": "string",
  • "metadata": {
    },
  • "config": {
    }
}

Load a table from the catalog

Load a table from the catalog.

+

The response contains both configuration and table metadata. The configuration, if non-empty is used as additional configuration for the table that overrides catalog configuration. For example, this configuration may change the FileIO implementation to be used for the table.

+

The response also contains the table's full metadata, matching the table metadata JSON file.

+

The catalog configuration may contain credentials that should be used for subsequent requests for the table. The configuration key "token" is used to pass an access token to be used as a bearer token for table requests. Otherwise, a token may be passed using a RFC 8693 token type as a configuration key. For example, "urn:ietf:params:oauth:token-type:jwt=".

+
Authorizations:
OAuth2BearerAuth
path Parameters
prefix
required
string

An optional prefix in the path

+
namespace
required
string
Examples:
  • accounting -
  • accounting%1Ftax -

A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (0x1F) byte.

+
table
required
string
Example: sales

A table name

+
query Parameters
snapshots
string
Enum: "all" "refs"

The snapshots to return in the body of the metadata. Setting the value to all would return the full set of snapshots currently valid for the table. Setting the value to refs would load all snapshots referenced by branches or tags. +Default if no param is provided is all.

+
header Parameters
X-Iceberg-Access-Delegation
string
Enum: "vended-credentials" "remote-signing"
Example: vended-credentials,remote-signing

Optional signal to the server that the client supports delegated access via a comma-separated list of access mechanisms. The server may choose to supply access via any or none of the requested mechanisms.

+

Specific properties and handling for vended-credentials is documented in the LoadTableResult schema section of this spec document.

+

The protocol and specification for remote-signing is documented in the s3-signer-open-api.yaml OpenApi spec in the aws module.

+

Responses

Response samples

Content type
application/json
{
  • "metadata-location": "string",
  • "metadata": {
    },
  • "config": {
    }
}

Commit updates to a table

Commit updates to a table.

+

Commits have two parts, requirements and updates. Requirements are assertions that will be validated before attempting to make and commit changes. For example, assert-ref-snapshot-id will check that a named ref's snapshot ID has a certain value.

+

Updates are changes to make to table metadata. For example, after asserting that the current main ref is at the expected snapshot, a commit may add a new child snapshot and set the ref to the new snapshot id.

+

Create table transactions that are started by createTable with stage-create set to true are committed using this route. Transactions should include all changes to the table, including table initialization, like AddSchemaUpdate and SetCurrentSchemaUpdate. The assert-create requirement is used to ensure that the table was not created concurrently.

+
Authorizations:
OAuth2BearerAuth
path Parameters
prefix
required
string

An optional prefix in the path

+
namespace
required
string
Examples:
  • accounting -
  • accounting%1Ftax -

A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (0x1F) byte.

+
table
required
string
Example: sales

A table name

+
Request Body schema: application/json
required
object (TableIdentifier)
required
Array of objects (TableRequirement)
required
Array of AssignUUIDUpdate (object) or UpgradeFormatVersionUpdate (object) or AddSchemaUpdate (object) or SetCurrentSchemaUpdate (object) or AddPartitionSpecUpdate (object) or SetDefaultSpecUpdate (object) or AddSortOrderUpdate (object) or SetDefaultSortOrderUpdate (object) or AddSnapshotUpdate (object) or SetSnapshotRefUpdate (object) or RemoveSnapshotsUpdate (object) or RemoveSnapshotRefUpdate (object) or SetLocationUpdate (object) or SetPropertiesUpdate (object) or RemovePropertiesUpdate (object) or SetStatisticsUpdate (object) or RemoveStatisticsUpdate (object) (TableUpdate)

Responses

Request samples

Content type
application/json
{
  • "identifier": {
    },
  • "requirements": [
    ],
  • "updates": [
    ]
}

Response samples

Content type
application/json
{
  • "metadata-location": "string",
  • "metadata": {
    }
}

Drop a table from the catalog

Remove a table from the catalog

+
Authorizations:
OAuth2BearerAuth
path Parameters
prefix
required
string

An optional prefix in the path

+
namespace
required
string
Examples:
  • accounting -
  • accounting%1Ftax -

A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (0x1F) byte.

+
table
required
string
Example: sales

A table name

+
query Parameters
purgeRequested
boolean
Default: false

Whether the user requested to purge the underlying table's data and metadata

+

Responses

Response samples

Content type
application/json
{
  • "error": {
    }
}

Check if a table exists

Check if a table exists within a given namespace. The response does not contain a body.

+
Authorizations:
OAuth2BearerAuth
path Parameters
prefix
required
string

An optional prefix in the path

+
namespace
required
string
Examples:
  • accounting -
  • accounting%1Ftax -

A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (0x1F) byte.

+
table
required
string
Example: sales

A table name

+

Responses

Response samples

Content type
application/json
{
  • "error": {
    }
}

Rename a table from its current name to a new name

Rename a table from one identifier to another. It's valid to move a table across namespaces, but the server implementation is not required to support it.

+
Authorizations:
OAuth2BearerAuth
path Parameters
prefix
required
string

An optional prefix in the path

+
Request Body schema: application/json
required

Current table identifier to rename and new table identifier to rename to

+
required
object (TableIdentifier)
required
object (TableIdentifier)

Responses

Request samples

Content type
application/json
{
  • "source": {
    },
  • "destination": {
    }
}

Response samples

Content type
application/json
{
  • "error": {
    }
}

Send a metrics report to this endpoint to be processed by the backend

Authorizations:
OAuth2BearerAuth
path Parameters
prefix
required
string

An optional prefix in the path

+
namespace
required
string
Examples:
  • accounting -
  • accounting%1Ftax -

A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (0x1F) byte.

+
table
required
string
Example: sales

A table name

+
Request Body schema: application/json
required

The request containing the metrics report to be sent

+
Any of
table-name
required
string
snapshot-id
required
integer <int64>
required
AndOrExpression (object) or NotExpression (object) or SetExpression (object) or LiteralExpression (object) or UnaryExpression (object) (Expression)
schema-id
required
integer
projected-field-ids
required
Array of integers
projected-field-names
required
Array of strings
required
object (Metrics)
object
report-type
required
string

Responses

Request samples

Content type
application/json
Example
{
  • "table-name": "string",
  • "snapshot-id": 0,
  • "filter": {
    },
  • "schema-id": 0,
  • "projected-field-ids": [
    ],
  • "projected-field-names": [
    ],
  • "metrics": {
    },
  • "metadata": {
    },
  • "report-type": "string"
}

Response samples

Content type
application/json
{
  • "error": {
    }
}

Commit updates to multiple tables in an atomic operation

Authorizations:
OAuth2BearerAuth
path Parameters
prefix
required
string

An optional prefix in the path

+
Request Body schema: application/json
required

Commit updates to multiple tables in an atomic operation

+

A commit for a single table consists of a table identifier with requirements and updates. Requirements are assertions that will be validated before attempting to make and commit changes. For example, assert-ref-snapshot-id will check that a named ref's snapshot ID has a certain value.

+

Updates are changes to make to table metadata. For example, after asserting that the current main ref is at the expected snapshot, a commit may add a new child snapshot and set the ref to the new snapshot id.

+
required
Array of objects (CommitTableRequest)

Responses

Request samples

Content type
application/json
{
  • "table-changes": [
    ]
}

Response samples

Content type
application/json
{
  • "error": {
    }
}

List all view identifiers underneath a given namespace

Return all view identifiers under this namespace

+
Authorizations:
OAuth2BearerAuth
path Parameters
prefix
required
string

An optional prefix in the path

+
namespace
required
string
Examples:
  • accounting -
  • accounting%1Ftax -

A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (0x1F) byte.

+
query Parameters
pageToken
string or null (PageToken)

An opaque token that allows clients to make use of pagination for list APIs (e.g. ListTables). Clients may initiate the first paginated request by sending an empty query parameter pageToken to the server. +Servers that support pagination should identify the pageToken parameter and return a next-page-token in the response if there are more results available. After the initial request, the value of next-page-token from each response must be used as the pageToken parameter value for the next request. The server must return null value for the next-page-token in the last response. +Servers that support pagination must return all results in a single response with the value of next-page-token set to null if the query parameter pageToken is not set in the request. +Servers that do not support pagination should ignore the pageToken parameter and return all results in a single response. The next-page-token must be omitted from the response. +Clients must interpret either null or missing response value of next-page-token as the end of the listing results.

+
pageSize
integer >= 1

For servers that support pagination, this signals an upper bound of the number of results that a client will receive. For servers that do not support pagination, clients may receive results larger than the indicated pageSize.

+

Responses

Response samples

Content type
application/json
Example
{
  • "identifiers": [
    ]
}

Create a view in the given namespace

Create a view in the given namespace.

+
Authorizations:
OAuth2BearerAuth
path Parameters
prefix
required
string

An optional prefix in the path

+
namespace
required
string
Examples:
  • accounting -
  • accounting%1Ftax -

A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (0x1F) byte.

+
Request Body schema: application/json
required
name
required
string
location
string
required
object (Schema)
required
object (ViewVersion)
required
object

Responses

Request samples

Content type
application/json
{
  • "name": "string",
  • "location": "string",
  • "schema": {
    },
  • "view-version": {
    },
  • "properties": {
    }
}

Response samples

Content type
application/json
{
  • "metadata-location": "string",
  • "metadata": {
    },
  • "config": {
    }
}

Load a view from the catalog

Load a view from the catalog.

+

The response contains both configuration and view metadata. The configuration, if non-empty is used as additional configuration for the view that overrides catalog configuration.

+

The response also contains the view's full metadata, matching the view metadata JSON file.

+

The catalog configuration may contain credentials that should be used for subsequent requests for the view. The configuration key "token" is used to pass an access token to be used as a bearer token for view requests. Otherwise, a token may be passed using a RFC 8693 token type as a configuration key. For example, "urn:ietf:params:oauth:token-type:jwt=".

+
Authorizations:
OAuth2BearerAuth
path Parameters
prefix
required
string

An optional prefix in the path

+
namespace
required
string
Examples:
  • accounting -
  • accounting%1Ftax -

A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (0x1F) byte.

+
view
required
string
Example: sales

A view name

+

Responses

Response samples

Content type
application/json
{
  • "metadata-location": "string",
  • "metadata": {
    },
  • "config": {
    }
}

Replace a view

Commit updates to a view.

+
Authorizations:
OAuth2BearerAuth
path Parameters
prefix
required
string

An optional prefix in the path

+
namespace
required
string
Examples:
  • accounting -
  • accounting%1Ftax -

A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (0x1F) byte.

+
view
required
string
Example: sales

A view name

+
Request Body schema: application/json
required
object (TableIdentifier)
Array of objects (ViewRequirement)
required
Array of AssignUUIDUpdate (object) or UpgradeFormatVersionUpdate (object) or AddSchemaUpdate (object) or SetLocationUpdate (object) or SetPropertiesUpdate (object) or RemovePropertiesUpdate (object) or AddViewVersionUpdate (object) or SetCurrentViewVersionUpdate (object) (ViewUpdate)

Responses

Request samples

Content type
application/json
{
  • "identifier": {
    },
  • "requirements": [
    ],
  • "updates": [
    ]
}

Response samples

Content type
application/json
{
  • "metadata-location": "string",
  • "metadata": {
    },
  • "config": {
    }
}

Drop a view from the catalog

Remove a view from the catalog

+
Authorizations:
OAuth2BearerAuth
path Parameters
prefix
required
string

An optional prefix in the path

+
namespace
required
string
Examples:
  • accounting -
  • accounting%1Ftax -

A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (0x1F) byte.

+
view
required
string
Example: sales

A view name

+

Responses

Response samples

Content type
application/json
{
  • "error": {
    }
}

Check if a view exists

Check if a view exists within a given namespace. This request does not return a response body.

+
Authorizations:
OAuth2BearerAuth
path Parameters
prefix
required
string

An optional prefix in the path

+
namespace
required
string
Examples:
  • accounting -
  • accounting%1Ftax -

A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (0x1F) byte.

+
view
required
string
Example: sales

A view name

+

Responses

Response samples

Content type
application/json
{
  • "error": {
    }
}

Rename a view from its current name to a new name

Rename a view from one identifier to another. It's valid to move a view across namespaces, but the server implementation is not required to support it.

+
Authorizations:
OAuth2BearerAuth
path Parameters
prefix
required
string

An optional prefix in the path

+
Request Body schema: application/json
required

Current view identifier to rename and new view identifier to rename to

+
required
object (TableIdentifier)
required
object (TableIdentifier)

Responses

Request samples

Content type
application/json
{
  • "source": {
    },
  • "destination": {
    }
}

Response samples

Content type
application/json
{
  • "error": {
    }
}
+ + + + diff --git a/docs/img/quickstart/privilege-illustration-1.png b/docs/img/quickstart/privilege-illustration-1.png new file mode 100644 index 0000000000000000000000000000000000000000..69328caf59f71365670d33d45fd20f1523ffedac GIT binary patch literal 61323 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 literal 0 HcmV?d00001 diff --git a/docs/img/quickstart/privilege-illustration-2.png b/docs/img/quickstart/privilege-illustration-2.png new file mode 100644 index 0000000000000000000000000000000000000000..a26428ca655333b13b08a4798633cddf1bb3da43 GIT binary patch literal 88965 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 z z_ffHT3-eCiFKJ3wOWPG=k)9_b#Mm(qmYtX5+)`m;>O5h7WM^qpG+@jK$el6 zbWru?04SsmdP)flFcp9L>HbIDeyk-dU>Ac*ZXdLbcZo8RXTErV7c@DdqzRI+kTJ(A zS-9})N;>C47~gN7&h!|J?uOnj4mu(?77oa(t_J8vFAj#hcf`r?xrh|t%zr~q_qz6)6YE}xrlRLn9 ze>jkTN=Z{%;ViwzWglZu7UOb8qf|BG_Runh}VU_7Y;ePW$pdhPU`xa@y{L$6GF03ztke3l7<6L7#S3cGiTcmvR zPWyr3g)i!k%w<|hlvEwdc0@s zX9OQ1#<8e=L!+WinhHbVMYaVaV;30Mvs2d|fiNp;%Ey{E3o=uu*cM^4cU)3oE+<#P zQnVDFb56!2ipwv@mfV#5&D#tD-sV`-#YnD5C9hr{C(JbSl}W!8zEJSBA_{LRg?#Oxk;4PT-%0$Tx6efeeF{t8L^bpD615&AI~+g4}Ge_#o{Y5nyT?< z>F5ons=BD6+KqeMjH>J_m|~7DQ@*>JuYm?BoAa=;o<>tApR&)%+3gfPG*!5PqHl6| zve7a6g`L5n8bt%ir9JG9FD0gyziIoZmXbe2)U#@|NnUa z(Z0Xsf720R-0InzzVqkzPlLv%3wbwHZl$Z^#__UW5Oewjm_j%!(_}6natE)BkhxGT zaU{Jmn0;rZbClB9+T(ZE=t*t5_~wa$HGT=7lrB2Tq^e^erB;cub^2U&xDBmz3=|+& zTLo(TQUnk>#r*(?iL5&JY`j1eC~CI)@q?aE5gtkm=yPY4IsG7;8XfkY0m(PNA8U}S zc3mgaaC$Acg+ z?ebha^;_FBsuZZlgQ*<4^}sJ4F1|v$_v!E6n7iEUdmnA z&+SY0Ai<5Y!WK{Ndw82Ozx0FxP0~OUSvgD&D6UTa3&6y;;f%;ngTb%eV1zj2*j%Tt z&>f)qI_&hxVDL6=e{iEW8=b`K%gCKWdlrl!AT}k(!Dh0N~qk?u;ZCP5)!623Z5P(q01CdikDL za*Jr>!deCBu7(`Il?vsPm z4G@Gsj?Ev(24LWiFaO7v2W;^FC0||}G`qO+@{|7lm)tmw+n3ZtYuv9C)M$trVD1b@6lgVKH9SL^KZ&{!a};ntL!cVyJKyWnn%9!Bv@>x2 zJ4LEzE`=`NbgD|+_oB3B>0(dR`4{}=kzeZh{KT>G^^=nW59bJ1nj0L5@zSp&zyWz*K zq8b9zGYfPSFxO%?d=>&j+6vh3d;R7vTHwMAfob*yj+2eh>aL`>!)y{E0hF9(Za#gQu&X%Q~loCIFk# BEgJv; literal 0 HcmV?d00001 diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 0000000000..b9ed858545 --- /dev/null +++ b/docs/index.html @@ -0,0 +1,381 @@ + + + + + Polaris Management Service + + + + + + + + + + \ No newline at end of file diff --git a/docs/polaris-management/index.html b/docs/polaris-management/index.html new file mode 100644 index 0000000000..ebdbd1fa64 --- /dev/null +++ b/docs/polaris-management/index.html @@ -0,0 +1,861 @@ + + + + + + Polaris Management Service + + + + + + + + + +

Polaris Management Service (0.0.1)

Download OpenAPI specification:Download

Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals

+

listCatalogs

List all catalogs in this polaris service

+
Authorizations:
OAuth2

Responses

Response samples

Content type
application/json
{
  • "catalogs": [
    ]
}

createCatalog

Add a new Catalog

+
Authorizations:
OAuth2
Request Body schema: application/json
required

The Catalog to create

+
type
required
string
Default: "INTERNAL"

the type of catalog - internal or external

+
name
required
string

The name of the catalog

+
readOnly
boolean
Default: false

True if writes should be disabled from query engines

+
object
createTimestamp
integer <int64>

The creation time represented as unix epoch timestamp in milliseconds

+
lastUpdateTimestamp
integer <int64>

The last update time represented as unix epoch timestamp in milliseconds

+
entityVersion
integer

The version of the catalog object used to determine if the catalog metadata has changed

+
object (StorageConfigInfo)

A storage configuration used by catalogs

+

Responses

Request samples

Content type
application/json
Example
{
  • "type": "INTERNAL",
  • "name": "string",
  • "readOnly": false,
  • "properties": {
    },
  • "createTimestamp": 0,
  • "lastUpdateTimestamp": 0,
  • "entityVersion": 0,
  • "storageConfigInfo": {
    }
}

getCatalog

Get the details of a catalog

+
Authorizations:
OAuth2
path Parameters
catalogName
required
string

The name of the catalog

+

Responses

Response samples

Content type
application/json
Example
{
  • "type": "INTERNAL",
  • "name": "string",
  • "readOnly": false,
  • "properties": {
    },
  • "createTimestamp": 0,
  • "lastUpdateTimestamp": 0,
  • "entityVersion": 0,
  • "storageConfigInfo": {
    }
}

updateCatalog

Update an existing catalog

+
Authorizations:
OAuth2
path Parameters
catalogName
required
string

The name of the catalog

+
Request Body schema: application/json
required

The catalog details to use in the update

+
currentEntityVersion
integer

The version of the object onto which this update is applied; if the object changed, the update will fail and the caller should retry after fetching the latest version.

+
object
object (StorageConfigInfo)

A storage configuration used by catalogs

+

Responses

Request samples

Content type
application/json
{
  • "currentEntityVersion": 0,
  • "properties": {
    },
  • "storageConfigInfo": {
    }
}

Response samples

Content type
application/json
Example
{
  • "type": "INTERNAL",
  • "name": "string",
  • "readOnly": false,
  • "properties": {
    },
  • "createTimestamp": 0,
  • "lastUpdateTimestamp": 0,
  • "entityVersion": 0,
  • "storageConfigInfo": {
    }
}

deleteCatalog

Delete an existing catalog. This is a cascading operation that deletes all metadata, including principals, roles and grants. If the catalog is an internal catalog, all tables and namespaces are dropped without purge.

+
Authorizations:
OAuth2
path Parameters
catalogName
required
string

The name of the catalog

+

Responses

listPrincipals

List the principals for the current catalog

+
Authorizations:
OAuth2

Responses

Response samples

Content type
application/json
{
  • "principals": [
    ]
}

createPrincipal

Create a principal

+
Authorizations:
OAuth2
Request Body schema: application/json
required

The principal to create

+
type
required
string
Value: "SERVICE"
name
required
string
clientId
string

The output-only OAuth clientId associated with this principal if applicable

+
object
createTimestamp
integer <int64>
lastUpdateTimestamp
integer <int64>
entityVersion
integer

The version of the principal object used to determine if the principal metadata has changed

+

Responses

Request samples

Content type
application/json
{
  • "type": "SERVICE",
  • "name": "string",
  • "clientId": "string",
  • "properties": {
    },
  • "createTimestamp": 0,
  • "lastUpdateTimestamp": 0,
  • "entityVersion": 0
}

Response samples

Content type
application/json
{
  • "principal": {
    },
  • "credentials": {
    }
}

getPrincipal

Get the principal details

+
Authorizations:
OAuth2
path Parameters
principalName
required
string

The principal name

+

Responses

Response samples

Content type
application/json
{
  • "type": "SERVICE",
  • "name": "string",
  • "clientId": "string",
  • "properties": {
    },
  • "createTimestamp": 0,
  • "lastUpdateTimestamp": 0,
  • "entityVersion": 0
}

updatePrincipal

Update an existing principal

+
Authorizations:
OAuth2
path Parameters
principalName
required
string

The principal name

+
Request Body schema: application/json
required

The principal details to use in the update

+
currentEntityVersion
integer

The version of the object onto which this update is applied; if the object changed, the update will fail and the caller should retry after fetching the latest version.

+
object

Responses

Request samples

Content type
application/json
{
  • "currentEntityVersion": 0,
  • "properties": {
    }
}

Response samples

Content type
application/json
{
  • "type": "SERVICE",
  • "name": "string",
  • "clientId": "string",
  • "properties": {
    },
  • "createTimestamp": 0,
  • "lastUpdateTimestamp": 0,
  • "entityVersion": 0
}

deletePrincipal

Remove a principal from polaris

+
Authorizations:
OAuth2
path Parameters
principalName
required
string

The principal name

+

Responses

rotateCredentials

Rotate a principal's credentials. The new credentials will be returned in the response. This is the only API, aside from createPrincipal, that returns the user's credentials. This API is not idempotent.

+
Authorizations:
OAuth2
path Parameters
principalName
required
string

The user name

+

Responses

Response samples

Content type
application/json
{
  • "principal": {
    },
  • "credentials": {
    }
}

listPrincipalRolesAssigned

List the roles assigned to the principal

+
Authorizations:
OAuth2
path Parameters
principalName
required
string

The name of the target principal

+

Responses

Response samples

Content type
application/json
{
  • "roles": [
    ]
}

assignPrincipalRole

Add a role to the principal

+
Authorizations:
OAuth2
path Parameters
principalName
required
string

The name of the target principal

+
Request Body schema: application/json
required

The principal role to assign

+
name
required
string

The name of the role

+
object
createTimestamp
integer <int64>
lastUpdateTimestamp
integer <int64>
entityVersion
integer

The version of the principal role object used to determine if the principal role metadata has changed

+

Responses

Request samples

Content type
application/json
{
  • "name": "string",
  • "properties": {
    },
  • "createTimestamp": 0,
  • "lastUpdateTimestamp": 0,
  • "entityVersion": 0
}

revokePrincipalRole

Remove a role from a catalog principal

+
Authorizations:
OAuth2
path Parameters
principalName
required
string

The name of the target principal

+
principalRoleName
required
string

The name of the role

+

Responses

listPrincipalRoles

List the principal roles

+
Authorizations:
OAuth2

Responses

Response samples

Content type
application/json
{
  • "roles": [
    ]
}

createPrincipalRole

Create a principal role

+
Authorizations:
OAuth2
Request Body schema: application/json
required

The principal to create

+
name
required
string

The name of the role

+
object
createTimestamp
integer <int64>
lastUpdateTimestamp
integer <int64>
entityVersion
integer

The version of the principal role object used to determine if the principal role metadata has changed

+

Responses

Request samples

Content type
application/json
{
  • "name": "string",
  • "properties": {
    },
  • "createTimestamp": 0,
  • "lastUpdateTimestamp": 0,
  • "entityVersion": 0
}

getPrincipalRole

Get the principal role details

+
Authorizations:
OAuth2
path Parameters
principalRoleName
required
string

The principal role name

+

Responses

Response samples

Content type
application/json
{
  • "name": "string",
  • "properties": {
    },
  • "createTimestamp": 0,
  • "lastUpdateTimestamp": 0,
  • "entityVersion": 0
}

updatePrincipalRole

Update an existing principalRole

+
Authorizations:
OAuth2
path Parameters
principalRoleName
required
string

The principal role name

+
Request Body schema: application/json
required

The principalRole details to use in the update

+
currentEntityVersion
integer

The version of the object onto which this update is applied; if the object changed, the update will fail and the caller should retry after fetching the latest version.

+
object

Responses

Request samples

Content type
application/json
{
  • "currentEntityVersion": 0,
  • "properties": {
    }
}

Response samples

Content type
application/json
{
  • "name": "string",
  • "properties": {
    },
  • "createTimestamp": 0,
  • "lastUpdateTimestamp": 0,
  • "entityVersion": 0
}

deletePrincipalRole

Remove a principal role from polaris

+
Authorizations:
OAuth2
path Parameters
principalRoleName
required
string

The principal role name

+

Responses

listAssigneePrincipalsForPrincipalRole

List the Principals to whom the target principal role has been assigned

+
Authorizations:
OAuth2
path Parameters
principalRoleName
required
string

The principal role name

+

Responses

Response samples

Content type
application/json
{
  • "principals": [
    ]
}

listCatalogRolesForPrincipalRole

Get the catalog roles mapped to the principal role

+
Authorizations:
OAuth2
path Parameters
principalRoleName
required
string

The principal role name

+
catalogName
required
string

The name of the catalog where the catalogRoles reside

+

Responses

Response samples

Content type
application/json
{
  • "roles": [
    ]
}

assignCatalogRoleToPrincipalRole

Assign a catalog role to a principal role

+
Authorizations:
OAuth2
path Parameters
principalRoleName
required
string

The principal role name

+
catalogName
required
string

The name of the catalog where the catalogRoles reside

+
Request Body schema: application/json
required

The principal to create

+
name
required
string

The name of the role

+
object
createTimestamp
integer <int64>
lastUpdateTimestamp
integer <int64>
entityVersion
integer

The version of the catalog role object used to determine if the catalog role metadata has changed

+

Responses

Request samples

Content type
application/json
{
  • "name": "string",
  • "properties": {
    },
  • "createTimestamp": 0,
  • "lastUpdateTimestamp": 0,
  • "entityVersion": 0
}

revokeCatalogRoleFromPrincipalRole

Remove a catalog role from a principal role

+
Authorizations:
OAuth2
path Parameters
principalRoleName
required
string

The principal role name

+
catalogName
required
string

The name of the catalog that contains the role to revoke

+
catalogRoleName
required
string

The name of the catalog role that should be revoked

+

Responses

listCatalogRoles

List existing roles in the catalog

+
Authorizations:
OAuth2
path Parameters
catalogName
required
string

The catalog for which we are reading/updating roles

+

Responses

Response samples

Content type
application/json
{
  • "roles": [
    ]
}

createCatalogRole

Create a new role in the catalog

+
Authorizations:
OAuth2
path Parameters
catalogName
required
string

The catalog for which we are reading/updating roles

+
Request Body schema: application/json
name
required
string

The name of the role

+
object
createTimestamp
integer <int64>
lastUpdateTimestamp
integer <int64>
entityVersion
integer

The version of the catalog role object used to determine if the catalog role metadata has changed

+

Responses

Request samples

Content type
application/json
{
  • "name": "string",
  • "properties": {
    },
  • "createTimestamp": 0,
  • "lastUpdateTimestamp": 0,
  • "entityVersion": 0
}

getCatalogRole

Get the details of an existing role

+
Authorizations:
OAuth2
path Parameters
catalogName
required
string

The catalog for which we are retrieving roles

+
catalogRoleName
required
string

The name of the role

+

Responses

Response samples

Content type
application/json
{
  • "name": "string",
  • "properties": {
    },
  • "createTimestamp": 0,
  • "lastUpdateTimestamp": 0,
  • "entityVersion": 0
}

updateCatalogRole

Update an existing role in the catalog

+
Authorizations:
OAuth2
path Parameters
catalogName
required
string

The catalog for which we are retrieving roles

+
catalogRoleName
required
string

The name of the role

+
Request Body schema: application/json
currentEntityVersion
integer

The version of the object onto which this update is applied; if the object changed, the update will fail and the caller should retry after fetching the latest version.

+
object

Responses

Request samples

Content type
application/json
{
  • "currentEntityVersion": 0,
  • "properties": {
    }
}

Response samples

Content type
application/json
{
  • "name": "string",
  • "properties": {
    },
  • "createTimestamp": 0,
  • "lastUpdateTimestamp": 0,
  • "entityVersion": 0
}

deleteCatalogRole

Delete an existing role from the catalog. All associated grants will also be deleted

+
Authorizations:
OAuth2
path Parameters
catalogName
required
string

The catalog for which we are retrieving roles

+
catalogRoleName
required
string

The name of the role

+

Responses

listAssigneePrincipalRolesForCatalogRole

List the PrincipalRoles to whome the tagetcatalog role has been assigned

+
Authorizations:
OAuth2
path Parameters
catalogName
required
string

The name of the catalog where the catalog role resides

+
catalogRoleName
required
string

The name of the catalog role

+

Responses

Response samples

Content type
application/json
{
  • "roles": [
    ]
}

listGrantsForCatalogRole

List the grants the catalog role holds

+
Authorizations:
OAuth2
path Parameters
catalogName
required
string

The name of the catalog where the role will receive the grant

+
catalogRoleName
required
string

The name of the role receiving the grant (must exist)

+

Responses

Response samples

Content type
application/json
{
  • "grants": [
    ]
}

addGrantToCatalogRole

Add a new grant to the catalog role

+
Authorizations:
OAuth2
path Parameters
catalogName
required
string

The name of the catalog where the role will receive the grant

+
catalogRoleName
required
string

The name of the role receiving the grant (must exist)

+
Request Body schema: application/json
type
required
string
privilege
required
string (CatalogPrivilege)
Enum: "MANAGE_CATALOG" "NAMESPACE_CREATE" "TABLE_CREATE" "VIEW_CREATE" "NAMESPACE_DROP" "TABLE_DROP" "VIEW_DROP" "NAMESPACE_LIST" "TABLE_LIST" "VIEW_LIST" "NAMESPACE_READ_PROPERTIES" "TABLE_READ_PROPERTIES" "VIEW_READ_PROPERTIES" "NAMESPACE_WRITE_PROPERTIES" "TABLE_WRITE_PROPERTIES" "VIEW_WRITE_PROPERTIES" "TABLE_READ_DATA" "TABLE_WRITE_DATA" "NAMESPACE_FULL" "TABLE_FULL" "VIEW_FULL"

Responses

Request samples

Content type
application/json
Example
{
  • "type": "catalog",
  • "privilege": "MANAGE_CATALOG"
}

revokeGrantFromCatalogRole

Delete a specific grant from the role. This may be a subset or a superset of the grants the role has. In case of a subset, the role will retain the grants not specified. If the cascade parameter is true, grant revocation will have a cascading effect - that is, if a principal has specific grants on a subresource, and grants are revoked on a parent resource, the grants present on the subresource will be revoked as well. By default, this behavior is disabled and grant revocation only affects the specified resource.

+
Authorizations:
OAuth2
path Parameters
catalogName
required
string

The name of the catalog where the role will receive the grant

+
catalogRoleName
required
string

The name of the role receiving the grant (must exist)

+
query Parameters
cascade
boolean
Default: false

If true, the grant revocation cascades to all subresources.

+
Request Body schema: application/json
type
required
string
privilege
required
string (CatalogPrivilege)
Enum: "MANAGE_CATALOG" "NAMESPACE_CREATE" "TABLE_CREATE" "VIEW_CREATE" "NAMESPACE_DROP" "TABLE_DROP" "VIEW_DROP" "NAMESPACE_LIST" "TABLE_LIST" "VIEW_LIST" "NAMESPACE_READ_PROPERTIES" "TABLE_READ_PROPERTIES" "VIEW_READ_PROPERTIES" "NAMESPACE_WRITE_PROPERTIES" "TABLE_WRITE_PROPERTIES" "VIEW_WRITE_PROPERTIES" "TABLE_READ_DATA" "TABLE_WRITE_DATA" "NAMESPACE_FULL" "TABLE_FULL" "VIEW_FULL"

Responses

Request samples

Content type
application/json
Example
{
  • "type": "catalog",
  • "privilege": "MANAGE_CATALOG"
}
+ + + + diff --git a/docs/quickstart.md b/docs/quickstart.md new file mode 100644 index 0000000000..d42dcdcc34 --- /dev/null +++ b/docs/quickstart.md @@ -0,0 +1,311 @@ +# Quick Start + +This guide serves as a introduction to several key entities that can be managed with Polaris, describes how to build and deploy Polaris locally, and finally includes examples of how to use Polaris with Spark and Trino. + +## Prerequisites + +This guide covers building Polaris, deploying it locally or via [Docker](https://www.docker.com/), and interacting with it using the command-line interface and [Apache Spark](https://spark.apache.org/). Before proceeding with Polaris, be sure to satisfy the relevant prerequisites listed here. + +### Building and Deploying Polaris + +To get the latest Polaris code, you'll need to clone the repository using [git](https://git-scm.com/). You can install git using [homebrew](https://brew.sh/): + +``` +brew install git +``` + +Then, use git to clone the Polaris repo: + +``` +cd ~ +git clone https://github.com/polaris-catalog/polaris.git +``` + +#### With Docker + +If you plan to deploy Polaris inside [Docker](https://www.docker.com/)], you'll need to install docker itself. For can be done using [homebrew](https://brew.sh/): + +``` +brew install docker +``` + +Once installed, make sure Docker is running. This can be done on macOS with: + +``` +open -a Docker +``` + +#### From Source + +If you plan to build Polaris from source yourself, you will need to satisfy a few prerequisites first. + +Polaris is built using [gradle](https://gradle.org/) and is compatible with Java 21. We recommend the use of [jenv](https://www.jenv.be/) to manage multiple Java versions. For example, to install Java 21 via [homebre]w(https://brew.sh/) and configure it with jenv: + +``` +cd ~/polaris +jenv local 21 +brew install openjdk@21 gradle@8 jenv +jenv add $(brew --prefix openjdk@21) +jenv local 21 +``` + +### Connecting to Polaris + +Polaris is compatible with any [Apache Iceberg](https://iceberg.apache.org/) client that supports the REST API. Depending on the client you plan to use, refer to the prerequisites below. + +#### With Spark + +If you want to connect to Polaris with [Apache Spark](https://spark.apache.org/), you'll need to start by cloning Spark. As [above](#building-and-deploying-polaris), make sure [git](https://git-scm.com/) is installed first. You can install it with [homebrew](https://brew.sh/): + +``` +brew install git +``` + +Then, clone Spark and check out a versioned branch. This guide uses [Spark 3.5.0](https://spark.apache.org/releases/spark-release-3-5-0.html). + +``` +cd ~ +git clone https://github.com/apache/spark.git +cd ~/spark +git checkout branch-3.5.0 +``` + +## Deploying Polaris + +Polaris can be deployed via a lightweight docker image or as a standalone process. Before starting, be sure that you've satisfied the relevant [prerequisites](#building-and-deploying-polaris) detailed above. + +### Docker Image + +To start using Polaris in Docker, launch Polaris while Docker is running: + +``` +cd ~/polaris +docker compose -f docker-compose.yml up --build +``` + +Once the `polaris-polaris` container is up, you can continue to [Defining a Catalog](#defining-a-catalog). + +### Building Polaris + +Run Polaris locally with: + +``` +cd ~/polaris +./gradlew runApp +``` + +You should see output for some time as Polaris builds and starts up. Eventually, you won’t see any more logs and should see messages that resemble the following: + +``` +INFO [...] [main] [] o.e.j.s.handler.ContextHandler: Started i.d.j.MutableServletContextHandler@... +INFO [...] [main] [] o.e.j.server.AbstractConnector: Started application@... +INFO [...] [main] [] o.e.j.server.AbstractConnector: Started admin@... +INFO [...] [main] [] o.eclipse.jetty.server.Server: Started Server@... +``` + +At this point, Polaris is running. + +## Bootstrapping Polaris + +For this tutortial, we'll launch an instance of Polaris that stores entities only in-memory. This means that any entities that you define will be destroyed when Polaris is shut down. It also means that Polaris will automatically bootstrap itself with root credentials. For more information on how to configure Polaris for production usage, see the [docs](./configuring-polaris-for-production.md). + +When Polaris is launched using in-memory mode the root `CLIENT_ID` and `CLIENT_SECRET` can be found in stdout on initial startup. For example: + +``` +Bootstrapped with credentials: {"client-id": "XXXX", "client-secret": "YYYY"} +``` + +Be sure to note of these credentials as we'll be using them below. + +## Defining a Catalog + +In Polaris, the [catalog](./entities/catalog.md) is the top-level entity that objects like [tables](./entities.md#table) and [views](./entities.md#view) are organized under. With a Polaris service running, you can create a catalog like so: + +``` +cd ~/polaris + +./polaris \ + --client-id ${CLIENT_ID} \ + --client-secret ${CLIENT_SECRET} \ + catalogs \ + create \ + --storage-type s3 \ + --default-base-location ${DEFAULT_BASE_LOCATION} \ + --role-arn ${ROLE_ARN} \ + quickstart_catalog +``` + +This will create a new catalog called **quickstart_catalog**. + +The `DEFAULT_BASE_LOCATION` you provide will be the default location that objects in this catalog should be stored in, and the `ROLE_ARN` you provide should be a [Role ARN](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference-arns.html) with access to read and write data in that location. These credentials will be provided to engines reading data from the catalog once they have authenticated with Polaris using credentials that have access to those resources. + +If you’re using a storage type other than S3, such as Azure, you’ll provide a different type of credential than a Role ARN. For more details on supported storage types, see the [docs](./entities.md#storage-type). + +Additionally, if Polaris is running somewhere other than `localhost:8181`, you can specify the correct hostname and port by providing `--host` and `--port` flags. For the full set of options supported by the CLI, please refer to the [docs](./command-line-interface.md). + + +### Creating a Principal and Assigning it Privileges + +With a catalog created, we can create a [principal](./entities.md#principal) that has access to manage that catalog. For details on how to configure the Polaris CLI, see [the section above](#defining-a-catalog) or refer to the [docs](./command-line-interface.md). + +``` +./polaris \ + --client-id ${CLIENT_ID} \ + --client-secret ${CLIENT_SECRET} \ + principals \ + create \ + quickstart_user + +./polaris \ + --client-id ${CLIENT_ID} \ + --client-secret ${CLIENT_SECRET} \ + principal-roles \ + create \ + quickstart_user_role + +./polaris \ + --client-id ${CLIENT_ID} \ + --client-secret ${CLIENT_SECRET} \ + catalog-roles \ + create \ + --catalog quickstart_catalog \ + quickstart_catalog_role +``` + + +Be sure to provide the necessary credentials, hostname, and port as before. + +When the `principals create` command completes successfully, it will return the credentials for this new principal. Be sure to note these down for later. For example: + +``` +./polaris ... principals create example +{"clientId": "XXXX", "clientSecret": "YYYY"} +``` + +Now, we grant the principal the [principal role](./entities.md#principal-role) we created, and grant the [catalog role](./entities.md#catalog-role) the principal role we created. For more information on these entities, please refer to the linked documentation. + +``` +./polaris \ + --client-id ${CLIENT_ID} \ + --client-secret ${CLIENT_SECRET} \ + principal-roles \ + grant \ + --principal quickstart_user \ + quickstart_user_role + +./polaris \ + --client-id ${CLIENT_ID} \ + --client-secret ${CLIENT_SECRET} \ + catalog-roles \ + grant \ + --catalog quickstart_catalog \ + --principal-role quickstart_user_role \ + quickstart_catalog_role +``` + +Now, we’ve linked our principal to the catalog via roles like so: + +![Principal to Catalog](./img/quickstart/privilege-illustration-1.png "Principal to Catalog") + +In order to give this principal the ability to interact with the catalog, we must assign some [privileges](./entities.md#privileges). For the time being, we will give this principal the ability to fully manage content in our new catalog. We can do this with the CLI like so: + +``` +./polaris \ + --client-id ${CLIENT_ID} \ + --client-secret ${CLIENT_SECRET} \ + privileges \ + --catalog quickstart_catalog \ + --catalog-role quickstart_catalog_role \ + catalog \ + grant \ + CATALOG_MANAGE_CONTENT +``` + +This grants the [catalog privileges](./entities.md#privilege) `CATALOG_MANAGE_CONTENT` to our catalog role, linking everything together like so: + +![Principal to Catalog with Catalog Role](./img/quickstart/privilege-illustration-2.png "Principal to Catalog with Catalog Role") + +`CATALOG_MANAGE_CONTENT` has create/list/read/write privileges on all entities within the catalog. The same privilege could be granted to a namespace, in which case the principal could create/list/read/write any entity under that namespace. + +## Using Iceberg & Polaris + +At this point, we’ve created a principal and granted it the ability to manage a catalog. We can now use an external engine to assume that principal, access our catalog, and store data in that catalog using [Apache Iceberg](https://iceberg.apache.org/). + +### Connecting with Spark + +To use a Polaris-managed catalog in [Apache Spark](https://spark.apache.org/), we can configure Spark to use the Iceberg catalog REST API. + +This guide uses [Apache Spark 3.5](https://spark.apache.org/releases/spark-release-3-5-0.html), but be sure to find [the appropriate iceberg-spark package for your Spark version](https://mvnrepository.com/artifact/org.apache.iceberg/iceberg-spark). With a local Spark clone, we on the `branch-3.5` branch we can run the following: + +_Note: the credentials provided here are those for our principal, not the root credentials._ + +``` +bin/spark-shell \ +--packages org.apache.iceberg:iceberg-spark-runtime-3.5_2.12:1.5.2,org.apache.hadoop:hadoop-aws:3.4.0 \ +--conf spark.sql.extensions=org.apache.iceberg.spark.extensions.IcebergSparkSessionExtensions \ +--conf spark.sql.catalog.quickstart_catalog.warehouse=quickstart_catalog \ +--conf spark.sql.catalog.quickstart_catalog.header.X-Iceberg-Access-Delegation=true \ +--conf spark.sql.catalog.quickstart_catalog=org.apache.iceberg.spark.SparkCatalog \ +--conf spark.sql.catalog.quickstart_catalog.catalog-impl=org.apache.iceberg.rest.RESTCatalog \ +--conf spark.sql.catalog.quickstart_catalog.uri=http://localhost:8181/api/catalog \ +--conf spark.sql.catalog.quickstart_catalog.credential='XXXX:YYYY' \ +--conf spark.sql.catalog.quickstart_catalog.scope='PRINCIPAL_ROLE:ALL' \ +--conf spark.sql.catalog.quickstart_catalog.token-refresh-enabled=true +``` + + +Replace `XXXX` and `YYYY` with the client ID and client secret generated when you created the `quickstart_user` principal. + +Similar to the CLI commands above, this configures Spark to use the Polaris running at `localhost:8181` as a catalog. If your Polaris server is running elsewhere, but sure to update the configuration appropriately. + +Finally, note that we include the `hadoop-aws` package here. If your table is using a different filesystem, be sure to include the appropriate dependency. + +Once the Spark session starts, we can create a namespace and table within the catalog: + +``` +spark.sql("USE quickstart_catalog") +spark.sql("CREATE NAMESPACE IF NOT EXISTS quickstart_namespace") +spark.sql("CREATE NAMESPACE IF NOT EXISTS quickstart_namespace.schema") +spark.sql("USE NAMESPACE quickstart_namespace.schema") +spark.sql(""" + CREATE TABLE IF NOT EXISTS quickstart_table ( + id BIGINT, data STRING + ) +USING ICEBERG +""") +``` + +We can now use this table like any other: + +``` +spark.sql("INSERT INTO quickstart_table VALUES (1, 'some data')") +spark.sql("SELECT * FROM quickstart_table").show(false) +. . . ++---+---------+ +|id |data | ++---+---------+ +|1 |some data| ++---+---------+ +``` + +If at any time access is revoked... + +``` +./polaris \ + --client-id ${CLIENT_ID} \ + --client-secret ${CLIENT_SECRET} \ + privileges \ + --catalog quickstart_catalog \ + --catalog-role quickstart_catalog_role \ + catalog \ + revoke \ + CATALOG_MANAGE_CONTENT +``` + +Spark will lose access to the table: + +``` +spark.sql("SELECT * FROM quickstart_table").show(false) + +org.apache.iceberg.exceptions.ForbiddenException: Forbidden: Principal 'quickstart_user' with activated PrincipalRoles '[]' and activated ids '[6, 7]' is not authorized for op LOAD_TABLE_WITH_READ_DELEGATION +``` \ No newline at end of file diff --git a/extension/persistence/eclipselink/build.gradle b/extension/persistence/eclipselink/build.gradle new file mode 100644 index 0000000000..ed4b0b4ed9 --- /dev/null +++ b/extension/persistence/eclipselink/build.gradle @@ -0,0 +1,9 @@ +dependencies { + implementation project(":polaris-core") + implementation project(":polaris-service") + implementation "org.eclipse.persistence:eclipselink:4.0.3" + implementation "io.dropwizard:dropwizard-jackson:${dropwizardVersion}" + + testImplementation 'com.h2database:h2:2.2.224' + testImplementation(testFixtures(project(":polaris-core"))) +} diff --git a/extension/persistence/eclipselink/src/main/java/io/polaris/extension/persistence/impl/eclipselink/EclipseLinkPolarisMetaStoreManagerFactory.java b/extension/persistence/eclipselink/src/main/java/io/polaris/extension/persistence/impl/eclipselink/EclipseLinkPolarisMetaStoreManagerFactory.java new file mode 100644 index 0000000000..b282a85229 --- /dev/null +++ b/extension/persistence/eclipselink/src/main/java/io/polaris/extension/persistence/impl/eclipselink/EclipseLinkPolarisMetaStoreManagerFactory.java @@ -0,0 +1,36 @@ +package io.polaris.extension.persistence.impl.eclipselink; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeName; +import io.polaris.core.PolarisDiagnostics; +import io.polaris.core.context.RealmContext; +import io.polaris.core.persistence.LocalPolarisMetaStoreManagerFactory; +import io.polaris.core.persistence.PolarisMetaStoreManager; +import io.polaris.core.persistence.PolarisMetaStoreSession; +import org.jetbrains.annotations.NotNull; + +/** + * The implementation of Configuration interface for configuring the {@link PolarisMetaStoreManager} + * using an EclipseLink based meta store to store and retrieve all Polaris metadata. It can be + * configured through persistence.xml to use supported RDBMS as the meta store. + */ +@JsonTypeName("eclipse-link") +public class EclipseLinkPolarisMetaStoreManagerFactory + extends LocalPolarisMetaStoreManagerFactory< + PolarisEclipseLinkStore, PolarisEclipseLinkMetaStoreSessionImpl> { + @JsonProperty("conf-file") + private String confFile; + + @JsonProperty("persistence-unit") + private String persistenceUnitName; + + protected PolarisEclipseLinkStore createBackingStore(@NotNull PolarisDiagnostics diagnostics) { + return new PolarisEclipseLinkStore(diagnostics); + } + + protected PolarisMetaStoreSession createMetaStoreSession( + @NotNull PolarisEclipseLinkStore store, @NotNull RealmContext realmContext) { + return new PolarisEclipseLinkMetaStoreSessionImpl( + store, storageIntegration, realmContext, confFile, persistenceUnitName); + } +} diff --git a/extension/persistence/eclipselink/src/main/java/io/polaris/extension/persistence/impl/eclipselink/PolarisEclipseLinkMetaStoreSessionImpl.java b/extension/persistence/eclipselink/src/main/java/io/polaris/extension/persistence/impl/eclipselink/PolarisEclipseLinkMetaStoreSessionImpl.java new file mode 100644 index 0000000000..1aae23a107 --- /dev/null +++ b/extension/persistence/eclipselink/src/main/java/io/polaris/extension/persistence/impl/eclipselink/PolarisEclipseLinkMetaStoreSessionImpl.java @@ -0,0 +1,678 @@ +package io.polaris.extension.persistence.impl.eclipselink; + +import static org.eclipse.persistence.config.PersistenceUnitProperties.ECLIPSELINK_PERSISTENCE_XML; +import static org.eclipse.persistence.config.PersistenceUnitProperties.JDBC_URL; + +import com.google.common.base.Predicates; +import com.google.common.collect.Maps; +import io.polaris.core.PolarisCallContext; +import io.polaris.core.context.RealmContext; +import io.polaris.core.entity.PolarisBaseEntity; +import io.polaris.core.entity.PolarisChangeTrackingVersions; +import io.polaris.core.entity.PolarisEntitiesActiveKey; +import io.polaris.core.entity.PolarisEntityActiveRecord; +import io.polaris.core.entity.PolarisEntityCore; +import io.polaris.core.entity.PolarisEntityId; +import io.polaris.core.entity.PolarisEntityType; +import io.polaris.core.entity.PolarisGrantRecord; +import io.polaris.core.entity.PolarisPrincipalSecrets; +import io.polaris.core.persistence.PolarisMetaStoreManagerImpl; +import io.polaris.core.persistence.PolarisMetaStoreSession; +import io.polaris.core.persistence.RetryOnConcurrencyException; +import io.polaris.core.persistence.models.ModelEntity; +import io.polaris.core.persistence.models.ModelEntityActive; +import io.polaris.core.persistence.models.ModelEntityChangeTracking; +import io.polaris.core.persistence.models.ModelGrantRecord; +import io.polaris.core.persistence.models.ModelPrincipalSecrets; +import io.polaris.core.storage.PolarisStorageConfigurationInfo; +import io.polaris.core.storage.PolarisStorageIntegration; +import io.polaris.core.storage.PolarisStorageIntegrationProvider; +import jakarta.persistence.EntityManager; +import jakarta.persistence.EntityManagerFactory; +import jakarta.persistence.EntityTransaction; +import jakarta.persistence.OptimisticLockException; +import jakarta.persistence.Persistence; +import java.io.InputStream; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathFactory; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.NodeList; + +/** + * EclipseLink implementation of a Polaris metadata store supporting persisting and retrieving all + * Polaris metadata from/to the configured database systems. + */ +public class PolarisEclipseLinkMetaStoreSessionImpl implements PolarisMetaStoreSession { + private static final Logger LOG = + LoggerFactory.getLogger(PolarisEclipseLinkMetaStoreSessionImpl.class); + + private EntityManagerFactory emf; + private ThreadLocal localSession = new ThreadLocal<>(); + private final PolarisEclipseLinkStore store; + private final PolarisStorageIntegrationProvider storageIntegrationProvider; + private static volatile Map properties; + + /** + * Create a meta store session against provided realm. Each realm has its own database. + * + * @param store Backing store of EclipseLink implementation + * @param storageIntegrationProvider Storage integration provider + * @param realmContext Realm context used to communicate with different database. + * @param confFile Optional EclipseLink configuration file. Default to 'META-INF/persistence.xml'. + * @param persistenceUnitName Optional persistence-unit name in confFile. Default to 'polaris'. + */ + public PolarisEclipseLinkMetaStoreSessionImpl( + @NotNull PolarisEclipseLinkStore store, + @NotNull PolarisStorageIntegrationProvider storageIntegrationProvider, + @NotNull RealmContext realmContext, + @Nullable String confFile, + @Nullable String persistenceUnitName) { + persistenceUnitName = persistenceUnitName == null ? "polaris" : persistenceUnitName; + Map properties = + loadProperties( + confFile == null ? "META-INF/persistence.xml" : confFile, persistenceUnitName); + // Replace database name in JDBC URL with realm + if (properties.containsKey(JDBC_URL)) { + properties.put( + JDBC_URL, properties.get(JDBC_URL).replace("{realm}", realmContext.getRealmIdentifier())); + } + properties.put(ECLIPSELINK_PERSISTENCE_XML, confFile); + + emf = Persistence.createEntityManagerFactory(persistenceUnitName, properties); + + LOG.debug("Create EclipseLink Meta Store Session for {}", realmContext.getRealmIdentifier()); + + // init store + this.store = store; + this.storageIntegrationProvider = storageIntegrationProvider; + } + + /** Load the persistence unit properties from a given configuration file */ + private Map loadProperties(String confFile, String persistenceUnitName) { + if (this.properties != null) { + return this.properties; + } + + try { + InputStream input = this.getClass().getClassLoader().getResourceAsStream(confFile); + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + DocumentBuilder builder = factory.newDocumentBuilder(); + Document doc = builder.parse(input); + XPath xPath = XPathFactory.newInstance().newXPath(); + String expression = + "/persistence/persistence-unit[@name='" + persistenceUnitName + "']/properties/property"; + NodeList nodeList = + (NodeList) xPath.compile(expression).evaluate(doc, XPathConstants.NODESET); + Map properties = new HashMap<>(); + for (int i = 0; i < nodeList.getLength(); i++) { + NamedNodeMap nodeMap = nodeList.item(i).getAttributes(); + properties.put( + nodeMap.getNamedItem("name").getNodeValue(), + nodeMap.getNamedItem("value").getNodeValue()); + } + + this.properties = properties; + return properties; + } catch (Exception e) { + LOG.warn( + "Cannot find or parse the configuration file {} for persistence-unit {}", + confFile, + persistenceUnitName); + } + + return Maps.newHashMap(); + } + + /** {@inheritDoc} */ + @Override + public T runInTransaction( + @NotNull PolarisCallContext callCtx, @NotNull Supplier transactionCode) { + callCtx.getDiagServices().check(localSession.get() == null, "cannot nest transaction"); + + try (EntityManager session = emf.createEntityManager()) { + localSession.set(session); + EntityTransaction tr = session.getTransaction(); + try { + tr.begin(); + + T result = transactionCode.get(); + + // Commit when it's not rolled back by the client + if (session.getTransaction().isActive()) { + tr.commit(); + LOG.debug("transaction committed"); + } + + return result; + } catch (Exception e) { + tr.rollback(); + LOG.debug("transaction rolled back: {}", e); + + if (e instanceof OptimisticLockException + || e.getCause() instanceof OptimisticLockException) { + throw new RetryOnConcurrencyException(e); + } + + throw e; + } finally { + localSession.remove(); + } + } + } + + /** {@inheritDoc} */ + @Override + public void runActionInTransaction( + @NotNull PolarisCallContext callCtx, @NotNull Runnable transactionCode) { + callCtx.getDiagServices().check(localSession.get() == null, "cannot nest transaction"); + + try (EntityManager session = emf.createEntityManager()) { + localSession.set(session); + EntityTransaction tr = session.getTransaction(); + try { + tr.begin(); + + transactionCode.run(); + + // Commit when it's not rolled back by the client + if (session.getTransaction().isActive()) { + tr.commit(); + LOG.debug("transaction committed"); + } + } catch (Exception e) { + tr.rollback(); + LOG.debug("transaction rolled back"); + + if (e instanceof OptimisticLockException + || e.getCause() instanceof OptimisticLockException) { + throw new RetryOnConcurrencyException(e); + } + + throw e; + } finally { + localSession.remove(); + } + } + } + + /** {@inheritDoc} */ + @Override + public T runInReadTransaction( + @NotNull PolarisCallContext callCtx, @NotNull Supplier transactionCode) { + // EclipseLink doesn't support readOnly transaction + return runInTransaction(callCtx, transactionCode); + } + + /** {@inheritDoc} */ + @Override + public void runActionInReadTransaction( + @NotNull PolarisCallContext callCtx, @NotNull Runnable transactionCode) { + // EclipseLink doesn't support readOnly transaction + runActionInTransaction(callCtx, transactionCode); + } + + /** + * @return new unique entity identifier + */ + @Override + public long generateNewId(@NotNull PolarisCallContext callCtx) { + // This function can be called within a transaction or out of transaction. + // If called out of transaction, create a new transaction, otherwise run in current transaction + return localSession.get() != null + ? this.store.getNextSequence(localSession.get()) + : runInReadTransaction(callCtx, () -> generateNewId(callCtx)); + } + + /** {@inheritDoc} */ + @Override + public void writeToEntities( + @NotNull PolarisCallContext callCtx, @NotNull PolarisBaseEntity entity) { + this.store.writeToEntities(localSession.get(), entity); + } + + /** {@inheritDoc} */ + @Override + public void persistStorageIntegrationIfNeeded( + @NotNull PolarisCallContext callContext, + @NotNull PolarisBaseEntity entity, + @Nullable PolarisStorageIntegration storageIntegration) { + // not implemented for eclipselink store + } + + /** {@inheritDoc} */ + @Override + public void writeToEntitiesActive( + @NotNull PolarisCallContext callCtx, @NotNull PolarisBaseEntity entity) { + // write it + this.store.writeToEntitiesActive(localSession.get(), entity); + } + + /** {@inheritDoc} */ + @Override + public void writeToEntitiesDropped( + @NotNull PolarisCallContext callCtx, @NotNull PolarisBaseEntity entity) { + // write it + this.store.writeToEntitiesDropped(localSession.get(), entity); + } + + /** {@inheritDoc} */ + @Override + public void writeToEntitiesChangeTracking( + @NotNull PolarisCallContext callCtx, @NotNull PolarisBaseEntity entity) { + // write it + this.store.writeToEntitiesChangeTracking(localSession.get(), entity); + } + + /** {@inheritDoc} */ + @Override + public void writeToGrantRecords( + @NotNull PolarisCallContext callCtx, @NotNull PolarisGrantRecord grantRec) { + // write it + this.store.writeToGrantRecords(localSession.get(), grantRec); + } + + /** {@inheritDoc} */ + @Override + public void deleteFromEntities( + @NotNull PolarisCallContext callCtx, @NotNull PolarisEntityCore entity) { + + // delete it + this.store.deleteFromEntities(localSession.get(), entity.getCatalogId(), entity.getId()); + } + + /** {@inheritDoc} */ + @Override + public void deleteFromEntitiesActive( + @NotNull PolarisCallContext callCtx, @NotNull PolarisEntityCore entity) { + // delete it + this.store.deleteFromEntitiesActive(localSession.get(), new PolarisEntitiesActiveKey(entity)); + } + + /** {@inheritDoc} */ + @Override + public void deleteFromEntitiesDropped( + @NotNull PolarisCallContext callCtx, @NotNull PolarisBaseEntity entity) { + // delete it + this.store.deleteFromEntitiesDropped(localSession.get(), entity.getCatalogId(), entity.getId()); + } + + /** + * {@inheritDoc} + * + * @param callCtx + * @param entity entity record to delete + */ + @Override + public void deleteFromEntitiesChangeTracking( + @NotNull PolarisCallContext callCtx, @NotNull PolarisEntityCore entity) { + // delete it + this.store.deleteFromEntitiesChangeTracking(localSession.get(), entity); + } + + /** {@inheritDoc} */ + @Override + public void deleteFromGrantRecords( + @NotNull PolarisCallContext callCtx, @NotNull PolarisGrantRecord grantRec) { + this.store.deleteFromGrantRecords(localSession.get(), grantRec); + } + + /** {@inheritDoc} */ + @Override + public void deleteAllEntityGrantRecords( + @NotNull PolarisCallContext callCtx, + @NotNull PolarisEntityCore entity, + @NotNull List grantsOnGrantee, + @NotNull List grantsOnSecurable) { + this.store.deleteAllEntityGrantRecords(localSession.get(), entity); + } + + /** {@inheritDoc} */ + @Override + public void deleteAll(@NotNull PolarisCallContext callCtx) { + this.store.deleteAll(localSession.get()); + } + + /** {@inheritDoc} */ + @Override + public @Nullable PolarisBaseEntity lookupEntity( + @NotNull PolarisCallContext callCtx, long catalogId, long entityId) { + return ModelEntity.toEntity(this.store.lookupEntity(localSession.get(), catalogId, entityId)); + } + + @Override + public @NotNull List lookupEntities( + @NotNull PolarisCallContext callCtx, List entityIds) { + return this.store.lookupEntities(localSession.get(), entityIds).stream() + .map(model -> ModelEntity.toEntity(model)) + .toList(); + } + + /** {@inheritDoc} */ + @Override + public int lookupEntityVersion( + @NotNull PolarisCallContext callCtx, long catalogId, long entityId) { + ModelEntity model = this.store.lookupEntity(localSession.get(), catalogId, entityId); + return model == null ? 0 : model.getEntityVersion(); + } + + /** {@inheritDoc} */ + @Override + public @NotNull List lookupEntityVersions( + @NotNull PolarisCallContext callCtx, List entityIds) { + Map idToEntityMap = + this.store.lookupEntities(localSession.get(), entityIds).stream() + .collect( + Collectors.toMap( + entry -> new PolarisEntityId(entry.getCatalogId(), entry.getId()), + entry -> entry)); + return entityIds.stream() + .map( + entityId -> { + ModelEntity entity = idToEntityMap.getOrDefault(entityId, null); + return entity == null + ? null + : new PolarisChangeTrackingVersions( + entity.getEntityVersion(), entity.getGrantRecordsVersion()); + }) + .collect(Collectors.toList()); + } + + /** {@inheritDoc} */ + @Override + @Nullable + public PolarisEntityActiveRecord lookupEntityActive( + @NotNull PolarisCallContext callCtx, @NotNull PolarisEntitiesActiveKey entityActiveKey) { + // lookup the active entity slice + return ModelEntityActive.toEntityActive( + this.store.lookupEntityActive(localSession.get(), entityActiveKey)); + } + + /** {@inheritDoc} */ + @Override + @NotNull + public List lookupEntityActiveBatch( + @NotNull PolarisCallContext callCtx, + @NotNull List entityActiveKeys) { + // now build a list to quickly verify that nothing has changed + return entityActiveKeys.stream() + .map(entityActiveKey -> this.lookupEntityActive(callCtx, entityActiveKey)) + .collect(Collectors.toList()); + } + + /** {@inheritDoc} */ + @Override + public @NotNull List listActiveEntities( + @NotNull PolarisCallContext callCtx, + long catalogId, + long parentId, + @NotNull PolarisEntityType entityType) { + return listActiveEntities(callCtx, catalogId, parentId, entityType, Predicates.alwaysTrue()); + } + + @Override + public @NotNull List listActiveEntities( + @NotNull PolarisCallContext callCtx, + long catalogId, + long parentId, + @NotNull PolarisEntityType entityType, + @NotNull Predicate entityFilter) { + // full range scan under the parent for that type + return listActiveEntities( + callCtx, + catalogId, + parentId, + entityType, + Integer.MAX_VALUE, + entityFilter, + entity -> + new PolarisEntityActiveRecord( + entity.getCatalogId(), + entity.getId(), + entity.getParentId(), + entity.getName(), + entity.getTypeCode(), + entity.getSubTypeCode())); + } + + @Override + public @NotNull List listActiveEntities( + @NotNull PolarisCallContext callCtx, + long catalogId, + long parentId, + @NotNull PolarisEntityType entityType, + int limit, + @NotNull Predicate entityFilter, + @NotNull Function transformer) { + // full range scan under the parent for that type + return this.store + .lookupFullEntitiesActive(localSession.get(), catalogId, parentId, entityType) + .stream() + .map(model -> ModelEntity.toEntity(model)) + .filter(entityFilter) + .limit(limit) + .map(transformer) + .collect(Collectors.toList()); + } + + /** {@inheritDoc} */ + public boolean hasChildren( + @NotNull PolarisCallContext callContext, + @Nullable PolarisEntityType entityType, + long catalogId, + long parentId) { + // check if it has children + return this.store.countActiveChildEntities(localSession.get(), catalogId, parentId, entityType) + > 0; + } + + /** {@inheritDoc} */ + @Override + public int lookupEntityGrantRecordsVersion( + @NotNull PolarisCallContext callCtx, long catalogId, long entityId) { + ModelEntityChangeTracking entity = + this.store.lookupEntityChangeTracking(localSession.get(), catalogId, entityId); + + // does not exist, 0 + return entity == null ? 0 : entity.getGrantRecordsVersion(); + } + + /** {@inheritDoc} */ + @Override + public @Nullable PolarisGrantRecord lookupGrantRecord( + @NotNull PolarisCallContext callCtx, + long securableCatalogId, + long securableId, + long granteeCatalogId, + long granteeId, + int privilegeCode) { + // lookup the grants records slice to find the usage role + return ModelGrantRecord.toGrantRecord( + this.store.lookupGrantRecord( + localSession.get(), + securableCatalogId, + securableId, + granteeCatalogId, + granteeId, + privilegeCode)); + } + + /** {@inheritDoc} */ + @Override + public @NotNull List loadAllGrantRecordsOnSecurable( + @NotNull PolarisCallContext callCtx, long securableCatalogId, long securableId) { + // now fetch all grants for this securable + return this.store + .lookupAllGrantRecordsOnSecurable(localSession.get(), securableCatalogId, securableId) + .stream() + .map(model -> ModelGrantRecord.toGrantRecord(model)) + .toList(); + } + + /** {@inheritDoc} */ + @Override + public @NotNull List loadAllGrantRecordsOnGrantee( + @NotNull PolarisCallContext callCtx, long granteeCatalogId, long granteeId) { + // now fetch all grants assigned to this grantee + return this.store + .lookupGrantRecordsOnGrantee(localSession.get(), granteeCatalogId, granteeId) + .stream() + .map(model -> ModelGrantRecord.toGrantRecord(model)) + .toList(); + } + + /** {@inheritDoc} */ + @Override + public @Nullable PolarisPrincipalSecrets loadPrincipalSecrets( + @NotNull PolarisCallContext callCtx, @NotNull String clientId) { + return ModelPrincipalSecrets.toPrincipalSecrets( + this.store.lookupPrincipalSecrets(localSession.get(), clientId)); + } + + /** {@inheritDoc} */ + @Override + public @NotNull PolarisPrincipalSecrets generateNewPrincipalSecrets( + @NotNull PolarisCallContext callCtx, @NotNull String principalName, long principalId) { + // ensure principal client id is unique + PolarisPrincipalSecrets principalSecrets; + ModelPrincipalSecrets lookupPrincipalSecrets; + do { + // generate new random client id and secrets + principalSecrets = new PolarisPrincipalSecrets(principalId); + + // load the existing secrets + lookupPrincipalSecrets = + this.store.lookupPrincipalSecrets( + localSession.get(), principalSecrets.getPrincipalClientId()); + } while (lookupPrincipalSecrets != null); + + // write new principal secrets + this.store.writePrincipalSecrets(localSession.get(), principalSecrets); + + // if not found, return null + return principalSecrets; + } + + /** {@inheritDoc} */ + @Override + public @NotNull PolarisPrincipalSecrets rotatePrincipalSecrets( + @NotNull PolarisCallContext callCtx, + @NotNull String clientId, + long principalId, + @NotNull String mainSecretToRotate, + boolean reset) { + + // load the existing secrets + PolarisPrincipalSecrets principalSecrets = + ModelPrincipalSecrets.toPrincipalSecrets( + this.store.lookupPrincipalSecrets(localSession.get(), clientId)); + + // should be found + callCtx + .getDiagServices() + .checkNotNull( + principalSecrets, + "cannot_find_secrets", + "client_id={} principalId={}", + clientId, + principalId); + + // ensure principal id is matching + callCtx + .getDiagServices() + .check( + principalId == principalSecrets.getPrincipalId(), + "principal_id_mismatch", + "expectedId={} id={}", + principalId, + principalSecrets.getPrincipalId()); + + // rotate the secrets + principalSecrets.rotateSecrets(mainSecretToRotate); + if (reset) { + principalSecrets.rotateSecrets(principalSecrets.getMainSecret()); + } + + // write back new secrets + this.store.writePrincipalSecrets(localSession.get(), principalSecrets); + + // return those + return principalSecrets; + } + + /** {@inheritDoc} */ + @Override + public void deletePrincipalSecrets( + @NotNull PolarisCallContext callCtx, @NotNull String clientId, long principalId) { + // load the existing secrets + ModelPrincipalSecrets principalSecrets = + this.store.lookupPrincipalSecrets(localSession.get(), clientId); + + // should be found + callCtx + .getDiagServices() + .checkNotNull( + principalSecrets, + "cannot_find_secrets", + "client_id={} principalId={}", + clientId, + principalId); + + // ensure principal id is matching + callCtx + .getDiagServices() + .check( + principalId == principalSecrets.getPrincipalId(), + "principal_id_mismatch", + "expectedId={} id={}", + principalId, + principalSecrets.getPrincipalId()); + + // delete these secrets + this.store.deletePrincipalSecrets(localSession.get(), clientId); + } + + /** {@inheritDoc} */ + @Override + public @Nullable + PolarisStorageIntegration createStorageIntegration( + @NotNull PolarisCallContext callCtx, + long catalogId, + long entityId, + PolarisStorageConfigurationInfo polarisStorageConfigurationInfo) { + return storageIntegrationProvider.getStorageIntegrationForConfig( + polarisStorageConfigurationInfo); + } + + /** {@inheritDoc} */ + @Override + public @Nullable + PolarisStorageIntegration loadPolarisStorageIntegration( + @NotNull PolarisCallContext callCtx, @NotNull PolarisBaseEntity entity) { + PolarisStorageConfigurationInfo storageConfig = + PolarisMetaStoreManagerImpl.readStorageConfiguration(callCtx, entity); + return storageIntegrationProvider.getStorageIntegrationForConfig(storageConfig); + } + + @Override + public void rollback() { + EntityManager session = localSession.get(); + if (session != null) { + session.getTransaction().rollback(); + } + } +} diff --git a/extension/persistence/eclipselink/src/main/java/io/polaris/extension/persistence/impl/eclipselink/PolarisEclipseLinkStore.java b/extension/persistence/eclipselink/src/main/java/io/polaris/extension/persistence/impl/eclipselink/PolarisEclipseLinkStore.java new file mode 100644 index 0000000000..7bdeaf0163 --- /dev/null +++ b/extension/persistence/eclipselink/src/main/java/io/polaris/extension/persistence/impl/eclipselink/PolarisEclipseLinkStore.java @@ -0,0 +1,397 @@ +package io.polaris.extension.persistence.impl.eclipselink; + +import io.polaris.core.PolarisDiagnostics; +import io.polaris.core.entity.PolarisBaseEntity; +import io.polaris.core.entity.PolarisEntitiesActiveKey; +import io.polaris.core.entity.PolarisEntityActiveRecord; +import io.polaris.core.entity.PolarisEntityCore; +import io.polaris.core.entity.PolarisEntityId; +import io.polaris.core.entity.PolarisEntityType; +import io.polaris.core.entity.PolarisGrantRecord; +import io.polaris.core.entity.PolarisPrincipalSecrets; +import io.polaris.core.persistence.models.ModelEntity; +import io.polaris.core.persistence.models.ModelEntityActive; +import io.polaris.core.persistence.models.ModelEntityChangeTracking; +import io.polaris.core.persistence.models.ModelEntityDropped; +import io.polaris.core.persistence.models.ModelGrantRecord; +import io.polaris.core.persistence.models.ModelPrincipalSecrets; +import jakarta.persistence.EntityManager; +import jakarta.persistence.TypedQuery; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Implements an EclipseLink based metastore for Polaris which can be configured for any database + * with EclipseLink support + */ +public class PolarisEclipseLinkStore { + private static final Logger LOG = LoggerFactory.getLogger(PolarisEclipseLinkStore.class); + + // diagnostic services + private PolarisDiagnostics diagnosticServices; + + /** + * Constructor, allocate everything at once + * + * @param diagnostics diagnostic services + */ + public PolarisEclipseLinkStore(@NotNull PolarisDiagnostics diagnostics) { + this.diagnosticServices = diagnostics; + } + + long getNextSequence(EntityManager session) { + diagnosticServices.check(session != null, "session_is_null"); + // implement with a sequence table POLARIS_SEQUENCE + return (long) session.createNativeQuery("SELECT NEXTVAL('POLARIS_SEQ')").getSingleResult(); + } + + void writeToEntities(EntityManager session, PolarisBaseEntity entity) { + diagnosticServices.check(session != null, "session_is_null"); + + ModelEntity model = lookupEntity(session, entity.getCatalogId(), entity.getId()); + if (model != null) { + // Update if the same entity already exists + model.update(entity); + } else { + model = ModelEntity.fromEntity(entity); + } + + session.persist(model); + } + + void writeToEntitiesActive(EntityManager session, PolarisBaseEntity entity) { + diagnosticServices.check(session != null, "session_is_null"); + + ModelEntityActive model = lookupEntityActive(session, new PolarisEntitiesActiveKey(entity)); + if (model == null) { + session.persist(ModelEntityActive.fromEntityActive(new PolarisEntityActiveRecord(entity))); + } + } + + void writeToEntitiesDropped(EntityManager session, PolarisBaseEntity entity) { + diagnosticServices.check(session != null, "session_is_null"); + + ModelEntityDropped entityDropped = + lookupEntityDropped(session, entity.getCatalogId(), entity.getId()); + if (entityDropped == null) { + session.persist(ModelEntityDropped.fromEntity(entity)); + } + } + + void writeToEntitiesChangeTracking(EntityManager session, PolarisBaseEntity entity) { + diagnosticServices.check(session != null, "session_is_null"); + + // Update the existing change tracking if a record with the same ids exists; otherwise, persist + // a new one + ModelEntityChangeTracking entityChangeTracking = + lookupEntityChangeTracking(session, entity.getCatalogId(), entity.getId()); + if (entityChangeTracking != null) { + entityChangeTracking.update(entity); + } else { + entityChangeTracking = new ModelEntityChangeTracking(entity); + } + + session.persist(entityChangeTracking); + } + + void writeToGrantRecords(EntityManager session, PolarisGrantRecord grantRec) { + diagnosticServices.check(session != null, "session_is_null"); + + session.persist(ModelGrantRecord.fromGrantRecord(grantRec)); + } + + void deleteFromEntities(EntityManager session, long catalogId, long entityId) { + diagnosticServices.check(session != null, "session_is_null"); + + ModelEntity model = lookupEntity(session, catalogId, entityId); + diagnosticServices.check(model != null, "entity_not_found"); + + session.remove(model); + } + + void deleteFromEntitiesActive(EntityManager session, PolarisEntitiesActiveKey key) { + diagnosticServices.check(session != null, "session_is_null"); + + ModelEntityActive entity = lookupEntityActive(session, key); + diagnosticServices.check(entity != null, "active_entity_not_found"); + session.remove(entity); + } + + void deleteFromEntitiesDropped(EntityManager session, long catalogId, long entityId) { + diagnosticServices.check(session != null, "session_is_null"); + + ModelEntityDropped entity = lookupEntityDropped(session, catalogId, entityId); + diagnosticServices.check(entity != null, "dropped_entity_not_found"); + + session.remove(entity); + } + + void deleteFromEntitiesChangeTracking(EntityManager session, PolarisEntityCore entity) { + diagnosticServices.check(session != null, "session_is_null"); + + ModelEntityChangeTracking entityChangeTracking = + lookupEntityChangeTracking(session, entity.getCatalogId(), entity.getId()); + diagnosticServices.check(entityChangeTracking != null, "change_tracking_entity_not_found"); + + session.remove(entityChangeTracking); + } + + void deleteFromGrantRecords(EntityManager session, PolarisGrantRecord grantRec) { + diagnosticServices.check(session != null, "session_is_null"); + + ModelGrantRecord lookupGrantRecord = + lookupGrantRecord( + session, + grantRec.getSecurableCatalogId(), + grantRec.getSecurableId(), + grantRec.getGranteeCatalogId(), + grantRec.getGranteeId(), + grantRec.getPrivilegeCode()); + + diagnosticServices.check(lookupGrantRecord != null, "grant_record_not_found"); + + session.remove(lookupGrantRecord); + } + + void deleteAllEntityGrantRecords(EntityManager session, PolarisEntityCore entity) { + diagnosticServices.check(session != null, "session_is_null"); + + // Delete grant records from grantRecords tables + lookupAllGrantRecordsOnSecurable(session, entity.getCatalogId(), entity.getId()) + .forEach(session::remove); + + // Delete grantee records from grantRecords tables + lookupGrantRecordsOnGrantee(session, entity.getCatalogId(), entity.getId()) + .forEach(session::remove); + } + + void deleteAll(EntityManager session) { + diagnosticServices.check(session != null, "session_is_null"); + + session.createQuery("DELETE from ModelEntity").executeUpdate(); + session.createQuery("DELETE from ModelEntityActive").executeUpdate(); + session.createQuery("DELETE from ModelEntityDropped").executeUpdate(); + session.createQuery("DELETE from ModelEntityChangeTracking").executeUpdate(); + session.createQuery("DELETE from ModelGrantRecord").executeUpdate(); + session.createQuery("DELETE from ModelPrincipalSecrets").executeUpdate(); + + LOG.debug("All entities deleted."); + } + + ModelEntity lookupEntity(EntityManager session, long catalogId, long entityId) { + diagnosticServices.check(session != null, "session_is_null"); + + return session + .createQuery( + "SELECT m from ModelEntity m where m.catalogId=:catalogId and m.id=:id", + ModelEntity.class) + .setParameter("catalogId", catalogId) + .setParameter("id", entityId) + .getResultStream() + .findFirst() + .orElse(null); + } + + @SuppressWarnings("unchecked") + List lookupEntities(EntityManager session, List entityIds) { + diagnosticServices.check(session != null, "session_is_null"); + + if (entityIds == null || entityIds.isEmpty()) return new ArrayList<>(); + + // TODO Support paging + String inClause = + entityIds.stream() + .map(entityId -> "(" + entityId.getCatalogId() + "," + entityId.getId() + ")") + .collect(Collectors.joining(",")); + + String hql = "SELECT * from ENTITIES m where (m.catalogId, m.id) in (" + inClause + ")"; + return (List) session.createNativeQuery(hql, ModelEntity.class).getResultList(); + } + + ModelEntityActive lookupEntityActive( + EntityManager session, PolarisEntitiesActiveKey entityActiveKey) { + diagnosticServices.check(session != null, "session_is_null"); + + return session + .createQuery( + "SELECT m from ModelEntityActive m where m.catalogId=:catalogId and m.parentId=:parentId and m.typeCode=:typeCode and m.name=:name", + ModelEntityActive.class) + .setParameter("catalogId", entityActiveKey.getCatalogId()) + .setParameter("parentId", entityActiveKey.getParentId()) + .setParameter("typeCode", entityActiveKey.getTypeCode()) + .setParameter("name", entityActiveKey.getName()) + .getResultStream() + .findFirst() + .orElse(null); + } + + long countActiveChildEntities( + EntityManager session, + long catalogId, + long parentId, + @Nullable PolarisEntityType entityType) { + diagnosticServices.check(session != null, "session_is_null"); + + String hql = + "SELECT COUNT(m) from ModelEntityActive m where m.catalogId=:catalogId and m.parentId=:parentId"; + if (entityType != null) { + hql += " and m.typeCode=:typeCode"; + } + + TypedQuery query = + session + .createQuery(hql, Long.class) + .setParameter("catalogId", catalogId) + .setParameter("parentId", parentId); + if (entityType != null) { + query.setParameter("typeCode", entityType.getCode()); + } + + return query.getSingleResult(); + } + + List lookupFullEntitiesActive( + EntityManager session, long catalogId, long parentId, @NotNull PolarisEntityType entityType) { + diagnosticServices.check(session != null, "session_is_null"); + + // Currently check against ENTITIES not joining with ENTITIES_ACTIVE + String hql = + "SELECT m from ModelEntity m where m.catalogId=:catalogId and m.parentId=:parentId and m.typeCode=:typeCode"; + + TypedQuery query = + session + .createQuery(hql, ModelEntity.class) + .setParameter("catalogId", catalogId) + .setParameter("parentId", parentId) + .setParameter("typeCode", entityType.getCode()); + + return query.getResultList(); + } + + ModelEntityDropped lookupEntityDropped(EntityManager session, long catalogId, long entityId) { + diagnosticServices.check(session != null, "session_is_null"); + + return session + .createQuery( + "SELECT m from ModelEntityDropped m where m.catalogId=:catalogId and m.id=:id", + ModelEntityDropped.class) + .setParameter("catalogId", catalogId) + .setParameter("id", entityId) + .getResultStream() + .findFirst() + .orElse(null); + } + + ModelEntityChangeTracking lookupEntityChangeTracking( + EntityManager session, long catalogId, long entityId) { + diagnosticServices.check(session != null, "session_is_null"); + + return session + .createQuery( + "SELECT m from ModelEntityChangeTracking m where m.catalogId=:catalogId and m.id=:id", + ModelEntityChangeTracking.class) + .setParameter("catalogId", catalogId) + .setParameter("id", entityId) + .getResultStream() + .findFirst() + .orElse(null); + } + + ModelGrantRecord lookupGrantRecord( + EntityManager session, + long securableCatalogId, + long securableId, + long granteeCatalogId, + long granteeId, + int privilegeCode) { + diagnosticServices.check(session != null, "session_is_null"); + + return session + .createQuery( + "SELECT m from ModelGrantRecord m where m.securableCatalogId=:securableCatalogId " + + "and m.securableId=:securableId " + + "and m.granteeCatalogId=:granteeCatalogId " + + "and m.granteeId=:granteeId " + + "and m.privilegeCode=:privilegeCode", + ModelGrantRecord.class) + .setParameter("securableCatalogId", securableCatalogId) + .setParameter("securableId", securableId) + .setParameter("granteeCatalogId", granteeCatalogId) + .setParameter("granteeId", granteeId) + .setParameter("privilegeCode", privilegeCode) + .getResultStream() + .findFirst() + .orElse(null); + } + + List lookupAllGrantRecordsOnSecurable( + EntityManager session, long securableCatalogId, long securableId) { + diagnosticServices.check(session != null, "session_is_null"); + + return session + .createQuery( + "SELECT m from ModelGrantRecord m " + + "where m.securableCatalogId=:securableCatalogId " + + "and m.securableId=:securableId", + ModelGrantRecord.class) + .setParameter("securableCatalogId", securableCatalogId) + .setParameter("securableId", securableId) + .getResultList(); + } + + List lookupGrantRecordsOnGrantee( + EntityManager session, long granteeCatalogId, long granteeId) { + diagnosticServices.check(session != null, "session_is_null"); + + return session + .createQuery( + "SELECT m from ModelGrantRecord m " + + "where m.granteeCatalogId=:granteeCatalogId " + + "and m.granteeId=:granteeId", + ModelGrantRecord.class) + .setParameter("granteeCatalogId", granteeCatalogId) + .setParameter("granteeId", granteeId) + .getResultList(); + } + + ModelPrincipalSecrets lookupPrincipalSecrets(EntityManager session, String clientId) { + diagnosticServices.check(session != null, "session_is_null"); + + return session + .createQuery( + "SELECT m from ModelPrincipalSecrets m where m.principalClientId=:clientId", + ModelPrincipalSecrets.class) + .setParameter("clientId", clientId) + .getResultStream() + .findFirst() + .orElse(null); + } + + void writePrincipalSecrets(EntityManager session, PolarisPrincipalSecrets principalSecrets) { + diagnosticServices.check(session != null, "session_is_null"); + + ModelPrincipalSecrets modelPrincipalSecrets = + lookupPrincipalSecrets(session, principalSecrets.getPrincipalClientId()); + if (modelPrincipalSecrets != null) { + modelPrincipalSecrets.update(principalSecrets); + } else { + modelPrincipalSecrets = ModelPrincipalSecrets.fromPrincipalSecrets(principalSecrets); + } + + session.persist(modelPrincipalSecrets); + } + + void deletePrincipalSecrets(EntityManager session, String clientId) { + diagnosticServices.check(session != null, "session_is_null"); + + ModelPrincipalSecrets modelPrincipalSecrets = lookupPrincipalSecrets(session, clientId); + diagnosticServices.check(modelPrincipalSecrets != null, "principal_secretes_not_found"); + + session.remove(modelPrincipalSecrets); + } +} diff --git a/extension/persistence/eclipselink/src/test/java/com/snowflake/polaris/persistence/impl/eclipselink/PolarisEclipseLinkMetaStoreTest.java b/extension/persistence/eclipselink/src/test/java/com/snowflake/polaris/persistence/impl/eclipselink/PolarisEclipseLinkMetaStoreTest.java new file mode 100644 index 0000000000..b9d1b1c7e9 --- /dev/null +++ b/extension/persistence/eclipselink/src/test/java/com/snowflake/polaris/persistence/impl/eclipselink/PolarisEclipseLinkMetaStoreTest.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. + */ + +package com.snowflake.polaris.persistence.impl.eclipselink; + +import io.polaris.core.PolarisCallContext; +import io.polaris.core.PolarisConfigurationStore; +import io.polaris.core.PolarisDefaultDiagServiceImpl; +import io.polaris.core.PolarisDiagnostics; +import io.polaris.core.persistence.PolarisMetaStoreManagerImpl; +import io.polaris.core.persistence.PolarisMetaStoreManagerTest; +import io.polaris.core.persistence.PolarisTestMetaStoreManager; +import io.polaris.extension.persistence.impl.eclipselink.PolarisEclipseLinkMetaStoreSessionImpl; +import io.polaris.extension.persistence.impl.eclipselink.PolarisEclipseLinkStore; +import java.time.ZoneId; +import org.mockito.Mockito; + +/** + * Integration test for EclipseLink based metastore implementation + * + * @author aixu + */ +public class PolarisEclipseLinkMetaStoreTest extends PolarisMetaStoreManagerTest { + + @Override + protected PolarisTestMetaStoreManager createPolarisTestMetaStoreManager() { + PolarisDiagnostics diagServices = new PolarisDefaultDiagServiceImpl(); + PolarisEclipseLinkStore store = new PolarisEclipseLinkStore(diagServices); + PolarisEclipseLinkMetaStoreSessionImpl session = + new PolarisEclipseLinkMetaStoreSessionImpl( + store, Mockito.mock(), () -> "realm", null, "polaris-dev"); + return new PolarisTestMetaStoreManager( + new PolarisMetaStoreManagerImpl(), + new PolarisCallContext( + session, + diagServices, + new PolarisConfigurationStore() {}, + timeSource.withZone(ZoneId.systemDefault()))); + } +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000000..97ecbda280 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,2 @@ +group=io.polaris +version=1.0.0 \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..d64cd4917707c1f8861d8cb53dd15194d4248596 GIT binary patch literal 43462 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 literal 0 HcmV?d00001 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..a80b22ce5c --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000000..1aa94a4269 --- /dev/null +++ b/gradlew @@ -0,0 +1,249 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# 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 +# +# https://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. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/kind-registry.sh b/kind-registry.sh new file mode 100755 index 0000000000..9fe55a821a --- /dev/null +++ b/kind-registry.sh @@ -0,0 +1,64 @@ +#!/bin/sh +set -o errexit + +# 1. Create registry container unless it already exists +reg_name='kind-registry' +reg_port='5001' +if [ "$(docker inspect -f '{{.State.Running}}' "${reg_name}" 2>/dev/null || true)" != 'true' ]; then + docker run \ + -d --restart=always -p "127.0.0.1:${reg_port}:5000" --network bridge --name "${reg_name}" \ + registry:2 +fi + +# 2. Create kind cluster with containerd registry config dir enabled +# TODO: kind will eventually enable this by default and this patch will +# be unnecessary. +# +# See: +# https://github.com/kubernetes-sigs/kind/issues/2875 +# https://github.com/containerd/containerd/blob/main/docs/cri/config.md#registry-configuration +# See: https://github.com/containerd/containerd/blob/main/docs/hosts.md +cat < /dev/null && pwd ) + +if [ ! -d ${SCRIPT_DIR}/polaris-venv ]; then + echo "Performing first-time setup for the Python client..." + python3 -m venv ${SCRIPT_DIR}/polaris-venv + . ${SCRIPT_DIR}/polaris-venv/bin/activate + pip install poetry==1.5.0 + + cp ${SCRIPT_DIR}/regtests/client/python/pyproject.toml ${SCRIPT_DIR} + pushd $SCRIPT_DIR && poetry install ; popd + + deactivate + echo "First time setup complete." +fi + +pushd $SCRIPT_DIR > /dev/null +PYTHONPATH=regtests/client/python ${SCRIPT_DIR}/polaris-venv/bin/python3 regtests/client/python/cli/polaris_cli.py "$@" +popd > /dev/null + diff --git a/polaris-core/build.gradle b/polaris-core/build.gradle new file mode 100644 index 0000000000..1767982a4f --- /dev/null +++ b/polaris-core/build.gradle @@ -0,0 +1,131 @@ +plugins { + id 'org.openapi.generator' version '7.6.0' + id("java-library") + id("java-test-fixtures") +} + +compileJava { + sourceCompatibility = 11 + targetCompatibility = 11 +} + +dependencies { + implementation "org.apache.iceberg:iceberg-api:${icebergVersion}" + implementation "org.apache.iceberg:iceberg-core:${icebergVersion}" + constraints { + implementation("io.airlift:aircompressor:0.27") { + because 'Vulnerability detected in 0.25' + } + } + // TODO - this is only here for the Discoverable interface + // We should use a different mechanism to discover the plugin implementations + implementation "io.dropwizard:dropwizard-jackson:${dropwizardVersion}" + + implementation "com.fasterxml.jackson.core:jackson-annotations:${jacksonVersion}" + implementation "com.fasterxml.jackson.core:jackson-core:${jacksonVersion}" + implementation "com.fasterxml.jackson.core:jackson-databind:${jacksonVersion}" + implementation 'com.github.ben-manes.caffeine:caffeine:3.1.8' + implementation 'org.apache.commons:commons-lang3:3.14.0' + implementation 'commons-codec:commons-codec:1.17.0' + + implementation("org.apache.hadoop:hadoop-common:${hadoopVersion}") { + exclude group: 'org.slf4j', module: 'slf4j-reload4j' + exclude group: 'org.slf4j', module: 'slf4j-log4j12' + exclude group: 'ch.qos.reload4j', module: 'reload4j' + exclude group: 'log4j', module: 'log4j' + exclude group: 'org.apache.zookeeper', module: 'zookeeper' + } + constraints { + implementation("org.xerial.snappy:snappy-java:1.1.10.4") { + because 'Vulnerability detected in 1.1.8.2' + } + implementation("org.codehaus.jettison:jettison:1.5.4") { + because 'Vulnerability detected in 1.1' + } + implementation("org.apache.commons:commons-configuration2:2.10.1") { + because 'Vulnerability detected in 2.8.0' + } + implementation("org.apache.commons:commons-compress:1.26.0") { + because 'Vulnerability detected in 1.21' + } + implementation("com.nimbusds:nimbus-jose-jwt:9.37.2") { + because 'Vulnerability detected in 9.8.1' + } + + } + implementation "org.apache.hadoop:hadoop-hdfs-client:${hadoopVersion}" + + implementation "javax.inject:javax.inject:1" + implementation "io.swagger:swagger-annotations:1.6.14" + implementation "io.swagger:swagger-jaxrs:1.6.14" + implementation "jakarta.validation:jakarta.validation-api:3.0.2" + + implementation "org.apache.iceberg:iceberg-aws:${icebergVersion}" + implementation "software.amazon.awssdk:sts:2.25.61" + implementation "software.amazon.awssdk:iam-policy-builder:2.25.61" + implementation "software.amazon.awssdk:s3:2.25.61" + + implementation "org.apache.iceberg:iceberg-azure:${icebergVersion}" + implementation "com.azure:azure-storage-blob:12.18.0" + implementation "com.azure:azure-storage-common:12.14.2" + implementation "com.azure:azure-identity:1.12.2" + implementation "com.azure:azure-storage-file-datalake:12.19.0" + constraints { + implementation("io.netty:netty-codec-http2:4.1.100") { + because 'Vulnerability detected in 4.1.72' + } + implementation("io.projectreactor.netty:reactor-netty-http:1.1.13") { + because 'Vulnerability detected in 1.0.45' + } + } + + implementation "org.apache.iceberg:iceberg-gcp:${icebergVersion}" + implementation "com.fasterxml.jackson.core:jackson-core:${jacksonVersion}" + implementation "com.fasterxml.jackson.core:jackson-databind:${jacksonVersion}" + implementation "com.google.cloud:google-cloud-storage:2.39.0" + + implementation 'io.micrometer:micrometer-core:1.13.2' + + testFixturesApi 'org.junit.jupiter:junit-jupiter:5.7.1' + testFixturesApi 'org.assertj:assertj-core:3.25.3' + testFixturesApi "org.mockito:mockito-core:5.11.0" + testFixturesApi "com.fasterxml.jackson.core:jackson-core:${jacksonVersion}" + testFixturesApi "com.fasterxml.jackson.core:jackson-databind:${jacksonVersion}" + testFixturesApi 'org.apache.commons:commons-lang3:3.14.0' + testFixturesApi "org.jetbrains:annotations:24.0.0" + + compileOnly "jakarta.annotation:jakarta.annotation-api:2.1.1" + compileOnly "jakarta.persistence:jakarta.persistence-api:3.1.0" +} + +openApiValidate { + inputSpec = "$rootDir/spec/polaris-management-service.yml" +} + +task generatePolarisService(type: org.openapitools.generator.gradle.plugin.tasks.GenerateTask) { + inputSpec = "$rootDir/spec/polaris-management-service.yml" + generatorName = "jaxrs-resteasy" + outputDir = "$buildDir/generated" + modelPackage = "io.polaris.core.admin.model" + ignoreFileOverride = "$rootDir/.openapi-generator-ignore" + removeOperationIdPrefix = true + templateDir = "$rootDir/server-templates" + globalProperties = [ + apis : "false", + models : "", + apiDocs : "false", + modelTests: "false" + ] + configOptions = [ + useBeanValidation : "true", + sourceFolder : "src/main/java", + useJakartaEe : "true", + generateBuilders : "true", + generateConstructorWithAllArgs: "true", + ] + additionalProperties = [apiNamePrefix: "Polaris", apiNameSuffix: "Api", metricsPrefix: "polaris"] + serverVariables = [basePath: "api/v1"] +} + +compileJava.dependsOn tasks.generatePolarisService +sourceSets.main.java.srcDirs += ["$buildDir/generated/src/main/java"] diff --git a/polaris-core/src/main/java/io/polaris/core/PolarisCallContext.java b/polaris-core/src/main/java/io/polaris/core/PolarisCallContext.java new file mode 100644 index 0000000000..52e018c316 --- /dev/null +++ b/polaris-core/src/main/java/io/polaris/core/PolarisCallContext.java @@ -0,0 +1,58 @@ +package io.polaris.core; + +import io.polaris.core.persistence.PolarisMetaStoreSession; +import java.time.Clock; +import java.time.ZoneId; +import org.jetbrains.annotations.NotNull; + +/** + * The Call context is allocated each time a new REST request is processed. It contains instances of + * low-level services required to process that request + */ +public class PolarisCallContext { + + // meta store which is used to persist Polaris entity metadata + private final PolarisMetaStoreSession metaStore; + + // diag services + private final PolarisDiagnostics diagServices; + + private final PolarisConfigurationStore configurationStore; + + private final Clock clock; + + public PolarisCallContext( + @NotNull PolarisMetaStoreSession metaStore, + @NotNull PolarisDiagnostics diagServices, + @NotNull PolarisConfigurationStore configurationStore, + @NotNull Clock clock) { + this.metaStore = metaStore; + this.diagServices = diagServices; + this.configurationStore = configurationStore; + this.clock = clock; + } + + public PolarisCallContext( + @NotNull PolarisMetaStoreSession metaStore, @NotNull PolarisDiagnostics diagServices) { + this.metaStore = metaStore; + this.diagServices = diagServices; + this.configurationStore = new PolarisConfigurationStore() {}; + this.clock = Clock.system(ZoneId.systemDefault()); + } + + public PolarisMetaStoreSession getMetaStore() { + return metaStore; + } + + public PolarisDiagnostics getDiagServices() { + return diagServices; + } + + public PolarisConfigurationStore getConfigurationStore() { + return configurationStore; + } + + public Clock getClock() { + return clock; + } +} diff --git a/polaris-core/src/main/java/io/polaris/core/PolarisConfiguration.java b/polaris-core/src/main/java/io/polaris/core/PolarisConfiguration.java new file mode 100644 index 0000000000..fae652bb57 --- /dev/null +++ b/polaris-core/src/main/java/io/polaris/core/PolarisConfiguration.java @@ -0,0 +1,9 @@ +package io.polaris.core; + +public class PolarisConfiguration { + + public static final String ENFORCE_PRINCIPAL_CREDENTIAL_ROTATION_REQUIRED_CHECKING = + "ENFORCE_PRINCIPAL_CREDENTIAL_ROTATION_REQUIRED_CHECKING"; + + private PolarisConfiguration() {} +} diff --git a/polaris-core/src/main/java/io/polaris/core/PolarisConfigurationStore.java b/polaris-core/src/main/java/io/polaris/core/PolarisConfigurationStore.java new file mode 100644 index 0000000000..bcb5084ad0 --- /dev/null +++ b/polaris-core/src/main/java/io/polaris/core/PolarisConfigurationStore.java @@ -0,0 +1,41 @@ +package io.polaris.core; + +import com.google.common.base.Preconditions; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Dynamic configuration store used to retrieve runtime parameters, which may vary by realm or by + * request. + */ +public interface PolarisConfigurationStore { + + /** + * Retrieve the current value for a configuration key. May be null if not set. + * + * @param ctx the current call context + * @param configName the name of the configuration key to check + * @return the current value set for the configuration key or null if not set + * @param the type of the configuration value + */ + default @Nullable T getConfiguration(PolarisCallContext ctx, String configName) { + return null; + } + + /** + * Retrieve the current value for a configuration key. If not set, return the non-null default + * value. + * + * @param ctx the current call context + * @param configName the name of the configuration key to check + * @param defaultValue the default value if the configuration key has no value + * @return the current value or the supplied default value + * @param the type of the configuration value + */ + default @NotNull T getConfiguration( + PolarisCallContext ctx, String configName, @NotNull T defaultValue) { + Preconditions.checkNotNull(defaultValue, "Cannot pass null as a default value"); + T configValue = getConfiguration(ctx, configName); + return configValue != null ? configValue : defaultValue; + } +} diff --git a/polaris-core/src/main/java/io/polaris/core/PolarisDefaultDiagServiceImpl.java b/polaris-core/src/main/java/io/polaris/core/PolarisDefaultDiagServiceImpl.java new file mode 100644 index 0000000000..d9cc339228 --- /dev/null +++ b/polaris-core/src/main/java/io/polaris/core/PolarisDefaultDiagServiceImpl.java @@ -0,0 +1,112 @@ +package io.polaris.core; + +import com.google.common.base.Preconditions; +import java.util.Arrays; +import org.jetbrains.annotations.Contract; + +/** Default implementation of the PolarisDiagServices. */ +public class PolarisDefaultDiagServiceImpl implements PolarisDiagnostics { + + /** + * Fail with an exception + * + * @param signature signature, small unique string to identify this assertion within the method, + * like "path_cannot_be_null" + * @param extraInfoFormat extra information regarding the assertion. Generally a set of name/value + * pairs: "id={} fileName={}" + * @param extraInfoArgs extra information arguments + */ + @Override + public RuntimeException fail(String signature, String extraInfoFormat, Object... extraInfoArgs) { + Preconditions.checkState(false, "%s: %s, %s", signature, extraInfoFormat, extraInfoArgs); + throw new RuntimeException(signature); + } + + /** + * Fail because of an exception + * + * @param signature signature, small unique string to identify this assertion within the method, + * like "path_cannot_be_null" + * @param cause exception which cause the issue + * @param extraInfoFormat extra information regarding the assertion. Generally a set of name/value + * pairs: "id={} fileName={}" + * @param extraInfoArgs extra information arguments + */ + @Override + public RuntimeException fail( + String signature, Throwable cause, String extraInfoFormat, Object... extraInfoArgs) { + Preconditions.checkState( + false, "%s: %s, %s (cause: %s)", signature, extraInfoFormat, extraInfoArgs, cause); + throw new RuntimeException(cause.getMessage()); + } + + /** + * Ensures that an object reference passed as a parameter to the calling method is not null + * + * @param reference an object reference + * @param signature signature, small unique string to identify this assertion within the method, + * like "path_cannot_be_null" + * @return the non-null reference that was validated + * @throws RuntimeException if `reference` is null + */ + @Contract("null, _ -> fail") + public T checkNotNull(final T reference, final String signature) { + return Preconditions.checkNotNull(reference, signature); + } + + /** + * Ensures that an object reference passed as a parameter to the calling method is not null + * + * @param reference an object reference + * @param signature signature, small unique string to identify this assertion within the method, + * like "path_cannot_be_null" + * @param extraInfoFormat extra information regarding the assertion. Generally a set of name/value + * pairs: "id={} fileName={}" + * @param extraInfoArgs extra information arguments + * @return the non-null reference that was validated + * @throws RuntimeException if `reference` is null + */ + @Contract("null, _, _, _ -> fail") + public T checkNotNull( + final T reference, + final String signature, + final String extraInfoFormat, + final Object... extraInfoArgs) { + return Preconditions.checkNotNull( + reference, "%s: %s, %s", signature, extraInfoFormat, Arrays.toString(extraInfoArgs)); + } + + /** + * Create a fatal incident if expression is false + * + * @param expression condition to test for + * @param signature signature, small unique string to identify this assertion within the method, + * like "path_cannot_be_null" + * @throws RuntimeException if `condition` is not true + */ + @Contract("false, _ -> fail") + public void check(final boolean expression, final String signature) { + Preconditions.checkState(expression, signature); + } + + /** + * Create a fatal incident if expression is false + * + * @param expression condition to test for + * @param signature signature, small unique string to identify this assertion within the method, + * like "path_cannot_be_null" + * @param extraInfoFormat extra information regarding the incident. Generally a set of name/value + * pairs: "fileId={} accountId={} fileName={}" + * @param extraInfoArgs extra information arguments + * @throws RuntimeException if condition` is not true + */ + @Contract("false, _, _, _ -> fail") + public void check( + final boolean expression, + final String signature, + final String extraInfoFormat, + final Object... extraInfoArgs) { + Preconditions.checkState( + expression, "%s: %s, %s", signature, extraInfoFormat, Arrays.toString(extraInfoArgs)); + } +} diff --git a/polaris-core/src/main/java/io/polaris/core/PolarisDiagnostics.java b/polaris-core/src/main/java/io/polaris/core/PolarisDiagnostics.java new file mode 100644 index 0000000000..39d7e82414 --- /dev/null +++ b/polaris-core/src/main/java/io/polaris/core/PolarisDiagnostics.java @@ -0,0 +1,96 @@ +package io.polaris.core; + +import org.jetbrains.annotations.Contract; + +public interface PolarisDiagnostics { + + /** + * Fail with an exception + * + * @param signature signature, small unique string to identify this assertion within the method, + * like "path_cannot_be_null" + * @param extraInfoFormat extra information regarding the assertion. Generally a set of name/value + * pairs: "id={} fileName={}" + * @param extraInfoArgs extra information arguments + */ + @Contract("_, _, _ -> fail") + RuntimeException fail( + final String signature, final String extraInfoFormat, final Object... extraInfoArgs); + + /** + * Fail because of an exception + * + * @param signature signature, small unique string to identify this assertion within the method, + * like "path_cannot_be_null" + * @param cause exception which cause the issue + * @param extraInfoFormat extra information regarding the assertion. Generally a set of name/value + * pairs: "id={} fileName={}" + * @param extraInfoArgs extra information arguments + */ + @Contract("_, _, _, _ -> fail") + RuntimeException fail( + final String signature, + final Throwable cause, + final String extraInfoFormat, + final Object... extraInfoArgs); + + /** + * Ensures that an object reference passed as a parameter to the calling method is not null + * + * @param reference an object reference + * @param signature signature, small unique string to identify this assertion within the method, + * like "path_cannot_be_null" + * @return the non-null reference that was validated + * @throws RuntimeException if `reference` is null + */ + @Contract("null, _ -> fail") + T checkNotNull(final T reference, final String signature); + + /** + * Ensures that an object reference passed as a parameter to the calling method is not null + * + * @param reference an object reference + * @param signature signature, small unique string to identify this assertion within the method, + * like "path_cannot_be_null" + * @param extraInfoFormat extra information regarding the assertion. Generally a set of name/value + * pairs: "id={} fileName={}" + * @param extraInfoArgs extra information arguments + * @return the non-null reference that was validated + * @throws RuntimeException if `reference` is null + */ + @Contract("null, _, _, _ -> fail") + T checkNotNull( + final T reference, + final String signature, + final String extraInfoFormat, + final Object... extraInfoArgs); + + /** + * Create a fatal incident if expression is false + * + * @param expression condition to test for + * @param signature signature, small unique string to identify this assertion within the method, + * like "path_cannot_be_null" + * @throws RuntimeException if `condition` is not true + */ + @Contract("false, _ -> fail") + void check(final boolean expression, final String signature); + + /** + * Create a fatal incident if expression is false + * + * @param expression condition to test for + * @param signature signature, small unique string to identify this assertion within the method, + * like "path_cannot_be_null" + * @param extraInfoFormat extra information regarding the incident. Generally a set of name/value + * pairs: "fileId={} accountId={} fileName={}" + * @param extraInfoArgs extra information arguments + * @throws RuntimeException if `condition` is not true + */ + @Contract("false, _, _, _ -> fail") + void check( + final boolean expression, + final String signature, + final String extraInfoFormat, + final Object... extraInfoArgs); +} diff --git a/polaris-core/src/main/java/io/polaris/core/auth/AuthenticatedPolarisPrincipal.java b/polaris-core/src/main/java/io/polaris/core/auth/AuthenticatedPolarisPrincipal.java new file mode 100644 index 0000000000..6e18b9a9ab --- /dev/null +++ b/polaris-core/src/main/java/io/polaris/core/auth/AuthenticatedPolarisPrincipal.java @@ -0,0 +1,53 @@ +package io.polaris.core.auth; + +import io.polaris.core.entity.PolarisEntity; +import io.polaris.core.entity.PrincipalRoleEntity; +import java.util.List; +import java.util.Set; +import org.jetbrains.annotations.NotNull; + +/** Holds the results of request authentication. */ +public class AuthenticatedPolarisPrincipal implements java.security.Principal { + private final PolarisEntity principalEntity; + private final Set activatedPrincipalRoleNames; + // only known and set after the above set of principal role names have been resolved. Before + // this, this list is null + private List activatedPrincipalRoles; + + public AuthenticatedPolarisPrincipal( + @NotNull PolarisEntity principalEntity, @NotNull Set activatedPrincipalRoles) { + this.principalEntity = principalEntity; + this.activatedPrincipalRoleNames = activatedPrincipalRoles; + this.activatedPrincipalRoles = null; + } + + @Override + public String getName() { + return principalEntity.getName(); + } + + public PolarisEntity getPrincipalEntity() { + return principalEntity; + } + + public Set getActivatedPrincipalRoleNames() { + return activatedPrincipalRoleNames; + } + + public List getActivatedPrincipalRoles() { + return activatedPrincipalRoles; + } + + public void setActivatedPrincipalRoles(List activatedPrincipalRoles) { + this.activatedPrincipalRoles = activatedPrincipalRoles; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("principalEntity=" + getPrincipalEntity()); + sb.append(";activatedPrincipalRoleNames=" + getActivatedPrincipalRoleNames()); + sb.append(";activatedPrincipalRoles=" + getActivatedPrincipalRoles()); + return sb.toString(); + } +} diff --git a/polaris-core/src/main/java/io/polaris/core/auth/PolarisAuthorizableOperation.java b/polaris-core/src/main/java/io/polaris/core/auth/PolarisAuthorizableOperation.java new file mode 100644 index 0000000000..4a77858f27 --- /dev/null +++ b/polaris-core/src/main/java/io/polaris/core/auth/PolarisAuthorizableOperation.java @@ -0,0 +1,223 @@ +package io.polaris.core.auth; + +import static io.polaris.core.entity.PolarisPrivilege.CATALOG_CREATE; +import static io.polaris.core.entity.PolarisPrivilege.CATALOG_DROP; +import static io.polaris.core.entity.PolarisPrivilege.CATALOG_LIST; +import static io.polaris.core.entity.PolarisPrivilege.CATALOG_LIST_GRANTS; +import static io.polaris.core.entity.PolarisPrivilege.CATALOG_MANAGE_GRANTS_ON_SECURABLE; +import static io.polaris.core.entity.PolarisPrivilege.CATALOG_READ_PROPERTIES; +import static io.polaris.core.entity.PolarisPrivilege.CATALOG_ROLE_CREATE; +import static io.polaris.core.entity.PolarisPrivilege.CATALOG_ROLE_DROP; +import static io.polaris.core.entity.PolarisPrivilege.CATALOG_ROLE_LIST; +import static io.polaris.core.entity.PolarisPrivilege.CATALOG_ROLE_LIST_GRANTS; +import static io.polaris.core.entity.PolarisPrivilege.CATALOG_ROLE_MANAGE_GRANTS_FOR_GRANTEE; +import static io.polaris.core.entity.PolarisPrivilege.CATALOG_ROLE_MANAGE_GRANTS_ON_SECURABLE; +import static io.polaris.core.entity.PolarisPrivilege.CATALOG_ROLE_READ_PROPERTIES; +import static io.polaris.core.entity.PolarisPrivilege.CATALOG_ROLE_WRITE_PROPERTIES; +import static io.polaris.core.entity.PolarisPrivilege.CATALOG_WRITE_PROPERTIES; +import static io.polaris.core.entity.PolarisPrivilege.NAMESPACE_CREATE; +import static io.polaris.core.entity.PolarisPrivilege.NAMESPACE_DROP; +import static io.polaris.core.entity.PolarisPrivilege.NAMESPACE_LIST; +import static io.polaris.core.entity.PolarisPrivilege.NAMESPACE_LIST_GRANTS; +import static io.polaris.core.entity.PolarisPrivilege.NAMESPACE_MANAGE_GRANTS_ON_SECURABLE; +import static io.polaris.core.entity.PolarisPrivilege.NAMESPACE_READ_PROPERTIES; +import static io.polaris.core.entity.PolarisPrivilege.NAMESPACE_WRITE_PROPERTIES; +import static io.polaris.core.entity.PolarisPrivilege.PRINCIPAL_CREATE; +import static io.polaris.core.entity.PolarisPrivilege.PRINCIPAL_DROP; +import static io.polaris.core.entity.PolarisPrivilege.PRINCIPAL_LIST; +import static io.polaris.core.entity.PolarisPrivilege.PRINCIPAL_LIST_GRANTS; +import static io.polaris.core.entity.PolarisPrivilege.PRINCIPAL_MANAGE_GRANTS_FOR_GRANTEE; +import static io.polaris.core.entity.PolarisPrivilege.PRINCIPAL_MANAGE_GRANTS_ON_SECURABLE; +import static io.polaris.core.entity.PolarisPrivilege.PRINCIPAL_READ_PROPERTIES; +import static io.polaris.core.entity.PolarisPrivilege.PRINCIPAL_RESET_CREDENTIALS; +import static io.polaris.core.entity.PolarisPrivilege.PRINCIPAL_ROLE_CREATE; +import static io.polaris.core.entity.PolarisPrivilege.PRINCIPAL_ROLE_DROP; +import static io.polaris.core.entity.PolarisPrivilege.PRINCIPAL_ROLE_LIST; +import static io.polaris.core.entity.PolarisPrivilege.PRINCIPAL_ROLE_LIST_GRANTS; +import static io.polaris.core.entity.PolarisPrivilege.PRINCIPAL_ROLE_MANAGE_GRANTS_FOR_GRANTEE; +import static io.polaris.core.entity.PolarisPrivilege.PRINCIPAL_ROLE_MANAGE_GRANTS_ON_SECURABLE; +import static io.polaris.core.entity.PolarisPrivilege.PRINCIPAL_ROLE_READ_PROPERTIES; +import static io.polaris.core.entity.PolarisPrivilege.PRINCIPAL_ROLE_WRITE_PROPERTIES; +import static io.polaris.core.entity.PolarisPrivilege.PRINCIPAL_ROTATE_CREDENTIALS; +import static io.polaris.core.entity.PolarisPrivilege.PRINCIPAL_WRITE_PROPERTIES; +import static io.polaris.core.entity.PolarisPrivilege.SERVICE_MANAGE_ACCESS; +import static io.polaris.core.entity.PolarisPrivilege.TABLE_CREATE; +import static io.polaris.core.entity.PolarisPrivilege.TABLE_DROP; +import static io.polaris.core.entity.PolarisPrivilege.TABLE_LIST; +import static io.polaris.core.entity.PolarisPrivilege.TABLE_LIST_GRANTS; +import static io.polaris.core.entity.PolarisPrivilege.TABLE_MANAGE_GRANTS_ON_SECURABLE; +import static io.polaris.core.entity.PolarisPrivilege.TABLE_READ_DATA; +import static io.polaris.core.entity.PolarisPrivilege.TABLE_READ_PROPERTIES; +import static io.polaris.core.entity.PolarisPrivilege.TABLE_WRITE_DATA; +import static io.polaris.core.entity.PolarisPrivilege.TABLE_WRITE_PROPERTIES; +import static io.polaris.core.entity.PolarisPrivilege.VIEW_CREATE; +import static io.polaris.core.entity.PolarisPrivilege.VIEW_DROP; +import static io.polaris.core.entity.PolarisPrivilege.VIEW_LIST; +import static io.polaris.core.entity.PolarisPrivilege.VIEW_LIST_GRANTS; +import static io.polaris.core.entity.PolarisPrivilege.VIEW_MANAGE_GRANTS_ON_SECURABLE; +import static io.polaris.core.entity.PolarisPrivilege.VIEW_READ_PROPERTIES; +import static io.polaris.core.entity.PolarisPrivilege.VIEW_WRITE_PROPERTIES; + +import io.polaris.core.entity.PolarisPrivilege; +import java.util.EnumSet; + +/** + * Denotes the fine-grained expansion of all Polaris operations that are associated with some set of + * authorization requirements to enact. + */ +public enum PolarisAuthorizableOperation { + LIST_NAMESPACES(NAMESPACE_LIST), + CREATE_NAMESPACE(NAMESPACE_CREATE), + LOAD_NAMESPACE_METADATA(NAMESPACE_READ_PROPERTIES), + NAMESPACE_EXISTS(NAMESPACE_LIST), + DROP_NAMESPACE(NAMESPACE_DROP), + UPDATE_NAMESPACE_PROPERTIES(NAMESPACE_WRITE_PROPERTIES), + LIST_TABLES(TABLE_LIST), + CREATE_TABLE_DIRECT(TABLE_CREATE), + CREATE_TABLE_STAGED(TABLE_CREATE), + CREATE_TABLE_STAGED_WITH_WRITE_DELEGATION(EnumSet.of(TABLE_CREATE, TABLE_WRITE_DATA)), + REGISTER_TABLE(TABLE_CREATE), + LOAD_TABLE(TABLE_READ_PROPERTIES), + LOAD_TABLE_WITH_READ_DELEGATION(TABLE_READ_DATA), + LOAD_TABLE_WITH_WRITE_DELEGATION(TABLE_WRITE_DATA), + UPDATE_TABLE(TABLE_WRITE_PROPERTIES), + UPDATE_TABLE_FOR_STAGED_CREATE(TABLE_CREATE), + DROP_TABLE_WITHOUT_PURGE(TABLE_DROP), + DROP_TABLE_WITH_PURGE(EnumSet.of(TABLE_DROP, TABLE_WRITE_DATA)), + TABLE_EXISTS(TABLE_LIST), + RENAME_TABLE(TABLE_DROP, EnumSet.of(TABLE_LIST, TABLE_CREATE)), + COMMIT_TRANSACTION(EnumSet.of(TABLE_WRITE_PROPERTIES, TABLE_CREATE)), + LIST_VIEWS(VIEW_LIST), + CREATE_VIEW(VIEW_CREATE), + LOAD_VIEW(VIEW_READ_PROPERTIES), + REPLACE_VIEW(VIEW_WRITE_PROPERTIES), + DROP_VIEW(VIEW_DROP), + VIEW_EXISTS(VIEW_LIST), + RENAME_VIEW(VIEW_DROP, EnumSet.of(VIEW_LIST, VIEW_CREATE)), + REPORT_METRICS(EnumSet.noneOf(PolarisPrivilege.class)), + SEND_NOTIFICATIONS( + EnumSet.of( + TABLE_CREATE, TABLE_WRITE_PROPERTIES, TABLE_DROP, NAMESPACE_CREATE, NAMESPACE_DROP)), + LIST_CATALOGS(CATALOG_LIST), + CREATE_CATALOG(CATALOG_CREATE), + GET_CATALOG(CATALOG_READ_PROPERTIES), + UPDATE_CATALOG(CATALOG_WRITE_PROPERTIES), + DELETE_CATALOG(CATALOG_DROP), + LIST_PRINCIPALS(PRINCIPAL_LIST), + CREATE_PRINCIPAL(PRINCIPAL_CREATE), + GET_PRINCIPAL(PRINCIPAL_READ_PROPERTIES), + UPDATE_PRINCIPAL(PRINCIPAL_WRITE_PROPERTIES), + DELETE_PRINCIPAL(PRINCIPAL_DROP), + ROTATE_CREDENTIALS(PRINCIPAL_ROTATE_CREDENTIALS), + RESET_CREDENTIALS(PRINCIPAL_RESET_CREDENTIALS), + LIST_PRINCIPAL_ROLES_ASSIGNED(PRINCIPAL_LIST_GRANTS), + ASSIGN_PRINCIPAL_ROLE( + PRINCIPAL_ROLE_MANAGE_GRANTS_ON_SECURABLE, PRINCIPAL_MANAGE_GRANTS_FOR_GRANTEE), + REVOKE_PRINCIPAL_ROLE( + PRINCIPAL_ROLE_MANAGE_GRANTS_ON_SECURABLE, PRINCIPAL_MANAGE_GRANTS_FOR_GRANTEE), + LIST_PRINCIPAL_ROLES(PRINCIPAL_ROLE_LIST), + CREATE_PRINCIPAL_ROLE(PRINCIPAL_ROLE_CREATE), + GET_PRINCIPAL_ROLE(PRINCIPAL_ROLE_READ_PROPERTIES), + UPDATE_PRINCIPAL_ROLE(PRINCIPAL_ROLE_WRITE_PROPERTIES), + DELETE_PRINCIPAL_ROLE(PRINCIPAL_ROLE_DROP), + LIST_ASSIGNEE_PRINCIPALS_FOR_PRINCIPAL_ROLE(PRINCIPAL_ROLE_LIST_GRANTS), + LIST_CATALOG_ROLES_FOR_PRINCIPAL_ROLE(PRINCIPAL_ROLE_LIST_GRANTS), + ASSIGN_CATALOG_ROLE_TO_PRINCIPAL_ROLE( + CATALOG_ROLE_MANAGE_GRANTS_ON_SECURABLE, PRINCIPAL_ROLE_MANAGE_GRANTS_FOR_GRANTEE), + REVOKE_CATALOG_ROLE_FROM_PRINCIPAL_ROLE( + CATALOG_ROLE_MANAGE_GRANTS_ON_SECURABLE, PRINCIPAL_ROLE_MANAGE_GRANTS_FOR_GRANTEE), + LIST_CATALOG_ROLES(CATALOG_ROLE_LIST), + CREATE_CATALOG_ROLE(CATALOG_ROLE_CREATE), + GET_CATALOG_ROLE(CATALOG_ROLE_READ_PROPERTIES), + UPDATE_CATALOG_ROLE(CATALOG_ROLE_WRITE_PROPERTIES), + DELETE_CATALOG_ROLE(CATALOG_ROLE_DROP), + LIST_ASSIGNEE_PRINCIPAL_ROLES_FOR_CATALOG_ROLE(CATALOG_ROLE_LIST_GRANTS), + LIST_GRANTS_FOR_CATALOG_ROLE(CATALOG_ROLE_LIST_GRANTS), + ADD_ROOT_GRANT_TO_PRINCIPAL_ROLE(SERVICE_MANAGE_ACCESS, PRINCIPAL_ROLE_MANAGE_GRANTS_FOR_GRANTEE), + REVOKE_ROOT_GRANT_FROM_PRINCIPAL_ROLE( + SERVICE_MANAGE_ACCESS, PRINCIPAL_ROLE_MANAGE_GRANTS_FOR_GRANTEE), + LIST_GRANTS_ON_ROOT(SERVICE_MANAGE_ACCESS), + ADD_PRINCIPAL_GRANT_TO_PRINCIPAL_ROLE( + PRINCIPAL_MANAGE_GRANTS_ON_SECURABLE, PRINCIPAL_ROLE_MANAGE_GRANTS_FOR_GRANTEE), + REVOKE_PRINCIPAL_GRANT_FROM_PRINCIPAL_ROLE( + PRINCIPAL_MANAGE_GRANTS_ON_SECURABLE, PRINCIPAL_ROLE_MANAGE_GRANTS_FOR_GRANTEE), + LIST_GRANTS_ON_PRINCIPAL(PRINCIPAL_LIST_GRANTS), + ADD_PRINCIPAL_ROLE_GRANT_TO_PRINCIPAL_ROLE( + PRINCIPAL_ROLE_MANAGE_GRANTS_ON_SECURABLE, PRINCIPAL_ROLE_MANAGE_GRANTS_FOR_GRANTEE), + REVOKE_PRINCIPAL_ROLE_GRANT_FROM_PRINCIPAL_ROLE( + PRINCIPAL_ROLE_MANAGE_GRANTS_ON_SECURABLE, PRINCIPAL_ROLE_MANAGE_GRANTS_FOR_GRANTEE), + LIST_GRANTS_ON_PRINCIPAL_ROLE(PRINCIPAL_ROLE_LIST_GRANTS), + ADD_CATALOG_ROLE_GRANT_TO_CATALOG_ROLE( + CATALOG_ROLE_MANAGE_GRANTS_ON_SECURABLE, CATALOG_ROLE_MANAGE_GRANTS_FOR_GRANTEE), + REVOKE_CATALOG_ROLE_GRANT_FROM_CATALOG_ROLE( + CATALOG_ROLE_MANAGE_GRANTS_ON_SECURABLE, CATALOG_ROLE_MANAGE_GRANTS_FOR_GRANTEE), + LIST_GRANTS_ON_CATALOG_ROLE(CATALOG_ROLE_LIST_GRANTS), + ADD_CATALOG_GRANT_TO_CATALOG_ROLE( + CATALOG_MANAGE_GRANTS_ON_SECURABLE, CATALOG_ROLE_MANAGE_GRANTS_FOR_GRANTEE), + REVOKE_CATALOG_GRANT_FROM_CATALOG_ROLE( + CATALOG_MANAGE_GRANTS_ON_SECURABLE, CATALOG_ROLE_MANAGE_GRANTS_FOR_GRANTEE), + LIST_GRANTS_ON_CATALOG(CATALOG_LIST_GRANTS), + ADD_NAMESPACE_GRANT_TO_CATALOG_ROLE( + NAMESPACE_MANAGE_GRANTS_ON_SECURABLE, CATALOG_ROLE_MANAGE_GRANTS_FOR_GRANTEE), + REVOKE_NAMESPACE_GRANT_FROM_CATALOG_ROLE( + NAMESPACE_MANAGE_GRANTS_ON_SECURABLE, CATALOG_ROLE_MANAGE_GRANTS_FOR_GRANTEE), + LIST_GRANTS_ON_NAMESPACE(NAMESPACE_LIST_GRANTS), + ADD_TABLE_GRANT_TO_CATALOG_ROLE( + TABLE_MANAGE_GRANTS_ON_SECURABLE, CATALOG_ROLE_MANAGE_GRANTS_FOR_GRANTEE), + REVOKE_TABLE_GRANT_FROM_CATALOG_ROLE( + TABLE_MANAGE_GRANTS_ON_SECURABLE, CATALOG_ROLE_MANAGE_GRANTS_FOR_GRANTEE), + LIST_GRANTS_ON_TABLE(TABLE_LIST_GRANTS), + ADD_VIEW_GRANT_TO_CATALOG_ROLE( + VIEW_MANAGE_GRANTS_ON_SECURABLE, CATALOG_ROLE_MANAGE_GRANTS_FOR_GRANTEE), + REVOKE_VIEW_GRANT_FROM_CATALOG_ROLE( + VIEW_MANAGE_GRANTS_ON_SECURABLE, CATALOG_ROLE_MANAGE_GRANTS_FOR_GRANTEE), + LIST_GRANTS_ON_VIEW(VIEW_LIST_GRANTS), + ; + + private final EnumSet privilegesOnTarget; + private final EnumSet privilegesOnSecondary; + + /** Most common case -- single privilege on target entities. */ + PolarisAuthorizableOperation(PolarisPrivilege targetPrivilege) { + this(targetPrivilege == null ? null : EnumSet.of(targetPrivilege), null); + } + + /** Require multiple simultaneous privileges on target entities. */ + PolarisAuthorizableOperation(EnumSet privilegesOnTarget) { + this(privilegesOnTarget, null); + } + + /** Single privilege on target entities, multiple privileges on secondary. */ + PolarisAuthorizableOperation( + PolarisPrivilege targetPrivilege, EnumSet privilegesOnSecondary) { + this(targetPrivilege == null ? null : EnumSet.of(targetPrivilege), privilegesOnSecondary); + } + + /** Single privilege on target, single privilege on targetParent. */ + PolarisAuthorizableOperation( + PolarisPrivilege targetPrivilege, PolarisPrivilege secondaryPrivilege) { + this( + targetPrivilege == null ? null : EnumSet.of(targetPrivilege), + secondaryPrivilege == null ? null : EnumSet.of(secondaryPrivilege)); + } + + /** EnumSets on target, targetParent */ + PolarisAuthorizableOperation( + EnumSet privilegesOnTarget, + EnumSet privilegesOnSecondary) { + this.privilegesOnTarget = + privilegesOnTarget == null ? EnumSet.noneOf(PolarisPrivilege.class) : privilegesOnTarget; + this.privilegesOnSecondary = + privilegesOnSecondary == null + ? EnumSet.noneOf(PolarisPrivilege.class) + : privilegesOnSecondary; + } + + public EnumSet getPrivilegesOnTarget() { + return privilegesOnTarget; + } + + public EnumSet getPrivilegesOnSecondary() { + return privilegesOnSecondary; + } +} diff --git a/polaris-core/src/main/java/io/polaris/core/auth/PolarisAuthorizer.java b/polaris-core/src/main/java/io/polaris/core/auth/PolarisAuthorizer.java new file mode 100644 index 0000000000..60dbf88f69 --- /dev/null +++ b/polaris-core/src/main/java/io/polaris/core/auth/PolarisAuthorizer.java @@ -0,0 +1,615 @@ +package io.polaris.core.auth; + +import static io.polaris.core.entity.PolarisPrivilege.CATALOG_CREATE; +import static io.polaris.core.entity.PolarisPrivilege.CATALOG_DROP; +import static io.polaris.core.entity.PolarisPrivilege.CATALOG_FULL_METADATA; +import static io.polaris.core.entity.PolarisPrivilege.CATALOG_LIST; +import static io.polaris.core.entity.PolarisPrivilege.CATALOG_LIST_GRANTS; +import static io.polaris.core.entity.PolarisPrivilege.CATALOG_MANAGE_ACCESS; +import static io.polaris.core.entity.PolarisPrivilege.CATALOG_MANAGE_CONTENT; +import static io.polaris.core.entity.PolarisPrivilege.CATALOG_MANAGE_GRANTS_ON_SECURABLE; +import static io.polaris.core.entity.PolarisPrivilege.CATALOG_MANAGE_METADATA; +import static io.polaris.core.entity.PolarisPrivilege.CATALOG_READ_PROPERTIES; +import static io.polaris.core.entity.PolarisPrivilege.CATALOG_ROLE_CREATE; +import static io.polaris.core.entity.PolarisPrivilege.CATALOG_ROLE_DROP; +import static io.polaris.core.entity.PolarisPrivilege.CATALOG_ROLE_FULL_METADATA; +import static io.polaris.core.entity.PolarisPrivilege.CATALOG_ROLE_LIST; +import static io.polaris.core.entity.PolarisPrivilege.CATALOG_ROLE_LIST_GRANTS; +import static io.polaris.core.entity.PolarisPrivilege.CATALOG_ROLE_MANAGE_GRANTS_FOR_GRANTEE; +import static io.polaris.core.entity.PolarisPrivilege.CATALOG_ROLE_MANAGE_GRANTS_ON_SECURABLE; +import static io.polaris.core.entity.PolarisPrivilege.CATALOG_ROLE_READ_PROPERTIES; +import static io.polaris.core.entity.PolarisPrivilege.CATALOG_ROLE_USAGE; +import static io.polaris.core.entity.PolarisPrivilege.CATALOG_ROLE_WRITE_PROPERTIES; +import static io.polaris.core.entity.PolarisPrivilege.CATALOG_WRITE_PROPERTIES; +import static io.polaris.core.entity.PolarisPrivilege.NAMESPACE_CREATE; +import static io.polaris.core.entity.PolarisPrivilege.NAMESPACE_DROP; +import static io.polaris.core.entity.PolarisPrivilege.NAMESPACE_FULL_METADATA; +import static io.polaris.core.entity.PolarisPrivilege.NAMESPACE_LIST; +import static io.polaris.core.entity.PolarisPrivilege.NAMESPACE_LIST_GRANTS; +import static io.polaris.core.entity.PolarisPrivilege.NAMESPACE_MANAGE_GRANTS_ON_SECURABLE; +import static io.polaris.core.entity.PolarisPrivilege.NAMESPACE_READ_PROPERTIES; +import static io.polaris.core.entity.PolarisPrivilege.NAMESPACE_WRITE_PROPERTIES; +import static io.polaris.core.entity.PolarisPrivilege.PRINCIPAL_CREATE; +import static io.polaris.core.entity.PolarisPrivilege.PRINCIPAL_DROP; +import static io.polaris.core.entity.PolarisPrivilege.PRINCIPAL_FULL_METADATA; +import static io.polaris.core.entity.PolarisPrivilege.PRINCIPAL_LIST; +import static io.polaris.core.entity.PolarisPrivilege.PRINCIPAL_LIST_GRANTS; +import static io.polaris.core.entity.PolarisPrivilege.PRINCIPAL_MANAGE_GRANTS_FOR_GRANTEE; +import static io.polaris.core.entity.PolarisPrivilege.PRINCIPAL_MANAGE_GRANTS_ON_SECURABLE; +import static io.polaris.core.entity.PolarisPrivilege.PRINCIPAL_READ_PROPERTIES; +import static io.polaris.core.entity.PolarisPrivilege.PRINCIPAL_RESET_CREDENTIALS; +import static io.polaris.core.entity.PolarisPrivilege.PRINCIPAL_ROLE_CREATE; +import static io.polaris.core.entity.PolarisPrivilege.PRINCIPAL_ROLE_DROP; +import static io.polaris.core.entity.PolarisPrivilege.PRINCIPAL_ROLE_FULL_METADATA; +import static io.polaris.core.entity.PolarisPrivilege.PRINCIPAL_ROLE_LIST; +import static io.polaris.core.entity.PolarisPrivilege.PRINCIPAL_ROLE_LIST_GRANTS; +import static io.polaris.core.entity.PolarisPrivilege.PRINCIPAL_ROLE_MANAGE_GRANTS_FOR_GRANTEE; +import static io.polaris.core.entity.PolarisPrivilege.PRINCIPAL_ROLE_MANAGE_GRANTS_ON_SECURABLE; +import static io.polaris.core.entity.PolarisPrivilege.PRINCIPAL_ROLE_READ_PROPERTIES; +import static io.polaris.core.entity.PolarisPrivilege.PRINCIPAL_ROLE_USAGE; +import static io.polaris.core.entity.PolarisPrivilege.PRINCIPAL_ROLE_WRITE_PROPERTIES; +import static io.polaris.core.entity.PolarisPrivilege.PRINCIPAL_ROTATE_CREDENTIALS; +import static io.polaris.core.entity.PolarisPrivilege.PRINCIPAL_WRITE_PROPERTIES; +import static io.polaris.core.entity.PolarisPrivilege.SERVICE_MANAGE_ACCESS; +import static io.polaris.core.entity.PolarisPrivilege.TABLE_CREATE; +import static io.polaris.core.entity.PolarisPrivilege.TABLE_DROP; +import static io.polaris.core.entity.PolarisPrivilege.TABLE_FULL_METADATA; +import static io.polaris.core.entity.PolarisPrivilege.TABLE_LIST; +import static io.polaris.core.entity.PolarisPrivilege.TABLE_LIST_GRANTS; +import static io.polaris.core.entity.PolarisPrivilege.TABLE_MANAGE_GRANTS_ON_SECURABLE; +import static io.polaris.core.entity.PolarisPrivilege.TABLE_READ_DATA; +import static io.polaris.core.entity.PolarisPrivilege.TABLE_READ_PROPERTIES; +import static io.polaris.core.entity.PolarisPrivilege.TABLE_WRITE_DATA; +import static io.polaris.core.entity.PolarisPrivilege.TABLE_WRITE_PROPERTIES; +import static io.polaris.core.entity.PolarisPrivilege.VIEW_CREATE; +import static io.polaris.core.entity.PolarisPrivilege.VIEW_DROP; +import static io.polaris.core.entity.PolarisPrivilege.VIEW_FULL_METADATA; +import static io.polaris.core.entity.PolarisPrivilege.VIEW_LIST; +import static io.polaris.core.entity.PolarisPrivilege.VIEW_LIST_GRANTS; +import static io.polaris.core.entity.PolarisPrivilege.VIEW_MANAGE_GRANTS_ON_SECURABLE; +import static io.polaris.core.entity.PolarisPrivilege.VIEW_READ_PROPERTIES; +import static io.polaris.core.entity.PolarisPrivilege.VIEW_WRITE_PROPERTIES; + +import com.google.common.base.Preconditions; +import com.google.common.collect.HashMultimap; +import com.google.common.collect.SetMultimap; +import io.polaris.core.PolarisConfiguration; +import io.polaris.core.PolarisConfigurationStore; +import io.polaris.core.context.CallContext; +import io.polaris.core.entity.PolarisEntityConstants; +import io.polaris.core.entity.PolarisGrantRecord; +import io.polaris.core.entity.PolarisPrivilege; +import io.polaris.core.persistence.PolarisResolvedPathWrapper; +import io.polaris.core.persistence.ResolvedPolarisEntity; +import java.util.List; +import java.util.Set; +import org.apache.iceberg.exceptions.ForbiddenException; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Performs hierarchical resolution logic by matching the transively expanded set of grants to a + * calling principal against the cascading permissions over the parent hierarchy of a target + * Securable. + * + *

Additionally, encompasses "specialty" permission resolution logic, such as checking whether + * the expanded roles of the calling Principal hold SERVICE_MANAGE_ACCESS on the "root" catalog, + * which translates into a cross-catalog permission. + */ +public class PolarisAuthorizer { + private static final Logger LOG = LoggerFactory.getLogger(PolarisAuthorizer.class); + + private static final SetMultimap SUPER_PRIVILEGES = + HashMultimap.create(); + + static { + SUPER_PRIVILEGES.putAll(SERVICE_MANAGE_ACCESS, List.of(SERVICE_MANAGE_ACCESS)); + SUPER_PRIVILEGES.putAll(CATALOG_MANAGE_ACCESS, List.of(CATALOG_MANAGE_ACCESS)); + SUPER_PRIVILEGES.putAll(CATALOG_ROLE_USAGE, List.of(CATALOG_ROLE_USAGE)); + SUPER_PRIVILEGES.putAll(PRINCIPAL_ROLE_USAGE, List.of(PRINCIPAL_ROLE_USAGE)); + + // Namespace, Table, View privileges + SUPER_PRIVILEGES.putAll( + NAMESPACE_CREATE, + List.of( + CATALOG_MANAGE_CONTENT, + CATALOG_MANAGE_METADATA, + NAMESPACE_CREATE, + NAMESPACE_FULL_METADATA)); + SUPER_PRIVILEGES.putAll( + TABLE_CREATE, + List.of( + CATALOG_MANAGE_CONTENT, CATALOG_MANAGE_METADATA, TABLE_CREATE, TABLE_FULL_METADATA)); + SUPER_PRIVILEGES.putAll( + VIEW_CREATE, + List.of(CATALOG_MANAGE_CONTENT, CATALOG_MANAGE_METADATA, VIEW_CREATE, VIEW_FULL_METADATA)); + SUPER_PRIVILEGES.putAll( + NAMESPACE_DROP, + List.of( + CATALOG_MANAGE_CONTENT, + CATALOG_MANAGE_METADATA, + NAMESPACE_DROP, + NAMESPACE_FULL_METADATA)); + SUPER_PRIVILEGES.putAll( + TABLE_DROP, + List.of(CATALOG_MANAGE_CONTENT, CATALOG_MANAGE_METADATA, TABLE_DROP, TABLE_FULL_METADATA)); + SUPER_PRIVILEGES.putAll( + VIEW_DROP, + List.of(CATALOG_MANAGE_CONTENT, CATALOG_MANAGE_METADATA, VIEW_DROP, VIEW_FULL_METADATA)); + SUPER_PRIVILEGES.putAll( + NAMESPACE_LIST, + List.of( + CATALOG_MANAGE_CONTENT, + CATALOG_MANAGE_METADATA, + NAMESPACE_CREATE, + NAMESPACE_FULL_METADATA, + NAMESPACE_LIST, + NAMESPACE_READ_PROPERTIES, + NAMESPACE_WRITE_PROPERTIES)); + SUPER_PRIVILEGES.putAll( + TABLE_LIST, + List.of( + CATALOG_MANAGE_CONTENT, + CATALOG_MANAGE_METADATA, + TABLE_CREATE, + TABLE_FULL_METADATA, + TABLE_LIST, + TABLE_READ_DATA, + TABLE_READ_PROPERTIES, + TABLE_WRITE_DATA, + TABLE_WRITE_PROPERTIES)); + SUPER_PRIVILEGES.putAll( + VIEW_LIST, + List.of( + CATALOG_MANAGE_CONTENT, + CATALOG_MANAGE_METADATA, + VIEW_CREATE, + VIEW_FULL_METADATA, + VIEW_LIST, + VIEW_READ_PROPERTIES, + VIEW_WRITE_PROPERTIES)); + SUPER_PRIVILEGES.putAll( + NAMESPACE_READ_PROPERTIES, + List.of( + CATALOG_MANAGE_CONTENT, + CATALOG_MANAGE_METADATA, + NAMESPACE_FULL_METADATA, + NAMESPACE_READ_PROPERTIES, + NAMESPACE_WRITE_PROPERTIES)); + SUPER_PRIVILEGES.putAll( + TABLE_READ_PROPERTIES, + List.of( + CATALOG_MANAGE_CONTENT, + CATALOG_MANAGE_METADATA, + TABLE_FULL_METADATA, + TABLE_READ_DATA, + TABLE_READ_PROPERTIES, + TABLE_WRITE_DATA, + TABLE_WRITE_PROPERTIES)); + SUPER_PRIVILEGES.putAll( + VIEW_READ_PROPERTIES, + List.of( + CATALOG_MANAGE_CONTENT, + CATALOG_MANAGE_METADATA, + VIEW_FULL_METADATA, + VIEW_READ_PROPERTIES, + VIEW_WRITE_PROPERTIES)); + SUPER_PRIVILEGES.putAll( + NAMESPACE_WRITE_PROPERTIES, + List.of( + CATALOG_MANAGE_CONTENT, + CATALOG_MANAGE_METADATA, + NAMESPACE_FULL_METADATA, + NAMESPACE_WRITE_PROPERTIES)); + SUPER_PRIVILEGES.putAll( + TABLE_WRITE_PROPERTIES, + List.of( + CATALOG_MANAGE_CONTENT, + CATALOG_MANAGE_METADATA, + TABLE_FULL_METADATA, + TABLE_WRITE_DATA, + TABLE_WRITE_PROPERTIES)); + SUPER_PRIVILEGES.putAll( + VIEW_WRITE_PROPERTIES, + List.of( + CATALOG_MANAGE_CONTENT, + CATALOG_MANAGE_METADATA, + VIEW_FULL_METADATA, + VIEW_WRITE_PROPERTIES)); + SUPER_PRIVILEGES.putAll( + TABLE_READ_DATA, List.of(CATALOG_MANAGE_CONTENT, TABLE_READ_DATA, TABLE_WRITE_DATA)); + SUPER_PRIVILEGES.putAll(TABLE_WRITE_DATA, List.of(CATALOG_MANAGE_CONTENT, TABLE_WRITE_DATA)); + SUPER_PRIVILEGES.putAll( + NAMESPACE_FULL_METADATA, + List.of(CATALOG_MANAGE_CONTENT, CATALOG_MANAGE_METADATA, NAMESPACE_FULL_METADATA)); + SUPER_PRIVILEGES.putAll( + TABLE_FULL_METADATA, + List.of(CATALOG_MANAGE_CONTENT, CATALOG_MANAGE_METADATA, TABLE_FULL_METADATA)); + SUPER_PRIVILEGES.putAll( + VIEW_FULL_METADATA, + List.of(CATALOG_MANAGE_CONTENT, CATALOG_MANAGE_METADATA, VIEW_FULL_METADATA)); + + // Catalog privileges + SUPER_PRIVILEGES.putAll( + CATALOG_MANAGE_METADATA, List.of(CATALOG_MANAGE_METADATA, CATALOG_MANAGE_CONTENT)); + SUPER_PRIVILEGES.putAll(CATALOG_MANAGE_CONTENT, List.of(CATALOG_MANAGE_CONTENT)); + SUPER_PRIVILEGES.putAll( + CATALOG_CREATE, List.of(CATALOG_CREATE, CATALOG_FULL_METADATA, SERVICE_MANAGE_ACCESS)); + SUPER_PRIVILEGES.putAll( + CATALOG_DROP, List.of(CATALOG_DROP, CATALOG_FULL_METADATA, SERVICE_MANAGE_ACCESS)); + SUPER_PRIVILEGES.putAll( + CATALOG_LIST, + List.of( + CATALOG_CREATE, + CATALOG_FULL_METADATA, + CATALOG_LIST, + CATALOG_MANAGE_CONTENT, + CATALOG_MANAGE_METADATA, + CATALOG_READ_PROPERTIES, + CATALOG_WRITE_PROPERTIES, + SERVICE_MANAGE_ACCESS)); + SUPER_PRIVILEGES.putAll( + CATALOG_READ_PROPERTIES, + List.of( + CATALOG_FULL_METADATA, + CATALOG_MANAGE_CONTENT, + CATALOG_MANAGE_METADATA, + CATALOG_READ_PROPERTIES, + CATALOG_WRITE_PROPERTIES, + SERVICE_MANAGE_ACCESS)); + SUPER_PRIVILEGES.putAll( + CATALOG_WRITE_PROPERTIES, + List.of( + CATALOG_FULL_METADATA, + CATALOG_MANAGE_CONTENT, + CATALOG_MANAGE_METADATA, + CATALOG_WRITE_PROPERTIES, + SERVICE_MANAGE_ACCESS)); + SUPER_PRIVILEGES.putAll( + CATALOG_FULL_METADATA, List.of(CATALOG_FULL_METADATA, SERVICE_MANAGE_ACCESS)); + + // _LIST_GRANTS + SUPER_PRIVILEGES.putAll( + PRINCIPAL_LIST_GRANTS, + List.of( + PRINCIPAL_LIST_GRANTS, + PRINCIPAL_MANAGE_GRANTS_ON_SECURABLE, + PRINCIPAL_MANAGE_GRANTS_FOR_GRANTEE, + SERVICE_MANAGE_ACCESS)); + SUPER_PRIVILEGES.putAll( + PRINCIPAL_ROLE_LIST_GRANTS, + List.of( + PRINCIPAL_ROLE_LIST_GRANTS, + PRINCIPAL_ROLE_MANAGE_GRANTS_ON_SECURABLE, + PRINCIPAL_ROLE_MANAGE_GRANTS_FOR_GRANTEE, + SERVICE_MANAGE_ACCESS)); + SUPER_PRIVILEGES.putAll( + CATALOG_ROLE_LIST_GRANTS, + List.of( + CATALOG_ROLE_LIST_GRANTS, + CATALOG_ROLE_MANAGE_GRANTS_ON_SECURABLE, + CATALOG_ROLE_MANAGE_GRANTS_FOR_GRANTEE, + CATALOG_MANAGE_ACCESS)); + SUPER_PRIVILEGES.putAll( + CATALOG_LIST_GRANTS, + List.of(CATALOG_LIST_GRANTS, CATALOG_MANAGE_GRANTS_ON_SECURABLE, CATALOG_MANAGE_ACCESS)); + SUPER_PRIVILEGES.putAll( + NAMESPACE_LIST_GRANTS, + List.of( + NAMESPACE_LIST_GRANTS, NAMESPACE_MANAGE_GRANTS_ON_SECURABLE, CATALOG_MANAGE_ACCESS)); + SUPER_PRIVILEGES.putAll( + TABLE_LIST_GRANTS, + List.of(TABLE_LIST_GRANTS, TABLE_MANAGE_GRANTS_ON_SECURABLE, CATALOG_MANAGE_ACCESS)); + SUPER_PRIVILEGES.putAll( + VIEW_LIST_GRANTS, + List.of(VIEW_LIST_GRANTS, VIEW_MANAGE_GRANTS_ON_SECURABLE, CATALOG_MANAGE_ACCESS)); + + // _MANAGE_GRANTS_ON_SECURABLE for CATALOG, NAMESPACE, TABLE, VIEW + SUPER_PRIVILEGES.putAll( + CATALOG_MANAGE_GRANTS_ON_SECURABLE, + List.of(CATALOG_MANAGE_GRANTS_ON_SECURABLE, CATALOG_MANAGE_ACCESS)); + SUPER_PRIVILEGES.putAll( + NAMESPACE_MANAGE_GRANTS_ON_SECURABLE, + List.of(NAMESPACE_MANAGE_GRANTS_ON_SECURABLE, CATALOG_MANAGE_ACCESS)); + SUPER_PRIVILEGES.putAll( + TABLE_MANAGE_GRANTS_ON_SECURABLE, + List.of(TABLE_MANAGE_GRANTS_ON_SECURABLE, CATALOG_MANAGE_ACCESS)); + SUPER_PRIVILEGES.putAll( + VIEW_MANAGE_GRANTS_ON_SECURABLE, + List.of(VIEW_MANAGE_GRANTS_ON_SECURABLE, CATALOG_MANAGE_ACCESS)); + + // PRINCIPAL CRUDL + SUPER_PRIVILEGES.putAll( + PRINCIPAL_CREATE, + List.of(PRINCIPAL_CREATE, PRINCIPAL_FULL_METADATA, SERVICE_MANAGE_ACCESS)); + SUPER_PRIVILEGES.putAll( + PRINCIPAL_DROP, List.of(PRINCIPAL_DROP, PRINCIPAL_FULL_METADATA, SERVICE_MANAGE_ACCESS)); + SUPER_PRIVILEGES.putAll( + PRINCIPAL_LIST, + List.of( + PRINCIPAL_LIST, + PRINCIPAL_CREATE, + PRINCIPAL_READ_PROPERTIES, + PRINCIPAL_WRITE_PROPERTIES, + PRINCIPAL_FULL_METADATA, + SERVICE_MANAGE_ACCESS)); + SUPER_PRIVILEGES.putAll( + PRINCIPAL_READ_PROPERTIES, + List.of( + PRINCIPAL_READ_PROPERTIES, + PRINCIPAL_WRITE_PROPERTIES, + PRINCIPAL_FULL_METADATA, + SERVICE_MANAGE_ACCESS)); + SUPER_PRIVILEGES.putAll( + PRINCIPAL_WRITE_PROPERTIES, + List.of(PRINCIPAL_WRITE_PROPERTIES, PRINCIPAL_FULL_METADATA, SERVICE_MANAGE_ACCESS)); + SUPER_PRIVILEGES.putAll( + PRINCIPAL_FULL_METADATA, List.of(PRINCIPAL_FULL_METADATA, SERVICE_MANAGE_ACCESS)); + + // PRINCIPAL MANAGE_GRANTS + SUPER_PRIVILEGES.putAll( + PRINCIPAL_MANAGE_GRANTS_ON_SECURABLE, + List.of(PRINCIPAL_MANAGE_GRANTS_ON_SECURABLE, SERVICE_MANAGE_ACCESS)); + SUPER_PRIVILEGES.putAll( + PRINCIPAL_MANAGE_GRANTS_FOR_GRANTEE, + List.of(PRINCIPAL_MANAGE_GRANTS_FOR_GRANTEE, SERVICE_MANAGE_ACCESS)); + + // PRINCIPAL special privileges + SUPER_PRIVILEGES.putAll(PRINCIPAL_ROTATE_CREDENTIALS, List.of(PRINCIPAL_ROTATE_CREDENTIALS)); + SUPER_PRIVILEGES.putAll( + PRINCIPAL_RESET_CREDENTIALS, List.of(PRINCIPAL_RESET_CREDENTIALS, SERVICE_MANAGE_ACCESS)); + + // PRINCIPAL_ROLE CRUDL + SUPER_PRIVILEGES.putAll( + PRINCIPAL_ROLE_CREATE, + List.of(PRINCIPAL_ROLE_CREATE, PRINCIPAL_ROLE_FULL_METADATA, SERVICE_MANAGE_ACCESS)); + SUPER_PRIVILEGES.putAll( + PRINCIPAL_ROLE_DROP, + List.of(PRINCIPAL_ROLE_DROP, PRINCIPAL_ROLE_FULL_METADATA, SERVICE_MANAGE_ACCESS)); + SUPER_PRIVILEGES.putAll( + PRINCIPAL_ROLE_LIST, + List.of( + PRINCIPAL_ROLE_LIST, + PRINCIPAL_ROLE_CREATE, + PRINCIPAL_ROLE_READ_PROPERTIES, + PRINCIPAL_ROLE_WRITE_PROPERTIES, + PRINCIPAL_ROLE_FULL_METADATA, + SERVICE_MANAGE_ACCESS)); + SUPER_PRIVILEGES.putAll( + PRINCIPAL_ROLE_READ_PROPERTIES, + List.of( + PRINCIPAL_ROLE_READ_PROPERTIES, + PRINCIPAL_ROLE_WRITE_PROPERTIES, + PRINCIPAL_ROLE_FULL_METADATA, + SERVICE_MANAGE_ACCESS)); + SUPER_PRIVILEGES.putAll( + PRINCIPAL_ROLE_WRITE_PROPERTIES, + List.of( + PRINCIPAL_ROLE_WRITE_PROPERTIES, PRINCIPAL_ROLE_FULL_METADATA, SERVICE_MANAGE_ACCESS)); + SUPER_PRIVILEGES.putAll( + PRINCIPAL_ROLE_FULL_METADATA, List.of(PRINCIPAL_ROLE_FULL_METADATA, SERVICE_MANAGE_ACCESS)); + + // PRINCIPAL_ROLE_ROLE MANAGE_GRANTS + SUPER_PRIVILEGES.putAll( + PRINCIPAL_ROLE_MANAGE_GRANTS_ON_SECURABLE, + List.of(PRINCIPAL_ROLE_MANAGE_GRANTS_ON_SECURABLE, SERVICE_MANAGE_ACCESS)); + SUPER_PRIVILEGES.putAll( + PRINCIPAL_ROLE_MANAGE_GRANTS_FOR_GRANTEE, + List.of(PRINCIPAL_ROLE_MANAGE_GRANTS_FOR_GRANTEE, SERVICE_MANAGE_ACCESS)); + + // CATALOG_ROLE CRUDL + SUPER_PRIVILEGES.putAll( + CATALOG_ROLE_CREATE, + List.of(CATALOG_ROLE_CREATE, CATALOG_ROLE_FULL_METADATA, CATALOG_MANAGE_ACCESS)); + SUPER_PRIVILEGES.putAll( + CATALOG_ROLE_DROP, + List.of(CATALOG_ROLE_DROP, CATALOG_ROLE_FULL_METADATA, CATALOG_MANAGE_ACCESS)); + SUPER_PRIVILEGES.putAll( + CATALOG_ROLE_LIST, + List.of( + CATALOG_ROLE_LIST, + CATALOG_ROLE_CREATE, + CATALOG_ROLE_READ_PROPERTIES, + CATALOG_ROLE_WRITE_PROPERTIES, + CATALOG_ROLE_FULL_METADATA, + CATALOG_MANAGE_ACCESS)); + SUPER_PRIVILEGES.putAll( + CATALOG_ROLE_READ_PROPERTIES, + List.of( + CATALOG_ROLE_READ_PROPERTIES, + CATALOG_ROLE_WRITE_PROPERTIES, + CATALOG_ROLE_FULL_METADATA, + CATALOG_MANAGE_ACCESS)); + SUPER_PRIVILEGES.putAll( + CATALOG_ROLE_WRITE_PROPERTIES, + List.of(CATALOG_ROLE_WRITE_PROPERTIES, CATALOG_ROLE_FULL_METADATA, CATALOG_MANAGE_ACCESS)); + SUPER_PRIVILEGES.putAll( + CATALOG_ROLE_FULL_METADATA, List.of(CATALOG_ROLE_FULL_METADATA, CATALOG_MANAGE_ACCESS)); + + // CATALOG_ROLE_ROLE MANAGE_GRANTS + SUPER_PRIVILEGES.putAll( + CATALOG_ROLE_MANAGE_GRANTS_ON_SECURABLE, + List.of(CATALOG_ROLE_MANAGE_GRANTS_ON_SECURABLE, CATALOG_MANAGE_ACCESS)); + SUPER_PRIVILEGES.putAll( + CATALOG_ROLE_MANAGE_GRANTS_FOR_GRANTEE, + List.of(CATALOG_ROLE_MANAGE_GRANTS_FOR_GRANTEE, CATALOG_MANAGE_ACCESS)); + } + + private final PolarisConfigurationStore featureConfig; + + public PolarisAuthorizer(PolarisConfigurationStore featureConfig) { + this.featureConfig = featureConfig; + } + + /** + * Checks whether the {@code grantedPrivilege} is sufficient to confer {@code desiredPrivilege}, + * assuming the privileges are referring to the same securable object. In other words, whether the + * grantedPrivilege is "better than or equal to" the desiredPrivilege. + */ + public boolean matchesOrIsSubsumedBy( + PolarisPrivilege desiredPrivilege, PolarisPrivilege grantedPrivilege) { + if (grantedPrivilege == desiredPrivilege) { + return true; + } + + if (SUPER_PRIVILEGES.containsKey(desiredPrivilege) + && SUPER_PRIVILEGES.get(desiredPrivilege).contains(grantedPrivilege)) { + return true; + } + // TODO: Fill out the map, maybe in the PolarisPrivilege enum definition itself. + return false; + } + + public void authorizeOrThrow( + @NotNull AuthenticatedPolarisPrincipal authenticatedPrincipal, + @NotNull Set activatedGranteeIds, + @NotNull PolarisAuthorizableOperation authzOp, + @Nullable PolarisResolvedPathWrapper target, + @Nullable PolarisResolvedPathWrapper secondary) { + authorizeOrThrow( + authenticatedPrincipal, + activatedGranteeIds, + authzOp, + target == null ? null : List.of(target), + secondary == null ? null : List.of(secondary)); + } + + public void authorizeOrThrow( + @NotNull AuthenticatedPolarisPrincipal authenticatedPrincipal, + @NotNull Set activatedGranteeIds, + @NotNull PolarisAuthorizableOperation authzOp, + @Nullable List targets, + @Nullable List secondaries) { + boolean enforceCredentialRotationRequiredState = + featureConfig.getConfiguration( + CallContext.getCurrentContext().getPolarisCallContext(), + PolarisConfiguration.ENFORCE_PRINCIPAL_CREDENTIAL_ROTATION_REQUIRED_CHECKING, + false); + if (enforceCredentialRotationRequiredState + && authenticatedPrincipal + .getPrincipalEntity() + .getInternalPropertiesAsMap() + .containsKey(PolarisEntityConstants.PRINCIPAL_CREDENTIAL_ROTATION_REQUIRED_STATE) + && authzOp != PolarisAuthorizableOperation.ROTATE_CREDENTIALS) { + throw new ForbiddenException( + "Principal '%s' is not authorized for op %s due to PRINCIPAL_CREDENTIAL_ROTATION_REQUIRED_STATE", + authenticatedPrincipal.getName(), authzOp); + } else if (!isAuthorized( + authenticatedPrincipal, activatedGranteeIds, authzOp, targets, secondaries)) { + throw new ForbiddenException( + "Principal '%s' with activated PrincipalRoles '%s' and activated ids '%s' is not authorized for op %s", + authenticatedPrincipal.getName(), + authenticatedPrincipal.getActivatedPrincipalRoleNames(), + activatedGranteeIds, + authzOp); + } + } + + /** + * Based on the required target/targetParent/secondary/secondaryParent privileges mapped from + * {@code authzOp}, determines whether the caller's set of activatedGranteeIds is authorized for + * the operation. + */ + public boolean isAuthorized( + @NotNull AuthenticatedPolarisPrincipal authenticatedPolarisPrincipal, + @NotNull Set activatedGranteeIds, + @NotNull PolarisAuthorizableOperation authzOp, + @Nullable PolarisResolvedPathWrapper target, + @Nullable PolarisResolvedPathWrapper secondary) { + return isAuthorized( + authenticatedPolarisPrincipal, + activatedGranteeIds, + authzOp, + target == null ? null : List.of(target), + secondary == null ? null : List.of(secondary)); + } + + public boolean isAuthorized( + @NotNull AuthenticatedPolarisPrincipal authenticatedPolarisPrincipal, + @NotNull Set activatedGranteeIds, + @NotNull PolarisAuthorizableOperation authzOp, + @Nullable List targets, + @Nullable List secondaries) { + for (PolarisPrivilege privilegeOnTarget : authzOp.getPrivilegesOnTarget()) { + // If any privileges are required on target, the target must be non-null. + Preconditions.checkState( + targets != null, + "Got null target when authorizing authzOp %s for privilege %s", + authzOp, + privilegeOnTarget); + for (PolarisResolvedPathWrapper target : targets) { + if (!hasTransitivePrivilege( + authenticatedPolarisPrincipal, activatedGranteeIds, privilegeOnTarget, target)) { + // TODO: Collect missing privileges to report all at the end and/or return to code + // that throws NotAuthorizedException for more useful messages. + return false; + } + } + } + for (PolarisPrivilege privilegeOnSecondary : authzOp.getPrivilegesOnSecondary()) { + Preconditions.checkState( + secondaries != null, + "Got null secondary when authorizing authzOp %s for privilege %s", + authzOp, + privilegeOnSecondary); + for (PolarisResolvedPathWrapper secondary : secondaries) { + if (!hasTransitivePrivilege( + authenticatedPolarisPrincipal, activatedGranteeIds, privilegeOnSecondary, secondary)) { + return false; + } + } + } + return true; + } + + /** + * Checks whether the resolvedPrincipal in the {@code resolved} resolvedPath has role-expanded + * permissions matching {@code privilege} on any entity in the resolvedPath of the resolvedPath. + * + *

The caller is responsible for translating these checks into either behavioral actions (e.g. + * returning 404 instead of 403, checking other root privileges that supercede the checked + * privilege, choosing whether to vend credentials) or throwing relevant Unauthorized + * errors/exceptions. + */ + public boolean hasTransitivePrivilege( + @NotNull AuthenticatedPolarisPrincipal authenticatedPolarisPrincipal, + Set activatedGranteeIds, + PolarisPrivilege desiredPrivilege, + PolarisResolvedPathWrapper resolvedPath) { + + // Iterate starting at the parent, since the most common case should be to manage grants as + // high up in the resource hierarchy as possible, so we expect earlier termination. + for (ResolvedPolarisEntity resolvedSecurableEntity : resolvedPath.getResolvedFullPath()) { + Preconditions.checkState( + resolvedSecurableEntity.getGrantRecordsAsSecurable() != null, + "Got null grantRecordsAsSecurable for resolvedSecurableEntity %s", + resolvedSecurableEntity); + for (PolarisGrantRecord grantRecord : resolvedSecurableEntity.getGrantRecordsAsSecurable()) { + if (matchesOrIsSubsumedBy( + desiredPrivilege, PolarisPrivilege.fromCode(grantRecord.getPrivilegeCode()))) { + // Found a potential candidate for satisfying our authz goal. + if (activatedGranteeIds.contains(grantRecord.getGranteeId())) { + LOG.debug( + "Satisfied privilege {} with grantRecord {} from securable {} for " + + "principalName {} and activatedIds {}", + desiredPrivilege, + grantRecord, + resolvedSecurableEntity, + authenticatedPolarisPrincipal.getName(), + activatedGranteeIds); + return true; + } + } + } + } + + LOG.debug( + "Failed to satisfy privilege {} for principalName {} on resolvedPath {}", + desiredPrivilege, + authenticatedPolarisPrincipal.getName(), + resolvedPath); + return false; + } +} diff --git a/polaris-core/src/main/java/io/polaris/core/catalog/PolarisCatalogHelpers.java b/polaris-core/src/main/java/io/polaris/core/catalog/PolarisCatalogHelpers.java new file mode 100644 index 0000000000..44d47d2d3e --- /dev/null +++ b/polaris-core/src/main/java/io/polaris/core/catalog/PolarisCatalogHelpers.java @@ -0,0 +1,77 @@ +package io.polaris.core.catalog; + +import io.polaris.core.entity.PolarisEntity; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import org.apache.iceberg.catalog.Namespace; +import org.apache.iceberg.catalog.TableIdentifier; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Holds helper methods translating between persistence-layer structs and Iceberg objects shared by + * different Polaris components. + */ +public class PolarisCatalogHelpers { + private static final Logger LOG = LoggerFactory.getLogger(PolarisCatalogHelpers.class); + + /** Not intended for instantiation. */ + private PolarisCatalogHelpers() {} + + public static List tableIdentifierToList(TableIdentifier identifier) { + List fullList = new ArrayList<>(); + fullList.addAll(Arrays.asList(identifier.namespace().levels())); + fullList.add(identifier.name()); + return fullList; + } + + public static TableIdentifier listToTableIdentifier(List ids) { + return TableIdentifier.of(ids.toArray(new String[0])); + } + + public static Namespace getParentNamespace(Namespace namespace) { + if (namespace.isEmpty() || namespace.length() == 1) { + return Namespace.empty(); + } + String[] parentLevels = new String[namespace.length() - 1]; + for (int i = 0; i < parentLevels.length; ++i) { + parentLevels[i] = namespace.level(i); + } + return Namespace.of(parentLevels); + } + + public static List nameAndIdToNamespaces( + List catalogPath, List entities) { + // Skip element 0 which is the catalog entity + String[] parentNamespaces = new String[catalogPath.size() - 1]; + for (int i = 0; i < parentNamespaces.length; ++i) { + parentNamespaces[i] = catalogPath.get(i + 1).getName(); + } + List namespaces = new ArrayList<>(); + for (PolarisEntity.NameAndId entity : entities) { + String[] fullName = Arrays.copyOf(parentNamespaces, parentNamespaces.length + 1); + fullName[fullName.length - 1] = entity.getName(); + namespaces.add(Namespace.of(fullName)); + } + return namespaces; + } + + /** + * Given the shortnames/ids of entities that all live under the given catalogPath, reconstructs + * TableIdentifier objects for each that all hold the catalogPath excluding the catalog entity. + */ + public static List nameAndIdToTableIdentifiers( + List catalogPath, List entities) { + // Skip element 0 which is the catalog entity + String[] parentNamespaces = new String[catalogPath.size() - 1]; + for (int i = 0; i < parentNamespaces.length; ++i) { + parentNamespaces[i] = catalogPath.get(i + 1).getName(); + } + Namespace sharedNamespace = Namespace.of(parentNamespaces); + return entities.stream() + .map(entity -> TableIdentifier.of(sharedNamespace, entity.getName())) + .collect(Collectors.toList()); + } +} diff --git a/polaris-core/src/main/java/io/polaris/core/context/CallContext.java b/polaris-core/src/main/java/io/polaris/core/context/CallContext.java new file mode 100644 index 0000000000..b6aa0c8804 --- /dev/null +++ b/polaris-core/src/main/java/io/polaris/core/context/CallContext.java @@ -0,0 +1,141 @@ +package io.polaris.core.context; + +import io.polaris.core.PolarisCallContext; +import io.polaris.core.PolarisDiagnostics; +import io.polaris.core.auth.AuthenticatedPolarisPrincipal; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; +import org.apache.iceberg.io.CloseableGroup; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Stores elements associated with an individual REST request such as RealmContext, caller + * identity/role, authn/authz, etc. This class is distinct from RealmContext because implementations + * may need to first independently resolve a RealmContext before resolving the identity/role + * elements of the CallContext that reside exclusively within the resolved Realm. For example, the + * principal/role entities may be defined within a Realm-specific persistence layer, and the + * underlying nature of the persistence layer may differ between different realms. + */ +public interface CallContext extends AutoCloseable { + InheritableThreadLocal CURRENT_CONTEXT = new InheritableThreadLocal<>(); + + // For requests that make use of a Catalog instance, this holds the instance that was + // created, scoped to the current call context. + public static final String REQUEST_PATH_CATALOG_INSTANCE_KEY = "REQUEST_PATH_CATALOG_INSTANCE"; + + // Authenticator filters should populate this field alongside resolving a SecurityContext. + // Value type: AuthenticatedPolarisPrincipal + String AUTHENTICATED_PRINCIPAL = "AUTHENTICATED_PRINCIPAL"; + String CLOSEABLES = "closeables"; + + static CallContext setCurrentContext(CallContext context) { + CURRENT_CONTEXT.set(context); + return context; + } + + static CallContext getCurrentContext() { + return CURRENT_CONTEXT.get(); + } + + static PolarisDiagnostics getDiagnostics() { + return CURRENT_CONTEXT.get().getPolarisCallContext().getDiagServices(); + } + + static AuthenticatedPolarisPrincipal getAuthenticatedPrincipal() { + return (AuthenticatedPolarisPrincipal) + CallContext.getCurrentContext().contextVariables().get(CallContext.AUTHENTICATED_PRINCIPAL); + } + + static void unsetCurrentContext() { + CURRENT_CONTEXT.remove(); + } + + static CallContext of( + final RealmContext realmContext, final PolarisCallContext polarisCallContext) { + Map map = new HashMap<>(); + return new CallContext() { + @Override + public RealmContext getRealmContext() { + return realmContext; + } + + @Override + public PolarisCallContext getPolarisCallContext() { + return polarisCallContext; + } + + @Override + public Map contextVariables() { + return map; + } + }; + } + + /** + * Copy the {@link CallContext}. {@link #contextVariables()} will be copied except for {@link + * #closeables()}. The original {@link #contextVariables()} map is untouched and {@link + * #closeables()} in the original {@link CallContext} should be closed along with the {@link + * CallContext}. + * + * @param base + * @return + */ + static CallContext copyOf(CallContext base) { + RealmContext realmContext = base.getRealmContext(); + PolarisCallContext polarisCallContext = base.getPolarisCallContext(); + Map contextVariables = + base.contextVariables().entrySet().stream() + .filter(e -> !e.getKey().equals(CLOSEABLES)) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + return new CallContext() { + @Override + public RealmContext getRealmContext() { + return realmContext; + } + + @Override + public PolarisCallContext getPolarisCallContext() { + return polarisCallContext; + } + + @Override + public Map contextVariables() { + return contextVariables; + } + }; + } + + RealmContext getRealmContext(); + + /** + * @return the inner context used for delegating services + */ + PolarisCallContext getPolarisCallContext(); + + Map contextVariables(); + + default @NotNull CloseableGroup closeables() { + return (CloseableGroup) + contextVariables().computeIfAbsent(CLOSEABLES, key -> new CloseableGroup()); + } + + default void close() { + if (CURRENT_CONTEXT.get() == this) { + unsetCurrentContext(); + CloseableGroup closeables = closeables(); + try { + closeables.close(); + } catch (IOException e) { + Logger logger = LoggerFactory.getLogger(CallContext.class); + logger + .atWarn() + .addKeyValue("closeableGroup", closeables) + .log("Unable to close closeable group", e); + } + } + } +} diff --git a/polaris-core/src/main/java/io/polaris/core/context/RealmContext.java b/polaris-core/src/main/java/io/polaris/core/context/RealmContext.java new file mode 100644 index 0000000000..64db6af249 --- /dev/null +++ b/polaris-core/src/main/java/io/polaris/core/context/RealmContext.java @@ -0,0 +1,10 @@ +package io.polaris.core.context; + +/** + * Represents the elements of a REST request associated with routing to independent and isolated + * "universes". This may include properties such as region, deployment environment (e.g. dev, qa, + * prod), and/or account. + */ +public interface RealmContext { + String getRealmIdentifier(); +} diff --git a/polaris-core/src/main/java/io/polaris/core/entity/AsyncTaskType.java b/polaris-core/src/main/java/io/polaris/core/entity/AsyncTaskType.java new file mode 100644 index 0000000000..2b85ceb0ec --- /dev/null +++ b/polaris-core/src/main/java/io/polaris/core/entity/AsyncTaskType.java @@ -0,0 +1,30 @@ +package io.polaris.core.entity; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +public enum AsyncTaskType { + ENTITY_CLEANUP_SCHEDULER(1), + FILE_CLEANUP(2); + + private final int typeCode; + + AsyncTaskType(int typeCode) { + this.typeCode = typeCode; + } + + @JsonValue + public int typeCode() { + return typeCode; + } + + @JsonCreator + public static AsyncTaskType fromTypeCode(int typeCode) { + for (AsyncTaskType taskType : AsyncTaskType.values()) { + if (taskType.typeCode == typeCode) { + return taskType; + } + } + return null; + } +} diff --git a/polaris-core/src/main/java/io/polaris/core/entity/CatalogEntity.java b/polaris-core/src/main/java/io/polaris/core/entity/CatalogEntity.java new file mode 100644 index 0000000000..1e16ec0c0d --- /dev/null +++ b/polaris-core/src/main/java/io/polaris/core/entity/CatalogEntity.java @@ -0,0 +1,268 @@ +package io.polaris.core.entity; + +import static io.polaris.core.admin.model.StorageConfigInfo.StorageTypeEnum.AZURE; + +import io.polaris.core.PolarisDefaultDiagServiceImpl; +import io.polaris.core.admin.model.AwsStorageConfigInfo; +import io.polaris.core.admin.model.AzureStorageConfigInfo; +import io.polaris.core.admin.model.Catalog; +import io.polaris.core.admin.model.CatalogProperties; +import io.polaris.core.admin.model.ExternalCatalog; +import io.polaris.core.admin.model.FileStorageConfigInfo; +import io.polaris.core.admin.model.GcpStorageConfigInfo; +import io.polaris.core.admin.model.PolarisCatalog; +import io.polaris.core.admin.model.StorageConfigInfo; +import io.polaris.core.storage.FileStorageConfigurationInfo; +import io.polaris.core.storage.PolarisStorageConfigurationInfo; +import io.polaris.core.storage.aws.AwsStorageConfigurationInfo; +import io.polaris.core.storage.azure.AzureStorageConfigurationInfo; +import io.polaris.core.storage.gcp.GcpStorageConfigurationInfo; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import org.apache.iceberg.exceptions.BadRequestException; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Catalog specific subclass of the {@link PolarisEntity} that handles conversion from the {@link + * Catalog} model to the persistent entity model. + */ +public class CatalogEntity extends PolarisEntity { + private static final Logger LOG = LoggerFactory.getLogger(CatalogEntity.class); + + public static final long ROOT_CATALOG_ID = 0; + public static final String CATALOG_TYPE_PROPERTY = "catalogType"; + + // Specifies the object-store base location used for all Table file locations under the + // catalog, stored in the "properties" map. + public static final String DEFAULT_BASE_LOCATION_KEY = "default-base-location"; + + // Specifies a prefix that will be replaced with the catalog's default-base-location whenever + // it matches a specified new table or view location. For example, if the catalog base location + // is "s3://my-bucket/base/location" and the prefix specified here is "file:/tmp" then any + // new table attempting to specify a base location of "file:/tmp/ns1/ns2/table1" will be + // translated into "s3://my-bucket/base/location/ns1/ns2/table1". + public static final String REPLACE_NEW_LOCATION_PREFIX_WITH_CATALOG_DEFAULT_KEY = + "replace-new-location-prefix-with-catalog-default"; + public static final String REMOTE_URL = "remoteUrl"; + + public CatalogEntity(PolarisBaseEntity sourceEntity) { + super(sourceEntity); + } + + public static CatalogEntity of(PolarisBaseEntity sourceEntity) { + if (sourceEntity != null) { + return new CatalogEntity(sourceEntity); + } + return null; + } + + public static CatalogEntity fromCatalog(Catalog catalog) { + + Builder builder = + new Builder() + .setName(catalog.getName()) + .setProperties(catalog.getProperties().toMap()) + .setCatalogType(catalog.getType().name()); + Map internalProperties = new HashMap<>(); + if (catalog instanceof ExternalCatalog) { + internalProperties.put(REMOTE_URL, ((ExternalCatalog) catalog).getRemoteUrl()); + } + internalProperties.put(CATALOG_TYPE_PROPERTY, catalog.getType().name()); + builder.setInternalProperties(internalProperties); + builder.setStorageConfigurationInfo( + catalog.getStorageConfigInfo(), getDefaultBaseLocation(catalog)); + return builder.build(); + } + + public Catalog asCatalog() { + Map internalProperties = getInternalPropertiesAsMap(); + Catalog.TypeEnum catalogType = + Optional.ofNullable(internalProperties.get(CATALOG_TYPE_PROPERTY)) + .map(Catalog.TypeEnum::valueOf) + .orElseGet(() -> getName().equalsIgnoreCase("ROOT") ? Catalog.TypeEnum.INTERNAL : null); + Map propertiesMap = getPropertiesAsMap(); + CatalogProperties catalogProps = + CatalogProperties.builder(propertiesMap.get(DEFAULT_BASE_LOCATION_KEY)) + .putAll(propertiesMap) + .build(); + return catalogType == Catalog.TypeEnum.INTERNAL + ? PolarisCatalog.builder() + .setType(Catalog.TypeEnum.INTERNAL) + .setName(getName()) + .setProperties(catalogProps) + .setCreateTimestamp(getCreateTimestamp()) + .setLastUpdateTimestamp(getLastUpdateTimestamp()) + .setEntityVersion(getEntityVersion()) + .setStorageConfigInfo(getStorageInfo(internalProperties)) + .build() + : ExternalCatalog.builder() + .setType(Catalog.TypeEnum.EXTERNAL) + .setName(getName()) + .setRemoteUrl(getInternalPropertiesAsMap().get(REMOTE_URL)) + .setProperties(catalogProps) + .setCreateTimestamp(getCreateTimestamp()) + .setLastUpdateTimestamp(getLastUpdateTimestamp()) + .setEntityVersion(getEntityVersion()) + .setStorageConfigInfo(getStorageInfo(internalProperties)) + .build(); + } + + private StorageConfigInfo getStorageInfo(Map internalProperties) { + if (internalProperties.containsKey(PolarisEntityConstants.getStorageConfigInfoPropertyName())) { + PolarisStorageConfigurationInfo configInfo = getStorageConfigurationInfo(); + PolarisStorageConfigurationInfo.StorageType storageType = configInfo.getStorageType(); + if (configInfo instanceof AwsStorageConfigurationInfo) { + AwsStorageConfigurationInfo awsConfig = (AwsStorageConfigurationInfo) configInfo; + return AwsStorageConfigInfo.builder() + .setRoleArn(awsConfig.getRoleARN()) + .setExternalId(awsConfig.getExternalId()) + .setUserArn(awsConfig.getUserARN()) + .setStorageType(StorageConfigInfo.StorageTypeEnum.S3) + .setAllowedLocations(awsConfig.getAllowedLocations()) + .build(); + } + if (configInfo instanceof AzureStorageConfigurationInfo) { + AzureStorageConfigurationInfo azureConfig = (AzureStorageConfigurationInfo) configInfo; + return AzureStorageConfigInfo.builder() + .setTenantId(azureConfig.getTenantId()) + .setMultiTenantAppName(azureConfig.getMultiTenantAppName()) + .setConsentUrl(azureConfig.getConsentUrl()) + .setStorageType(AZURE) + .setAllowedLocations(azureConfig.getAllowedLocations()) + .build(); + } + if (configInfo instanceof GcpStorageConfigurationInfo) { + GcpStorageConfigurationInfo gcpConfigModel = (GcpStorageConfigurationInfo) configInfo; + return GcpStorageConfigInfo.builder() + .setGcsServiceAccount(gcpConfigModel.getGcpServiceAccount()) + .setStorageType(StorageConfigInfo.StorageTypeEnum.GCS) + .setAllowedLocations(gcpConfigModel.getAllowedLocations()) + .build(); + } + if (configInfo instanceof FileStorageConfigurationInfo) { + FileStorageConfigurationInfo fileConfigModel = (FileStorageConfigurationInfo) configInfo; + return new FileStorageConfigInfo( + StorageConfigInfo.StorageTypeEnum.FILE, fileConfigModel.getAllowedLocations()); + } + return null; + } + return null; + } + + public String getDefaultBaseLocation() { + return getPropertiesAsMap().get(DEFAULT_BASE_LOCATION_KEY); + } + + public String getReplaceNewLocationPrefixWithCatalogDefault() { + return getPropertiesAsMap().get(REPLACE_NEW_LOCATION_PREFIX_WITH_CATALOG_DEFAULT_KEY); + } + + public @Nullable PolarisStorageConfigurationInfo getStorageConfigurationInfo() { + String configStr = + getInternalPropertiesAsMap().get(PolarisEntityConstants.getStorageConfigInfoPropertyName()); + if (configStr != null) { + return PolarisStorageConfigurationInfo.deserialize( + new PolarisDefaultDiagServiceImpl(), configStr); + } + return null; + } + + public Catalog.TypeEnum getCatalogType() { + return Optional.ofNullable(getInternalPropertiesAsMap().get(CATALOG_TYPE_PROPERTY)) + .map(Catalog.TypeEnum::valueOf) + .orElse(null); + } + + public static class Builder extends PolarisEntity.BaseBuilder { + public Builder() { + super(); + setType(PolarisEntityType.CATALOG); + setCatalogId(PolarisEntityConstants.getNullId()); + setParentId(PolarisEntityConstants.getRootEntityId()); + } + + public Builder(CatalogEntity original) { + super(original); + } + + public Builder setCatalogType(String type) { + internalProperties.put(CATALOG_TYPE_PROPERTY, type); + return this; + } + + public Builder setDefaultBaseLocation(String defaultBaseLocation) { + // Note that this member lives in the main 'properties' map rather tha internalProperties. + properties.put(DEFAULT_BASE_LOCATION_KEY, defaultBaseLocation); + return this; + } + + public Builder setReplaceNewLocationPrefixWithCatalogDefault(String value) { + // Note that this member lives in the main 'properties' map rather tha internalProperties. + properties.put(REPLACE_NEW_LOCATION_PREFIX_WITH_CATALOG_DEFAULT_KEY, value); + return this; + } + + public Builder setStorageConfigurationInfo( + StorageConfigInfo storageConfigModel, String defaultBaseLocation) { + if (storageConfigModel != null) { + PolarisStorageConfigurationInfo config; + Set allowedLocations = new HashSet<>(storageConfigModel.getAllowedLocations()); + + // TODO: Reconsider whether this should actually just be a check up-front or if we + // actually want to silently add to the allowed locations. Maybe ideally we only + // add to the allowedLocations if allowedLocations is empty for the simple case, + // but if the caller provided allowedLocations explicitly, then we just verify that + // the defaultBaseLocation is at least a subpath of one of the allowedLocations. + if (defaultBaseLocation == null) { + throw new BadRequestException("Must specify default base location"); + } + allowedLocations.add(defaultBaseLocation); + switch (storageConfigModel.getStorageType()) { + case S3: + AwsStorageConfigInfo awsConfigModel = (AwsStorageConfigInfo) storageConfigModel; + config = + new AwsStorageConfigurationInfo( + PolarisStorageConfigurationInfo.StorageType.S3, + new ArrayList<>(allowedLocations), + awsConfigModel.getRoleArn(), + awsConfigModel.getExternalId()); + ((AwsStorageConfigurationInfo) config).validateArn(awsConfigModel.getRoleArn()); + break; + case AZURE: + AzureStorageConfigInfo azureConfigModel = (AzureStorageConfigInfo) storageConfigModel; + config = + new AzureStorageConfigurationInfo( + new ArrayList<>(allowedLocations), azureConfigModel.getTenantId()); + break; + case GCS: + config = new GcpStorageConfigurationInfo(new ArrayList<>(allowedLocations)); + break; + case FILE: + config = new FileStorageConfigurationInfo(new ArrayList<>(allowedLocations)); + break; + default: + throw new IllegalStateException( + "Unsupported storage type: " + storageConfigModel.getStorageType()); + } + internalProperties.put( + PolarisEntityConstants.getStorageConfigInfoPropertyName(), config.serialize()); + } + return this; + } + + public CatalogEntity build() { + return new CatalogEntity(buildBase()); + } + } + + protected static @NotNull String getDefaultBaseLocation(Catalog catalog) { + return catalog.getProperties().getDefaultBaseLocation(); + } +} diff --git a/polaris-core/src/main/java/io/polaris/core/entity/CatalogRoleEntity.java b/polaris-core/src/main/java/io/polaris/core/entity/CatalogRoleEntity.java new file mode 100644 index 0000000000..1c4c247588 --- /dev/null +++ b/polaris-core/src/main/java/io/polaris/core/entity/CatalogRoleEntity.java @@ -0,0 +1,50 @@ +package io.polaris.core.entity; + +import io.polaris.core.admin.model.CatalogRole; + +/** Wrapper for translating between the REST CatalogRole object and the base PolarisEntity type. */ +public class CatalogRoleEntity extends PolarisEntity { + public CatalogRoleEntity(PolarisBaseEntity sourceEntity) { + super(sourceEntity); + } + + public static CatalogRoleEntity of(PolarisBaseEntity sourceEntity) { + if (sourceEntity != null) { + return new CatalogRoleEntity(sourceEntity); + } + return null; + } + + public static CatalogRoleEntity fromCatalogRole(CatalogRole catalogRole) { + return new Builder() + .setName(catalogRole.getName()) + .setProperties(catalogRole.getProperties()) + .build(); + } + + public CatalogRole asCatalogRole() { + CatalogRole catalogRole = + new CatalogRole( + getName(), + getPropertiesAsMap(), + getCreateTimestamp(), + getLastUpdateTimestamp(), + getEntityVersion()); + return catalogRole; + } + + public static class Builder extends PolarisEntity.BaseBuilder { + public Builder() { + super(); + setType(PolarisEntityType.CATALOG_ROLE); + } + + public Builder(CatalogRoleEntity original) { + super(original); + } + + public CatalogRoleEntity build() { + return new CatalogRoleEntity(buildBase()); + } + } +} diff --git a/polaris-core/src/main/java/io/polaris/core/entity/NamespaceEntity.java b/polaris-core/src/main/java/io/polaris/core/entity/NamespaceEntity.java new file mode 100644 index 0000000000..6b352834c2 --- /dev/null +++ b/polaris-core/src/main/java/io/polaris/core/entity/NamespaceEntity.java @@ -0,0 +1,63 @@ +package io.polaris.core.entity; + +import io.polaris.core.catalog.PolarisCatalogHelpers; +import org.apache.iceberg.catalog.Namespace; +import org.apache.iceberg.rest.RESTUtil; + +/** + * Namespace-specific subclass of the {@link PolarisEntity} that provides accessors interacting with + * internalProperties specific to the NAMESPACE type. + */ +public class NamespaceEntity extends PolarisEntity { + // RESTUtil-encoded parent namespace. + public static final String PARENT_NAMESPACE_KEY = "parent-namespace"; + + public NamespaceEntity(PolarisBaseEntity sourceEntity) { + super(sourceEntity); + } + + public static NamespaceEntity of(PolarisBaseEntity sourceEntity) { + if (sourceEntity != null) { + return new NamespaceEntity(sourceEntity); + } + return null; + } + + public Namespace getParentNamespace() { + String encodedNamespace = getInternalPropertiesAsMap().get(PARENT_NAMESPACE_KEY); + if (encodedNamespace == null) { + return Namespace.empty(); + } + return RESTUtil.decodeNamespace(encodedNamespace); + } + + public Namespace asNamespace() { + Namespace parent = getParentNamespace(); + String[] levels = new String[parent.length() + 1]; + for (int i = 0; i < parent.length(); ++i) { + levels[i] = parent.level(i); + } + levels[levels.length - 1] = getName(); + return Namespace.of(levels); + } + + public static class Builder extends PolarisEntity.BaseBuilder { + public Builder(Namespace namespace) { + super(); + setType(PolarisEntityType.NAMESPACE); + setParentNamespace(PolarisCatalogHelpers.getParentNamespace(namespace)); + setName(namespace.level(namespace.length() - 1)); + } + + public NamespaceEntity build() { + return new NamespaceEntity(buildBase()); + } + + public Builder setParentNamespace(Namespace namespace) { + if (namespace != null && !namespace.isEmpty()) { + internalProperties.put(PARENT_NAMESPACE_KEY, RESTUtil.encodeNamespace(namespace)); + } + return this; + } + } +} diff --git a/polaris-core/src/main/java/io/polaris/core/entity/PolarisBaseEntity.java b/polaris-core/src/main/java/io/polaris/core/entity/PolarisBaseEntity.java new file mode 100644 index 0000000000..9e6ee28b60 --- /dev/null +++ b/polaris-core/src/main/java/io/polaris/core/entity/PolarisBaseEntity.java @@ -0,0 +1,341 @@ +/* + * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. + */ + +package io.polaris.core.entity; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +/** + * Base polaris entity representing all attributes of a Polaris Entity. This is used to exchange + * full entity information between the client and the GS backend + */ +public class PolarisBaseEntity extends PolarisEntityCore { + + public static final String EMPTY_MAP_STRING = "{}"; + + // to serialize/deserialize properties + private static final ObjectMapper MAPPER = new ObjectMapper(); + + // the type of the entity when it was resolved + protected int subTypeCode; + + // timestamp when this entity was created + protected long createTimestamp; + + // when this entity was dropped. Null if was never dropped + protected long dropTimestamp; + + // when did we start purging this entity. When not null, un-drop is no longer possible + protected long purgeTimestamp; + + // when should we start purging this entity + protected long toPurgeTimestamp; + + // last time this entity was updated, only for troubleshooting + protected long lastUpdateTimestamp; + + // properties, serialized as a JSON string + protected String properties; + + // internal properties, serialized as a JSON string + protected String internalProperties; + + // current version for that entity, will be monotonically incremented + protected int grantRecordsVersion; + + public int getSubTypeCode() { + return subTypeCode; + } + + public void setSubTypeCode(int subTypeCode) { + this.subTypeCode = subTypeCode; + } + + public long getCreateTimestamp() { + return createTimestamp; + } + + public void setCreateTimestamp(long createTimestamp) { + this.createTimestamp = createTimestamp; + } + + public long getDropTimestamp() { + return dropTimestamp; + } + + public void setDropTimestamp(long dropTimestamp) { + this.dropTimestamp = dropTimestamp; + } + + public long getPurgeTimestamp() { + return purgeTimestamp; + } + + public void setPurgeTimestamp(long purgeTimestamp) { + this.purgeTimestamp = purgeTimestamp; + } + + public long getToPurgeTimestamp() { + return toPurgeTimestamp; + } + + public void setToPurgeTimestamp(long toPurgeTimestamp) { + this.toPurgeTimestamp = toPurgeTimestamp; + } + + public long getLastUpdateTimestamp() { + return lastUpdateTimestamp; + } + + public void setLastUpdateTimestamp(long lastUpdateTimestamp) { + this.lastUpdateTimestamp = lastUpdateTimestamp; + } + + public String getProperties() { + return properties != null ? properties : EMPTY_MAP_STRING; + } + + @JsonIgnore + public Map getPropertiesAsMap() { + if (properties == null) { + return new HashMap<>(); + } + try { + return MAPPER.readValue(properties, new TypeReference<>() {}); + } catch (JsonProcessingException ex) { + throw new IllegalStateException( + String.format("Failed to deserialize json. properties %s", properties), ex); + } + } + + /** + * Set one single property + * + * @param propName name of the property + * @param propValue value of that property + */ + public void addProperty(String propName, String propValue) { + Map props = this.getPropertiesAsMap(); + props.put(propName, propValue); + this.setPropertiesAsMap(props); + } + + public void setProperties(String properties) { + this.properties = properties; + } + + @JsonIgnore + public void setPropertiesAsMap(Map properties) { + try { + this.properties = properties == null ? null : MAPPER.writeValueAsString(properties); + } catch (JsonProcessingException ex) { + throw new IllegalStateException( + String.format("Failed to serialize json. properties %s", properties), ex); + } + } + + public String getInternalProperties() { + return internalProperties != null ? internalProperties : EMPTY_MAP_STRING; + } + + @JsonIgnore + public Map getInternalPropertiesAsMap() { + if (this.internalProperties == null) { + return new HashMap<>(); + } + try { + return MAPPER.readValue(this.internalProperties, new TypeReference<>() {}); + } catch (JsonProcessingException ex) { + throw new IllegalStateException( + String.format( + "Failed to deserialize json. internalProperties %s", this.internalProperties), + ex); + } + } + + /** + * Set one single internal property + * + * @param propName name of the property + * @param propValue value of that property + */ + public void addInternalProperty(String propName, String propValue) { + Map props = this.getInternalPropertiesAsMap(); + props.put(propName, propValue); + this.setInternalPropertiesAsMap(props); + } + + public void setInternalProperties(String internalProperties) { + this.internalProperties = internalProperties; + } + + @JsonIgnore + public void setInternalPropertiesAsMap(Map internalProperties) { + try { + this.internalProperties = + internalProperties == null ? null : MAPPER.writeValueAsString(internalProperties); + } catch (JsonProcessingException ex) { + throw new IllegalStateException( + String.format("Failed to serialize json. internalProperties %s", internalProperties), ex); + } + } + + public int getGrantRecordsVersion() { + return grantRecordsVersion; + } + + public void setGrantRecordsVersion(int grantRecordsVersion) { + this.grantRecordsVersion = grantRecordsVersion; + } + + public static PolarisBaseEntity fromCore( + PolarisEntityCore coreEntity, PolarisEntityType entityType, PolarisEntitySubType subType) { + return new PolarisBaseEntity( + coreEntity.getCatalogId(), + coreEntity.getId(), + entityType, + subType, + coreEntity.getParentId(), + coreEntity.getName()); + } + + /** + * Copy constructor + * + * @param entity entity to copy + */ + public PolarisBaseEntity(PolarisBaseEntity entity) { + super( + entity.getCatalogId(), + entity.getId(), + entity.getParentId(), + entity.getTypeCode(), + entity.getName(), + entity.getEntityVersion()); + this.subTypeCode = entity.getSubTypeCode(); + this.createTimestamp = entity.getCreateTimestamp(); + this.dropTimestamp = entity.getDropTimestamp(); + this.purgeTimestamp = entity.getPurgeTimestamp(); + this.toPurgeTimestamp = entity.getToPurgeTimestamp(); + this.lastUpdateTimestamp = entity.getLastUpdateTimestamp(); + this.properties = entity.getProperties(); + this.internalProperties = entity.getInternalProperties(); + this.grantRecordsVersion = entity.getGrantRecordsVersion(); + } + + /** Build the DTO for a new entity */ + public PolarisBaseEntity( + long catalogId, + long id, + PolarisEntityType type, + PolarisEntitySubType subType, + long parentId, + String name) { + this(catalogId, id, type.getCode(), subType.getCode(), parentId, name); + } + + /** Build the DTO for a new entity */ + protected PolarisBaseEntity( + long catalogId, long id, int typeCode, int subTypeCode, long parentId, String name) { + super(catalogId, id, parentId, typeCode, name, 1); + this.subTypeCode = subTypeCode; + this.createTimestamp = System.currentTimeMillis(); + this.dropTimestamp = 0; + this.purgeTimestamp = 0; + this.toPurgeTimestamp = 0; + this.lastUpdateTimestamp = this.createTimestamp; + this.properties = null; + this.internalProperties = null; + this.grantRecordsVersion = 1; + } + + /** Build the DTO for a new entity */ + protected PolarisBaseEntity() { + super(); + } + + /** + * @return the subtype of this entity + */ + public @JsonIgnore PolarisEntitySubType getSubType() { + return PolarisEntitySubType.fromCode(this.subTypeCode); + } + + /** + * @return true if this entity has been dropped + */ + public @JsonIgnore boolean isDropped() { + return this.dropTimestamp != 0; + } + + @Override + public boolean equals(Object o) { + if (!super.equals(o)) { + return false; + } + if (this == o) { + return true; + } + if (!(o instanceof PolarisBaseEntity)) { + return false; + } + PolarisBaseEntity that = (PolarisBaseEntity) o; + return subTypeCode == that.subTypeCode + && createTimestamp == that.createTimestamp + && dropTimestamp == that.dropTimestamp + && purgeTimestamp == that.purgeTimestamp + && toPurgeTimestamp == that.toPurgeTimestamp + && lastUpdateTimestamp == that.lastUpdateTimestamp + && grantRecordsVersion == that.grantRecordsVersion + && Objects.equals(properties, that.properties) + && Objects.equals(internalProperties, that.internalProperties); + } + + @Override + public int hashCode() { + return Objects.hash( + catalogId, + id, + parentId, + typeCode, + name, + entityVersion, + subTypeCode, + createTimestamp, + dropTimestamp, + purgeTimestamp, + toPurgeTimestamp, + lastUpdateTimestamp, + properties, + internalProperties, + grantRecordsVersion); + } + + @Override + public String toString() { + return "PolarisBaseEntity{" + + super.toString() + + ", subTypeCode=" + + subTypeCode + + ", createTimestamp=" + + createTimestamp + + ", dropTimestamp=" + + dropTimestamp + + ", purgeTimestamp=" + + purgeTimestamp + + ", toPurgeTimestamp=" + + toPurgeTimestamp + + ", lastUpdateTimestamp=" + + lastUpdateTimestamp + + ", grantRecordsVersion=" + + grantRecordsVersion + + '}'; + } +} diff --git a/polaris-core/src/main/java/io/polaris/core/entity/PolarisChangeTrackingVersions.java b/polaris-core/src/main/java/io/polaris/core/entity/PolarisChangeTrackingVersions.java new file mode 100644 index 0000000000..7b0c320c06 --- /dev/null +++ b/polaris-core/src/main/java/io/polaris/core/entity/PolarisChangeTrackingVersions.java @@ -0,0 +1,45 @@ +package io.polaris.core.entity; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** Simple class to represent the version and grant records version associated to an entity */ +public class PolarisChangeTrackingVersions { + // entity version + private final int entityVersion; + + // entity grant records version + private final int grantRecordsVersion; + + /** + * Constructor + * + * @param entityVersion entity version + * @param grantRecordsVersion entity grant records version + */ + @JsonCreator + public PolarisChangeTrackingVersions( + @JsonProperty("entityVersion") int entityVersion, + @JsonProperty("grantRecordsVersion") int grantRecordsVersion) { + this.entityVersion = entityVersion; + this.grantRecordsVersion = grantRecordsVersion; + } + + public int getEntityVersion() { + return entityVersion; + } + + public int getGrantRecordsVersion() { + return grantRecordsVersion; + } + + @Override + public String toString() { + return "PolarisChangeTrackingVersions{" + + "entityVersion=" + + entityVersion + + ", grantRecordsVersion=" + + grantRecordsVersion + + '}'; + } +} diff --git a/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntitiesActiveKey.java b/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntitiesActiveKey.java new file mode 100644 index 0000000000..809e4e2180 --- /dev/null +++ b/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntitiesActiveKey.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. + */ + +package io.polaris.core.entity; + +public class PolarisEntitiesActiveKey { + + // entity catalog id + private final long catalogId; + + // parent id of the entity + private final long parentId; + + // code representing the type of that entity + private final int typeCode; + + // name of the entity + private final String name; + + public PolarisEntitiesActiveKey(long catalogId, long parentId, int typeCode, String name) { + this.catalogId = catalogId; + this.parentId = parentId; + this.typeCode = typeCode; + this.name = name; + } + + /** Constructor to create the object with provided entity */ + public PolarisEntitiesActiveKey(PolarisEntityCore entity) { + this.catalogId = entity.getCatalogId(); + this.parentId = entity.getParentId(); + this.typeCode = entity.getTypeCode(); + this.name = entity.getName(); + } + + public long getCatalogId() { + return catalogId; + } + + public long getParentId() { + return parentId; + } + + public int getTypeCode() { + return typeCode; + } + + public String getName() { + return name; + } + + @Override + public String toString() { + return "PolarisEntitiesActiveKey{" + + "catalogId=" + + catalogId + + ", parentId=" + + parentId + + ", typeCode=" + + typeCode + + ", name='" + + name + + '\'' + + '}'; + } +} diff --git a/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntity.java b/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntity.java new file mode 100644 index 0000000000..1fb25e67ab --- /dev/null +++ b/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntity.java @@ -0,0 +1,403 @@ +package io.polaris.core.entity; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.polaris.core.persistence.PolarisMetaStoreManager; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import org.jetbrains.annotations.NotNull; + +public class PolarisEntity extends PolarisBaseEntity { + + public static class NameAndId { + private final String name; + private final long id; + + public NameAndId(String name, long id) { + this.name = name; + this.id = id; + } + + public String getName() { + return name; + } + + public long getId() { + return id; + } + } + + public static class TypeSubTypeAndName { + private final PolarisEntityType type; + private final PolarisEntitySubType subType; + private final String name; + + public TypeSubTypeAndName(PolarisEntityType type, PolarisEntitySubType subType, String name) { + this.type = type; + this.subType = subType; + this.name = name; + } + + public PolarisEntityType getType() { + return type; + } + + public PolarisEntitySubType getSubType() { + return subType; + } + + public String getName() { + return name; + } + } + + @JsonCreator + private PolarisEntity( + @JsonProperty("catalogId") long catalogId, + @JsonProperty("typeCode") PolarisEntityType type, + @JsonProperty("subTypeCode") PolarisEntitySubType subType, + @JsonProperty("id") long id, + @JsonProperty("parentId") long parentId, + @JsonProperty("name") String name, + @JsonProperty("createTimestamp") long createTimestamp, + @JsonProperty("dropTimestamp") long dropTimestamp, + @JsonProperty("purgeTimestamp") long purgeTimestamp, + @JsonProperty("lastUpdateTimestamp") long lastUpdateTimestamp, + @JsonProperty("properties") String properties, + @JsonProperty("internalProperties") String internalProperties, + @JsonProperty("entityVersion") int entityVersion, + @JsonProperty("grantRecordsVersion") int grantRecordsVersion) { + super(catalogId, id, type, subType, parentId, name); + this.createTimestamp = createTimestamp; + this.dropTimestamp = dropTimestamp; + this.purgeTimestamp = purgeTimestamp; + this.lastUpdateTimestamp = lastUpdateTimestamp; + this.properties = properties; + this.internalProperties = internalProperties; + this.entityVersion = entityVersion; + this.grantRecordsVersion = grantRecordsVersion; + } + + public PolarisEntity( + long catalogId, + PolarisEntityType type, + PolarisEntitySubType subType, + long id, + long parentId, + String name, + long createTimestamp, + long dropTimestamp, + long purgeTimestamp, + long lastUpdateTimestamp, + Map properties, + Map internalProperties, + int entityVersion, + int grantRecordsVersion) { + super(catalogId, id, type, subType, parentId, name); + this.createTimestamp = createTimestamp; + this.dropTimestamp = dropTimestamp; + this.purgeTimestamp = purgeTimestamp; + this.lastUpdateTimestamp = lastUpdateTimestamp; + this.setPropertiesAsMap(properties); + this.setInternalPropertiesAsMap(internalProperties); + this.entityVersion = entityVersion; + this.grantRecordsVersion = grantRecordsVersion; + } + + public static PolarisEntity of(PolarisBaseEntity sourceEntity) { + if (sourceEntity != null) { + return new PolarisEntity(sourceEntity); + } + return null; + } + + public static PolarisEntity of(PolarisMetaStoreManager.EntityResult result) { + if (result.isSuccess()) { + return new PolarisEntity(result.getEntity()); + } + return null; + } + + public static PolarisEntityCore toCore(PolarisBaseEntity entity) { + PolarisEntityCore entityCore = + new PolarisEntityCore( + entity.getCatalogId(), + entity.getId(), + entity.getParentId(), + entity.getTypeCode(), + entity.getName(), + entity.getEntityVersion()); + return entityCore; + } + + public static List toCoreList(List path) { + return Optional.ofNullable(path) + .filter(Predicate.not(List::isEmpty)) + .map(list -> list.stream().map(PolarisEntity::toCore).collect(Collectors.toList())) + .orElse(null); + } + + public static List toNameAndIdList(List entities) { + return Optional.ofNullable(entities) + .map( + list -> + list.stream() + .map(record -> new NameAndId(record.getName(), record.getId())) + .collect(Collectors.toList())) + .orElse(null); + } + + public PolarisEntity(@NotNull PolarisBaseEntity sourceEntity) { + super( + sourceEntity.getCatalogId(), + sourceEntity.getId(), + sourceEntity.getTypeCode(), + sourceEntity.getSubTypeCode(), + sourceEntity.getParentId(), + sourceEntity.getName()); + this.createTimestamp = sourceEntity.getCreateTimestamp(); + this.dropTimestamp = sourceEntity.getDropTimestamp(); + this.purgeTimestamp = sourceEntity.getPurgeTimestamp(); + this.lastUpdateTimestamp = sourceEntity.getLastUpdateTimestamp(); + this.properties = sourceEntity.getProperties(); + this.internalProperties = sourceEntity.getInternalProperties(); + this.entityVersion = sourceEntity.getEntityVersion(); + this.grantRecordsVersion = sourceEntity.getGrantRecordsVersion(); + } + + @JsonIgnore + public PolarisEntityType getType() { + return PolarisEntityType.fromCode(getTypeCode()); + } + + @JsonIgnore + public PolarisEntitySubType getSubType() { + return PolarisEntitySubType.fromCode(getSubTypeCode()); + } + + @JsonIgnore + public NameAndId nameAndId() { + return new NameAndId(name, id); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("name=" + getName()); + sb.append(";id=" + getId()); + sb.append(";parentId=" + getParentId()); + sb.append(";entityVersion=" + getEntityVersion()); + sb.append(";type=" + getType()); + sb.append(";subType=" + getSubType()); + sb.append(";internalProperties=" + getInternalPropertiesAsMap()); + return sb.toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof PolarisEntity)) return false; + PolarisEntity that = (PolarisEntity) o; + return catalogId == that.catalogId + && id == that.id + && parentId == that.parentId + && createTimestamp == that.createTimestamp + && dropTimestamp == that.dropTimestamp + && purgeTimestamp == that.purgeTimestamp + && lastUpdateTimestamp == that.lastUpdateTimestamp + && entityVersion == that.entityVersion + && grantRecordsVersion == that.grantRecordsVersion + && typeCode == that.typeCode + && subTypeCode == that.subTypeCode + && Objects.equals(name, that.name) + && Objects.equals(properties, that.properties) + && Objects.equals(internalProperties, that.internalProperties); + } + + @Override + public int hashCode() { + return Objects.hash( + typeCode, + subTypeCode, + catalogId, + id, + parentId, + name, + createTimestamp, + dropTimestamp, + purgeTimestamp, + lastUpdateTimestamp, + properties, + internalProperties, + entityVersion, + grantRecordsVersion); + } + + public static class Builder extends BaseBuilder { + public Builder() { + super(); + } + + public Builder(PolarisEntity original) { + super(original); + } + + public PolarisEntity build() { + return buildBase(); + } + } + + @SuppressWarnings("unchecked") + public abstract static class BaseBuilder> { + protected long catalogId; + protected PolarisEntityType type; + protected PolarisEntitySubType subType; + protected long id; + protected long parentId; + protected String name; + protected long createTimestamp; + protected long dropTimestamp; + protected long purgeTimestamp; + protected long lastUpdateTimestamp; + protected Map properties; + protected Map internalProperties; + protected int entityVersion; + protected int grantRecordsVersion; + + protected BaseBuilder() { + this.catalogId = -1; + this.type = PolarisEntityType.NULL_TYPE; + this.subType = PolarisEntitySubType.NULL_SUBTYPE; + this.id = -1; + this.parentId = 0; + this.name = null; + this.createTimestamp = 0; + this.dropTimestamp = 0; + this.purgeTimestamp = 0; + this.lastUpdateTimestamp = 0; + this.properties = new HashMap<>(); + this.internalProperties = new HashMap<>(); + this.entityVersion = 1; + this.grantRecordsVersion = 1; + } + + protected BaseBuilder(T original) { + this.catalogId = original.catalogId; + this.type = original.getType(); + this.subType = original.getSubType(); + this.id = original.id; + this.parentId = original.parentId; + this.name = original.name; + this.createTimestamp = original.createTimestamp; + this.dropTimestamp = original.dropTimestamp; + this.purgeTimestamp = original.purgeTimestamp; + this.lastUpdateTimestamp = original.lastUpdateTimestamp; + this.properties = new HashMap<>(original.getPropertiesAsMap()); + this.internalProperties = new HashMap<>(original.getInternalPropertiesAsMap()); + this.entityVersion = original.entityVersion; + this.grantRecordsVersion = original.grantRecordsVersion; + } + + public abstract T build(); + + public PolarisEntity buildBase() { + // TODO: Validate required fields + // id > 0 already -- client must always supply id for idempotency purposes. + return new PolarisEntity( + catalogId, + type, + subType, + id, + parentId, + name, + createTimestamp, + dropTimestamp, + purgeTimestamp, + lastUpdateTimestamp, + properties, + internalProperties, + entityVersion, + grantRecordsVersion); + } + + public B setCatalogId(long catalogId) { + this.catalogId = catalogId; + return (B) this; + } + + public B setType(PolarisEntityType type) { + this.type = type; + return (B) this; + } + + public B setSubType(PolarisEntitySubType subType) { + this.subType = subType; + return (B) this; + } + + public B setId(long id) { + // TODO: Maybe block this one whenever builder is created from previously-existing entity + // since re-opening an entity should only be for modifying the mutable fields for a given + // logical entity. Would require separate builder type for "clone"-style copies, but + // usually when creating from other entity we want to preserve the id. + this.id = id; + return (B) this; + } + + public B setParentId(long parentId) { + this.parentId = parentId; + return (B) this; + } + + public B setName(String name) { + this.name = name; + return (B) this; + } + + public B setCreateTimestamp(long createTimestamp) { + this.createTimestamp = createTimestamp; + return (B) this; + } + + public B setDropTimestamp(long dropTimestamp) { + this.dropTimestamp = dropTimestamp; + return (B) this; + } + + public B setPurgeTimestamp(long purgeTimestamp) { + this.purgeTimestamp = purgeTimestamp; + return (B) this; + } + + public B setLastUpdateTimestamp(long lastUpdateTimestamp) { + this.lastUpdateTimestamp = lastUpdateTimestamp; + return (B) this; + } + + public B setProperties(Map properties) { + this.properties = new HashMap<>(properties); + return (B) this; + } + + public B setInternalProperties(Map internalProperties) { + this.internalProperties = new HashMap<>(internalProperties); + return (B) this; + } + + public B setEntityVersion(int entityVersion) { + this.entityVersion = entityVersion; + return (B) this; + } + + public B setGrantRecordsVersion(int grantRecordsVersion) { + this.grantRecordsVersion = grantRecordsVersion; + return (B) this; + } + } +} diff --git a/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntityActiveRecord.java b/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntityActiveRecord.java new file mode 100644 index 0000000000..e4d659fbc7 --- /dev/null +++ b/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntityActiveRecord.java @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. + */ + +package io.polaris.core.entity; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Objects; + +public class PolarisEntityActiveRecord { + // entity catalog id + private final long catalogId; + + // id of the entity + private final long id; + + // parent id of the entity + private final long parentId; + + // name of the entity + private final String name; + + // code representing the type of that entity + private final int typeCode; + + // code representing the subtype of that entity + private final int subTypeCode; + + public long getCatalogId() { + return catalogId; + } + + public long getId() { + return id; + } + + public long getParentId() { + return parentId; + } + + public String getName() { + return name; + } + + public int getTypeCode() { + return typeCode; + } + + public PolarisEntityType getType() { + return PolarisEntityType.fromCode(this.typeCode); + } + + public int getSubTypeCode() { + return subTypeCode; + } + + public PolarisEntitySubType getSubType() { + return PolarisEntitySubType.fromCode(this.subTypeCode); + } + + @JsonCreator + public PolarisEntityActiveRecord( + @JsonProperty("catalogId") long catalogId, + @JsonProperty("id") long id, + @JsonProperty("parentId") long parentId, + @JsonProperty("name") String name, + @JsonProperty("typeCode") int typeCode, + @JsonProperty("subTypeCode") int subTypeCode) { + this.catalogId = catalogId; + this.id = id; + this.parentId = parentId; + this.name = name; + this.typeCode = typeCode; + this.subTypeCode = subTypeCode; + } + + /** Constructor to create the object with provided entity */ + public PolarisEntityActiveRecord(PolarisBaseEntity entity) { + this.catalogId = entity.getCatalogId(); + this.id = entity.getId(); + this.parentId = entity.getParentId(); + this.typeCode = entity.getTypeCode(); + this.name = entity.getName(); + this.subTypeCode = entity.getSubTypeCode(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof PolarisEntityActiveRecord)) return false; + PolarisEntityActiveRecord that = (PolarisEntityActiveRecord) o; + return catalogId == that.catalogId + && id == that.id + && parentId == that.parentId + && typeCode == that.typeCode + && subTypeCode == that.subTypeCode + && Objects.equals(name, that.name); + } + + @Override + public int hashCode() { + return Objects.hash(catalogId, id, parentId, name, typeCode, subTypeCode); + } + + @Override + public String toString() { + return "PolarisEntitiesActiveRecord{" + + "catalogId=" + + catalogId + + ", id=" + + id + + ", parentId=" + + parentId + + ", name='" + + name + + '\'' + + ", typeCode=" + + typeCode + + ", subTypeCode=" + + subTypeCode + + '}'; + } +} diff --git a/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntityConstants.java b/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntityConstants.java new file mode 100644 index 0000000000..0715c77452 --- /dev/null +++ b/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntityConstants.java @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. + */ + +package io.polaris.core.entity; + +public class PolarisEntityConstants { + + // the key for the client_id property associated with a principal + private static final String CLIENT_ID_PROPERTY_NAME = "client_id"; + + // id of the root entity + private static final long ROOT_ENTITY_ID = 0L; + + // special 0 value to represent a NULL value. For example the catalog id is null for a top-level + // entity like a catalog + private static final long NULL_ID = 0L; + + // the name of the single root container representing an entire realm + private static final String ROOT_CONTAINER_NAME = "root_container"; + + // the name of the catalog/root admin role + private static final String ADMIN_CATALOG_ROLE_NAME = "catalog_admin"; + + // the name of the root principal we create at bootstrap time + private static final String ROOT_PRINCIPAL_NAME = "root"; + + // the name of the principal role we create to manage the entire Polaris service + private static final String ADMIN_PRINCIPAL_ROLE_NAME = "service_admin"; + + // 24 hours retention before purging. This should be a config + private static final long RETENTION_TIME_IN_MS = 24 * 3600_000; + + private static final String STORAGE_CONFIGURATION_INFO_PROPERTY_NAME = + "storage_configuration_info"; + + private static final String STORAGE_INTEGRATION_IDENTIFIER_PROPERTY_NAME = + "storage_integration_identifier"; + + private static final String PRINCIPAL_TYPE_NAME = "principal_type_name"; + + public static final String PRINCIPAL_CREDENTIAL_ROTATION_REQUIRED_STATE = + "CREDENTIAL_ROTATION_REQUIRED"; + + /** + * Name format of storage integration for polaris entity: POLARIS__ . This + * name format gives us flexibility to switch to use integration name in the future if we want. + */ + public static final String POLARIS_STORAGE_INT_NAME_FORMAT = "POLARIS_%s_%s"; + + public static long getRootEntityId() { + return ROOT_ENTITY_ID; + } + + public static long getNullId() { + return NULL_ID; + } + + public static String getRootContainerName() { + return ROOT_CONTAINER_NAME; + } + + public static String getNameOfCatalogAdminRole() { + return ADMIN_CATALOG_ROLE_NAME; + } + + public static String getRootPrincipalName() { + return ROOT_PRINCIPAL_NAME; + } + + public static String getNameOfPrincipalServiceAdminRole() { + return ADMIN_PRINCIPAL_ROLE_NAME; + } + + public static long getRetentionTimeInMs() { + return RETENTION_TIME_IN_MS; + } + + public static String getClientIdPropertyName() { + return CLIENT_ID_PROPERTY_NAME; + } + + public static String getStorageIntegrationIdentifierPropertyName() { + return STORAGE_INTEGRATION_IDENTIFIER_PROPERTY_NAME; + } + + public static String getStorageConfigInfoPropertyName() { + return STORAGE_CONFIGURATION_INFO_PROPERTY_NAME; + } + + public static String getPolarisStorageIntegrationNameFormat() { + return POLARIS_STORAGE_INT_NAME_FORMAT; + } + + public static String getPrincipalTypeName() { + return PRINCIPAL_TYPE_NAME; + } +} diff --git a/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntityCore.java b/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntityCore.java new file mode 100644 index 0000000000..1b7054401f --- /dev/null +++ b/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntityCore.java @@ -0,0 +1,174 @@ +/* + * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. + */ + +package io.polaris.core.entity; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import java.util.Objects; + +/** + * Core attributes representing basic information about an entity. Change generally means that the + * entity will be renamed, dropped, re-created, re-parented. Basically any change to the structure + * of the entity tree. For some operations like updating the entity, change will mean any change, + * i.e. entity version mismatch. + */ +public class PolarisEntityCore { + + // the id of the catalog associated to that entity. NULL_ID if this entity is top-level like + // a catalog + protected long catalogId; + + // the id of the entity which was resolved + protected long id; + + // the id of the parent of this entity, use 0 for a top-level entity whose parent is the account + protected long parentId; + + // the type of the entity when it was resolved + protected int typeCode; + + // the name that this entity had when it was resolved + protected String name; + + // the version that this entity had when it was resolved + protected int entityVersion; + + public PolarisEntityCore() {} + + public PolarisEntityCore( + long catalogId, long id, long parentId, int typeCode, String name, int entityVersion) { + this.catalogId = catalogId; + this.id = id; + this.parentId = parentId; + this.typeCode = typeCode; + this.name = name; + this.entityVersion = entityVersion; + } + + public PolarisEntityCore(PolarisBaseEntity entity) { + this.catalogId = entity.getCatalogId(); + this.id = entity.getId(); + this.parentId = entity.getParentId(); + this.typeCode = entity.getTypeCode(); + this.name = entity.getName(); + this.entityVersion = entity.getEntityVersion(); + } + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public long getParentId() { + return parentId; + } + + public void setParentId(long parentId) { + this.parentId = parentId; + } + + public int getTypeCode() { + return typeCode; + } + + public void setTypeCode(int typeCode) { + this.typeCode = typeCode; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getEntityVersion() { + return entityVersion; + } + + public long getCatalogId() { + return catalogId; + } + + public void setCatalogId(long catalogId) { + this.catalogId = catalogId; + } + + public void setEntityVersion(int entityVersion) { + this.entityVersion = entityVersion; + } + + /** + * @return the type of this entity + */ + @JsonIgnore + public PolarisEntityType getType() { + return PolarisEntityType.fromCode(this.typeCode); + } + + /** + * @return true if this entity cannot be dropped or renamed. Applies to the admin catalog role and + * the polaris service admin principal role. + */ + @JsonIgnore + public boolean cannotBeDroppedOrRenamed() { + return (this.typeCode == PolarisEntityType.CATALOG_ROLE.getCode() + && this.name.equals(PolarisEntityConstants.getNameOfCatalogAdminRole())) + || (this.typeCode == PolarisEntityType.PRINCIPAL_ROLE.getCode() + && this.name.equals(PolarisEntityConstants.getNameOfPrincipalServiceAdminRole())); + } + + /** + * @return true if this entity is top-level, like a catalog, a principal, + */ + @JsonIgnore + public boolean isTopLevel() { + return this.getType().isTopLevel(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof PolarisEntityCore)) { + return false; + } + PolarisEntityCore that = (PolarisEntityCore) o; + return catalogId == that.catalogId + && id == that.id + && parentId == that.parentId + && typeCode == that.typeCode + && entityVersion == that.entityVersion + && Objects.equals(name, that.name); + } + + @Override + public int hashCode() { + return Objects.hash(catalogId, id, parentId, typeCode, name, entityVersion); + } + + @Override + public String toString() { + return "PolarisEntityCore{" + + "catalogId=" + + catalogId + + ", id=" + + id + + ", parentId=" + + parentId + + ", typeCode=" + + typeCode + + ", name='" + + name + + '\'' + + ", entityVersion=" + + entityVersion + + '}'; + } +} diff --git a/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntityId.java b/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntityId.java new file mode 100644 index 0000000000..5f9b5f0ae6 --- /dev/null +++ b/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntityId.java @@ -0,0 +1,48 @@ +package io.polaris.core.entity; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Objects; + +/** Simple record like class to represent the unique identifier of an entity */ +public class PolarisEntityId { + + // id of the catalog for this entity. If this entity is top-level, this will be NULL. Only not + // null if this entity is a catalog entity like a namespace, a role, a table, a view, ... + private final long catalogId; + + // entity id + private final long id; + + @JsonCreator + public PolarisEntityId(@JsonProperty("catalogId") long catalogId, @JsonProperty("id") long id) { + this.catalogId = catalogId; + this.id = id; + } + + public long getCatalogId() { + return catalogId; + } + + public long getId() { + return id; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PolarisEntityId that = (PolarisEntityId) o; + return catalogId == that.catalogId && id == that.id; + } + + @Override + public int hashCode() { + return Objects.hash(catalogId, id); + } + + @Override + public String toString() { + return "PolarisEntityId{" + "catalogId=" + catalogId + ", id=" + id + '}'; + } +} diff --git a/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntitySubType.java b/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntitySubType.java new file mode 100644 index 0000000000..0d1adca120 --- /dev/null +++ b/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntitySubType.java @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. + */ + +package io.polaris.core.entity; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import org.jetbrains.annotations.Nullable; + +/** Subtype for an entity */ +public enum PolarisEntitySubType { + // ANY_SUBTYPE is not stored but is used to indicate that any subtype entities should be + // returned, for example when doing a list operation or checking if a table like object of + // name X exists + ANY_SUBTYPE(-1, null), + // the NULL value is used when an entity has no subtype, i.e. NOT_APPLICABLE really + NULL_SUBTYPE(0, null), + TABLE(2, PolarisEntityType.TABLE_LIKE), + VIEW(3, PolarisEntityType.TABLE_LIKE); + + // to efficiently map the code of a subtype to its corresponding subtype enum, use a reverse + // array which is initialized below + private static final PolarisEntitySubType[] REVERSE_MAPPING_ARRAY; + + static { + // find max array size + int maxId = 0; + for (PolarisEntitySubType entitySubType : PolarisEntitySubType.values()) { + if (maxId < entitySubType.code) { + maxId = entitySubType.code; + } + } + + // allocate mapping array + REVERSE_MAPPING_ARRAY = new PolarisEntitySubType[maxId + 1]; + + // populate mapping array, only for positive indices + for (PolarisEntitySubType entitySubType : PolarisEntitySubType.values()) { + if (entitySubType.code >= 0) { + REVERSE_MAPPING_ARRAY[entitySubType.code] = entitySubType; + } + } + } + + // unique code associated to that entity subtype + private final int code; + + // parent type for this entity + private final PolarisEntityType parentType; + + PolarisEntitySubType(int code, PolarisEntityType parentType) { + // remember the id of this entity + this.code = code; + this.parentType = parentType; + } + + /** + * @return the code associated to a subtype, will be stored in FDB + */ + @JsonValue + public int getCode() { + return code; + } + + /** + * @return parent type of that entity + */ + public PolarisEntityType getParentType() { + return this.parentType; + } + + /** + * Given the id of the subtype of an entity, return the subtype associated to it. Return null if + * not found + * + * @param entitySubTypeCode code associated to the entity type + * @return entity subtype corresponding to that code or null if mapping not found + */ + @JsonCreator + public static @Nullable PolarisEntitySubType fromCode(int entitySubTypeCode) { + // ensure it is within bounds + if (entitySubTypeCode >= REVERSE_MAPPING_ARRAY.length) { + return null; + } + + // get value + if (entitySubTypeCode >= 0) { + return REVERSE_MAPPING_ARRAY[entitySubTypeCode]; + } else { + for (PolarisEntitySubType entitySubType : PolarisEntitySubType.values()) { + if (entitySubType.code == entitySubTypeCode) { + return entitySubType; + } + } + } + + return null; + } +} diff --git a/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntityType.java b/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntityType.java new file mode 100644 index 0000000000..b3a009a0a2 --- /dev/null +++ b/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntityType.java @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. + */ + +package io.polaris.core.entity; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import org.jetbrains.annotations.Nullable; + +/** Types of entities with their id */ +public enum PolarisEntityType { + NULL_TYPE(0, null, false, false), + ROOT(1, null, false, false), + PRINCIPAL(2, ROOT, true, false), + PRINCIPAL_ROLE(3, ROOT, true, false), + CATALOG(4, ROOT, false, false), + CATALOG_ROLE(5, CATALOG, true, false), + NAMESPACE(6, CATALOG, false, true), + // generic table is either a view or a real table + TABLE_LIKE(7, NAMESPACE, false, false), + TASK(8, ROOT, false, false), + FILE(9, TABLE_LIKE, false, false); + + // to efficiently map a code to its corresponding entity type, use a reverse array which + // is initialized below + private static final PolarisEntityType[] REVERSE_MAPPING_ARRAY; + + static { + // find max array size + int maxId = 0; + for (PolarisEntityType entityType : PolarisEntityType.values()) { + if (maxId < entityType.code) { + maxId = entityType.code; + } + } + + // allocate mapping array + REVERSE_MAPPING_ARRAY = new PolarisEntityType[maxId + 1]; + + // populate mapping array + for (PolarisEntityType entityType : PolarisEntityType.values()) { + REVERSE_MAPPING_ARRAY[entityType.code] = entityType; + } + } + + // unique id for an entity type + private final int code; + + // true if this entity is a grantee, i.e. is an entity which can be on the receiving end of + // a grant. Only roles and principals are grantees + private final boolean isGrantee; + + // true if the parent entity type can also be the same type (e.g. namespaces) + private final boolean parentSelfReference; + + // parent entity type, null for an ACCOUNT + private final PolarisEntityType parentType; + + PolarisEntityType(int id, PolarisEntityType parentType, boolean isGrantee, boolean sefRef) { + // remember the id of this entity + this.code = id; + this.isGrantee = isGrantee; + this.parentType = parentType; + this.parentSelfReference = sefRef; + } + + /** + * @return the code associated to the specified the entity type, will be stored in FDB + */ + @JsonValue + public int getCode() { + return code; + } + + /** + * @return true if this entity is a grantee, i.e. an entity which can receive grants + */ + public boolean isGrantee() { + return this.isGrantee; + } + + /** + * @return true if this entity can be nested with itself (like a NAMESPACE) + */ + public boolean isParentSelfReference() { + return parentSelfReference; + } + + /** + * Given the code associated to the type of entity, return the subtype associated to it. Return + * null if not found + * + * @param entityTypeCode code associated to the entity type + * @return entity type corresponding to that code or null if mapping not found + */ + @JsonCreator + public static @Nullable PolarisEntityType fromCode(int entityTypeCode) { + // ensure it is within bounds + if (entityTypeCode >= REVERSE_MAPPING_ARRAY.length) { + return null; + } + + // get value + return REVERSE_MAPPING_ARRAY[entityTypeCode]; + } + + /** + * @return TRUE if this entity is top-level + */ + public boolean isTopLevel() { + return (this.parentType == ROOT || this == ROOT); + } + + /** + * @return the parent type of this type in the entity hierarchy + */ + public PolarisEntityType getParentType() { + return this.parentType; + } +} diff --git a/polaris-core/src/main/java/io/polaris/core/entity/PolarisGrantRecord.java b/polaris-core/src/main/java/io/polaris/core/entity/PolarisGrantRecord.java new file mode 100644 index 0000000000..b7c8539284 --- /dev/null +++ b/polaris-core/src/main/java/io/polaris/core/entity/PolarisGrantRecord.java @@ -0,0 +1,142 @@ +/* + * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. + */ + +package io.polaris.core.entity; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class PolarisGrantRecord { + + // id of the catalog where the securable entity resides, NULL_ID if this entity is a top-level + // account entity + private long securableCatalogId; + + // id of the securable + private long securableId; + + // id of the catalog where the grantee entity resides, NULL_ID if this entity is a top-level + // account entity + private long granteeCatalogId; + + // id of the grantee + private long granteeId; + + // id associated to the privilege + private int privilegeCode; + + public PolarisGrantRecord() {} + + public long getSecurableCatalogId() { + return securableCatalogId; + } + + public void setSecurableCatalogId(long securableCatalogId) { + this.securableCatalogId = securableCatalogId; + } + + public long getSecurableId() { + return securableId; + } + + public void setSecurableId(long securableId) { + this.securableId = securableId; + } + + public long getGranteeCatalogId() { + return granteeCatalogId; + } + + public void setGranteeCatalogId(long granteeCatalogId) { + this.granteeCatalogId = granteeCatalogId; + } + + public long getGranteeId() { + return granteeId; + } + + public void setGranteeId(long granteeId) { + this.granteeId = granteeId; + } + + public int getPrivilegeCode() { + return privilegeCode; + } + + public void setPrivilegeCode(int privilegeCode) { + this.privilegeCode = privilegeCode; + } + + /** + * Constructor + * + * @param securableCatalogId catalog id for the securable. Can be NULL_ID if securable is + * top-level account entity + * @param securableId id of the securable + * @param granteeCatalogId catalog id for the grantee, Can be NULL_ID if grantee is top-level + * account entity + * @param granteeId id of the grantee + * @param privilegeCode privilege being granted to the grantee on the securable + */ + @JsonCreator + public PolarisGrantRecord( + @JsonProperty("securableCatalogId") long securableCatalogId, + @JsonProperty("securableId") long securableId, + @JsonProperty("granteeCatalogId") long granteeCatalogId, + @JsonProperty("granteeId") long granteeId, + @JsonProperty("privilegeCode") int privilegeCode) { + this.securableCatalogId = securableCatalogId; + this.securableId = securableId; + this.granteeCatalogId = granteeCatalogId; + this.granteeId = granteeId; + this.privilegeCode = privilegeCode; + } + + /** + * Copy constructor + * + * @param grantRec grant rec to copy + */ + public PolarisGrantRecord(PolarisGrantRecord grantRec) { + this.securableCatalogId = grantRec.getSecurableCatalogId(); + this.securableId = grantRec.getSecurableId(); + this.granteeCatalogId = grantRec.getGranteeCatalogId(); + this.granteeId = grantRec.getGranteeId(); + this.privilegeCode = grantRec.getPrivilegeCode(); + } + + @Override + public String toString() { + return "PolarisGrantRec{" + + "securableCatalogId=" + + securableCatalogId + + ", securableId=" + + securableId + + ", granteeCatalogId=" + + granteeCatalogId + + ", granteeId=" + + granteeId + + ", privilegeCode=" + + privilegeCode + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PolarisGrantRecord that = (PolarisGrantRecord) o; + return securableCatalogId == that.securableCatalogId + && securableId == that.securableId + && granteeCatalogId == that.granteeCatalogId + && granteeId == that.granteeId + && privilegeCode == that.privilegeCode; + } + + @Override + public int hashCode() { + return java.util.Objects.hash( + securableCatalogId, securableId, granteeCatalogId, granteeId, privilegeCode); + } +} diff --git a/polaris-core/src/main/java/io/polaris/core/entity/PolarisPrincipalSecrets.java b/polaris-core/src/main/java/io/polaris/core/entity/PolarisPrincipalSecrets.java new file mode 100644 index 0000000000..a32326e257 --- /dev/null +++ b/polaris-core/src/main/java/io/polaris/core/entity/PolarisPrincipalSecrets.java @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. + */ + +package io.polaris.core.entity; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.security.SecureRandom; + +/** + * Simple class to represent the secrets used to authenticate a catalog principal, These secrets are + * managed separately. + */ +public class PolarisPrincipalSecrets { + + // secure random number generator + private static final SecureRandom secureRandom = new SecureRandom(); + + // the id of the principal + private final long principalId; + + // the client id for that principal + private final String principalClientId; + + // the main secret for that principal + private String mainSecret; + + // the secondary secret for that principal + private String secondarySecret; + + /** + * Generate a secure random string + * + * @return the secure random string we generated + */ + private String generateRandomHexString(int stringLength) { + + // generate random byte array + byte[] randomBytes = + new byte[stringLength / 2]; // Each byte will be represented by two hex characters + secureRandom.nextBytes(randomBytes); + + // build string + StringBuilder sb = new StringBuilder(); + for (byte randomByte : randomBytes) { + sb.append(String.format("%02x", randomByte)); + } + + return sb.toString(); + } + + @JsonCreator + public PolarisPrincipalSecrets( + @JsonProperty("principalId") long principalId, + @JsonProperty("principalClientId") String principalClientId, + @JsonProperty("mainSecret") String mainSecret, + @JsonProperty("secondarySecret") String secondarySecret) { + this.principalId = principalId; + this.principalClientId = principalClientId; + this.mainSecret = mainSecret; + this.secondarySecret = secondarySecret; + } + + public PolarisPrincipalSecrets(PolarisPrincipalSecrets principalSecrets) { + this.principalId = principalSecrets.getPrincipalId(); + this.principalClientId = principalSecrets.getPrincipalClientId(); + this.mainSecret = principalSecrets.getMainSecret(); + this.secondarySecret = principalSecrets.getSecondarySecret(); + } + + public PolarisPrincipalSecrets(long principalId) { + this.principalId = principalId; + this.principalClientId = this.generateRandomHexString(16); + this.mainSecret = this.generateRandomHexString(32); + this.secondarySecret = this.generateRandomHexString(32); + } + + /** + * Rotate the main secrets + * + * @param mainSecretToRotate the main secrets to rotate + */ + public void rotateSecrets(String mainSecretToRotate) { + this.secondarySecret = mainSecretToRotate; + this.mainSecret = this.generateRandomHexString(32); + } + + public long getPrincipalId() { + return principalId; + } + + public String getPrincipalClientId() { + return principalClientId; + } + + public String getMainSecret() { + return mainSecret; + } + + public String getSecondarySecret() { + return secondarySecret; + } +} diff --git a/polaris-core/src/main/java/io/polaris/core/entity/PolarisPrivilege.java b/polaris-core/src/main/java/io/polaris/core/entity/PolarisPrivilege.java new file mode 100644 index 0000000000..f17b5356cd --- /dev/null +++ b/polaris-core/src/main/java/io/polaris/core/entity/PolarisPrivilege.java @@ -0,0 +1,201 @@ +/* + * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. + */ + +package io.polaris.core.entity; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** List of privileges */ +public enum PolarisPrivilege { + SERVICE_MANAGE_ACCESS(1, PolarisEntityType.ROOT), + CATALOG_MANAGE_ACCESS(2, PolarisEntityType.CATALOG), + CATALOG_ROLE_USAGE( + 3, + PolarisEntityType.CATALOG_ROLE, + PolarisEntitySubType.NULL_SUBTYPE, + PolarisEntityType.PRINCIPAL_ROLE), + PRINCIPAL_ROLE_USAGE( + 4, + PolarisEntityType.PRINCIPAL_ROLE, + PolarisEntitySubType.NULL_SUBTYPE, + PolarisEntityType.PRINCIPAL), + NAMESPACE_CREATE(5, PolarisEntityType.NAMESPACE), + TABLE_CREATE(6, PolarisEntityType.NAMESPACE), + VIEW_CREATE(7, PolarisEntityType.NAMESPACE), + NAMESPACE_DROP(8, PolarisEntityType.NAMESPACE), + TABLE_DROP(9, PolarisEntityType.TABLE_LIKE, PolarisEntitySubType.TABLE), + VIEW_DROP(10, PolarisEntityType.TABLE_LIKE, PolarisEntitySubType.VIEW), + NAMESPACE_LIST(11, PolarisEntityType.NAMESPACE), + TABLE_LIST(12, PolarisEntityType.NAMESPACE), + VIEW_LIST(13, PolarisEntityType.NAMESPACE), + NAMESPACE_READ_PROPERTIES(14, PolarisEntityType.NAMESPACE), + TABLE_READ_PROPERTIES(15, PolarisEntityType.TABLE_LIKE, PolarisEntitySubType.TABLE), + VIEW_READ_PROPERTIES(16, PolarisEntityType.TABLE_LIKE, PolarisEntitySubType.VIEW), + NAMESPACE_WRITE_PROPERTIES(17, PolarisEntityType.NAMESPACE), + TABLE_WRITE_PROPERTIES(18, PolarisEntityType.TABLE_LIKE, PolarisEntitySubType.TABLE), + VIEW_WRITE_PROPERTIES(19, PolarisEntityType.TABLE_LIKE, PolarisEntitySubType.VIEW), + TABLE_READ_DATA(20, PolarisEntityType.TABLE_LIKE, PolarisEntitySubType.TABLE), + TABLE_WRITE_DATA(21, PolarisEntityType.TABLE_LIKE, PolarisEntitySubType.TABLE), + NAMESPACE_FULL_METADATA(22, PolarisEntityType.NAMESPACE), + TABLE_FULL_METADATA(23, PolarisEntityType.TABLE_LIKE, PolarisEntitySubType.TABLE), + VIEW_FULL_METADATA(24, PolarisEntityType.TABLE_LIKE, PolarisEntitySubType.VIEW), + CATALOG_CREATE(25, PolarisEntityType.ROOT), + CATALOG_DROP(26, PolarisEntityType.CATALOG), + CATALOG_LIST(27, PolarisEntityType.ROOT), + CATALOG_READ_PROPERTIES(28, PolarisEntityType.CATALOG), + CATALOG_WRITE_PROPERTIES(29, PolarisEntityType.CATALOG), + CATALOG_FULL_METADATA(30, PolarisEntityType.CATALOG), + CATALOG_MANAGE_METADATA(31, PolarisEntityType.CATALOG), + CATALOG_MANAGE_CONTENT(32, PolarisEntityType.CATALOG), + PRINCIPAL_LIST_GRANTS(33, PolarisEntityType.PRINCIPAL), + PRINCIPAL_ROLE_LIST_GRANTS(34, PolarisEntityType.PRINCIPAL), + CATALOG_ROLE_LIST_GRANTS(35, PolarisEntityType.PRINCIPAL), + CATALOG_LIST_GRANTS(36, PolarisEntityType.CATALOG), + NAMESPACE_LIST_GRANTS(37, PolarisEntityType.NAMESPACE), + TABLE_LIST_GRANTS(38, PolarisEntityType.TABLE_LIKE, PolarisEntitySubType.TABLE), + VIEW_LIST_GRANTS(39, PolarisEntityType.TABLE_LIKE, PolarisEntitySubType.VIEW), + CATALOG_MANAGE_GRANTS_ON_SECURABLE(40, PolarisEntityType.CATALOG), + NAMESPACE_MANAGE_GRANTS_ON_SECURABLE(41, PolarisEntityType.NAMESPACE), + TABLE_MANAGE_GRANTS_ON_SECURABLE(42, PolarisEntityType.TABLE_LIKE, PolarisEntitySubType.TABLE), + VIEW_MANAGE_GRANTS_ON_SECURABLE(43, PolarisEntityType.TABLE_LIKE, PolarisEntitySubType.VIEW), + PRINCIPAL_CREATE(44, PolarisEntityType.ROOT), + PRINCIPAL_DROP(45, PolarisEntityType.PRINCIPAL), + PRINCIPAL_LIST(46, PolarisEntityType.ROOT), + PRINCIPAL_READ_PROPERTIES(47, PolarisEntityType.PRINCIPAL), + PRINCIPAL_WRITE_PROPERTIES(48, PolarisEntityType.PRINCIPAL), + PRINCIPAL_FULL_METADATA(49, PolarisEntityType.PRINCIPAL), + PRINCIPAL_MANAGE_GRANTS_ON_SECURABLE(50, PolarisEntityType.PRINCIPAL), + PRINCIPAL_MANAGE_GRANTS_FOR_GRANTEE(51, PolarisEntityType.PRINCIPAL), + PRINCIPAL_ROTATE_CREDENTIALS(52, PolarisEntityType.PRINCIPAL), + PRINCIPAL_RESET_CREDENTIALS(53, PolarisEntityType.PRINCIPAL), + PRINCIPAL_ROLE_CREATE(54, PolarisEntityType.ROOT), + PRINCIPAL_ROLE_DROP(55, PolarisEntityType.PRINCIPAL_ROLE), + PRINCIPAL_ROLE_LIST(56, PolarisEntityType.ROOT), + PRINCIPAL_ROLE_READ_PROPERTIES(57, PolarisEntityType.PRINCIPAL_ROLE), + PRINCIPAL_ROLE_WRITE_PROPERTIES(58, PolarisEntityType.PRINCIPAL_ROLE), + PRINCIPAL_ROLE_FULL_METADATA(59, PolarisEntityType.PRINCIPAL_ROLE), + PRINCIPAL_ROLE_MANAGE_GRANTS_ON_SECURABLE(60, PolarisEntityType.PRINCIPAL_ROLE), + PRINCIPAL_ROLE_MANAGE_GRANTS_FOR_GRANTEE(61, PolarisEntityType.PRINCIPAL_ROLE), + CATALOG_ROLE_CREATE(62, PolarisEntityType.CATALOG), + CATALOG_ROLE_DROP(63, PolarisEntityType.CATALOG_ROLE), + CATALOG_ROLE_LIST(64, PolarisEntityType.CATALOG), + CATALOG_ROLE_READ_PROPERTIES(65, PolarisEntityType.CATALOG_ROLE), + CATALOG_ROLE_WRITE_PROPERTIES(66, PolarisEntityType.CATALOG_ROLE), + CATALOG_ROLE_FULL_METADATA(67, PolarisEntityType.CATALOG_ROLE), + CATALOG_ROLE_MANAGE_GRANTS_ON_SECURABLE(68, PolarisEntityType.CATALOG_ROLE), + CATALOG_ROLE_MANAGE_GRANTS_FOR_GRANTEE(69, PolarisEntityType.CATALOG_ROLE), + ; + + /** + * Full constructor + * + * @param code internal code associated to this privilege + * @param securableType securable type + * @param securableSubType securable subtype, mostly NULL_SUBTYPE + * @param granteeType grantee type, generally a ROLE + */ + PolarisPrivilege( + int code, + @NotNull PolarisEntityType securableType, + @NotNull PolarisEntitySubType securableSubType, + @NotNull PolarisEntityType granteeType) { + this.code = code; + this.securableType = securableType; + this.securableSubType = securableSubType; + this.granteeType = granteeType; + } + + /** + * Simple constructor, when the grantee is a role and the securable subtype is NULL_SUBTYPE + * + * @param code internal code associated to this privilege + * @param securableType securable type + */ + PolarisPrivilege(int code, @NotNull PolarisEntityType securableType) { + this.code = code; + this.securableType = securableType; + this.securableSubType = PolarisEntitySubType.NULL_SUBTYPE; + this.granteeType = PolarisEntityType.CATALOG_ROLE; + } + + /** + * Constructor when the grantee is a ROLE + * + * @param code internal code associated to this privilege + * @param securableType securable type + * @param securableSubType securable subtype, mostly NULL_SUBTYPE + */ + PolarisPrivilege( + int code, + @NotNull PolarisEntityType securableType, + @NotNull PolarisEntitySubType securableSubType) { + this.code = code; + this.securableType = securableType; + this.securableSubType = securableSubType; + this.granteeType = PolarisEntityType.CATALOG_ROLE; + } + + // internal code used to represent this privilege + private final int code; + + // the type of the securable for this privilege + private final PolarisEntityType securableType; + + // the subtype of the securable for this privilege + private final PolarisEntitySubType securableSubType; + + // the type of the securable for this privilege + private final PolarisEntityType granteeType; + + // to efficiently map a code to its corresponding entity type, use a reverse array which + // is initialized below + private static final PolarisPrivilege[] REVERSE_MAPPING_ARRAY; + + static { + // find max array size + int maxId = 0; + for (PolarisPrivilege privilegeDef : PolarisPrivilege.values()) { + if (maxId < privilegeDef.code) { + maxId = privilegeDef.code; + } + } + + // allocate mapping array + REVERSE_MAPPING_ARRAY = new PolarisPrivilege[maxId + 1]; + + // populate mapping array + for (PolarisPrivilege privilegeDef : PolarisPrivilege.values()) { + REVERSE_MAPPING_ARRAY[privilegeDef.code] = privilegeDef; + } + } + + /** + * @return the code associated to the specified privilege + */ + @JsonValue + public int getCode() { + return code; + } + + /** + * Given the code associated to a privilege, return the privilege associated to it. Return null if + * not found + * + * @param code code associated to the entity type + * @return entity type corresponding to that code or null if mapping not found + */ + @JsonCreator + public static @Nullable PolarisPrivilege fromCode(int code) { + // ensure it is within bounds + if (code >= REVERSE_MAPPING_ARRAY.length) { + return null; + } + + // get value + return REVERSE_MAPPING_ARRAY[code]; + } +} diff --git a/polaris-core/src/main/java/io/polaris/core/entity/PolarisTaskConstants.java b/polaris-core/src/main/java/io/polaris/core/entity/PolarisTaskConstants.java new file mode 100644 index 0000000000..26ddad3708 --- /dev/null +++ b/polaris-core/src/main/java/io/polaris/core/entity/PolarisTaskConstants.java @@ -0,0 +1,13 @@ +package io.polaris.core.entity; + +/** Constants used to store task properties and configuration parameters */ +public class PolarisTaskConstants { + public static final long TASK_TIMEOUT_MILLIS = 300000; + public static final String TASK_TIMEOUT_MILLIS_CONFIG = "POLARIS_TASK_TIMEOUT_MILLIS"; + public static final String LAST_ATTEMPT_EXECUTOR_ID = "lastAttemptExecutorId"; + public static final String LAST_ATTEMPT_START_TIME = "lastAttemptStartTime"; + public static final String ATTEMPT_COUNT = "attemptCount"; + public static final String TASK_DATA = "data"; + public static final String TASK_TYPE = "taskType"; + public static final String STORAGE_LOCATION = "storageLocation"; +} diff --git a/polaris-core/src/main/java/io/polaris/core/entity/PrincipalEntity.java b/polaris-core/src/main/java/io/polaris/core/entity/PrincipalEntity.java new file mode 100644 index 0000000000..8944bf0e51 --- /dev/null +++ b/polaris-core/src/main/java/io/polaris/core/entity/PrincipalEntity.java @@ -0,0 +1,67 @@ +package io.polaris.core.entity; + +import io.polaris.core.admin.model.Principal; + +/** Wrapper for translating between the REST Principal object and the base PolarisEntity type. */ +public class PrincipalEntity extends PolarisEntity { + public PrincipalEntity(PolarisBaseEntity sourceEntity) { + super(sourceEntity); + } + + public static PrincipalEntity of(PolarisBaseEntity sourceEntity) { + if (sourceEntity != null) { + return new PrincipalEntity(sourceEntity); + } + return null; + } + + public static PrincipalEntity fromPrincipal(Principal principal) { + return new Builder() + .setName(principal.getName()) + .setProperties(principal.getProperties()) + .setClientId(principal.getClientId()) + .build(); + } + + public Principal asPrincipal() { + return new Principal( + getName(), + getClientId(), + getPropertiesAsMap(), + getCreateTimestamp(), + getLastUpdateTimestamp(), + getEntityVersion()); + } + + public String getClientId() { + return getInternalPropertiesAsMap().get(PolarisEntityConstants.getClientIdPropertyName()); + } + + public static class Builder extends PolarisEntity.BaseBuilder { + public Builder() { + super(); + setType(PolarisEntityType.PRINCIPAL); + setCatalogId(PolarisEntityConstants.getNullId()); + setParentId(PolarisEntityConstants.getRootEntityId()); + } + + public Builder(PrincipalEntity original) { + super(original); + } + + public Builder setClientId(String clientId) { + internalProperties.put(PolarisEntityConstants.getClientIdPropertyName(), clientId); + return this; + } + + public Builder setCredentialRotationRequiredState() { + internalProperties.put( + PolarisEntityConstants.PRINCIPAL_CREDENTIAL_ROTATION_REQUIRED_STATE, "true"); + return this; + } + + public PrincipalEntity build() { + return new PrincipalEntity(buildBase()); + } + } +} diff --git a/polaris-core/src/main/java/io/polaris/core/entity/PrincipalRoleEntity.java b/polaris-core/src/main/java/io/polaris/core/entity/PrincipalRoleEntity.java new file mode 100644 index 0000000000..4049c8940c --- /dev/null +++ b/polaris-core/src/main/java/io/polaris/core/entity/PrincipalRoleEntity.java @@ -0,0 +1,54 @@ +package io.polaris.core.entity; + +import io.polaris.core.admin.model.PrincipalRole; + +/** + * Wrapper for translating between the REST PrincipalRole object and the base PolarisEntity type. + */ +public class PrincipalRoleEntity extends PolarisEntity { + public PrincipalRoleEntity(PolarisBaseEntity sourceEntity) { + super(sourceEntity); + } + + public static PrincipalRoleEntity of(PolarisBaseEntity sourceEntity) { + if (sourceEntity != null) { + return new PrincipalRoleEntity(sourceEntity); + } + return null; + } + + public static PrincipalRoleEntity fromPrincipalRole(PrincipalRole principalRole) { + return new Builder() + .setName(principalRole.getName()) + .setProperties(principalRole.getProperties()) + .build(); + } + + public PrincipalRole asPrincipalRole() { + PrincipalRole principalRole = + new PrincipalRole( + getName(), + getPropertiesAsMap(), + getCreateTimestamp(), + getLastUpdateTimestamp(), + getEntityVersion()); + return principalRole; + } + + public static class Builder extends PolarisEntity.BaseBuilder { + public Builder() { + super(); + setType(PolarisEntityType.PRINCIPAL_ROLE); + setCatalogId(PolarisEntityConstants.getNullId()); + setParentId(PolarisEntityConstants.getRootEntityId()); + } + + public Builder(PrincipalRoleEntity original) { + super(original); + } + + public PrincipalRoleEntity build() { + return new PrincipalRoleEntity(buildBase()); + } + } +} diff --git a/polaris-core/src/main/java/io/polaris/core/entity/TableLikeEntity.java b/polaris-core/src/main/java/io/polaris/core/entity/TableLikeEntity.java new file mode 100644 index 0000000000..bf4fbbf20e --- /dev/null +++ b/polaris-core/src/main/java/io/polaris/core/entity/TableLikeEntity.java @@ -0,0 +1,81 @@ +package io.polaris.core.entity; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import org.apache.iceberg.catalog.Namespace; +import org.apache.iceberg.catalog.TableIdentifier; +import org.apache.iceberg.rest.RESTUtil; + +public class TableLikeEntity extends PolarisEntity { + // For applicable types, this key on the "internalProperties" map will return the location + // of the internalProperties JSON file. + public static final String METADATA_LOCATION_KEY = "metadata-location"; + + public TableLikeEntity(PolarisBaseEntity sourceEntity) { + super(sourceEntity); + } + + public static TableLikeEntity of(PolarisBaseEntity sourceEntity) { + if (sourceEntity != null) { + return new TableLikeEntity(sourceEntity); + } + return null; + } + + @JsonIgnore + public TableIdentifier getTableIdentifier() { + Namespace parent = getParentNamespace(); + return TableIdentifier.of(parent, getName()); + } + + @JsonIgnore + public Namespace getParentNamespace() { + String encodedNamespace = + getInternalPropertiesAsMap().get(NamespaceEntity.PARENT_NAMESPACE_KEY); + if (encodedNamespace == null) { + return Namespace.empty(); + } + return RESTUtil.decodeNamespace(encodedNamespace); + } + + @JsonIgnore + public String getMetadataLocation() { + return getInternalPropertiesAsMap().get(METADATA_LOCATION_KEY); + } + + public static class Builder extends PolarisEntity.BaseBuilder { + public Builder(TableIdentifier identifier, String metadataLocation) { + super(); + setType(PolarisEntityType.TABLE_LIKE); + setTableIdentifier(identifier); + setMetadataLocation(metadataLocation); + } + + public Builder(TableLikeEntity original) { + super(original); + } + + public TableLikeEntity build() { + return new TableLikeEntity(buildBase()); + } + + public Builder setTableIdentifier(TableIdentifier identifier) { + Namespace namespace = identifier.namespace(); + setParentNamespace(namespace); + setName(identifier.name()); + return this; + } + + public Builder setParentNamespace(Namespace namespace) { + if (namespace != null && !namespace.isEmpty()) { + internalProperties.put( + NamespaceEntity.PARENT_NAMESPACE_KEY, RESTUtil.encodeNamespace(namespace)); + } + return this; + } + + public Builder setMetadataLocation(String location) { + internalProperties.put(METADATA_LOCATION_KEY, location); + return this; + } + } +} diff --git a/polaris-core/src/main/java/io/polaris/core/entity/TaskEntity.java b/polaris-core/src/main/java/io/polaris/core/entity/TaskEntity.java new file mode 100644 index 0000000000..2c335b8191 --- /dev/null +++ b/polaris-core/src/main/java/io/polaris/core/entity/TaskEntity.java @@ -0,0 +1,87 @@ +package io.polaris.core.entity; + +import io.polaris.core.PolarisCallContext; +import io.polaris.core.context.CallContext; +import io.polaris.core.persistence.PolarisObjectMapperUtil; + +/** + * Represents an asynchronous task entity in the persistence layer. A task executor is responsible + * for constructing the actual task instance based on the "data" and "taskType" properties + */ +public class TaskEntity extends PolarisEntity { + public TaskEntity(PolarisBaseEntity sourceEntity) { + super(sourceEntity); + } + + public static TaskEntity of(PolarisBaseEntity polarisEntity) { + if (polarisEntity != null) { + return new TaskEntity(polarisEntity); + } else { + return null; + } + } + + public T readData(Class klass) { + PolarisCallContext polarisCallContext = CallContext.getCurrentContext().getPolarisCallContext(); + return PolarisObjectMapperUtil.deserialize( + polarisCallContext, getPropertiesAsMap().get(PolarisTaskConstants.TASK_DATA), klass); + } + + public AsyncTaskType getTaskType() { + PolarisCallContext polarisCallContext = CallContext.getCurrentContext().getPolarisCallContext(); + return PolarisObjectMapperUtil.deserialize( + polarisCallContext, + getPropertiesAsMap().get(PolarisTaskConstants.TASK_TYPE), + AsyncTaskType.class); + } + + public static class Builder extends PolarisEntity.BaseBuilder { + public Builder() { + super(); + setType(PolarisEntityType.TASK); + setCatalogId(PolarisEntityConstants.getNullId()); + setParentId(PolarisEntityConstants.getRootEntityId()); + } + + public Builder(TaskEntity original) { + super(original); + } + + public Builder withTaskType(AsyncTaskType taskType) { + PolarisCallContext polarisCallContext = + CallContext.getCurrentContext().getPolarisCallContext(); + properties.put( + PolarisTaskConstants.TASK_TYPE, + PolarisObjectMapperUtil.serialize(polarisCallContext, taskType)); + return this; + } + + public Builder withData(Object data) { + PolarisCallContext polarisCallContext = + CallContext.getCurrentContext().getPolarisCallContext(); + properties.put( + PolarisTaskConstants.TASK_DATA, + PolarisObjectMapperUtil.serialize(polarisCallContext, data)); + return this; + } + + public Builder withLastAttemptExecutorId(String executorId) { + properties.put(PolarisTaskConstants.LAST_ATTEMPT_EXECUTOR_ID, executorId); + return this; + } + + public Builder withAttemptCount(int count) { + properties.put(PolarisTaskConstants.ATTEMPT_COUNT, String.valueOf(count)); + return this; + } + + public Builder withLastAttemptStartedTimestamp(long timestamp) { + properties.put(PolarisTaskConstants.LAST_ATTEMPT_START_TIME, String.valueOf(timestamp)); + return this; + } + + public TaskEntity build() { + return new TaskEntity(buildBase()); + } + } +} diff --git a/polaris-core/src/main/java/io/polaris/core/monitor/PolarisMetricRegistry.java b/polaris-core/src/main/java/io/polaris/core/monitor/PolarisMetricRegistry.java new file mode 100644 index 0000000000..eb2a219260 --- /dev/null +++ b/polaris-core/src/main/java/io/polaris/core/monitor/PolarisMetricRegistry.java @@ -0,0 +1,64 @@ +package io.polaris.core.monitor; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Timer; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.TimeUnit; + +/** + * Manages metrics for Polaris applications, providing functionality to record timers and increment + * error counters. Also records the same for a realm-specific metric by appending a suffix and + * tagging with the realm ID. Utilizes Micrometer for metrics collection. + */ +public class PolarisMetricRegistry { + private final MeterRegistry meterRegistry; + private final ConcurrentMap timers = new ConcurrentHashMap<>(); + private final ConcurrentMap errorCounters = new ConcurrentHashMap<>(); + private static final String TAG_REALM = "REALM_ID"; + private static final String TAG_RESP_CODE = "HTTP_RESPONSE_CODE"; + private static final String SUFFIX_ERROR = ".error"; + private static final String SUFFIX_REALM = ".realm"; + + public PolarisMetricRegistry(MeterRegistry meterRegistry) { + this.meterRegistry = meterRegistry; + } + + public void recordTimer(String metric, long elapsedTimeMs, String realmId) { + Timer timer = + timers.computeIfAbsent(metric, m -> Timer.builder(metric).register(meterRegistry)); + timer.record(elapsedTimeMs, TimeUnit.MILLISECONDS); + + Timer timerRealm = + timers.computeIfAbsent( + metric + SUFFIX_REALM, + m -> + Timer.builder(metric + SUFFIX_REALM) + .tag(TAG_REALM, realmId) + .register(meterRegistry)); + timerRealm.record(elapsedTimeMs, TimeUnit.MILLISECONDS); + } + + public void incrementErrorCounter(String metric, int statusCode, String realmId) { + String errorMetric = metric + SUFFIX_ERROR; + Counter errorCounter = + errorCounters.computeIfAbsent( + errorMetric, + m -> + Counter.builder(errorMetric) + .tag(TAG_RESP_CODE, String.valueOf(statusCode)) + .register(meterRegistry)); + errorCounter.increment(); + + Counter errorCounterRealm = + errorCounters.computeIfAbsent( + errorMetric + SUFFIX_REALM, + m -> + Counter.builder(errorMetric + SUFFIX_REALM) + .tag(TAG_RESP_CODE, String.valueOf(statusCode)) + .tag(TAG_REALM, realmId) + .register(meterRegistry)); + errorCounterRealm.increment(); + } +} diff --git a/polaris-core/src/main/java/io/polaris/core/persistence/LocalPolarisMetaStoreManagerFactory.java b/polaris-core/src/main/java/io/polaris/core/persistence/LocalPolarisMetaStoreManagerFactory.java new file mode 100644 index 0000000000..e56829ed9b --- /dev/null +++ b/polaris-core/src/main/java/io/polaris/core/persistence/LocalPolarisMetaStoreManagerFactory.java @@ -0,0 +1,199 @@ +package io.polaris.core.persistence; + +import io.micrometer.core.instrument.MeterRegistry; +import io.polaris.core.PolarisCallContext; +import io.polaris.core.PolarisDefaultDiagServiceImpl; +import io.polaris.core.PolarisDiagnostics; +import io.polaris.core.context.CallContext; +import io.polaris.core.context.RealmContext; +import io.polaris.core.entity.PolarisEntity; +import io.polaris.core.entity.PolarisEntityConstants; +import io.polaris.core.entity.PolarisEntitySubType; +import io.polaris.core.entity.PolarisEntityType; +import io.polaris.core.entity.PolarisPrincipalSecrets; +import io.polaris.core.storage.PolarisStorageIntegrationProvider; +import io.polaris.core.storage.cache.StorageCredentialCache; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; + +/** + * The common implementation of Configuration interface for configuring the {@link + * PolarisMetaStoreManager} using an underlying meta store to store and retrieve all Polaris + * metadata. + */ +public abstract class LocalPolarisMetaStoreManagerFactory< + StoreType, SessionType extends PolarisMetaStoreSession> + implements MetaStoreManagerFactory { + + Map metaStoreManagerMap = new HashMap<>(); + Map storageCredentialCacheMap = new HashMap<>(); + Map backingStoreMap = new HashMap<>(); + Map> sessionSupplierMap = new HashMap<>(); + protected PolarisDiagnostics diagServices = new PolarisDefaultDiagServiceImpl(); + + protected PolarisStorageIntegrationProvider storageIntegration; + + private Logger logger = + org.slf4j.LoggerFactory.getLogger(LocalPolarisMetaStoreManagerFactory.class); + + protected abstract StoreType createBackingStore(@NotNull PolarisDiagnostics diagnostics); + + protected abstract PolarisMetaStoreSession createMetaStoreSession( + @NotNull StoreType store, @NotNull RealmContext realmContext); + + private void initializeForRealm(RealmContext realmContext) { + final StoreType backingStore = createBackingStore(diagServices); + backingStoreMap.put(realmContext.getRealmIdentifier(), backingStore); + sessionSupplierMap.put( + realmContext.getRealmIdentifier(), + () -> createMetaStoreSession(backingStore, realmContext)); + + PolarisMetaStoreManager metaStoreManager = new PolarisMetaStoreManagerImpl(); + metaStoreManagerMap.put(realmContext.getRealmIdentifier(), metaStoreManager); + } + + @Override + public synchronized Map bootstrapRealms( + List realms) { + Map results = new HashMap<>(); + + for (String realm : realms) { + RealmContext realmContext = () -> realm; + if (!metaStoreManagerMap.containsKey(realmContext.getRealmIdentifier())) { + initializeForRealm(realmContext); + PolarisMetaStoreManager.PrincipalSecretsResult secretsResult = + bootstrapServiceAndCreatePolarisPrincipalForRealm( + realmContext, metaStoreManagerMap.get(realmContext.getRealmIdentifier())); + results.put(realmContext.getRealmIdentifier(), secretsResult); + } + } + + return results; + } + + @Override + public synchronized PolarisMetaStoreManager getOrCreateMetaStoreManager( + RealmContext realmContext) { + if (!metaStoreManagerMap.containsKey(realmContext.getRealmIdentifier())) { + initializeForRealm(realmContext); + checkPolarisServiceBootstrappedForRealm( + realmContext, metaStoreManagerMap.get(realmContext.getRealmIdentifier())); + } + return metaStoreManagerMap.get(realmContext.getRealmIdentifier()); + } + + @Override + public synchronized Supplier getOrCreateSessionSupplier( + RealmContext realmContext) { + if (!sessionSupplierMap.containsKey(realmContext.getRealmIdentifier())) { + initializeForRealm(realmContext); + checkPolarisServiceBootstrappedForRealm( + realmContext, metaStoreManagerMap.get(realmContext.getRealmIdentifier())); + } + return sessionSupplierMap.get(realmContext.getRealmIdentifier()); + } + + @Override + public synchronized StorageCredentialCache getOrCreateStorageCredentialCache( + RealmContext realmContext) { + if (!storageCredentialCacheMap.containsKey(realmContext.getRealmIdentifier())) { + storageCredentialCacheMap.put( + realmContext.getRealmIdentifier(), new StorageCredentialCache()); + } + + return storageCredentialCacheMap.get(realmContext.getRealmIdentifier()); + } + + @Override + public void setMetricRegistry(MeterRegistry metricRegistry) { + // no-op + } + + @Override + public void setStorageIntegrationProvider(PolarisStorageIntegrationProvider storageIntegration) { + this.storageIntegration = storageIntegration; + } + + /** + * This method bootstraps service for a given realm: i.e. creates all the needed entities in the + * metastore and creates a root service principal. After that we rotate the root principal + * credentials and print them to stdout + * + * @param realmContext + * @param metaStoreManager + */ + private PolarisMetaStoreManager.PrincipalSecretsResult + bootstrapServiceAndCreatePolarisPrincipalForRealm( + RealmContext realmContext, PolarisMetaStoreManager metaStoreManager) { + // While bootstrapping we need to act as a fake privileged context since the real + // CallContext hasn't even been resolved yet. + PolarisCallContext polarisContext = + new PolarisCallContext( + sessionSupplierMap.get(realmContext.getRealmIdentifier()).get(), diagServices); + CallContext.setCurrentContext(CallContext.of(realmContext, polarisContext)); + + metaStoreManager.bootstrapPolarisService(polarisContext); + + PolarisMetaStoreManager.EntityResult rootPrincipalLookup = + metaStoreManager.readEntityByName( + polarisContext, + null, + PolarisEntityType.PRINCIPAL, + PolarisEntitySubType.NULL_SUBTYPE, + PolarisEntityConstants.getRootPrincipalName()); + PolarisPrincipalSecrets secrets = + metaStoreManager + .loadPrincipalSecrets( + polarisContext, + PolarisEntity.of(rootPrincipalLookup.getEntity()) + .getInternalPropertiesAsMap() + .get(PolarisEntityConstants.getClientIdPropertyName())) + .getPrincipalSecrets(); + PolarisMetaStoreManager.PrincipalSecretsResult rotatedSecrets = + metaStoreManager.rotatePrincipalSecrets( + polarisContext, + secrets.getPrincipalClientId(), + secrets.getPrincipalId(), + secrets.getMainSecret(), + false); + return rotatedSecrets; + } + + /** + * In this method we check if Service was bootstrapped for a given realm, i.e. that all the + * entities were created (root principal, root principal role, etc) If service was not + * bootstrapped we are throwing IllegalStateException exception That will cause service to crash + * and force user to run Bootstrap command and initialize MetaStore and create all the required + * entities + * + * @param realmContext + * @param metaStoreManager + */ + private void checkPolarisServiceBootstrappedForRealm( + RealmContext realmContext, PolarisMetaStoreManager metaStoreManager) { + PolarisCallContext polarisContext = + new PolarisCallContext( + sessionSupplierMap.get(realmContext.getRealmIdentifier()).get(), diagServices); + CallContext.setCurrentContext(CallContext.of(realmContext, polarisContext)); + + PolarisMetaStoreManager.EntityResult rootPrincipalLookup = + metaStoreManager.readEntityByName( + polarisContext, + null, + PolarisEntityType.PRINCIPAL, + PolarisEntitySubType.NULL_SUBTYPE, + PolarisEntityConstants.getRootPrincipalName()); + + if (!rootPrincipalLookup.isSuccess()) { + logger.error( + "\n\n Realm {} is not bootstrapped, could not load root principal. Please run Bootstrap command. \n\n", + realmContext.getRealmIdentifier()); + throw new IllegalStateException( + "Realm is not bootstrapped, please run server in bootstrap mode."); + } + } +} diff --git a/polaris-core/src/main/java/io/polaris/core/persistence/MetaStoreManagerFactory.java b/polaris-core/src/main/java/io/polaris/core/persistence/MetaStoreManagerFactory.java new file mode 100644 index 0000000000..1b35787a9c --- /dev/null +++ b/polaris-core/src/main/java/io/polaris/core/persistence/MetaStoreManagerFactory.java @@ -0,0 +1,31 @@ +package io.polaris.core.persistence; + +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.dropwizard.jackson.Discoverable; +import io.micrometer.core.instrument.MeterRegistry; +import io.polaris.core.context.RealmContext; +import io.polaris.core.storage.PolarisStorageIntegrationProvider; +import io.polaris.core.storage.cache.StorageCredentialCache; +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; + +/** + * Configuration interface for configuring the {@link PolarisMetaStoreManager} via Dropwizard + * configuration + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type") +public interface MetaStoreManagerFactory extends Discoverable { + + PolarisMetaStoreManager getOrCreateMetaStoreManager(RealmContext realmContext); + + Supplier getOrCreateSessionSupplier(RealmContext realmContext); + + StorageCredentialCache getOrCreateStorageCredentialCache(RealmContext realmContext); + + void setStorageIntegrationProvider(PolarisStorageIntegrationProvider storageIntegrationProvider); + + void setMetricRegistry(MeterRegistry metricRegistry); + + Map bootstrapRealms(List realms); +} diff --git a/polaris-core/src/main/java/io/polaris/core/persistence/PolarisEntityManager.java b/polaris-core/src/main/java/io/polaris/core/persistence/PolarisEntityManager.java new file mode 100644 index 0000000000..683966e28f --- /dev/null +++ b/polaris-core/src/main/java/io/polaris/core/persistence/PolarisEntityManager.java @@ -0,0 +1,141 @@ +package io.polaris.core.persistence; + +import io.polaris.core.auth.AuthenticatedPolarisPrincipal; +import io.polaris.core.context.CallContext; +import io.polaris.core.entity.PolarisEntity; +import io.polaris.core.entity.PolarisEntityConstants; +import io.polaris.core.entity.PolarisEntitySubType; +import io.polaris.core.entity.PolarisEntityType; +import io.polaris.core.entity.PolarisGrantRecord; +import io.polaris.core.entity.PolarisPrivilege; +import io.polaris.core.persistence.cache.EntityCache; +import io.polaris.core.persistence.resolver.PolarisResolutionManifest; +import io.polaris.core.persistence.resolver.Resolver; +import io.polaris.core.storage.cache.StorageCredentialCache; +import java.util.List; +import java.util.function.Supplier; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Wraps logic of handling name-caching and entity-caching against a concrete underlying entity + * store while exposing methods more natural for the Catalog layer to use. Encapsulates the various + * id and name resolution mechanics around PolarisEntities. + */ +public class PolarisEntityManager { + private static final Logger LOG = LoggerFactory.getLogger(PolarisEntityManager.class); + + private final PolarisMetaStoreManager metaStoreManager; + private final Supplier sessionSupplier; + private final EntityCache entityCache; + + private final StorageCredentialCache credentialCache; + + // Lazily instantiated only a single time per entity manager. + private ResolvedPolarisEntity implicitResolvedRootContainerEntity = null; + + /** + * @param sessionSupplier must return a new independent metastore session affiliated with the + * backing store under the {@code delegate} on each invocation. + */ + public PolarisEntityManager( + PolarisMetaStoreManager metaStoreManager, + Supplier sessionSupplier, + StorageCredentialCache credentialCache) { + this.metaStoreManager = metaStoreManager; + this.sessionSupplier = sessionSupplier; + this.entityCache = new EntityCache(metaStoreManager); + this.credentialCache = credentialCache; + } + + public PolarisMetaStoreSession newMetaStoreSession() { + return sessionSupplier.get(); + } + + public PolarisMetaStoreManager getMetaStoreManager() { + return metaStoreManager; + } + + public Resolver prepareResolver( + @NotNull CallContext callContext, + @NotNull AuthenticatedPolarisPrincipal authenticatedPrincipal, + @Nullable String referenceCatalogName) { + return new Resolver( + callContext.getPolarisCallContext(), + metaStoreManager, + authenticatedPrincipal.getPrincipalEntity().getId(), + null, /* callerPrincipalName */ + authenticatedPrincipal.getActivatedPrincipalRoleNames().isEmpty() + ? null + : authenticatedPrincipal.getActivatedPrincipalRoleNames(), + entityCache, + referenceCatalogName); + } + + public PolarisResolutionManifest prepareResolutionManifest( + @NotNull CallContext callContext, + @NotNull AuthenticatedPolarisPrincipal authenticatedPrincipal, + @Nullable String referenceCatalogName) { + PolarisResolutionManifest manifest = + new PolarisResolutionManifest( + callContext, this, authenticatedPrincipal, referenceCatalogName); + manifest.setSimulatedResolvedRootContainerEntity( + getSimulatedResolvedRootContainerEntity(callContext)); + return manifest; + } + + /** + * Returns a ResolvedPolarisEntity representing the realm-level "root" entity that is the implicit + * parent container of all things in this realm. + */ + private synchronized ResolvedPolarisEntity getSimulatedResolvedRootContainerEntity( + CallContext callContext) { + if (implicitResolvedRootContainerEntity == null) { + // For now, the root container is only implicit and doesn't exist in the entity store, and + // only + // the service_admin PrincipalRole has the SERVICE_MANAGE_ACCESS grant on this entity. If it + // becomes + // possible to grant other PrincipalRoles with SERVICE_MANAGE_ACCESS or other privileges on + // this + // root entity, then we must actually create a representation of this root entity in the + // entity store itself. + PolarisEntity serviceAdminPrincipalRole = + PolarisEntity.of( + metaStoreManager + .readEntityByName( + callContext.getPolarisCallContext(), + null, + PolarisEntityType.PRINCIPAL_ROLE, + PolarisEntitySubType.NULL_SUBTYPE, + PolarisEntityConstants.getNameOfPrincipalServiceAdminRole()) + .getEntity()); + if (serviceAdminPrincipalRole == null) { + throw new IllegalStateException("Failed to resolve service_admin PrincipalRole"); + } + PolarisEntity rootContainerEntity = + new PolarisEntity.Builder() + .setId(0L) + .setCatalogId(0L) + .setType(PolarisEntityType.ROOT) + .setName("root") + .build(); + PolarisGrantRecord serviceAdminGrant = + new PolarisGrantRecord( + 0L, + 0L, + serviceAdminPrincipalRole.getCatalogId(), + serviceAdminPrincipalRole.getId(), + PolarisPrivilege.SERVICE_MANAGE_ACCESS.getCode()); + + implicitResolvedRootContainerEntity = + new ResolvedPolarisEntity(rootContainerEntity, null, List.of(serviceAdminGrant)); + } + return implicitResolvedRootContainerEntity; + } + + public StorageCredentialCache getCredentialCache() { + return credentialCache; + } +} diff --git a/polaris-core/src/main/java/io/polaris/core/persistence/PolarisEntityResolver.java b/polaris-core/src/main/java/io/polaris/core/persistence/PolarisEntityResolver.java new file mode 100644 index 0000000000..d054713829 --- /dev/null +++ b/polaris-core/src/main/java/io/polaris/core/persistence/PolarisEntityResolver.java @@ -0,0 +1,288 @@ +/* + * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. + */ + +package io.polaris.core.persistence; + +import io.polaris.core.PolarisCallContext; +import io.polaris.core.PolarisDiagnostics; +import io.polaris.core.entity.PolarisBaseEntity; +import io.polaris.core.entity.PolarisEntitiesActiveKey; +import io.polaris.core.entity.PolarisEntityActiveRecord; +import io.polaris.core.entity.PolarisEntityConstants; +import io.polaris.core.entity.PolarisEntityCore; +import io.polaris.core.entity.PolarisEntityType; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.stream.Collectors; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Utility class used by the meta store manager to ensure that all entities which had been resolved + * by the Polaris service outside a transaction have not been changed by a concurrent operation. In + * particular, we will ensure that all entities resolved outside the transaction are still active, + * have not been renamed/re-parented or replaced by another entity with the same name. + */ +public class PolarisEntityResolver { + + // cache diagnostics services + private final PolarisDiagnostics diagnostics; + + // result of the resolution + private final boolean isSuccess; + + // the catalog entity on the path. Only set if a catalog path is specified, i.e. if the entity + // being resolved is contain within a top-level catalog + private final PolarisEntityCore catalogEntity; + + // the parent id of the entity. We have 2 cases here: + // - a path was specified, in which case the parent is the last element in that path + // - a path was not specified, in which case the parent id is the account. + private final long parentEntityId; + + /** + * Full constructor for the resolver. The caller can specify a path inside a catalog which MUST + * start with the catalog itself. Then an optional entity to also resolve. This entity will be + * top-level if the catalogPath is null, else it will be under that path. Finally, the caller can + * specify other top-level entities to resolve, either catalog or account top-level. If a catalog + * top-level entity is specified, the catalogPath should be specified in order to know the parent + * catalog. + * + *

The resolver will ensure that none of the entities which are passed in have been dropped or + * were renamed or moved. + * + * @param callCtx call context + * @param ms meta store in read mode + * @param catalogPath path within the catalog. The first element MUST be a catalog entity. + * @param resolvedEntity optional entity to resolve under that catalog path. If a non-null value + * is supplied, we will resolve it with the rest, as if it had been concatenated to the input + * path. If catalogPath is null, this MUST be a top-level entity + * @param otherTopLevelEntities any other top-level entities like a catalog role, a principal role + * or a principal can be specified here + */ + PolarisEntityResolver( + @NotNull PolarisCallContext callCtx, + @NotNull PolarisMetaStoreSession ms, + @Nullable List catalogPath, + @Nullable PolarisEntityCore resolvedEntity, + @Nullable List otherTopLevelEntities) { + + // cache diagnostics services + this.diagnostics = callCtx.getDiagServices(); + + // validate path if one was specified + if (catalogPath != null) { + // cannot be an empty list + callCtx.getDiagServices().check(!catalogPath.isEmpty(), "catalogPath_cannot_be_empty"); + // first in the path should be the catalog + callCtx + .getDiagServices() + .check( + catalogPath.get(0).getTypeCode() == PolarisEntityType.CATALOG.getCode(), + "entity_is_not_catalog", + "entity={}", + this); + } else if (resolvedEntity != null) { + // if an entity is specified without any path, it better be a top-level entity + callCtx + .getDiagServices() + .check( + resolvedEntity.getType().isTopLevel(), + "not_top_level_entity", + "resolvedEntity={}", + resolvedEntity); + } + + // validate the otherTopLevelCatalogEntities list. Must be top-level catalog entities + if (otherTopLevelEntities != null) { + // ensure all entities are top-level + for (PolarisEntityCore topLevelCatalogEntityDto : otherTopLevelEntities) { + // top-level (catalog or account) and is catalog, catalog path must be specified + callCtx + .getDiagServices() + .check( + topLevelCatalogEntityDto.isTopLevel() + || (topLevelCatalogEntityDto.getType().getParentType() + == PolarisEntityType.CATALOG + && catalogPath != null), + "not_top_level_or_missing_catalog_path", + "entity={} catalogPath={}", + topLevelCatalogEntityDto, + catalogPath); + } + } + + // call the resolution logic + this.isSuccess = + this.resolveEntitiesIfNeeded( + callCtx, ms, catalogPath, resolvedEntity, otherTopLevelEntities); + + // process result + if (!this.isSuccess) { + // if failed, initialized if NA values + this.catalogEntity = null; + this.parentEntityId = PolarisEntityConstants.getNullId(); + } else if (catalogPath != null) { + this.catalogEntity = catalogPath.get(0); + this.parentEntityId = catalogPath.get(catalogPath.size() - 1).getId(); + } else { + this.catalogEntity = null; + this.parentEntityId = PolarisEntityConstants.getRootEntityId(); + } + } + + /** + * Constructor for the resolver, when we only need to resolve a path + * + * @param callCtx call context + * @param ms meta store in read mode + * @param catalogPath input path, can be null or empty list if the entity is a top-level entity + * like a catalog. + */ + PolarisEntityResolver( + @NotNull PolarisCallContext callCtx, + @NotNull PolarisMetaStoreSession ms, + @Nullable List catalogPath) { + this(callCtx, ms, catalogPath, null, null); + } + + /** + * Constructor for the resolver, when we only need to resolve a path + * + * @param callCtx call context + * @param ms meta store in read mode + * @param catalogPath input path, can be null or empty list if the entity is a top-level entity + * like a catalog. + * @param resolvedEntityDto resolved entity DTO + */ + PolarisEntityResolver( + @NotNull PolarisCallContext callCtx, + @NotNull PolarisMetaStoreSession ms, + @Nullable List catalogPath, + PolarisEntityCore resolvedEntityDto) { + this(callCtx, ms, catalogPath, resolvedEntityDto, null); + } + + /** + * Constructor for the resolver, when we only need to resolve a path + * + * @param callCtx call context + * @param ms meta store in read mode + * @param catalogPath input path, can be null or empty list if the entity is a top-level entity + * like a catalog. + * @param entity Polaris base entity + */ + PolarisEntityResolver( + @NotNull PolarisCallContext callCtx, + @NotNull PolarisMetaStoreSession ms, + @Nullable List catalogPath, + @NotNull PolarisBaseEntity entity) { + this(callCtx, ms, catalogPath, new PolarisEntityCore(entity), null); + } + + /** + * @return status of the resolution, if true we couldn't resolve everything + */ + boolean isFailure() { + return !this.isSuccess; + } + + /** + * @return If a non-null catalog path was specified at construction time, the id of the last + * entity in this path, else the pseudo account id, i.e. 0 + */ + long getParentId() { + this.diagnostics.check(this.isSuccess, "resolver_failed"); + return this.parentEntityId; + } + + /** + * @return id of the catalog or the "NULL" id if the entity is top-level + */ + long getCatalogIdOrNull() { + this.diagnostics.check(this.isSuccess, "resolver_failed"); + return this.catalogEntity == null + ? PolarisEntityConstants.getNullId() + : this.catalogEntity.getId(); + } + + /** + * Ensure all specified entities are still active, have not been renamed or re-parented. + * + * @param callCtx call context + * @param ms meta store in read mode + * @param catalogPath path within the catalog. The first element MUST be a catalog. Null or empty + * for top-level entities like catalog + * @param resolvedEntity optional entity to resolve under that catalog path. If a non-null value + * is supplied, we will resolve it with the rest, as if it had been concatenated to the input + * path. + * @param otherTopLevelEntities if non-null, these are top-level catalog entities under the + * catalog rooting the catalogPath. Hence, this can be specified only if catalogPath is not + * null + * @return true if all entities have been resolved successfully + */ + private boolean resolveEntitiesIfNeeded( + @NotNull PolarisCallContext callCtx, + @NotNull PolarisMetaStoreSession ms, + @Nullable List catalogPath, + @Nullable PolarisEntityCore resolvedEntity, + @Nullable List otherTopLevelEntities) { + + // determine the number of entities to resolved + int resolveCount = + ((catalogPath != null) ? catalogPath.size() : 0) + + ((resolvedEntity != null) ? 1 : 0) + + ((otherTopLevelEntities != null) ? otherTopLevelEntities.size() : 0); + + // nothing to do if 0 + if (resolveCount == 0) { + return true; + } + + // construct full list of entities to resolve + final List toResolve = new ArrayList<>(resolveCount); + + // first add the other top-level catalog entities, then the catalog path, then the entity + if (otherTopLevelEntities != null) { + toResolve.addAll(otherTopLevelEntities); + } + if (catalogPath != null) { + toResolve.addAll(catalogPath); + } + if (resolvedEntity != null) { + toResolve.add(resolvedEntity); + } + + // now build a list of entity active keys + List entityActiveKeys = + toResolve.stream() + .map( + entityCore -> + new PolarisEntitiesActiveKey( + entityCore.getCatalogId(), + entityCore.getParentId(), + entityCore.getTypeCode(), + entityCore.getName())) + .collect(Collectors.toList()); + + // now lookup all these entities by name + Iterator activeRecordIt = + ms.lookupEntityActiveBatch(callCtx, entityActiveKeys).iterator(); + + // now validate if there was a change and if yes, re-resolve again + for (PolarisEntityCore resolveEntity : toResolve) { + // get associate active record + PolarisEntityActiveRecord activeEntityRecord = activeRecordIt.next(); + + // if this entity has been dropped (null) or replaced (<> ids), then fail validation + if (activeEntityRecord == null || activeEntityRecord.getId() != resolveEntity.getId()) { + return false; + } + } + + // all good, everything was resolved successfully + return true; + } +} diff --git a/polaris-core/src/main/java/io/polaris/core/persistence/PolarisMetaStoreManager.java b/polaris-core/src/main/java/io/polaris/core/persistence/PolarisMetaStoreManager.java new file mode 100644 index 0000000000..a0e6e0822e --- /dev/null +++ b/polaris-core/src/main/java/io/polaris/core/persistence/PolarisMetaStoreManager.java @@ -0,0 +1,1471 @@ +/* + * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. + */ + +package io.polaris.core.persistence; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.polaris.core.PolarisCallContext; +import io.polaris.core.entity.PolarisBaseEntity; +import io.polaris.core.entity.PolarisChangeTrackingVersions; +import io.polaris.core.entity.PolarisEntity; +import io.polaris.core.entity.PolarisEntityActiveRecord; +import io.polaris.core.entity.PolarisEntityCore; +import io.polaris.core.entity.PolarisEntityId; +import io.polaris.core.entity.PolarisEntitySubType; +import io.polaris.core.entity.PolarisEntityType; +import io.polaris.core.entity.PolarisGrantRecord; +import io.polaris.core.entity.PolarisPrincipalSecrets; +import io.polaris.core.entity.PolarisPrivilege; +import io.polaris.core.storage.PolarisCredentialProperty; +import io.polaris.core.storage.PolarisStorageActions; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Polaris Metastore Manager manages all Polaris entities and associated grant records metadata for + * authorization. It uses the underlying persistent metastore to store and retrieve Polaris metadata + */ +public interface PolarisMetaStoreManager { + + /** Possible return code for the various API calls. */ + enum ReturnStatus { + // all good + SUCCESS(1), + + // an unexpected error was thrown, should result in a 500 error to the client + UNEXPECTED_ERROR_SIGNALED(2), + + // the specified catalog path cannot be resolved. There is a possibility that by the time a call + // is made by the client to the persistent storage, something has changed due to concurrent + // modification(s). The client should retry in that case. + CATALOG_PATH_CANNOT_BE_RESOLVED(3), + + // the specified entity (and its path) cannot be resolved. There is a possibility that by the + // time a call is made by the client to the persistent storage, something has changed due to + // concurrent modification(s). The client should retry in that case. + ENTITY_CANNOT_BE_RESOLVED(4), + + // entity not found + ENTITY_NOT_FOUND(5), + + // grant not found + GRANT_NOT_FOUND(6), + + // entity already exists + ENTITY_ALREADY_EXISTS(7), + + // entity cannot be dropped, it is one of the bootstrap object like a catalog admin role or the + // service admin principal role + ENTITY_UNDROPPABLE(8), + + // Namespace is not empty and cannot be dropped + NAMESPACE_NOT_EMPTY(9), + + // Catalog is not empty and cannot be dropped. All catalog roles (except the admin catalog + // role) and all namespaces in the catalog must be dropped before the namespace can be dropped + CATALOG_NOT_EMPTY(10), + + // The target entity was concurrently modified + TARGET_ENTITY_CONCURRENTLY_MODIFIED(11), + + // entity cannot be renamed + ENTITY_CANNOT_BE_RENAMED(12), + + // error caught while sub-scoping credentials. Error message will be returned + SUBSCOPE_CREDS_ERROR(13), + ; + + // code for the enum + private final int code; + + /** constructor */ + ReturnStatus(int code) { + this.code = code; + } + + int getCode() { + return this.code; + } + + // to efficiently map a code to its corresponding return status + private static final ReturnStatus[] REVERSE_MAPPING_ARRAY; + + static { + // find max array size + int maxCode = 0; + for (ReturnStatus returnStatus : ReturnStatus.values()) { + if (maxCode < returnStatus.code) { + maxCode = returnStatus.code; + } + } + + // allocate mapping array + REVERSE_MAPPING_ARRAY = new ReturnStatus[maxCode + 1]; + + // populate mapping array + for (ReturnStatus returnStatus : ReturnStatus.values()) { + REVERSE_MAPPING_ARRAY[returnStatus.code] = returnStatus; + } + } + + static ReturnStatus getStatus(int code) { + return code >= REVERSE_MAPPING_ARRAY.length ? null : REVERSE_MAPPING_ARRAY[code]; + } + } + + /** Base result class for any call to the persistence layer */ + class BaseResult { + // return code, indicates success or failure + private final int returnStatusCode; + + // additional information for some error return code + private final String extraInformation; + + public BaseResult() { + this.returnStatusCode = ReturnStatus.SUCCESS.getCode(); + this.extraInformation = null; + } + + public BaseResult(@NotNull PolarisMetaStoreManager.ReturnStatus returnStatus) { + this.returnStatusCode = returnStatus.getCode(); + this.extraInformation = null; + } + + @JsonCreator + public BaseResult( + @JsonProperty("returnStatus") @NotNull ReturnStatus returnStatus, + @JsonProperty("extraInformation") @Nullable String extraInformation) { + this.returnStatusCode = returnStatus.getCode(); + this.extraInformation = extraInformation; + } + + public ReturnStatus getReturnStatus() { + return ReturnStatus.getStatus(this.returnStatusCode); + } + + public String getExtraInformation() { + return extraInformation; + } + + public boolean isSuccess() { + return this.returnStatusCode == ReturnStatus.SUCCESS.getCode(); + } + + public boolean alreadyExists() { + return this.returnStatusCode == ReturnStatus.ENTITY_ALREADY_EXISTS.getCode(); + } + } + + /** + * Bootstrap the Polaris service, will remove ALL existing persisted entities, then will create + * the root catalog, root principal and associated service admin role. + * + *

*************************** WARNING ************************ + * + *

This will destroy whatever Polaris metadata exists in this account + * + * @param callCtx call context + * @return always success or unexpected error + */ + @NotNull + BaseResult bootstrapPolarisService(@NotNull PolarisCallContext callCtx); + + /** the return for an entity lookup call */ + class EntityResult extends BaseResult { + + // null if not success + private final PolarisBaseEntity entity; + + /** + * Constructor for an error + * + * @param errorCode error code, cannot be SUCCESS + * @param extraInformation extra information if error. Implementation specific + */ + public EntityResult( + @NotNull PolarisMetaStoreManager.ReturnStatus errorCode, + @Nullable String extraInformation) { + super(errorCode, extraInformation); + this.entity = null; + } + + /** + * Constructor for success + * + * @param entity the entity being looked-up + */ + public EntityResult(@NotNull PolarisBaseEntity entity) { + super(ReturnStatus.SUCCESS); + this.entity = entity; + } + + /** + * Constructor for an object already exists error where the subtype of the existing entity is + * returned + * + * @param errorStatus error status, cannot be SUCCESS + * @param subTypeCode existing entity subtype code + */ + public EntityResult( + @NotNull PolarisMetaStoreManager.ReturnStatus errorStatus, int subTypeCode) { + super(errorStatus, Integer.toString(subTypeCode)); + this.entity = null; + } + + /** + * For object already exist error, we use the extra information to serialize the subtype code of + * the existing object. Return the subtype + * + * @return object subtype or NULL (should not happen) if subtype code is missing or cannot be + * deserialized + */ + @Nullable + public PolarisEntitySubType getAlreadyExistsEntitySubType() { + if (this.getExtraInformation() == null) { + return null; + } else { + int subTypeCode; + try { + subTypeCode = Integer.parseInt(this.getExtraInformation()); + } catch (NumberFormatException e) { + return null; + } + return PolarisEntitySubType.fromCode(subTypeCode); + } + } + + @JsonCreator + private EntityResult( + @JsonProperty("returnStatus") @NotNull ReturnStatus returnStatus, + @JsonProperty("extraInformation") @Nullable String extraInformation, + @JsonProperty("entity") @Nullable PolarisBaseEntity entity) { + super(returnStatus, extraInformation); + this.entity = entity; + } + + public PolarisBaseEntity getEntity() { + return entity; + } + } + + /** + * Resolve an entity by name. Can be a top-level entity like a catalog or an entity inside a + * catalog like a namespace, a role, a table like entity, or a principal. If the entity is inside + * a catalog, the parameter catalogPath must be specified + * + * @param callCtx call context + * @param catalogPath path inside a catalog to that entity, rooted by the catalog. If null, the + * entity being resolved is a top-level account entity like a catalog. + * @param entityType entity type + * @param entitySubType entity subtype. Can be the special value ANY_SUBTYPE to match any + * subtypes. Else exact match on the subtype will be required. + * @param name name of the entity, cannot be null + * @return the result of the lookup operation. ENTITY_NOT_FOUND is returned if the specified + * entity is not found in the specified path. CONCURRENT_MODIFICATION_DETECTED_NEED_RETRY is + * returned if the specified catalog path cannot be resolved. + */ + @NotNull + PolarisMetaStoreManager.EntityResult readEntityByName( + @NotNull PolarisCallContext callCtx, + @Nullable List catalogPath, + @NotNull PolarisEntityType entityType, + @NotNull PolarisEntitySubType entitySubType, + @NotNull String name); + + /** the return the result for a list entities call */ + class ListEntitiesResult extends BaseResult { + + // null if not success. Else the list of entities being returned + private final List entities; + + /** + * Constructor for an error + * + * @param errorCode error code, cannot be SUCCESS + * @param extraInformation extra information + */ + public ListEntitiesResult( + @NotNull PolarisMetaStoreManager.ReturnStatus errorCode, + @Nullable String extraInformation) { + super(errorCode, extraInformation); + this.entities = null; + } + + /** + * Constructor for success + * + * @param entities list of entities being returned, implies success + */ + public ListEntitiesResult(@NotNull List entities) { + super(ReturnStatus.SUCCESS); + this.entities = entities; + } + + @JsonCreator + private ListEntitiesResult( + @JsonProperty("returnStatus") @NotNull ReturnStatus returnStatus, + @JsonProperty("extraInformation") String extraInformation, + @JsonProperty("entities") List entities) { + super(returnStatus, extraInformation); + this.entities = entities; + } + + public List getEntities() { + return entities; + } + } + + /** + * List all entities of the specified type under the specified catalogPath. If the catalogPath is + * null, listed entities will be top-level entities like catalogs. + * + * @param callCtx call context + * @param catalogPath path inside a catalog. If null or empty, the entities to list are top-level, + * like catalogs + * @param entityType entity type + * @param entitySubType entity subtype. Can be the special value ANY_SUBTYPE to match any subtype. + * Else exact match will be performed. + * @return all entities name, ids and subtype under the specified namespace. + */ + @NotNull + ListEntitiesResult listEntities( + @NotNull PolarisCallContext callCtx, + @Nullable List catalogPath, + @NotNull PolarisEntityType entityType, + @NotNull PolarisEntitySubType entitySubType); + + /** the return for a generate new entity id */ + class GenerateEntityIdResult extends BaseResult { + + // null if not success + private final Long id; + + /** + * Constructor for an error + * + * @param errorCode error code, cannot be SUCCESS + * @param extraInformation extra information + */ + public GenerateEntityIdResult( + @NotNull PolarisMetaStoreManager.ReturnStatus errorCode, + @Nullable String extraInformation) { + super(errorCode, extraInformation); + this.id = null; + } + + /** + * Constructor for success + * + * @param id the new id which was generated + */ + public GenerateEntityIdResult(@NotNull Long id) { + super(ReturnStatus.SUCCESS); + this.id = id; + } + + @JsonCreator + private GenerateEntityIdResult( + @JsonProperty("returnStatus") @NotNull ReturnStatus returnStatus, + @JsonProperty("extraInformation") @Nullable String extraInformation, + @JsonProperty("id") @Nullable Long id) { + super(returnStatus, extraInformation); + this.id = id; + } + + public Long getId() { + return id; + } + } + + /** + * Generate a new unique id that can be used by the Polaris client when it needs to create a new + * entity + * + * @param callCtx call context + * @return the newly created id, not expected to fail + */ + @NotNull + GenerateEntityIdResult generateNewEntityId(@NotNull PolarisCallContext callCtx); + + /** the return the result of a create-principal method */ + class CreatePrincipalResult extends BaseResult { + // the principal which has been created. Null if error + private final PolarisBaseEntity principal; + + // principal client identifier and associated secrets. Null if error + private final PolarisPrincipalSecrets principalSecrets; + + /** + * Constructor for an error + * + * @param errorCode error code, cannot be SUCCESS + * @param extraInformation extra information + */ + public CreatePrincipalResult( + @NotNull PolarisMetaStoreManager.ReturnStatus errorCode, + @Nullable String extraInformation) { + super(errorCode, extraInformation); + this.principal = null; + this.principalSecrets = null; + } + + /** + * Constructor for success + * + * @param principal the principal + * @param principalSecrets and associated secret information + */ + public CreatePrincipalResult( + @NotNull PolarisBaseEntity principal, @NotNull PolarisPrincipalSecrets principalSecrets) { + super(ReturnStatus.SUCCESS); + this.principal = principal; + this.principalSecrets = principalSecrets; + } + + @JsonCreator + private CreatePrincipalResult( + @JsonProperty("returnStatus") @NotNull ReturnStatus returnStatus, + @JsonProperty("extraInformation") @Nullable String extraInformation, + @JsonProperty("principal") @NotNull PolarisBaseEntity principal, + @JsonProperty("principalSecrets") @NotNull PolarisPrincipalSecrets principalSecrets) { + super(returnStatus, extraInformation); + this.principal = principal; + this.principalSecrets = principalSecrets; + } + + public PolarisBaseEntity getPrincipal() { + return principal; + } + + public PolarisPrincipalSecrets getPrincipalSecrets() { + return principalSecrets; + } + } + + /** + * Create a new principal. This not only creates the new principal entity but also generates a + * client_id/secret pair for this new principal. + * + * @param callCtx call context + * @param principal the principal entity to create + * @return the client_id/secret for the new principal which was created. Will return + * ENTITY_ALREADY_EXISTS if the principal already exists + */ + @NotNull + CreatePrincipalResult createPrincipal( + @NotNull PolarisCallContext callCtx, @NotNull PolarisBaseEntity principal); + + /** the result of load/rotate principal secrets */ + class PrincipalSecretsResult extends BaseResult { + + // principal client identifier and associated secrets. Null if error + private final PolarisPrincipalSecrets principalSecrets; + + /** + * Constructor for an error + * + * @param errorCode error code, cannot be SUCCESS + * @param extraInformation extra information + */ + public PrincipalSecretsResult( + @NotNull PolarisMetaStoreManager.ReturnStatus errorCode, + @Nullable String extraInformation) { + super(errorCode, extraInformation); + this.principalSecrets = null; + } + + /** + * Constructor for success + * + * @param principalSecrets and associated secret information + */ + public PrincipalSecretsResult(@NotNull PolarisPrincipalSecrets principalSecrets) { + super(ReturnStatus.SUCCESS); + this.principalSecrets = principalSecrets; + } + + @JsonCreator + private PrincipalSecretsResult( + @JsonProperty("returnStatus") @NotNull ReturnStatus returnStatus, + @JsonProperty("extraInformation") @Nullable String extraInformation, + @JsonProperty("principalSecrets") @NotNull PolarisPrincipalSecrets principalSecrets) { + super(returnStatus, extraInformation); + this.principalSecrets = principalSecrets; + } + + public PolarisPrincipalSecrets getPrincipalSecrets() { + return principalSecrets; + } + } + + /** + * Load the principal secrets given the client_id. + * + * @param callCtx call context + * @param clientId principal client id + * @return the secrets associated to that principal, including the entity id of the principal + */ + @NotNull + PrincipalSecretsResult loadPrincipalSecrets( + @NotNull PolarisCallContext callCtx, @NotNull String clientId); + + /** + * Rotate secrets + * + * @param callCtx call context + * @param clientId principal client id + * @param principalId id of the principal + * @param mainSecret main secret for the principal + * @param reset true if the principal's secrets should be disabled and replaced with a one-time + * password. if the principal's secret is already a one-time password, this flag is + * automatically true + * @return the secrets associated to that principal amd the id of the principal + */ + @NotNull + PrincipalSecretsResult rotatePrincipalSecrets( + @NotNull PolarisCallContext callCtx, + @NotNull String clientId, + long principalId, + @NotNull String mainSecret, + boolean reset); + + /** the return the result of a create-catalog method */ + class CreateCatalogResult extends BaseResult { + + // the catalog which has been created + private final PolarisBaseEntity catalog; + + // its associated catalog admin role + private final PolarisBaseEntity catalogAdminRole; + + /** + * Constructor for an error + * + * @param errorCode error code, cannot be SUCCESS + * @param extraInformation extra information + */ + public CreateCatalogResult( + @NotNull PolarisMetaStoreManager.ReturnStatus errorCode, + @Nullable String extraInformation) { + super(errorCode, extraInformation); + this.catalog = null; + this.catalogAdminRole = null; + } + + /** + * Constructor for success + * + * @param catalog the catalog + * @param catalogAdminRole and associated admin role + */ + public CreateCatalogResult( + @NotNull PolarisBaseEntity catalog, @NotNull PolarisBaseEntity catalogAdminRole) { + super(ReturnStatus.SUCCESS); + this.catalog = catalog; + this.catalogAdminRole = catalogAdminRole; + } + + @JsonCreator + private CreateCatalogResult( + @JsonProperty("returnStatus") @NotNull ReturnStatus returnStatus, + @JsonProperty("extraInformation") @Nullable String extraInformation, + @JsonProperty("catalog") @NotNull PolarisBaseEntity catalog, + @JsonProperty("catalogAdminRole") @NotNull PolarisBaseEntity catalogAdminRole) { + super(returnStatus, extraInformation); + this.catalog = catalog; + this.catalogAdminRole = catalogAdminRole; + } + + public PolarisBaseEntity getCatalog() { + return catalog; + } + + public PolarisBaseEntity getCatalogAdminRole() { + return catalogAdminRole; + } + } + + /** + * Create a new catalog. This not only creates the new catalog entity but also the initial admin + * role required to admin this catalog. If inline storage integration property is provided, create + * a storage integration. + * + * @param callCtx call context + * @param catalog the catalog entity to create + * @param principalRoles once the catalog has been created, list of principal roles to grant its + * catalog_admin role to. If no principal role is specified, we will grant the catalog_admin + * role of the newly created catalog to the service admin role. + * @return if success, the catalog which was created and its admin role. + */ + @NotNull + CreateCatalogResult createCatalog( + @NotNull PolarisCallContext callCtx, + @NotNull PolarisBaseEntity catalog, + @NotNull List principalRoles); + + /** + * Persist a newly created entity under the specified catalog path if specified, else this is a + * top-level entity. We will re-resolve the specified path to ensure nothing has changed since the + * Polaris app resolved the path. If the entity already exists with the same specified id, we will + * simply return it. This can happen when the client retries. If a catalogPath is specified and + * cannot be resolved, we will return null. And of course if another entity exists with the same + * name, we will fail and also return null. + * + * @param callCtx call context + * @param catalogPath path inside a catalog. If null, the entity to persist is assumed to be + * top-level. + * @param entity entity to write + * @return the newly created entity. If this entity was already created, we will simply return the + * already created entity. We will return null if a different entity with the same name exists + * or if the catalogPath couldn't be resolved. If null is returned, the client app should + * retry this operation. + */ + @NotNull + EntityResult createEntityIfNotExists( + @NotNull PolarisCallContext callCtx, + @Nullable List catalogPath, + @NotNull PolarisBaseEntity entity); + + /** a set of returned entities result */ + class EntitiesResult extends BaseResult { + + // null if not success. Else the list of entities being returned + private final List entities; + + /** + * Constructor for an error + * + * @param errorStatus error code, cannot be SUCCESS + * @param extraInformation extra information + */ + public EntitiesResult( + @NotNull PolarisMetaStoreManager.ReturnStatus errorStatus, + @Nullable String extraInformation) { + super(errorStatus, extraInformation); + this.entities = null; + } + + /** + * Constructor for success + * + * @param entities list of entities being returned, implies success + */ + public EntitiesResult(@NotNull List entities) { + super(ReturnStatus.SUCCESS); + this.entities = entities; + } + + @JsonCreator + private EntitiesResult( + @JsonProperty("returnStatus") @NotNull ReturnStatus returnStatus, + @JsonProperty("extraInformation") String extraInformation, + @JsonProperty("entities") List entities) { + super(returnStatus, extraInformation); + this.entities = entities; + } + + public List getEntities() { + return entities; + } + } + + /** + * Persist a batch of newly created entities under the specified catalog path if specified, else + * these are top-level entities. We will re-resolve the specified path to ensure nothing has + * changed since the Polaris app resolved the path. If any of the entities already exists with the + * same specified id, we will simply return it. This can happen when the client retries. If a + * catalogPath is specified and cannot be resolved, we will return null and none of the entities + * will be persisted. And of course if any entity conflicts with an existing entity with the same + * name, we will fail all entities and also return null. + * + * @param callCtx call context + * @param catalogPath path inside a catalog. If null, the entity to persist is assumed to be + * top-level. + * @param entities batch of entities to write + * @return the newly created entities. If the entities were already created, we will simply return + * the already created entity. We will return null if a different entity with the same name + * exists or if the catalogPath couldn't be resolved. If null is returned, the client app + * should retry this operation. + */ + @NotNull + EntitiesResult createEntitiesIfNotExist( + @NotNull PolarisCallContext callCtx, + @Nullable List catalogPath, + @NotNull List entities); + + /** + * Update some properties of this entity assuming it can still be resolved the same way and itself + * has not changed. If this is not the case we will return false. Else we will update both the + * internal and visible properties and return true + * + * @param callCtx call context + * @param catalogPath path to that entity. Could be null if this entity is top-level + * @param entity entity to update, cannot be null + * @return the entity we updated or null if the client should retry + */ + @NotNull + EntityResult updateEntityPropertiesIfNotChanged( + @NotNull PolarisCallContext callCtx, + @Nullable List catalogPath, + @NotNull PolarisBaseEntity entity); + + /** Class to represent an entity with its path */ + class EntityWithPath { + // path to that entity. Could be null if this entity is top-level + private final @NotNull List catalogPath; + + // the base entity itself + private final @NotNull PolarisBaseEntity entity; + + @JsonCreator + public EntityWithPath( + @JsonProperty("catalogPath") @NotNull List catalogPath, + @JsonProperty("entity") @NotNull PolarisBaseEntity entity) { + this.catalogPath = catalogPath; + this.entity = entity; + } + + public @NotNull List getCatalogPath() { + return catalogPath; + } + + public @NotNull PolarisBaseEntity getEntity() { + return entity; + } + } + + /** + * This works exactly like {@link #updateEntityPropertiesIfNotChanged(PolarisCallContext, List, + * PolarisBaseEntity)} but allows to operate on multiple entities at once. Just loop through the + * list, calling each entity update and return null if any of those fail. + * + * @param callCtx call context + * @param entities the set of entities to update + * @return list of all entities we updated or null if the client should retry because one update + * failed + */ + @NotNull + EntitiesResult updateEntitiesPropertiesIfNotChanged( + @NotNull PolarisCallContext callCtx, @NotNull List entities); + + /** + * Rename an entity, potentially re-parenting it. + * + * @param callCtx call context + * @param catalogPath path to that entity. Could be an empty list of the entity is a catalog. + * @param entityToRename entity to rename. This entity should have been resolved by the client + * @param newCatalogPath if not null, new catalog path + * @param renamedEntity the new renamed entity we need to persist. We will use this argument to + * also update the internal and external properties as part of the rename operation. This is + * required to update the namespace path of the entity if it has changed + * @return the entity after renaming it or null if the rename operation has failed + */ + @NotNull + EntityResult renameEntity( + @NotNull PolarisCallContext callCtx, + @Nullable List catalogPath, + @NotNull PolarisEntityCore entityToRename, + @Nullable List newCatalogPath, + @NotNull PolarisEntity renamedEntity); + + // the return the result of a drop entity + class DropEntityResult extends BaseResult { + + /** If cleanup was requested and a task was successfully scheduled, */ + private final Long cleanupTaskId; + + /** + * Constructor for an error + * + * @param errorStatus error code, cannot be SUCCESS + * @param extraInformation extra information + */ + public DropEntityResult( + @NotNull PolarisMetaStoreManager.ReturnStatus errorStatus, + @Nullable String extraInformation) { + super(errorStatus, extraInformation); + this.cleanupTaskId = null; + } + + /** Constructor for success when no cleanup needs to be performed */ + public DropEntityResult() { + super(ReturnStatus.SUCCESS); + this.cleanupTaskId = null; + } + + /** + * Constructor for success when a cleanup task has been scheduled + * + * @param cleanupTaskId id of the task which was created to clean up the table drop + */ + public DropEntityResult(long cleanupTaskId) { + super(ReturnStatus.SUCCESS); + this.cleanupTaskId = cleanupTaskId; + } + + @JsonCreator + private DropEntityResult( + @JsonProperty("returnStatus") @NotNull ReturnStatus returnStatus, + @JsonProperty("extraInformation") String extraInformation, + @JsonProperty("cleanupTaskId") Long cleanupTaskId) { + super(returnStatus, extraInformation); + this.cleanupTaskId = cleanupTaskId; + } + + public Long getCleanupTaskId() { + return cleanupTaskId; + } + + @JsonIgnore + public boolean failedBecauseNotEmpty() { + ReturnStatus status = this.getReturnStatus(); + return status == ReturnStatus.CATALOG_NOT_EMPTY || status == ReturnStatus.NAMESPACE_NOT_EMPTY; + } + + public boolean isEntityUnDroppable() { + return this.getReturnStatus() == ReturnStatus.ENTITY_UNDROPPABLE; + } + } + + /** + * Drop the specified entity assuming it exists + * + * @param callCtx call context + * @param catalogPath path to that entity. Could be an empty list of the entity is a catalog. + * @param entityToDrop entity to drop, must have been resolved by the client + * @param cleanupProperties if not null, properties that will be persisted with the cleanup task + * @param cleanup true if resources owned by this entity should be deleted as well + * @return the result of the drop entity call, either success or error. If the error, it could be + * that the namespace or catalog to drop still has children, this should not be retried and + * should cause a failure + */ + @NotNull + DropEntityResult dropEntityIfExists( + @NotNull PolarisCallContext callCtx, + @Nullable List catalogPath, + @NotNull PolarisEntityCore entityToDrop, + @Nullable Map cleanupProperties, + boolean cleanup); + + /** Result of a grant/revoke privilege call */ + class PrivilegeResult extends BaseResult { + + // null if not success. + private final PolarisGrantRecord grantRecord; + + /** + * Constructor for an error + * + * @param errorCode error code, cannot be SUCCESS + * @param extraInformation extra information + */ + public PrivilegeResult( + @NotNull PolarisMetaStoreManager.ReturnStatus errorCode, + @Nullable String extraInformation) { + super(errorCode, extraInformation); + this.grantRecord = null; + } + + /** + * Constructor for success + * + * @param grantRecord grant record being granted or revoked + */ + public PrivilegeResult(@NotNull PolarisGrantRecord grantRecord) { + super(ReturnStatus.SUCCESS); + this.grantRecord = grantRecord; + } + + @JsonCreator + private PrivilegeResult( + @JsonProperty("returnStatus") @NotNull ReturnStatus returnStatus, + @JsonProperty("extraInformation") String extraInformation, + @JsonProperty("grantRecord") PolarisGrantRecord grantRecord) { + super(returnStatus, extraInformation); + this.grantRecord = grantRecord; + } + + public PolarisGrantRecord getGrantRecord() { + return grantRecord; + } + } + + /** + * Grant usage on a role to a grantee, for example granting usage on a catalog role to a principal + * role or granting a principal role to a principal. + * + * @param callCtx call context + * @param catalog if the role is a catalog role, the caller needs to pass-in the catalog entity + * which was used to resolve that granted. Else null. + * @param role resolved catalog or principal role + * @param grantee principal role or principal as resolved by the caller + * @return the grant record we created for this grant. Will return ENTITY_NOT_FOUND if the + * specified role couldn't be found. Should be retried in that case + */ + @NotNull + PrivilegeResult grantUsageOnRoleToGrantee( + @NotNull PolarisCallContext callCtx, + @Nullable PolarisEntityCore catalog, + @NotNull PolarisEntityCore role, + @NotNull PolarisEntityCore grantee); + + /** + * Revoke usage on a role (a catalog or a principal role) from a grantee (e.g. a principal role or + * a principal). + * + * @param callCtx call context + * @param catalog if the granted is a catalog role, the caller needs to pass-in the catalog entity + * which was used to resolve that role. Else null should be passed-in. + * @param role a catalog/principal role as resolved by the caller + * @param grantee resolved principal role or principal + * @return the result. Will return ENTITY_NOT_FOUND if the * specified role couldn't be found. + * Should be retried in that case. Will return GRANT_NOT_FOUND if the grant to revoke cannot + * be found + */ + @NotNull + PrivilegeResult revokeUsageOnRoleFromGrantee( + @NotNull PolarisCallContext callCtx, + @Nullable PolarisEntityCore catalog, + @NotNull PolarisEntityCore role, + @NotNull PolarisEntityCore grantee); + + /** + * Grant a privilege on a catalog securable to a grantee. + * + * @param callCtx call context + * @param grantee resolved role, the grantee + * @param catalogPath path to that entity, cannot be null or empty unless securable is top-level + * @param securable securable entity, must have been resolved by the client. Can be the catalog + * itself + * @param privilege privilege to grant + * @return the grant record we created for this grant. Will return ENTITY_NOT_FOUND if the + * specified role couldn't be found. Should be retried in that case + */ + @NotNull + PrivilegeResult grantPrivilegeOnSecurableToRole( + @NotNull PolarisCallContext callCtx, + @NotNull PolarisEntityCore grantee, + @Nullable List catalogPath, + @NotNull PolarisEntityCore securable, + @NotNull PolarisPrivilege privilege); + + /** + * Revoke a privilege on a catalog securable from a grantee. + * + * @param callCtx call context + * @param grantee resolved role, the grantee + * @param catalogPath path to that entity, cannot be null or empty unless securable is top-level + * @param securable securable entity, must have been resolved by the client. Can be the catalog + * itself. + * @param privilege privilege to revoke + * @return the result. Will return ENTITY_NOT_FOUND if the * specified role couldn't be found. + * Should be retried in that case. Will return GRANT_NOT_FOUND if the grant to revoke cannot + * be found + */ + @NotNull + PrivilegeResult revokePrivilegeOnSecurableFromRole( + @NotNull PolarisCallContext callCtx, + @NotNull PolarisEntityCore grantee, + @Nullable List catalogPath, + @NotNull PolarisEntityCore securable, + @NotNull PolarisPrivilege privilege); + + /** Result of a load grants call */ + class LoadGrantsResult extends BaseResult { + // true if success. If false, the caller should retry because of some concurrent change + private final int grantsVersion; + + // null if not success. Else set of grants records on a securable or to a grantee + private final List grantRecords; + + // null if not success. Else, for each grant record, list of securable or grantee entities + private final List entities; + + /** + * Constructor for an error + * + * @param errorCode error code, cannot be SUCCESS + * @param extraInformation extra information + */ + public LoadGrantsResult( + @NotNull PolarisMetaStoreManager.ReturnStatus errorCode, + @Nullable String extraInformation) { + super(errorCode, extraInformation); + this.grantsVersion = 0; + this.grantRecords = null; + this.entities = null; + } + + /** + * Constructor for success + * + * @param grantsVersion version of the grants + * @param grantRecords set of grant records + */ + public LoadGrantsResult( + int grantsVersion, + @NotNull List grantRecords, + List entities) { + super(ReturnStatus.SUCCESS); + this.grantsVersion = grantsVersion; + this.grantRecords = grantRecords; + this.entities = entities; + } + + @JsonCreator + private LoadGrantsResult( + @JsonProperty("returnStatus") @NotNull ReturnStatus returnStatus, + @JsonProperty("extraInformation") String extraInformation, + @JsonProperty("grantsVersion") int grantsVersion, + @JsonProperty("grantRecords") List grantRecords, + @JsonProperty("entities") List entities) { + super(returnStatus, extraInformation); + this.grantsVersion = grantsVersion; + this.grantRecords = grantRecords; + // old GS code might not serialize this argument + this.entities = entities; + } + + public int getGrantsVersion() { + return grantsVersion; + } + + public List getGrantRecords() { + return grantRecords; + } + + public List getEntities() { + return entities; + } + + @JsonIgnore + public Map getEntitiesAsMap() { + return (this.getEntities() == null) + ? null + : this.getEntities().stream() + .collect(Collectors.toMap(PolarisBaseEntity::getId, entity -> entity)); + } + + @Override + public String toString() { + return "LoadGrantsResult{" + + "grantsVersion=" + + grantsVersion + + ", grantRecords=" + + grantRecords + + ", entities=" + + entities + + ", returnStatus=" + + getReturnStatus() + + '}'; + } + } + + /** + * This method should be used by the Polaris app to cache all grant records on a securable. + * + * @param callCtx call context + * @param securableCatalogId id of the catalog this securable belongs to + * @param securableId id of the securable + * @return the list of grants and the version of the grant records. We will return + * ENTITY_NOT_FOUND if the securable cannot be found + */ + @NotNull + LoadGrantsResult loadGrantsOnSecurable( + @NotNull PolarisCallContext callCtx, long securableCatalogId, long securableId); + + /** + * This method should be used by the Polaris app to load all grants made to a grantee, either a + * role or a principal. + * + * @param callCtx call context + * @param granteeCatalogId id of the catalog this grantee belongs to + * @param granteeId id of the grantee + * @return the list of grants and the version of the grant records. We will return NULL if the + * grantee does not exist + */ + @NotNull + LoadGrantsResult loadGrantsToGrantee( + PolarisCallContext callCtx, long granteeCatalogId, long granteeId); + + /** Result of a loadEntitiesChangeTracking call */ + class ChangeTrackingResult extends BaseResult { + + // null if not success. Else, will be null if the grant to revoke was not found + private final List changeTrackingVersions; + + /** + * Constructor for an error + * + * @param errorCode error code, cannot be SUCCESS + * @param extraInformation extra information + */ + public ChangeTrackingResult( + @NotNull PolarisMetaStoreManager.ReturnStatus errorCode, + @Nullable String extraInformation) { + super(errorCode, extraInformation); + this.changeTrackingVersions = null; + } + + /** + * Constructor for success + * + * @param changeTrackingVersions change tracking versions + */ + public ChangeTrackingResult( + @NotNull List changeTrackingVersions) { + super(ReturnStatus.SUCCESS); + this.changeTrackingVersions = changeTrackingVersions; + } + + @JsonCreator + private ChangeTrackingResult( + @JsonProperty("returnStatus") @NotNull ReturnStatus returnStatus, + @JsonProperty("extraInformation") String extraInformation, + @JsonProperty("changeTrackingVersions") + List changeTrackingVersions) { + super(returnStatus, extraInformation); + this.changeTrackingVersions = changeTrackingVersions; + } + + public List getChangeTrackingVersions() { + return changeTrackingVersions; + } + } + + /** + * Load change tracking information for a set of entities in one single shot and return for each + * the version for the entity itself and the version associated to its grant records. + * + * @param callCtx call context + * @param entityIds list of catalog/entity pair ids for which we need to efficiently load the + * version information, both entity version and grant records version. + * @return a list of version tracking information. Order in that returned list is the same as the + * input list. Some elements might be NULL if the entity has been purged. Not expected to fail + */ + @NotNull + ChangeTrackingResult loadEntitiesChangeTracking( + @NotNull PolarisCallContext callCtx, @NotNull List entityIds); + + /** + * Load the entity from backend store. Will return NULL if the entity does not exist, i.e. has + * been purged. The entity being loaded might have been dropped + * + * @param callCtx call context + * @param entityCatalogId id of the catalog for that entity + * @param entityId the id of the entity to load + */ + @NotNull + EntityResult loadEntity(@NotNull PolarisCallContext callCtx, long entityCatalogId, long entityId); + + /** + * Fetch a list of tasks to be completed. Tasks + * + * @param callCtx call context + * @param executorId executor id + * @param limit limit + * @return list of tasks to be completed + */ + @NotNull + EntitiesResult loadTasks(@NotNull PolarisCallContext callCtx, String executorId, int limit); + + /** Result of a getSubscopedCredsForEntity() call */ + class ScopedCredentialsResult extends BaseResult { + + // null if not success. Else, set of name/value pairs for the credentials + private final EnumMap credentials; + + /** + * Constructor for an error + * + * @param errorCode error code, cannot be SUCCESS + * @param extraInformation extra information + */ + public ScopedCredentialsResult( + @NotNull PolarisMetaStoreManager.ReturnStatus errorCode, + @Nullable String extraInformation) { + super(errorCode, extraInformation); + this.credentials = null; + } + + /** + * Constructor for success + * + * @param credentials credentials + */ + public ScopedCredentialsResult( + @NotNull EnumMap credentials) { + super(ReturnStatus.SUCCESS); + this.credentials = credentials; + } + + @JsonCreator + private ScopedCredentialsResult( + @JsonProperty("returnStatus") @NotNull ReturnStatus returnStatus, + @JsonProperty("extraInformation") String extraInformation, + @JsonProperty("credentials") Map credentials) { + super(returnStatus, extraInformation); + this.credentials = new EnumMap<>(PolarisCredentialProperty.class); + if (credentials != null) { + credentials.forEach( + (k, v) -> this.credentials.put(PolarisCredentialProperty.valueOf(k), v)); + } + } + + public EnumMap getCredentials() { + return credentials; + } + } + + /** + * Get a sub-scoped credentials for an entity against the provided allowed read and write + * locations. + * + * @param callCtx the polaris call context + * @param catalogId the catalog id + * @param entityId the entity id + * @param allowListOperation whether to allow LIST operation on the allowedReadLocations and + * allowedWriteLocations + * @param allowedReadLocations a set of allowed to read locations + * @param allowedWriteLocations a set of allowed to write locations + * @return an enum map containing the scoped credentials + */ + @NotNull + ScopedCredentialsResult getSubscopedCredsForEntity( + @NotNull PolarisCallContext callCtx, + long catalogId, + long entityId, + boolean allowListOperation, + @NotNull Set allowedReadLocations, + @NotNull Set allowedWriteLocations); + + /** Result of a validateAccessToLocations() call */ + class ValidateAccessResult extends BaseResult { + + // null if not success. Else, set of location/validationResult pairs for each location in the + // set + private final Map validateResult; + + /** + * Constructor for an error + * + * @param errorCode error code, cannot be SUCCESS + * @param extraInformation extra information + */ + public ValidateAccessResult( + @NotNull PolarisMetaStoreManager.ReturnStatus errorCode, + @Nullable String extraInformation) { + super(errorCode, extraInformation); + this.validateResult = null; + } + + /** + * Constructor for success + * + * @param validateResult validate result + */ + public ValidateAccessResult(@NotNull Map validateResult) { + super(ReturnStatus.SUCCESS); + this.validateResult = validateResult; + } + + @JsonCreator + private ValidateAccessResult( + @JsonProperty("returnStatus") @NotNull ReturnStatus returnStatus, + @JsonProperty("extraInformation") String extraInformation, + @JsonProperty("validateResult") Map validateResult) { + super(returnStatus, extraInformation); + this.validateResult = validateResult; + } + + public Map getValidateResult() { + return this.validateResult; + } + } + + /** + * Validate whether the entity has access to the locations with the provided target operations + * + * @param callCtx the polaris call context + * @param catalogId the catalog id + * @param entityId the entity id + * @param actions a set of operation actions: READ/WRITE/LIST/DELETE/ALL + * @param locations a set of locations to verify + * @return a Map of , a validate result value looks like this + *

+   * {
+   *   "status" : "failure",
+   *   "actions" : {
+   *     "READ" : {
+   *       "message" : "The specified file was not found",
+   *       "status" : "failure"
+   *     },
+   *     "DELETE" : {
+   *       "message" : "One or more objects could not be deleted (Status Code: 200; Error Code: null)",
+   *       "status" : "failure"
+   *     },
+   *     "LIST" : {
+   *       "status" : "success"
+   *     },
+   *     "WRITE" : {
+   *       "message" : "Access Denied (Status Code: 403; Error Code: AccessDenied)",
+   *       "status" : "failure"
+   *     }
+   *   },
+   *   "message" : "Some of the integration checks failed. Check the Snowflake documentation for more information."
+   * }
+   * 
+ */ + @NotNull + ValidateAccessResult validateAccessToLocations( + @NotNull PolarisCallContext callCtx, + long catalogId, + long entityId, + @NotNull Set actions, + @NotNull Set locations); + + /** + * Represents an entry in the cache. If we refresh a cached entry, we will only refresh the + * information which have changed, based on the version of the entity + */ + class CachedEntryResult extends BaseResult { + + // the entity itself if it was loaded + private final @Nullable PolarisBaseEntity entity; + + // version for the grant records, in case the entity was not loaded + private final int grantRecordsVersion; + + private final @Nullable List entityGrantRecords; + + /** + * Constructor for an error + * + * @param errorCode error code, cannot be SUCCESS + * @param extraInformation extra information + */ + public CachedEntryResult( + @NotNull PolarisMetaStoreManager.ReturnStatus errorCode, + @Nullable String extraInformation) { + super(errorCode, extraInformation); + this.entity = null; + this.entityGrantRecords = null; + this.grantRecordsVersion = 0; + } + + /** + * Constructor with success + * + * @param entity the entity for that cached entry + * @param grantRecordsVersion the version of the grant records + * @param entityGrantRecords the list of grant records + */ + public CachedEntryResult( + @Nullable PolarisBaseEntity entity, + int grantRecordsVersion, + @Nullable List entityGrantRecords) { + super(ReturnStatus.SUCCESS); + this.entity = entity; + this.entityGrantRecords = entityGrantRecords; + this.grantRecordsVersion = grantRecordsVersion; + } + + @JsonCreator + public CachedEntryResult( + @JsonProperty("returnStatus") @NotNull ReturnStatus returnStatus, + @JsonProperty("extraInformation") String extraInformation, + @Nullable @JsonProperty("entity") PolarisBaseEntity entity, + @JsonProperty("grantRecordsVersion") int grantRecordsVersion, + @Nullable @JsonProperty("entityGrantRecords") List entityGrantRecords) { + super(returnStatus, extraInformation); + this.entity = entity; + this.entityGrantRecords = entityGrantRecords; + this.grantRecordsVersion = grantRecordsVersion; + } + + public @Nullable PolarisBaseEntity getEntity() { + return entity; + } + + public int getGrantRecordsVersion() { + return grantRecordsVersion; + } + + public @Nullable List getEntityGrantRecords() { + return entityGrantRecords; + } + } + + /** + * Load a cached entry, i.e. an entity definition and associated grant records, from the backend + * store. The entity is identified by its id (entity catalog id and id). + * + *

For entities that can be grantees, the associated grant records will include both the grant + * records for this entity as a grantee and for this entity as a securable. + * + * @param callCtx call context + * @param entityCatalogId id of the catalog for that entity + * @param entityId id of the entity + * @return cached entry for this entity. Status will be ENTITY_NOT_FOUND if the entity was not + * found + */ + @NotNull + PolarisMetaStoreManager.CachedEntryResult loadCachedEntryById( + @NotNull PolarisCallContext callCtx, long entityCatalogId, long entityId); + + /** + * Load a cached entry, i.e. an entity definition and associated grant records, from the backend + * store. The entity is identified by its name. Will return NULL if the entity does not exist, + * i.e. has been purged or dropped. + * + *

For entities that can be grantees, the associated grant records will include both the grant + * records for this entity as a grantee and for this entity as a securable. + * + * @param callCtx call context + * @param entityCatalogId id of the catalog for that entity + * @param parentId the id of the parent of that entity + * @param entityType the type of this entity + * @param entityName the name of this entity + * @return cached entry for this entity. Status will be ENTITY_NOT_FOUND if the entity was not + * found + */ + @NotNull + PolarisMetaStoreManager.CachedEntryResult loadCachedEntryByName( + @NotNull PolarisCallContext callCtx, + long entityCatalogId, + long parentId, + @NotNull PolarisEntityType entityType, + @NotNull String entityName); + + /** + * Refresh a cached entity from the backend store. Will return NULL if the entity does not exist, + * i.e. has been purged or dropped. Else, will determine what has changed based on the version + * information sent by the caller and will return only what has changed. + * + *

For entities that can be grantees, the associated grant records will include both the grant + * records for this entity as a grantee and for this entity as a securable. + * + * @param callCtx call context + * @param entityType type of the entity whose cached entry we are refreshing + * @param entityCatalogId id of the catalog for that entity + * @param entityId the id of the entity to load + * @return cached entry for this entity. Status will be ENTITY_NOT_FOUND if the entity was not * + * found + */ + @NotNull + PolarisMetaStoreManager.CachedEntryResult refreshCachedEntity( + @NotNull PolarisCallContext callCtx, + int entityVersion, + int entityGrantRecordsVersion, + @NotNull PolarisEntityType entityType, + long entityCatalogId, + long entityId); +} diff --git a/polaris-core/src/main/java/io/polaris/core/persistence/PolarisMetaStoreManagerImpl.java b/polaris-core/src/main/java/io/polaris/core/persistence/PolarisMetaStoreManagerImpl.java new file mode 100644 index 0000000000..3a4ecd05f5 --- /dev/null +++ b/polaris-core/src/main/java/io/polaris/core/persistence/PolarisMetaStoreManagerImpl.java @@ -0,0 +1,2398 @@ +package io.polaris.core.persistence; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import io.polaris.core.PolarisCallContext; +import io.polaris.core.entity.AsyncTaskType; +import io.polaris.core.entity.PolarisBaseEntity; +import io.polaris.core.entity.PolarisChangeTrackingVersions; +import io.polaris.core.entity.PolarisEntitiesActiveKey; +import io.polaris.core.entity.PolarisEntity; +import io.polaris.core.entity.PolarisEntityActiveRecord; +import io.polaris.core.entity.PolarisEntityConstants; +import io.polaris.core.entity.PolarisEntityCore; +import io.polaris.core.entity.PolarisEntityId; +import io.polaris.core.entity.PolarisEntitySubType; +import io.polaris.core.entity.PolarisEntityType; +import io.polaris.core.entity.PolarisGrantRecord; +import io.polaris.core.entity.PolarisPrincipalSecrets; +import io.polaris.core.entity.PolarisPrivilege; +import io.polaris.core.entity.PolarisTaskConstants; +import io.polaris.core.storage.PolarisCredentialProperty; +import io.polaris.core.storage.PolarisStorageActions; +import io.polaris.core.storage.PolarisStorageConfigurationInfo; +import io.polaris.core.storage.PolarisStorageIntegration; +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Default implementation of the Polaris Meta Store Manager. Uses the underlying meta store to store + * and retrieve all Polaris metadata + */ +@SuppressFBWarnings("NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE") +public class PolarisMetaStoreManagerImpl implements PolarisMetaStoreManager { + + /** mapper, allows to serialize/deserialize properties to/from JSON */ + private static final ObjectMapper MAPPER = new ObjectMapper(); + + /** use synchronous drop for entities */ + private static final boolean USE_SYNCHRONOUS_DROP = true; + + /** + * Lookup an entity by its name + * + * @param callCtx call context + * @param ms meta store + * @param entityActiveKey lookup key + * @return the entity if it exists, null otherwise + */ + private @Nullable PolarisBaseEntity lookupEntityByName( + @NotNull PolarisCallContext callCtx, + @NotNull PolarisMetaStoreSession ms, + @NotNull PolarisEntitiesActiveKey entityActiveKey) { + // ensure that the entity exists + PolarisEntityActiveRecord entityActiveRecord = ms.lookupEntityActive(callCtx, entityActiveKey); + + // if not found, return null + if (entityActiveRecord == null) { + return null; + } + + // lookup the entity, should be there + PolarisBaseEntity entity = + ms.lookupEntity(callCtx, entityActiveRecord.getCatalogId(), entityActiveRecord.getId()); + callCtx + .getDiagServices() + .checkNotNull( + entity, "unexpected_not_found_entity", "entityActiveRecord={}", entityActiveRecord); + + // return it now + return entity; + } + + /** + * Write this entity to the meta store. + * + * @param callCtx call context + * @param ms meta store in read/write mode + * @param entity entity to persist + * @param writeToActive if true, write it to active + */ + private void writeEntity( + @NotNull PolarisCallContext callCtx, + @NotNull PolarisMetaStoreSession ms, + @NotNull PolarisBaseEntity entity, + boolean writeToActive) { + ms.writeToEntities(callCtx, entity); + ms.writeToEntitiesChangeTracking(callCtx, entity); + + if (writeToActive) { + ms.writeToEntitiesActive(callCtx, entity); + } + } + + /** + * Persist the specified new entity. Persist will write this entity in the ENTITIES, in the + * ENTITIES_ACTIVE and finally in the ENTITIES_CHANGE_TRACKING tables + * + * @param callCtx call context + * @param ms meta store in read/write mode + * @param entity entity we need a DPO for + */ + private void persistNewEntity( + @NotNull PolarisCallContext callCtx, + @NotNull PolarisMetaStoreSession ms, + @NotNull PolarisBaseEntity entity) { + + // validate the entity type and subtype + callCtx.getDiagServices().checkNotNull(entity, "unexpected_null_entity"); + callCtx + .getDiagServices() + .checkNotNull(entity.getName(), "unexpected_null_name", "entity={}", entity); + PolarisEntityType type = PolarisEntityType.fromCode(entity.getTypeCode()); + callCtx.getDiagServices().checkNotNull(type, "unknown_type", "entity={}", entity); + PolarisEntitySubType subType = PolarisEntitySubType.fromCode(entity.getSubTypeCode()); + callCtx.getDiagServices().checkNotNull(subType, "unexpected_null_subType", "entity={}", entity); + callCtx + .getDiagServices() + .check( + subType.getParentType() == null || subType.getParentType() == type, + "invalid_subtype", + "type={} subType={}", + type, + subType); + + // if top-level entity, its parent should be the account + callCtx + .getDiagServices() + .check( + !type.isTopLevel() || entity.getParentId() == PolarisEntityConstants.getRootEntityId(), + "top_level_parent_should_be_account", + "entity={}", + entity); + + // id should not be null + callCtx + .getDiagServices() + .check( + entity.getId() != 0 || type == PolarisEntityType.ROOT, + "id_not_set", + "entity={}", + entity); + + // creation timestamp must be filled + callCtx.getDiagServices().check(entity.getCreateTimestamp() != 0, "null_create_timestamp"); + + // this is the first change + entity.setLastUpdateTimestamp(entity.getCreateTimestamp()); + + // set all other timestamps to 0 + entity.setDropTimestamp(0); + entity.setPurgeTimestamp(0); + entity.setToPurgeTimestamp(0); + + // write it + this.writeEntity(callCtx, ms, entity, true); + } + + /** + * Persist the specified entity after it has been changed. We will update the last changed time, + * increment the entity version and persist it back to the ENTITIES and ENTITIES_CHANGE_TRACKING + * tables + * + * @param callCtx call context + * @param ms meta store + * @param entity the entity which has been changed + * @return the entity with its version and lastUpdateTimestamp updated + */ + private @NotNull PolarisBaseEntity persistEntityAfterChange( + @NotNull PolarisCallContext callCtx, + @NotNull PolarisMetaStoreSession ms, + @NotNull PolarisBaseEntity entity) { + + // validate the entity type and subtype + callCtx.getDiagServices().checkNotNull(entity, "unexpected_null_entity"); + callCtx + .getDiagServices() + .checkNotNull(entity.getName(), "unexpected_null_name", "entity={}", entity); + PolarisEntityType type = entity.getType(); + callCtx.getDiagServices().checkNotNull(type, "unexpected_null_type", "entity={}", entity); + PolarisEntitySubType subType = entity.getSubType(); + callCtx.getDiagServices().checkNotNull(subType, "unexpected_null_subType", "entity={}", entity); + callCtx + .getDiagServices() + .check( + subType.getParentType() == null || subType.getParentType() == type, + "invalid_subtype", + "type={} subType={} entity={}", + type, + subType, + entity); + + // entity should not have been dropped + callCtx + .getDiagServices() + .check(entity.getDropTimestamp() == 0, "entity_dropped", "entity={}", entity); + + // creation timestamp must be filled + long createTimestamp = entity.getCreateTimestamp(); + callCtx + .getDiagServices() + .check(createTimestamp != 0, "null_create_timestamp", "entity={}", entity); + + // ensure time is not moving backward... + long now = System.currentTimeMillis(); + if (now < entity.getCreateTimestamp()) { + now = entity.getCreateTimestamp() + 1; + } + + // update last update timestamp and increment entity version + entity.setLastUpdateTimestamp(now); + entity.setEntityVersion(entity.getEntityVersion() + 1); + + // persist it to the various slices + this.writeEntity(callCtx, ms, entity, false); + + // return it + return entity; + } + + /** + * Drop this entity. This will: + * + *

+   *   - validate that the entity has not yet been dropped
+   *   - error out if this entity is undroppable
+   *   - if this is a catalog or a namespace, error out if the entity still has children
+   *   - we will fully delete the entity from persistence store
+   * 
+ * + * @param callCtx call context + * @param ms meta store + * @param entity the entity being dropped + */ + private void dropEntity( + @NotNull PolarisCallContext callCtx, + @NotNull PolarisMetaStoreSession ms, + @NotNull PolarisBaseEntity entity) { + + // validate the entity type and subtype + callCtx.getDiagServices().checkNotNull(entity, "unexpected_null_dpo"); + callCtx.getDiagServices().checkNotNull(entity.getName(), "unexpected_null_name"); + + // creation timestamp must be filled + callCtx.getDiagServices().check(entity.getDropTimestamp() == 0, "already_dropped"); + + // delete it from active slice + ms.deleteFromEntitiesActive(callCtx, entity); + + // for now drop all entities synchronously + if (USE_SYNCHRONOUS_DROP) { + // use synchronous drop + + // delete ALL grant records to (if the entity is a grantee) and from that entity + final List grantsOnGrantee = + (entity.getType().isGrantee()) + ? ms.loadAllGrantRecordsOnGrantee(callCtx, entity.getCatalogId(), entity.getId()) + : List.of(); + final List grantsOnSecurable = + ms.loadAllGrantRecordsOnSecurable(callCtx, entity.getCatalogId(), entity.getId()); + ms.deleteAllEntityGrantRecords(callCtx, entity, grantsOnGrantee, grantsOnSecurable); + + // Now determine the set of entities on the other side of the grants we just removed. Grants + // from/to these entities has been removed, hence we need to update the grant version of + // each entity. Collect the id of each. + Set entityIdsGrantChanged = new HashSet<>(); + grantsOnGrantee.forEach( + gr -> + entityIdsGrantChanged.add( + new PolarisEntityId(gr.getSecurableCatalogId(), gr.getSecurableId()))); + grantsOnSecurable.forEach( + gr -> + entityIdsGrantChanged.add( + new PolarisEntityId(gr.getGranteeCatalogId(), gr.getGranteeId()))); + + // Bump up the grant version of these entities + List entities = + ms.lookupEntities(callCtx, new ArrayList<>(entityIdsGrantChanged)); + for (PolarisBaseEntity entityGrantChanged : entities) { + entityGrantChanged.setGrantRecordsVersion(entityGrantChanged.getGrantRecordsVersion() + 1); + ms.writeToEntities(callCtx, entityGrantChanged); + ms.writeToEntitiesChangeTracking(callCtx, entityGrantChanged); + } + + // remove the entity being dropped now + ms.deleteFromEntities(callCtx, entity); + ms.deleteFromEntitiesChangeTracking(callCtx, entity); + + // if it is a principal, we also need to drop the secrets + if (entity.getType() == PolarisEntityType.PRINCIPAL) { + // get internal properties + Map properties = + this.deserializeProperties(callCtx, entity.getInternalProperties()); + + // get client_id + String clientId = properties.get(PolarisEntityConstants.getClientIdPropertyName()); + + // delete it from the secret slice + ms.deletePrincipalSecrets(callCtx, clientId, entity.getId()); + } + } else { + + // update the entity to indicate it has been dropped + final long now = System.currentTimeMillis(); + entity.setDropTimestamp(now); + entity.setLastUpdateTimestamp(now); + + // schedule purge + entity.setToPurgeTimestamp(now + PolarisEntityConstants.getRetentionTimeInMs()); + + // increment version + entity.setEntityVersion(entity.getEntityVersion() + 1); + + // write to the dropped slice and to purge slice + ms.writeToEntities(callCtx, entity); + ms.writeToEntitiesDropped(callCtx, entity); + ms.writeToEntitiesChangeTracking(callCtx, entity); + } + } + + /** + * Create and persist a new grant record. This will at the same time invalidate the grant records + * of the grantee and the securable if the grantee is a catalog role + * + * @param callCtx call context + * @param ms meta store in read/write mode + * @param securable securable + * @param grantee grantee, either a catalog role, a principal role or a principal + * @param priv privilege + * @return new grant record which was created and persisted + */ + private @NotNull PolarisGrantRecord persistNewGrantRecord( + @NotNull PolarisCallContext callCtx, + @NotNull PolarisMetaStoreSession ms, + @NotNull PolarisEntityCore securable, + @NotNull PolarisEntityCore grantee, + @NotNull PolarisPrivilege priv) { + + // validate non null arguments + callCtx.getDiagServices().checkNotNull(securable, "unexpected_null_securable"); + callCtx.getDiagServices().checkNotNull(grantee, "unexpected_null_grantee"); + callCtx.getDiagServices().checkNotNull(priv, "unexpected_null_priv"); + + // ensure that this entity is indeed a grantee like entity + callCtx + .getDiagServices() + .check(grantee.getType().isGrantee(), "entity_must_be_grantee", "entity={}", grantee); + + // create new grant record + PolarisGrantRecord grantRecord = + new PolarisGrantRecord( + securable.getCatalogId(), + securable.getId(), + grantee.getCatalogId(), + grantee.getId(), + priv.getCode()); + + // persist the new grant + ms.writeToGrantRecords(callCtx, grantRecord); + + // load the grantee (either a catalog/principal role or a principal) and increment its grants + // version + PolarisBaseEntity granteeEntity = + ms.lookupEntity(callCtx, grantee.getCatalogId(), grantee.getId()); + callCtx + .getDiagServices() + .checkNotNull(granteeEntity, "grantee_not_found", "grantee={}", grantee); + + // grants have changed, we need to bump-up the grants version + granteeEntity.setGrantRecordsVersion(granteeEntity.getGrantRecordsVersion() + 1); + this.writeEntity(callCtx, ms, granteeEntity, false); + + // we also need to invalidate the grants on that securable so that we can reload them. + // load the securable and increment its grants version + PolarisBaseEntity securableEntity = + ms.lookupEntity(callCtx, securable.getCatalogId(), securable.getId()); + callCtx + .getDiagServices() + .checkNotNull(securableEntity, "securable_not_found", "securable={}", securable); + + // grants have changed, we need to bump-up the grants version + securableEntity.setGrantRecordsVersion(securableEntity.getGrantRecordsVersion() + 1); + this.writeEntity(callCtx, ms, securableEntity, false); + + // done, return the new grant record + return grantRecord; + } + + /** + * Delete the specified grant record from the GRANT_RECORDS table. This will at the same time + * invalidate the grant records of the grantee and the securable if the grantee is a role + * + * @param callCtx call context + * @param ms meta store + * @param securable the securable entity + * @param grantee the grantee entity + * @param grantRecord the grant record to remove, which was read in the same transaction + */ + private void revokeGrantRecord( + @NotNull PolarisCallContext callCtx, + @NotNull PolarisMetaStoreSession ms, + @NotNull PolarisEntityCore securable, + @NotNull PolarisEntityCore grantee, + @NotNull PolarisGrantRecord grantRecord) { + + // validate securable + callCtx + .getDiagServices() + .check( + securable.getCatalogId() == grantRecord.getSecurableCatalogId() + && securable.getId() == grantRecord.getSecurableId(), + "securable_mismatch", + "securable={} grantRec={}", + securable, + grantRecord); + + // validate grantee + callCtx + .getDiagServices() + .check( + grantee.getCatalogId() == grantRecord.getGranteeCatalogId() + && grantee.getId() == grantRecord.getGranteeId(), + "grantee_mismatch", + "grantee={} grantRec={}", + grantee, + grantRecord); + + // ensure the grantee is really a grantee + callCtx + .getDiagServices() + .check(grantee.getType().isGrantee(), "not_a_grantee", "grantee={}", grantee); + + // remove that grant + ms.deleteFromGrantRecords(callCtx, grantRecord); + + // load the grantee and increment its grants version + PolarisBaseEntity refreshGrantee = + ms.lookupEntity(callCtx, grantee.getCatalogId(), grantee.getId()); + callCtx + .getDiagServices() + .checkNotNull( + refreshGrantee, "missing_grantee", "grantRecord={} grantee={}", grantRecord, grantee); + + // grants have changed, we need to bump-up the grants version + refreshGrantee.setGrantRecordsVersion(refreshGrantee.getGrantRecordsVersion() + 1); + this.writeEntity(callCtx, ms, refreshGrantee, false); + + // we also need to invalidate the grants on that securable so that we can reload them. + // load the securable and increment its grants version + PolarisBaseEntity refreshSecurable = + ms.lookupEntity(callCtx, securable.getCatalogId(), securable.getId()); + callCtx + .getDiagServices() + .checkNotNull( + refreshSecurable, + "missing_securable", + "grantRecord={} securable={}", + grantRecord, + securable); + + // grants have changed, we need to bump-up the grants version + refreshSecurable.setGrantRecordsVersion(refreshSecurable.getGrantRecordsVersion() + 1); + this.writeEntity(callCtx, ms, refreshSecurable, false); + } + + /** + * Create a new catalog. This not only creates the new catalog entity but also the initial admin + * role required to admin this catalog. + * + * @param callCtx call context + * @param ms meta store in read/write mode + * @param catalog the catalog entity to create + * @param integration the storage integration that should be attached to the catalog. If null, do + * nothing, otherwise persist the integration. + * @param principalRoles once the catalog has been created, list of principal roles to grant its + * catalog_admin role to. If no principal role is specified, we will grant the catalog_admin + * role of the newly created catalog to the service admin role. + * @return the catalog we just created and its associated admin catalog role or error if we failed + * to + */ + private @NotNull CreateCatalogResult createCatalog( + @NotNull PolarisCallContext callCtx, + @NotNull PolarisMetaStoreSession ms, + @NotNull PolarisBaseEntity catalog, + @Nullable PolarisStorageIntegration integration, + @NotNull List principalRoles) { + // validate input + callCtx.getDiagServices().checkNotNull(catalog, "unexpected_null_catalog"); + + // check if that catalog has already been created + PolarisBaseEntity refreshCatalog = + ms.lookupEntity(callCtx, catalog.getCatalogId(), catalog.getId()); + + // if found, probably a retry, simply return the previously created catalog + if (refreshCatalog != null) { + // if found, ensure it is indeed a catalog + callCtx + .getDiagServices() + .check( + refreshCatalog.getTypeCode() == PolarisEntityType.CATALOG.getCode(), + "not_a_catalog", + "catalog={}", + catalog); + + // lookup catalog admin role, should exist + PolarisEntitiesActiveKey adminRoleKey = + new PolarisEntitiesActiveKey( + refreshCatalog.getId(), + refreshCatalog.getId(), + PolarisEntityType.CATALOG_ROLE.getCode(), + PolarisEntityConstants.getNameOfCatalogAdminRole()); + PolarisBaseEntity catalogAdminRole = this.lookupEntityByName(callCtx, ms, adminRoleKey); + + // if found, ensure not null + callCtx + .getDiagServices() + .checkNotNull( + catalogAdminRole, "catalog_admin_role_not_found", "catalog={}", refreshCatalog); + + // done, return the existing catalog + return new CreateCatalogResult(refreshCatalog, catalogAdminRole); + } + + // check that a catalog with the same name does not exist already + PolarisEntitiesActiveKey catalogNameKey = + new PolarisEntitiesActiveKey( + PolarisEntityConstants.getNullId(), + PolarisEntityConstants.getRootEntityId(), + PolarisEntityType.CATALOG.getCode(), + catalog.getName()); + PolarisEntityActiveRecord otherCatalogRecord = ms.lookupEntityActive(callCtx, catalogNameKey); + + // if it exists, this is an error, the client should retry + if (otherCatalogRecord != null) { + return new CreateCatalogResult(ReturnStatus.ENTITY_ALREADY_EXISTS, null); + } + + ms.persistStorageIntegrationIfNeeded(callCtx, catalog, integration); + + // now create and persist new catalog entity + this.persistNewEntity(callCtx, ms, catalog); + + // create the catalog admin role for this new catalog + long adminRoleId = ms.generateNewId(callCtx); + PolarisBaseEntity adminRole = + new PolarisBaseEntity( + catalog.getId(), + adminRoleId, + PolarisEntityType.CATALOG_ROLE, + PolarisEntitySubType.NULL_SUBTYPE, + catalog.getId(), + PolarisEntityConstants.getNameOfCatalogAdminRole()); + this.persistNewEntity(callCtx, ms, adminRole); + + // grant the catalog admin role access-management on the catalog + this.persistNewGrantRecord( + callCtx, ms, catalog, adminRole, PolarisPrivilege.CATALOG_MANAGE_ACCESS); + + // grant the catalog admin role metadata-management on the catalog; this one + // is revocable + this.persistNewGrantRecord( + callCtx, ms, catalog, adminRole, PolarisPrivilege.CATALOG_MANAGE_METADATA); + + // immediately assign its catalog_admin role + if (principalRoles.isEmpty()) { + // lookup service admin role, should exist + PolarisEntitiesActiveKey serviceAdminRoleKey = + new PolarisEntitiesActiveKey( + PolarisEntityConstants.getNullId(), + PolarisEntityConstants.getRootEntityId(), + PolarisEntityType.PRINCIPAL_ROLE.getCode(), + PolarisEntityConstants.getNameOfPrincipalServiceAdminRole()); + PolarisBaseEntity serviceAdminRole = + this.lookupEntityByName(callCtx, ms, serviceAdminRoleKey); + callCtx.getDiagServices().checkNotNull(serviceAdminRole, "missing_service_admin_role"); + this.persistNewGrantRecord( + callCtx, ms, adminRole, serviceAdminRole, PolarisPrivilege.CATALOG_ROLE_USAGE); + } else { + // grant to each principal role usage on its catalog_admin role + for (PolarisEntityCore principalRole : principalRoles) { + // validate not null and really a principal role + callCtx.getDiagServices().checkNotNull(principalRole, "null principal role"); + callCtx + .getDiagServices() + .check( + principalRole.getTypeCode() == PolarisEntityType.PRINCIPAL_ROLE.getCode(), + "not_principal_role", + "type={}", + principalRole.getType()); + + // grant usage on that catalog admin role to this principal + this.persistNewGrantRecord( + callCtx, ms, adminRole, principalRole, PolarisPrivilege.CATALOG_ROLE_USAGE); + } + } + + // success, return the two entities + return new CreateCatalogResult(catalog, adminRole); + } + + /** + * Bootstrap Polaris catalog service + * + * @param callCtx call context + * @param ms meta store in read/write mode + */ + private void bootstrapPolarisService( + @NotNull PolarisCallContext callCtx, @NotNull PolarisMetaStoreSession ms) { + + // cleanup everything, start from a blank slate + ms.deleteAll(callCtx); + + // Create a root container entity that can represent the securable for any top-level grants. + PolarisBaseEntity rootContainer = + new PolarisBaseEntity( + PolarisEntityConstants.getNullId(), + PolarisEntityConstants.getRootEntityId(), + PolarisEntityType.ROOT, + PolarisEntitySubType.NULL_SUBTYPE, + PolarisEntityConstants.getRootEntityId(), + PolarisEntityConstants.getRootContainerName()); + this.persistNewEntity(callCtx, ms, rootContainer); + + // Now bootstrap the service by creating the root principal and the service_admin principal + // role. The principal role will be granted to that root principal and the root catalog admin + // of the root catalog will be granted to that principal role. + long rootPrincipalId = ms.generateNewId(callCtx); + PolarisBaseEntity rootPrincipal = + new PolarisBaseEntity( + PolarisEntityConstants.getNullId(), + rootPrincipalId, + PolarisEntityType.PRINCIPAL, + PolarisEntitySubType.NULL_SUBTYPE, + PolarisEntityConstants.getRootEntityId(), + PolarisEntityConstants.getRootPrincipalName()); + + // create this principal + this.createPrincipal(callCtx, ms, rootPrincipal); + + // now create the account admin principal role + long serviceAdminPrincipalRoleId = ms.generateNewId(callCtx); + PolarisBaseEntity serviceAdminPrincipalRole = + new PolarisBaseEntity( + PolarisEntityConstants.getNullId(), + serviceAdminPrincipalRoleId, + PolarisEntityType.PRINCIPAL_ROLE, + PolarisEntitySubType.NULL_SUBTYPE, + PolarisEntityConstants.getRootEntityId(), + PolarisEntityConstants.getNameOfPrincipalServiceAdminRole()); + this.persistNewEntity(callCtx, ms, serviceAdminPrincipalRole); + + // we also need to grant usage on the account-admin principal to the principal + this.persistNewGrantRecord( + callCtx, + ms, + serviceAdminPrincipalRole, + rootPrincipal, + PolarisPrivilege.PRINCIPAL_ROLE_USAGE); + + // grant SERVICE_MANAGE_ACCESS on the rootContainer to the serviceAdminPrincipalRole + this.persistNewGrantRecord( + callCtx, + ms, + rootContainer, + serviceAdminPrincipalRole, + PolarisPrivilege.SERVICE_MANAGE_ACCESS); + } + + /** {@inheritDoc} */ + @Override + public @NotNull BaseResult bootstrapPolarisService(@NotNull PolarisCallContext callCtx) { + // get meta store we should be using + PolarisMetaStoreSession ms = callCtx.getMetaStore(); + + // run operation in a read/write transaction + ms.runActionInTransaction(callCtx, () -> this.bootstrapPolarisService(callCtx, ms)); + + // all good + return new BaseResult(ReturnStatus.SUCCESS); + } + + /** + * See {@link #readEntityByName(PolarisCallContext, List, PolarisEntityType, PolarisEntitySubType, + * String)} + */ + private @NotNull PolarisMetaStoreManager.EntityResult readEntityByName( + @NotNull PolarisCallContext callCtx, + @NotNull PolarisMetaStoreSession ms, + @Nullable List catalogPath, + @NotNull PolarisEntityType entityType, + @NotNull PolarisEntitySubType entitySubType, + @NotNull String name) { + // first resolve again the catalogPath to that entity + PolarisEntityResolver resolver = new PolarisEntityResolver(callCtx, ms, catalogPath); + + // return if we failed to resolve + if (resolver.isFailure()) { + return new EntityResult(ReturnStatus.CATALOG_PATH_CANNOT_BE_RESOLVED, null); + } + + // now looking the entity by name + PolarisEntitiesActiveKey entityActiveKey = + new PolarisEntitiesActiveKey( + resolver.getCatalogIdOrNull(), resolver.getParentId(), entityType.getCode(), name); + PolarisBaseEntity entity = this.lookupEntityByName(callCtx, ms, entityActiveKey); + + // if found, check if subType really matches + if (entity != null + && entitySubType != PolarisEntitySubType.ANY_SUBTYPE + && entity.getSubTypeCode() != entitySubType.getCode()) { + entity = null; + } + + // success, return what we found + return (entity == null) + ? new EntityResult(ReturnStatus.ENTITY_NOT_FOUND, null) + : new EntityResult(entity); + } + + /** {@inheritDoc} */ + @Override + public @NotNull PolarisMetaStoreManager.EntityResult readEntityByName( + @NotNull PolarisCallContext callCtx, + @Nullable List catalogPath, + @NotNull PolarisEntityType entityType, + @NotNull PolarisEntitySubType entitySubType, + @NotNull String name) { + // get meta store we should be using + PolarisMetaStoreSession ms = callCtx.getMetaStore(); + + // run operation in a read/write transaction + return ms.runInReadTransaction( + callCtx, () -> readEntityByName(callCtx, ms, catalogPath, entityType, entitySubType, name)); + } + + /** + * See {@link #listEntities(PolarisCallContext, List, PolarisEntityType, PolarisEntitySubType)} + */ + private @NotNull ListEntitiesResult listEntities( + @NotNull PolarisCallContext callCtx, + @NotNull PolarisMetaStoreSession ms, + @Nullable List catalogPath, + @NotNull PolarisEntityType entityType, + @NotNull PolarisEntitySubType entitySubType) { + // first resolve again the catalogPath to that entity + PolarisEntityResolver resolver = new PolarisEntityResolver(callCtx, ms, catalogPath); + + // return if we failed to resolve + if (resolver.isFailure()) { + return new ListEntitiesResult(ReturnStatus.CATALOG_PATH_CANNOT_BE_RESOLVED, null); + } + + // return list of active entities + List toreturnList = + ms.listActiveEntities( + callCtx, resolver.getCatalogIdOrNull(), resolver.getParentId(), entityType); + + // prune the returned list with only entities matching the entity subtype + if (entitySubType != PolarisEntitySubType.ANY_SUBTYPE) { + toreturnList = + toreturnList.stream() + .filter(rec -> rec.getSubTypeCode() == entitySubType.getCode()) + .collect(Collectors.toList()); + } + + // done + return new ListEntitiesResult(toreturnList); + } + + /** {@inheritDoc} */ + @Override + public @NotNull ListEntitiesResult listEntities( + @NotNull PolarisCallContext callCtx, + @Nullable List catalogPath, + @NotNull PolarisEntityType entityType, + @NotNull PolarisEntitySubType entitySubType) { + // get meta store we should be using + PolarisMetaStoreSession ms = callCtx.getMetaStore(); + + // run operation in a read transaction + return ms.runInReadTransaction( + callCtx, () -> listEntities(callCtx, ms, catalogPath, entityType, entitySubType)); + } + + /** {@inheritDoc} */ + @Override + public @NotNull GenerateEntityIdResult generateNewEntityId(@NotNull PolarisCallContext callCtx) { + // get meta store we should be using + PolarisMetaStoreSession ms = callCtx.getMetaStore(); + + return new GenerateEntityIdResult(ms.generateNewId(callCtx)); + } + + /** + * Given the internal property as a map of key/value pairs, serialize it to a String + * + * @param properties a map of key/value pairs + * @return a String, the JSON representation of the map + */ + public String serializeProperties(PolarisCallContext callCtx, Map properties) { + + String jsonString = null; + try { + // Deserialize the JSON string to a Map + jsonString = MAPPER.writeValueAsString(properties); + } catch (JsonProcessingException ex) { + callCtx.getDiagServices().fail("got_json_processing_exception", "ex={}", ex); + } + + return jsonString; + } + + /** + * Given the serialized properties, deserialize those to a Map + * + * @param properties a JSON string representing the set of properties + * @return a Map of string + */ + public Map deserializeProperties(PolarisCallContext callCtx, String properties) { + + Map retProperties = null; + try { + // Deserialize the JSON string to a Map + retProperties = MAPPER.readValue(properties, new TypeReference<>() {}); + } catch (JsonMappingException ex) { + callCtx.getDiagServices().fail("got_json_mapping_exception", "ex={}", ex); + } catch (JsonProcessingException ex) { + callCtx.getDiagServices().fail("got_json_processing_exception", "ex={}", ex); + } + + return retProperties; + } + + /** {@link #createPrincipal(PolarisCallContext, PolarisBaseEntity)} */ + private @NotNull CreatePrincipalResult createPrincipal( + @NotNull PolarisCallContext callCtx, + @NotNull PolarisMetaStoreSession ms, + @NotNull PolarisBaseEntity principal) { + // validate input + callCtx.getDiagServices().checkNotNull(principal, "unexpected_null_principal"); + + // check if that catalog has already been created + PolarisBaseEntity refreshPrincipal = + ms.lookupEntity(callCtx, principal.getCatalogId(), principal.getId()); + + // if found, probably a retry, simply return the previously created principal + if (refreshPrincipal != null) { + // if found, ensure it is indeed a principal + callCtx + .getDiagServices() + .check( + principal.getTypeCode() == PolarisEntityType.PRINCIPAL.getCode(), + "not_a_principal", + "principal={}", + principal); + + // get internal properties + Map properties = + this.deserializeProperties(callCtx, refreshPrincipal.getInternalProperties()); + + // get client_id + String clientId = properties.get(PolarisEntityConstants.getClientIdPropertyName()); + + // should not be null + callCtx + .getDiagServices() + .checkNotNull( + clientId, + "null_client_id", + "properties={}", + refreshPrincipal.getInternalProperties()); + // ensure non null and non empty + callCtx + .getDiagServices() + .check( + !clientId.isEmpty(), + "empty_client_id", + "properties={}", + refreshPrincipal.getInternalProperties()); + + // get the main and secondary secrets for that client + PolarisPrincipalSecrets principalSecrets = ms.loadPrincipalSecrets(callCtx, clientId); + + // should not be null + callCtx + .getDiagServices() + .checkNotNull( + principalSecrets, + "missing_principal_secrets", + "clientId={} principal={}", + clientId, + refreshPrincipal); + + // done, return the newly created principal + return new CreatePrincipalResult(refreshPrincipal, principalSecrets); + } + + // check that a principal with the same name does not exist already + PolarisEntitiesActiveKey principalNameKey = + new PolarisEntitiesActiveKey( + PolarisEntityConstants.getNullId(), + PolarisEntityConstants.getRootEntityId(), + PolarisEntityType.PRINCIPAL.getCode(), + principal.getName()); + PolarisEntityActiveRecord otherPrincipalRecord = + ms.lookupEntityActive(callCtx, principalNameKey); + + // if it exists, this is an error, the client should retry + if (otherPrincipalRecord != null) { + return new CreatePrincipalResult(ReturnStatus.ENTITY_ALREADY_EXISTS, null); + } + + // generate new secretes for this principal + PolarisPrincipalSecrets principalSecrets = + ms.generateNewPrincipalSecrets(callCtx, principal.getName(), principal.getId()); + + // generate properties + Map internalProperties = getInternalPropertyMap(callCtx, principal); + internalProperties.put( + PolarisEntityConstants.getClientIdPropertyName(), principalSecrets.getPrincipalClientId()); + + // remember client id + principal.setInternalProperties(this.serializeProperties(callCtx, internalProperties)); + + // now create and persist new catalog entity + this.persistNewEntity(callCtx, ms, principal); + + // success, return the two entities + return new CreatePrincipalResult(principal, principalSecrets); + } + + /** {@inheritDoc} */ + public @NotNull CreatePrincipalResult createPrincipal( + @NotNull PolarisCallContext callCtx, @NotNull PolarisBaseEntity principal) { + // get metastore we should be using + PolarisMetaStoreSession ms = callCtx.getMetaStore(); + + // need to run inside a read/write transaction + return ms.runInTransaction(callCtx, () -> this.createPrincipal(callCtx, ms, principal)); + } + + /** See {@link #loadPrincipalSecrets(PolarisCallContext, String)} */ + private @Nullable PolarisPrincipalSecrets loadPrincipalSecrets( + @NotNull PolarisCallContext callCtx, PolarisMetaStoreSession ms, @NotNull String clientId) { + return ms.loadPrincipalSecrets(callCtx, clientId); + } + + /** {@inheritDoc} */ + @Override + public @NotNull PrincipalSecretsResult loadPrincipalSecrets( + @NotNull PolarisCallContext callCtx, @NotNull String clientId) { + // get metastore we should be using + PolarisMetaStoreSession ms = callCtx.getMetaStore(); + + // need to run inside a read/write transaction + PolarisPrincipalSecrets secrets = + ms.runInTransaction(callCtx, () -> this.loadPrincipalSecrets(callCtx, ms, clientId)); + + return (secrets == null) + ? new PrincipalSecretsResult(ReturnStatus.ENTITY_NOT_FOUND, null) + : new PrincipalSecretsResult(secrets); + } + + /** See {@link #} */ + private @Nullable PolarisPrincipalSecrets rotatePrincipalSecrets( + @NotNull PolarisCallContext callCtx, + @NotNull PolarisMetaStoreSession ms, + @NotNull String clientId, + long principalId, + @NotNull String masterSecret, + boolean reset) { + // if not found, the principal must have been dropped + EntityResult loadEntityResult = + loadEntity(callCtx, ms, PolarisEntityConstants.getNullId(), principalId); + if (loadEntityResult.getReturnStatus() != ReturnStatus.SUCCESS) { + return null; + } + + PolarisBaseEntity principal = loadEntityResult.getEntity(); + Map internalProps = + PolarisObjectMapperUtil.deserializeProperties( + callCtx, + principal.getInternalProperties() == null ? "{}" : principal.getInternalProperties()); + + boolean doReset = + reset + || internalProps.get( + PolarisEntityConstants.PRINCIPAL_CREDENTIAL_ROTATION_REQUIRED_STATE) + != null; + PolarisPrincipalSecrets secrets = + ms.rotatePrincipalSecrets(callCtx, clientId, principalId, masterSecret, doReset); + + if (reset + && !internalProps.containsKey( + PolarisEntityConstants.PRINCIPAL_CREDENTIAL_ROTATION_REQUIRED_STATE)) { + internalProps.put( + PolarisEntityConstants.PRINCIPAL_CREDENTIAL_ROTATION_REQUIRED_STATE, "true"); + principal.setInternalProperties( + PolarisObjectMapperUtil.serializeProperties(callCtx, internalProps)); + principal.setEntityVersion(principal.getEntityVersion() + 1); + writeEntity(callCtx, ms, principal, true); + } else if (internalProps.containsKey( + PolarisEntityConstants.PRINCIPAL_CREDENTIAL_ROTATION_REQUIRED_STATE)) { + internalProps.remove(PolarisEntityConstants.PRINCIPAL_CREDENTIAL_ROTATION_REQUIRED_STATE); + principal.setInternalProperties( + PolarisObjectMapperUtil.serializeProperties(callCtx, internalProps)); + principal.setEntityVersion(principal.getEntityVersion() + 1); + writeEntity(callCtx, ms, principal, true); + } + return secrets; + } + + /** {@inheritDoc} */ + @Override + public @NotNull PrincipalSecretsResult rotatePrincipalSecrets( + @NotNull PolarisCallContext callCtx, + @NotNull String clientId, + long principalId, + @NotNull String mainSecret, + boolean reset) { + // get metastore we should be using + PolarisMetaStoreSession ms = callCtx.getMetaStore(); + + // need to run inside a read/write transaction + PolarisPrincipalSecrets secrets = + ms.runInTransaction( + callCtx, + () -> + this.rotatePrincipalSecrets(callCtx, ms, clientId, principalId, mainSecret, reset)); + + return (secrets == null) + ? new PrincipalSecretsResult(ReturnStatus.ENTITY_NOT_FOUND, null) + : new PrincipalSecretsResult(secrets); + } + + /** {@inheritDoc} */ + @Override + public @NotNull CreateCatalogResult createCatalog( + @NotNull PolarisCallContext callCtx, + @NotNull PolarisBaseEntity catalog, + @NotNull List principalRoles) { + // get metastore we should be using + PolarisMetaStoreSession ms = callCtx.getMetaStore(); + + Map internalProp = getInternalPropertyMap(callCtx, catalog); + String integrationIdentifierOrId = + internalProp.get(PolarisEntityConstants.getStorageIntegrationIdentifierPropertyName()); + String storageConfigInfoStr = + internalProp.get(PolarisEntityConstants.getStorageConfigInfoPropertyName()); + PolarisStorageIntegration integration; + // storageConfigInfo's presence is needed to create a storage integration + // and the catalog should not have an internal property of storage identifier or id yet + if (storageConfigInfoStr != null && integrationIdentifierOrId == null) { + integration = + ms.createStorageIntegration( + callCtx, + catalog.getCatalogId(), + catalog.getId(), + PolarisStorageConfigurationInfo.deserialize( + callCtx.getDiagServices(), storageConfigInfoStr)); + } else { + integration = null; + } + // need to run inside a read/write transaction + return ms.runInTransaction( + callCtx, () -> this.createCatalog(callCtx, ms, catalog, integration, principalRoles)); + } + + /** {@link #createEntityIfNotExists(PolarisCallContext, List, PolarisBaseEntity)} */ + private @NotNull EntityResult createEntityIfNotExists( + @NotNull PolarisCallContext callCtx, + @NotNull PolarisMetaStoreSession ms, + @Nullable List catalogPath, + @NotNull PolarisBaseEntity entity) { + + // entity cannot be null + callCtx.getDiagServices().checkNotNull(entity, "unexpected_null_entity"); + + // entity name must be specified + callCtx.getDiagServices().checkNotNull(entity.getName(), "unexpected_null_entity_name"); + + // first, check if the entity has already been created, in which case we will simply return it + PolarisBaseEntity entityFound = ms.lookupEntity(callCtx, entity.getCatalogId(), entity.getId()); + if (entityFound != null) { + // probably the client retried, simply return it + return new EntityResult(entityFound); + } + + // first resolve again the catalogPath + PolarisEntityResolver resolver = new PolarisEntityResolver(callCtx, ms, catalogPath); + + // return if we failed to resolve + if (resolver.isFailure()) { + return new EntityResult(ReturnStatus.CATALOG_PATH_CANNOT_BE_RESOLVED, null); + } + + // check if an entity does not already exist with the same name. If true, this is an error + PolarisEntitiesActiveKey entityActiveKey = + new PolarisEntitiesActiveKey( + entity.getCatalogId(), + entity.getParentId(), + entity.getType().getCode(), + entity.getName()); + PolarisEntityActiveRecord entityActiveRecord = ms.lookupEntityActive(callCtx, entityActiveKey); + if (entityActiveRecord != null) { + return new EntityResult( + ReturnStatus.ENTITY_ALREADY_EXISTS, entityActiveRecord.getSubTypeCode()); + } + + // persist that new entity + this.persistNewEntity(callCtx, ms, entity); + + // done, return that newly created entity + return new EntityResult(entity); + } + + /** {@inheritDoc} */ + @Override + public @NotNull EntityResult createEntityIfNotExists( + @NotNull PolarisCallContext callCtx, + @Nullable List catalogPath, + @NotNull PolarisBaseEntity entity) { + // get metastore we should be using + PolarisMetaStoreSession ms = callCtx.getMetaStore(); + + // need to run inside a read/write transaction + return ms.runInTransaction( + callCtx, () -> this.createEntityIfNotExists(callCtx, ms, catalogPath, entity)); + } + + @Override + public @NotNull EntitiesResult createEntitiesIfNotExist( + @NotNull PolarisCallContext callCtx, + @Nullable List catalogPath, + @NotNull List entities) { + // get metastore we should be using + PolarisMetaStoreSession ms = callCtx.getMetaStore(); + + // need to run inside a read/write transaction + return ms.runInTransaction( + callCtx, + () -> { + List createdEntities = new ArrayList<>(entities.size()); + for (PolarisBaseEntity entity : entities) { + EntityResult entityCreateResult = + createEntityIfNotExists(callCtx, ms, catalogPath, entity); + // abort everything if error + if (entityCreateResult.getReturnStatus() != ReturnStatus.SUCCESS) { + ms.rollback(); + return new EntitiesResult( + entityCreateResult.getReturnStatus(), entityCreateResult.getExtraInformation()); + } + createdEntities.add(entityCreateResult.getEntity()); + } + return new EntitiesResult(createdEntities); + }); + } + + /** + * See {@link #updateEntityPropertiesIfNotChanged(PolarisCallContext, List, PolarisBaseEntity)} + */ + private @NotNull EntityResult updateEntityPropertiesIfNotChanged( + @NotNull PolarisCallContext callCtx, + @NotNull PolarisMetaStoreSession ms, + @Nullable List catalogPath, + @NotNull PolarisBaseEntity entity) { + // entity cannot be null + callCtx.getDiagServices().checkNotNull(entity, "unexpected_null_entity"); + + // re-resolve everything including that entity + PolarisEntityResolver resolver = new PolarisEntityResolver(callCtx, ms, catalogPath, entity); + + // if resolution failed, return false + if (resolver.isFailure()) { + return new EntityResult(ReturnStatus.CATALOG_PATH_CANNOT_BE_RESOLVED, null); + } + + // lookup the entity, cannot be null + PolarisBaseEntity entityRefreshed = + ms.lookupEntity(callCtx, entity.getCatalogId(), entity.getId()); + callCtx + .getDiagServices() + .checkNotNull(entityRefreshed, "unexpected_entity_not_found", "entity={}", entity); + + // check that the version of the entity has not changed at all to avoid concurrent updates + if (entityRefreshed.getEntityVersion() != entity.getEntityVersion()) { + return new EntityResult(ReturnStatus.TARGET_ENTITY_CONCURRENTLY_MODIFIED, null); + } + + // update the two properties + entityRefreshed.setInternalProperties(entity.getInternalProperties()); + entityRefreshed.setProperties(entity.getProperties()); + + // persist this entity after changing it. This will update the version and update the last + // updated time. Because the entity version is changed, we will update the change tracking table + PolarisBaseEntity persistedEntity = this.persistEntityAfterChange(callCtx, ms, entityRefreshed); + return new EntityResult(persistedEntity); + } + + /** {@inheritDoc} */ + @Override + public @NotNull EntityResult updateEntityPropertiesIfNotChanged( + @NotNull PolarisCallContext callCtx, + @Nullable List catalogPath, + @NotNull PolarisBaseEntity entity) { + // get metastore we should be using + PolarisMetaStoreSession ms = callCtx.getMetaStore(); + + // need to run inside a read/write transaction + return ms.runInTransaction( + callCtx, () -> this.updateEntityPropertiesIfNotChanged(callCtx, ms, catalogPath, entity)); + } + + /** See {@link #updateEntitiesPropertiesIfNotChanged(PolarisCallContext, List)} */ + private @NotNull EntitiesResult updateEntitiesPropertiesIfNotChanged( + @NotNull PolarisCallContext callCtx, + @NotNull PolarisMetaStoreSession ms, + @NotNull List entities) { + // ensure that the entities list is not null + callCtx.getDiagServices().checkNotNull(entities, "unexpected_null_entities"); + + // list of all updated entities + List updatedEntities = new ArrayList<>(entities.size()); + + // iterate over the list and update each, one at a time + for (EntityWithPath entityWithPath : entities) { + // update that entity, abort if it fails + EntityResult updatedEntityResult = + this.updateEntityPropertiesIfNotChanged( + callCtx, ms, entityWithPath.getCatalogPath(), entityWithPath.getEntity()); + + // if failed, rollback and return the last error + if (updatedEntityResult.getReturnStatus() != ReturnStatus.SUCCESS) { + ms.rollback(); + return new EntitiesResult( + updatedEntityResult.getReturnStatus(), updatedEntityResult.getExtraInformation()); + } + + // one more was updated + updatedEntities.add(updatedEntityResult.getEntity()); + } + + // good, all success + return new EntitiesResult(updatedEntities); + } + + /** {@inheritDoc} */ + @Override + public @NotNull EntitiesResult updateEntitiesPropertiesIfNotChanged( + @NotNull PolarisCallContext callCtx, @NotNull List entities) { + // get metastore we should be using + PolarisMetaStoreSession ms = callCtx.getMetaStore(); + + // need to run inside a read/write transaction + return ms.runInTransaction( + callCtx, () -> this.updateEntitiesPropertiesIfNotChanged(callCtx, ms, entities)); + } + + /** + * See {@link PolarisMetaStoreManager#renameEntity(PolarisCallContext, List, PolarisEntityCore, + * List, PolarisEntity)} + */ + private @NotNull EntityResult renameEntity( + @NotNull PolarisCallContext callCtx, + @NotNull PolarisMetaStoreSession ms, + @Nullable List catalogPath, + @NotNull PolarisEntityCore entityToRename, + @Nullable List newCatalogPath, + @NotNull PolarisBaseEntity renamedEntity) { + + // entity and new name cannot be null + callCtx.getDiagServices().checkNotNull(entityToRename, "unexpected_null_entityToRename"); + callCtx.getDiagServices().checkNotNull(renamedEntity, "unexpected_null_renamedEntity"); + + // if a new catalog path is specified (i.e. re-parent operation), a catalog path should be + // specified too + callCtx + .getDiagServices() + .check( + (newCatalogPath == null) || (catalogPath != null), + "newCatalogPath_specified_without_catalogPath"); + + // null is shorthand for saying the path isn't changing + if (newCatalogPath == null) { + newCatalogPath = catalogPath; + } + + // re-resolve everything including that entity + PolarisEntityResolver resolver = + new PolarisEntityResolver(callCtx, ms, catalogPath, entityToRename); + + // if resolution failed, return false + if (resolver.isFailure()) { + return new EntityResult(ReturnStatus.ENTITY_CANNOT_BE_RESOLVED, null); + } + + // find the entity to rename + PolarisBaseEntity refreshEntityToRename = + ms.lookupEntity(callCtx, entityToRename.getCatalogId(), entityToRename.getId()); + + // if this entity was not found, return failure. Not expected here because it was + // resolved successfully (see above) + if (refreshEntityToRename == null) { + return new EntityResult(ReturnStatus.ENTITY_NOT_FOUND, null); + } + + // check that the source entity has not changed since it was updated by the caller + if (refreshEntityToRename.getEntityVersion() != renamedEntity.getEntityVersion()) { + return new EntityResult(ReturnStatus.TARGET_ENTITY_CONCURRENTLY_MODIFIED, null); + } + + // ensure it can be renamed + if (refreshEntityToRename.cannotBeDroppedOrRenamed()) { + return new EntityResult(ReturnStatus.ENTITY_CANNOT_BE_RENAMED, null); + } + + // re-resolve the new catalog path if this entity is going to be moved + if (newCatalogPath != null) { + resolver = new PolarisEntityResolver(callCtx, ms, newCatalogPath); + + // if resolution failed, return false + if (resolver.isFailure()) { + return new EntityResult(ReturnStatus.CATALOG_PATH_CANNOT_BE_RESOLVED, null); + } + } + + // ensure that nothing exists where we create that entity + PolarisEntitiesActiveKey entityActiveKey = + new PolarisEntitiesActiveKey( + resolver.getCatalogIdOrNull(), + resolver.getParentId(), + refreshEntityToRename.getTypeCode(), + renamedEntity.getName()); + // if this entity already exists, this is an error + PolarisEntityActiveRecord entityActiveRecord = ms.lookupEntityActive(callCtx, entityActiveKey); + if (entityActiveRecord != null) { + return new EntityResult( + ReturnStatus.ENTITY_ALREADY_EXISTS, entityActiveRecord.getSubTypeCode()); + } + + // all good, delete the existing entity from the active slice + ms.deleteFromEntitiesActive(callCtx, refreshEntityToRename); + + // change its name now + refreshEntityToRename.setName(renamedEntity.getName()); + refreshEntityToRename.setProperties(renamedEntity.getProperties()); + refreshEntityToRename.setInternalProperties(renamedEntity.getInternalProperties()); + + // re-parent if a new catalog path was specified + if (newCatalogPath != null) { + refreshEntityToRename.setParentId(resolver.getParentId()); + } + + // persist back to the active slice with its new name and parent + ms.writeToEntitiesActive(callCtx, refreshEntityToRename); + + // persist the entity after change. This wil update the lastUpdateTimestamp and bump up the + // version + PolarisBaseEntity renamedEntityToReturn = + this.persistEntityAfterChange(callCtx, ms, refreshEntityToRename); + return new EntityResult(renamedEntityToReturn); + } + + /** {@inheritDoc} */ + @Override + public @NotNull EntityResult renameEntity( + @NotNull PolarisCallContext callCtx, + @Nullable List catalogPath, + @NotNull PolarisEntityCore entityToRename, + @Nullable List newCatalogPath, + @NotNull PolarisEntity renamedEntity) { + // get metastore we should be using + PolarisMetaStoreSession ms = callCtx.getMetaStore(); + + // need to run inside a read/write transaction + return ms.runInTransaction( + callCtx, + () -> + this.renameEntity( + callCtx, ms, catalogPath, entityToRename, newCatalogPath, renamedEntity)); + } + + /** + * See + * + *

{@link #dropEntityIfExists(PolarisCallContext, List, PolarisEntityCore, Map, boolean)} + */ + private @NotNull DropEntityResult dropEntityIfExists( + @NotNull PolarisCallContext callCtx, + @NotNull PolarisMetaStoreSession ms, + @Nullable List catalogPath, + @NotNull PolarisEntityCore entityToDrop, + @Nullable Map cleanupProperties, + boolean cleanup) { + // entity cannot be null + callCtx.getDiagServices().checkNotNull(entityToDrop, "unexpected_null_entity"); + + // re-resolve everything including that entity + PolarisEntityResolver resolver = + new PolarisEntityResolver(callCtx, ms, catalogPath, entityToDrop); + + // if resolution failed, return false + if (resolver.isFailure()) { + return new DropEntityResult(ReturnStatus.CATALOG_PATH_CANNOT_BE_RESOLVED, null); + } + + // first find the entity to drop + PolarisBaseEntity refreshEntityToDrop = + ms.lookupEntity(callCtx, entityToDrop.getCatalogId(), entityToDrop.getId()); + + // if this entity was not found, return failure + if (refreshEntityToDrop == null) { + return new DropEntityResult(ReturnStatus.ENTITY_NOT_FOUND, null); + } + + // ensure that this entity is droppable + if (refreshEntityToDrop.cannotBeDroppedOrRenamed()) { + return new DropEntityResult(ReturnStatus.ENTITY_UNDROPPABLE, null); + } + + // check that the entity has children, in which case it is an error. This only applies to + // a namespaces or a catalog + if (refreshEntityToDrop.getType() == PolarisEntityType.CATALOG) { + // the id of the catalog + long catalogId = refreshEntityToDrop.getId(); + + // if not all namespaces are dropped, we cannot drop this catalog + if (ms.hasChildren(callCtx, PolarisEntityType.NAMESPACE, catalogId, catalogId)) { + return new DropEntityResult(ReturnStatus.NAMESPACE_NOT_EMPTY, null); + } + + // get the list of catalog roles, at most 2 + List catalogRoles = + ms.listActiveEntities( + callCtx, + catalogId, + catalogId, + PolarisEntityType.CATALOG_ROLE, + 2, + entity -> true, + Function.identity()); + + // if we have 2, we cannot drop the catalog. If only one left, better be the admin role + if (catalogRoles.size() > 1) { + return new DropEntityResult(ReturnStatus.CATALOG_NOT_EMPTY, null); + } + + // if 1, drop the last catalog role. Should be the catalog admin role but don't validate this + if (!catalogRoles.isEmpty()) { + // drop the last catalog role in that catalog, should be the admin catalog role + this.dropEntity(callCtx, ms, catalogRoles.get(0)); + } + } else if (refreshEntityToDrop.getType() == PolarisEntityType.NAMESPACE) { + if (ms.hasChildren( + callCtx, null, refreshEntityToDrop.getCatalogId(), refreshEntityToDrop.getId())) { + return new DropEntityResult(ReturnStatus.NAMESPACE_NOT_EMPTY, null); + } + } + + // simply delete that entity. Will be removed from entities_active, added to the + // entities_dropped and its version will be changed. + this.dropEntity(callCtx, ms, refreshEntityToDrop); + + // if cleanup, schedule a cleanup task for the entity. do this here, so that drop and scheduling + // the cleanup task is transactional. Otherwise, we'll be unable to schedule the cleanup task + // later + if (cleanup) { + PolarisBaseEntity taskEntity = + new PolarisEntity.Builder() + .setId(generateNewEntityId(callCtx).getId()) + .setCatalogId(0L) + .setName("entityCleanup_" + entityToDrop.getId()) + .setType(PolarisEntityType.TASK) + .setSubType(PolarisEntitySubType.NULL_SUBTYPE) + .setCreateTimestamp(callCtx.getClock().millis()) + .build(); + + Map properties = new HashMap<>(); + properties.put( + PolarisTaskConstants.TASK_TYPE, + String.valueOf(AsyncTaskType.ENTITY_CLEANUP_SCHEDULER.typeCode())); + properties.put("data", PolarisObjectMapperUtil.serialize(callCtx, refreshEntityToDrop)); + taskEntity.setProperties(PolarisObjectMapperUtil.serializeProperties(callCtx, properties)); + if (cleanupProperties != null) { + taskEntity.setInternalProperties( + PolarisObjectMapperUtil.serializeProperties(callCtx, cleanupProperties)); + } + createEntityIfNotExists(callCtx, ms, null, taskEntity); + return new DropEntityResult(taskEntity.getId()); + } + + // done, return success + return new DropEntityResult(); + } + + /** {@inheritDoc} */ + @Override + public @NotNull DropEntityResult dropEntityIfExists( + @NotNull PolarisCallContext callCtx, + @Nullable List catalogPath, + @NotNull PolarisEntityCore entityToDrop, + @Nullable Map cleanupProperties, + boolean cleanup) { + // get metastore we should be using + PolarisMetaStoreSession ms = callCtx.getMetaStore(); + + // need to run inside a read/write transaction + return ms.runInTransaction( + callCtx, + () -> + this.dropEntityIfExists( + callCtx, ms, catalogPath, entityToDrop, cleanupProperties, cleanup)); + } + + /** + * Resolve the arguments of granting/revoking a usage grant between a role (catalog or principal + * role) and a grantee (either a principal role or a principal) + * + * @param callCtx call context + * @param ms meta store in read/write mode + * @param catalog if the role is a catalog role, the caller needs to pass-in the catalog entity + * which was used to resolve that role. Else null. + * @param role the role, either a catalog or principal role + * @param grantee the grantee + * @return resolver for the specified entities + */ + private @NotNull PolarisEntityResolver resolveRoleToGranteeUsageGrant( + @NotNull PolarisCallContext callCtx, + @NotNull PolarisMetaStoreSession ms, + @Nullable PolarisEntityCore catalog, + @NotNull PolarisEntityCore role, + @NotNull PolarisEntityCore grantee) { + + // validate the grantee input + callCtx.getDiagServices().checkNotNull(grantee, "unexpected_null_grantee"); + callCtx + .getDiagServices() + .check(grantee.getType().isGrantee(), "not_a_grantee", "grantee={}", grantee); + + // validate role + callCtx.getDiagServices().checkNotNull(role, "unexpected_null_role"); + + // role should be a catalog or a principal role + boolean isCatalogRole = role.getTypeCode() == PolarisEntityType.CATALOG_ROLE.getCode(); + boolean isPrincipalRole = role.getTypeCode() == PolarisEntityType.PRINCIPAL_ROLE.getCode(); + callCtx.getDiagServices().check(isCatalogRole || isPrincipalRole, "not_a_role"); + + // if the role is a catalog role, ensure a catalog is specified and + // vice-versa, catalog should be null if the role is a principal role + callCtx + .getDiagServices() + .check( + (catalog == null && isPrincipalRole) || (catalog != null && isCatalogRole), + "catalog_mismatch", + "catalog={} role={}", + catalog, + role); + + // re-resolve now all these entities + List otherTopLevelEntities = new ArrayList<>(2); + otherTopLevelEntities.add(role); + otherTopLevelEntities.add(grantee); + + // ensure these entities have not changed + return new PolarisEntityResolver( + callCtx, ms, catalog != null ? List.of(catalog) : null, null, otherTopLevelEntities); + } + + /** + * Helper function to resolve the securable to role grant privilege + * + * @param grantee resolved grantee + * @param catalogPath path to that entity, cannot be null or empty if securable has a catalogId + * @param securable securable entity, must have been resolved by the client + * @return a resolver for the role, the catalog path and the securable + */ + private PolarisEntityResolver resolveSecurableToRoleGrant( + @NotNull PolarisCallContext callCtx, + @NotNull PolarisMetaStoreSession ms, + @NotNull PolarisEntityCore grantee, + @Nullable List catalogPath, + @NotNull PolarisEntityCore securable) { + // validate role input + callCtx.getDiagServices().checkNotNull(grantee, "unexpected_null_grantee"); + callCtx + .getDiagServices() + .check(grantee.getType().isGrantee(), "not_grantee_type", "grantee={}", grantee); + + // securable must be supplied + callCtx.getDiagServices().checkNotNull(securable, "unexpected_null_securable"); + if (securable.getCatalogId() > 0) { + // catalogPath must be supplied if the securable has a catalogId + callCtx.getDiagServices().checkNotNull(catalogPath, "unexpected_null_catalogPath"); + } + + // re-resolve now all these entities + return new PolarisEntityResolver(callCtx, ms, catalogPath, securable, List.of(grantee)); + } + + /** + * See {@link #grantUsageOnRoleToGrantee(PolarisCallContext, PolarisEntityCore, PolarisEntityCore, + * PolarisEntityCore)} + */ + private @NotNull PrivilegeResult grantUsageOnRoleToGrantee( + @NotNull PolarisCallContext callCtx, + @NotNull PolarisMetaStoreSession ms, + @Nullable PolarisEntityCore catalog, + @NotNull PolarisEntityCore role, + @NotNull PolarisEntityCore grantee) { + + // ensure these entities have not changed + PolarisEntityResolver resolver = + this.resolveRoleToGranteeUsageGrant(callCtx, ms, catalog, role, grantee); + + // if failure to resolve, let the caller know + if (resolver.isFailure()) { + return new PrivilegeResult(ReturnStatus.ENTITY_CANNOT_BE_RESOLVED, null); + } + + // the usage privilege to grant + PolarisPrivilege usagePriv = + (grantee.getType() == PolarisEntityType.PRINCIPAL_ROLE) + ? PolarisPrivilege.CATALOG_ROLE_USAGE + : PolarisPrivilege.PRINCIPAL_ROLE_USAGE; + + // grant usage on this role to this principal + callCtx + .getDiagServices() + .check(grantee.getType().isGrantee(), "not_a_grantee", "grantee={}", grantee); + PolarisGrantRecord grantRecord = + this.persistNewGrantRecord(callCtx, ms, role, grantee, usagePriv); + return new PrivilegeResult(grantRecord); + } + + /** {@inheritDoc} */ + @Override + public @NotNull PrivilegeResult grantUsageOnRoleToGrantee( + @NotNull PolarisCallContext callCtx, + @Nullable PolarisEntityCore catalog, + @NotNull PolarisEntityCore role, + @NotNull PolarisEntityCore grantee) { + // get metastore we should be using + PolarisMetaStoreSession ms = callCtx.getMetaStore(); + + // need to run inside a read/write transaction + return ms.runInTransaction( + callCtx, () -> this.grantUsageOnRoleToGrantee(callCtx, ms, catalog, role, grantee)); + } + + /** + * See {@link #revokeUsageOnRoleFromGrantee(PolarisCallContext, PolarisEntityCore, + * PolarisEntityCore, PolarisEntityCore)} + */ + private @NotNull PrivilegeResult revokeUsageOnRoleFromGrantee( + @NotNull PolarisCallContext callCtx, + @NotNull PolarisMetaStoreSession ms, + @Nullable PolarisEntityCore catalog, + @NotNull PolarisEntityCore role, + @NotNull PolarisEntityCore grantee) { + + // ensure these entities have not changed + PolarisEntityResolver resolver = + this.resolveRoleToGranteeUsageGrant(callCtx, ms, catalog, role, grantee); + + // if failure to resolve, let the caller know + if (resolver.isFailure()) { + return new PrivilegeResult(ReturnStatus.ENTITY_CANNOT_BE_RESOLVED, null); + } + + // the usage privilege to revoke + PolarisPrivilege usagePriv = + (grantee.getType() == PolarisEntityType.PRINCIPAL_ROLE) + ? PolarisPrivilege.CATALOG_ROLE_USAGE + : PolarisPrivilege.PRINCIPAL_ROLE_USAGE; + + // first, ensure that this privilege has been granted + PolarisGrantRecord grantRecord = + ms.lookupGrantRecord( + callCtx, + role.getCatalogId(), + role.getId(), + grantee.getCatalogId(), + grantee.getId(), + usagePriv.getCode()); + + // this is not a really bad error, no-op really + if (grantRecord == null) { + return new PrivilegeResult(ReturnStatus.GRANT_NOT_FOUND, null); + } + + // revoke usage on the role from the grantee + this.revokeGrantRecord(callCtx, ms, role, grantee, grantRecord); + + return new PrivilegeResult(grantRecord); + } + + /** {@inheritDoc} */ + @Override + public @NotNull PrivilegeResult revokeUsageOnRoleFromGrantee( + @NotNull PolarisCallContext callCtx, + @Nullable PolarisEntityCore catalog, + @NotNull PolarisEntityCore role, + @NotNull PolarisEntityCore grantee) { + // get metastore we should be using + PolarisMetaStoreSession ms = callCtx.getMetaStore(); + + // need to run inside a read/write transaction + return ms.runInTransaction( + callCtx, () -> this.revokeUsageOnRoleFromGrantee(callCtx, ms, catalog, role, grantee)); + } + + /** + * See {@link #grantPrivilegeOnSecurableToRole(PolarisCallContext, PolarisEntityCore, List, + * PolarisEntityCore, PolarisPrivilege)} + */ + private @NotNull PrivilegeResult grantPrivilegeOnSecurableToRole( + @NotNull PolarisCallContext callCtx, + @NotNull PolarisMetaStoreSession ms, + @NotNull PolarisEntityCore grantee, + @Nullable List catalogPath, + @NotNull PolarisEntityCore securable, + @NotNull PolarisPrivilege priv) { + + // re-resolve now all these entities + PolarisEntityResolver resolver = + this.resolveSecurableToRoleGrant(callCtx, ms, grantee, catalogPath, securable); + + // if failure to resolve, let the caller know + if (resolver.isFailure()) { + return new PrivilegeResult(ReturnStatus.ENTITY_CANNOT_BE_RESOLVED, null); + } + + // grant specified privilege on this securable to this role and return the grant + PolarisGrantRecord grantRecord = + this.persistNewGrantRecord(callCtx, ms, securable, grantee, priv); + return new PrivilegeResult(grantRecord); + } + + /** {@inheritDoc} */ + @Override + public @NotNull PrivilegeResult grantPrivilegeOnSecurableToRole( + @NotNull PolarisCallContext callCtx, + @NotNull PolarisEntityCore grantee, + @Nullable List catalogPath, + @NotNull PolarisEntityCore securable, + @NotNull PolarisPrivilege privilege) { + // get metastore we should be using + PolarisMetaStoreSession ms = callCtx.getMetaStore(); + + // need to run inside a read/write transaction + return ms.runInTransaction( + callCtx, + () -> + this.grantPrivilegeOnSecurableToRole( + callCtx, ms, grantee, catalogPath, securable, privilege)); + } + + /** + * See {@link #revokePrivilegeOnSecurableFromRole(PolarisCallContext, PolarisEntityCore, List, + * PolarisEntityCore, PolarisPrivilege)} + */ + private @NotNull PrivilegeResult revokePrivilegeOnSecurableFromRole( + @NotNull PolarisCallContext callCtx, + @NotNull PolarisMetaStoreSession ms, + @NotNull PolarisEntityCore grantee, + @Nullable List catalogPath, + @NotNull PolarisEntityCore securable, + @NotNull PolarisPrivilege priv) { + + // re-resolve now all these entities + PolarisEntityResolver resolver = + this.resolveSecurableToRoleGrant(callCtx, ms, grantee, catalogPath, securable); + + // if failure to resolve, let the caller know + if (resolver.isFailure()) { + return new PrivilegeResult(ReturnStatus.ENTITY_CANNOT_BE_RESOLVED, null); + } + + // lookup the grants records to find this grant + PolarisGrantRecord grantRecord = + ms.lookupGrantRecord( + callCtx, + securable.getCatalogId(), + securable.getId(), + grantee.getCatalogId(), + grantee.getId(), + priv.getCode()); + + // the grant does not exist, nothing to do really + if (grantRecord == null) { + return new PrivilegeResult(ReturnStatus.GRANT_NOT_FOUND, null); + } + + // revoke the specified privilege on this securable from this role + this.revokeGrantRecord(callCtx, ms, securable, grantee, grantRecord); + + // success! + return new PrivilegeResult(grantRecord); + } + + /** {@inheritDoc} */ + @Override + public @NotNull PrivilegeResult revokePrivilegeOnSecurableFromRole( + @NotNull PolarisCallContext callCtx, + @NotNull PolarisEntityCore grantee, + @Nullable List catalogPath, + @NotNull PolarisEntityCore securable, + @NotNull PolarisPrivilege privilege) { + // get metastore we should be using + PolarisMetaStoreSession ms = callCtx.getMetaStore(); + + // need to run inside a read/write transaction + return ms.runInTransaction( + callCtx, + () -> + this.revokePrivilegeOnSecurableFromRole( + callCtx, ms, grantee, catalogPath, securable, privilege)); + } + + /** {@link #loadGrantsOnSecurable(PolarisCallContext, long, long)} */ + private @NotNull LoadGrantsResult loadGrantsOnSecurable( + @NotNull PolarisCallContext callCtx, + @NotNull PolarisMetaStoreSession ms, + long securableCatalogId, + long securableId) { + + // lookup grants version for this securable entity + int grantsVersion = + ms.lookupEntityGrantRecordsVersion(callCtx, securableCatalogId, securableId); + + // return null if securable does not exists + if (grantsVersion == 0) { + return new LoadGrantsResult(ReturnStatus.ENTITY_NOT_FOUND, null); + } + + // now fetch all grants for this securable + final List returnGrantRecords = + ms.loadAllGrantRecordsOnSecurable(callCtx, securableCatalogId, securableId); + + // find all unique grantees + List entityIds = + returnGrantRecords.stream() + .map( + grantRecord -> + new PolarisEntityId( + grantRecord.getGranteeCatalogId(), grantRecord.getGranteeId())) + .distinct() + .collect(Collectors.toList()); + List entities = ms.lookupEntities(callCtx, entityIds); + + // done, return the list of grants and their version + return new LoadGrantsResult( + grantsVersion, + returnGrantRecords, + entities.stream().filter(Objects::nonNull).collect(Collectors.toList())); + } + + /** {@inheritDoc} */ + @Override + public @NotNull LoadGrantsResult loadGrantsOnSecurable( + @NotNull PolarisCallContext callCtx, long securableCatalogId, long securableId) { + // get metastore we should be using + PolarisMetaStoreSession ms = callCtx.getMetaStore(); + + // need to run inside a read transaction + return ms.runInReadTransaction( + callCtx, () -> this.loadGrantsOnSecurable(callCtx, ms, securableCatalogId, securableId)); + } + + /** {@link #loadGrantsToGrantee(PolarisCallContext, long, long)} */ + public @NotNull LoadGrantsResult loadGrantsToGrantee( + @NotNull PolarisCallContext callCtx, + @NotNull PolarisMetaStoreSession ms, + long granteeCatalogId, + long granteeId) { + + // lookup grants version for this grantee entity + int grantsVersion = ms.lookupEntityGrantRecordsVersion(callCtx, granteeCatalogId, granteeId); + + // return null if grantee does not exists + if (grantsVersion == 0) { + return new LoadGrantsResult(ReturnStatus.ENTITY_NOT_FOUND, null); + } + + // now fetch all grants for this grantee + final List returnGrantRecords = + ms.loadAllGrantRecordsOnGrantee(callCtx, granteeCatalogId, granteeId); + + // find all unique securables + List entityIds = + returnGrantRecords.stream() + .map( + grantRecord -> + new PolarisEntityId( + grantRecord.getSecurableCatalogId(), grantRecord.getSecurableId())) + .distinct() + .collect(Collectors.toList()); + List entities = ms.lookupEntities(callCtx, entityIds); + + // done, return the list of grants and their version + return new LoadGrantsResult( + grantsVersion, + returnGrantRecords, + entities.stream().filter(Objects::nonNull).collect(Collectors.toList())); + } + + /** {@inheritDoc} */ + @Override + public @NotNull LoadGrantsResult loadGrantsToGrantee( + @NotNull PolarisCallContext callCtx, long granteeCatalogId, long granteeId) { + // get metastore we should be using + PolarisMetaStoreSession ms = callCtx.getMetaStore(); + + // need to run inside a read transaction + return ms.runInReadTransaction( + callCtx, () -> this.loadGrantsToGrantee(callCtx, ms, granteeCatalogId, granteeId)); + } + + /** {@link PolarisMetaStoreManager#loadEntitiesChangeTracking(PolarisCallContext, List)} */ + private @NotNull ChangeTrackingResult loadEntitiesChangeTracking( + @NotNull PolarisCallContext callCtx, + @NotNull PolarisMetaStoreSession ms, + @NotNull List entityIds) { + List changeTracking = + ms.lookupEntityVersions(callCtx, entityIds); + return new ChangeTrackingResult(changeTracking); + } + + /** {@inheritDoc} */ + @Override + public @NotNull ChangeTrackingResult loadEntitiesChangeTracking( + @NotNull PolarisCallContext callCtx, @NotNull List entityIds) { + // get metastore we should be using + PolarisMetaStoreSession ms = callCtx.getMetaStore(); + + // need to run inside a read transaction + return ms.runInReadTransaction( + callCtx, () -> this.loadEntitiesChangeTracking(callCtx, ms, entityIds)); + } + + /** Refer to {@link #loadEntity(PolarisCallContext, long, long)} */ + private @NotNull EntityResult loadEntity( + @NotNull PolarisCallContext callCtx, + @NotNull PolarisMetaStoreSession ms, + long entityCatalogId, + long entityId) { + // this is an easy one + PolarisBaseEntity entity = ms.lookupEntity(callCtx, entityCatalogId, entityId); + return (entity != null) + ? new EntityResult(entity) + : new EntityResult(ReturnStatus.ENTITY_NOT_FOUND, null); + } + + /** {@inheritDoc} */ + @Override + public @NotNull EntityResult loadEntity( + @NotNull PolarisCallContext callCtx, long entityCatalogId, long entityId) { + // get metastore we should be using + PolarisMetaStoreSession ms = callCtx.getMetaStore(); + + // need to run inside a read transaction + return ms.runInReadTransaction( + callCtx, () -> this.loadEntity(callCtx, ms, entityCatalogId, entityId)); + } + + /** Refer to {@link #loadTasks(PolarisCallContext, String, int)} */ + private @NotNull EntitiesResult loadTasks( + @NotNull PolarisCallContext callCtx, + @NotNull PolarisMetaStoreSession ms, + String executorId, + int limit) { + + // find all available tasks + List availableTasks = + ms.listActiveEntities( + callCtx, + PolarisEntityConstants.getRootEntityId(), + PolarisEntityConstants.getRootEntityId(), + PolarisEntityType.TASK, + limit, + entity -> { + PolarisObjectMapperUtil.TaskExecutionState taskState = + PolarisObjectMapperUtil.parseTaskState(entity); + long taskAgeTimeout = + callCtx + .getConfigurationStore() + .getConfiguration( + callCtx, + PolarisTaskConstants.TASK_TIMEOUT_MILLIS_CONFIG, + PolarisTaskConstants.TASK_TIMEOUT_MILLIS); + return taskState == null + || taskState.executor == null + || callCtx.getClock().millis() - taskState.lastAttemptStartTime > taskAgeTimeout; + }, + Function.identity()); + + availableTasks.forEach( + task -> { + Map properties = + PolarisObjectMapperUtil.deserializeProperties(callCtx, task.getProperties()); + properties.put(PolarisTaskConstants.LAST_ATTEMPT_EXECUTOR_ID, executorId); + properties.put( + PolarisTaskConstants.LAST_ATTEMPT_START_TIME, + String.valueOf(callCtx.getClock().millis())); + properties.put( + PolarisTaskConstants.ATTEMPT_COUNT, + String.valueOf( + Integer.parseInt(properties.getOrDefault(PolarisTaskConstants.ATTEMPT_COUNT, "0")) + + 1)); + task.setEntityVersion(task.getEntityVersion() + 1); + task.setProperties(PolarisObjectMapperUtil.serializeProperties(callCtx, properties)); + writeEntity(callCtx, ms, task, false); + }); + return new EntitiesResult(availableTasks); + } + + @Override + public @NotNull EntitiesResult loadTasks( + @NotNull PolarisCallContext callCtx, String executorId, int limit) { + PolarisMetaStoreSession ms = callCtx.getMetaStore(); + return ms.runInTransaction(callCtx, () -> this.loadTasks(callCtx, ms, executorId, limit)); + } + + /** {@inheritDoc} */ + @Override + public @NotNull ScopedCredentialsResult getSubscopedCredsForEntity( + @NotNull PolarisCallContext callCtx, + long catalogId, + long entityId, + boolean allowListOperation, + @NotNull Set allowedReadLocations, + @NotNull Set allowedWriteLocations) { + + // get meta store session we should be using + PolarisMetaStoreSession ms = callCtx.getMetaStore(); + callCtx + .getDiagServices() + .check( + !allowedReadLocations.isEmpty() || !allowedWriteLocations.isEmpty(), + "allowed_locations_to_subscope_is_required"); + + // reload the entity, error out if not found + EntityResult reloadedEntity = loadEntity(callCtx, catalogId, entityId); + if (reloadedEntity.getReturnStatus() != ReturnStatus.SUCCESS) { + return new ScopedCredentialsResult( + reloadedEntity.getReturnStatus(), reloadedEntity.getExtraInformation()); + } + + // get storage integration + PolarisStorageIntegration storageIntegration = + ms.loadPolarisStorageIntegration(callCtx, reloadedEntity.getEntity()); + + // cannot be null + callCtx + .getDiagServices() + .checkNotNull( + storageIntegration, + "storage_integration_not_exists", + "catalogId={}, entityId={}", + catalogId, + entityId); + + PolarisStorageConfigurationInfo storageConfigurationInfo = + readStorageConfiguration(callCtx, reloadedEntity.getEntity()); + try { + EnumMap creds = + storageIntegration.getSubscopedCreds( + callCtx.getDiagServices(), + storageConfigurationInfo, + allowListOperation, + allowedReadLocations, + allowedWriteLocations); + return new ScopedCredentialsResult(creds); + } catch (Exception ex) { + return new ScopedCredentialsResult(ReturnStatus.SUBSCOPE_CREDS_ERROR, ex.getMessage()); + } + } + + /** {@inheritDoc} */ + @Override + public @NotNull ValidateAccessResult validateAccessToLocations( + @NotNull PolarisCallContext callCtx, + long catalogId, + long entityId, + @NotNull Set actions, + @NotNull Set locations) { + // get meta store we should be using + PolarisMetaStoreSession ms = callCtx.getMetaStore(); + callCtx + .getDiagServices() + .check( + !actions.isEmpty() && !locations.isEmpty(), + "locations_and_operations_privileges_are_required"); + // reload the entity, error out if not found + EntityResult reloadedEntity = loadEntity(callCtx, catalogId, entityId); + if (reloadedEntity.getReturnStatus() != ReturnStatus.SUCCESS) { + return new ValidateAccessResult( + reloadedEntity.getReturnStatus(), reloadedEntity.getExtraInformation()); + } + + // get storage integration, expect not null + PolarisStorageIntegration storageIntegration = + ms.loadPolarisStorageIntegration(callCtx, reloadedEntity.getEntity()); + callCtx + .getDiagServices() + .checkNotNull( + storageIntegration, + "storage_integration_not_exists", + "catalogId={}, entityId={}", + catalogId, + entityId); + + // validate access + PolarisStorageConfigurationInfo storageConfigurationInfo = + readStorageConfiguration(callCtx, reloadedEntity.getEntity()); + Map validateLocationAccess = + storageIntegration + .validateAccessToLocations(storageConfigurationInfo, actions, locations) + .entrySet() + .stream() + .collect( + Collectors.toMap( + Map.Entry::getKey, + e -> PolarisObjectMapperUtil.serialize(callCtx, e.getValue()))); + + // done, return result + return new ValidateAccessResult(validateLocationAccess); + } + + public static PolarisStorageConfigurationInfo readStorageConfiguration( + @NotNull PolarisCallContext callCtx, PolarisBaseEntity reloadedEntity) { + Map propMap = + PolarisObjectMapperUtil.deserializeProperties( + callCtx, reloadedEntity.getInternalProperties()); + String storageConfigInfoStr = + propMap.get(PolarisEntityConstants.getStorageConfigInfoPropertyName()); + + callCtx + .getDiagServices() + .check( + storageConfigInfoStr != null, + "missing_storage_configuration_info", + "catalogId={}, entityId={}", + reloadedEntity.getCatalogId(), + reloadedEntity.getId()); + return PolarisStorageConfigurationInfo.deserialize( + callCtx.getDiagServices(), storageConfigInfoStr); + } + + /** + * Get the internal property map for an entity + * + * @param callCtx the polaris call context + * @param entity the target entity + * @return a map of string representing the internal properties + */ + public Map getInternalPropertyMap( + @NotNull PolarisCallContext callCtx, @NotNull PolarisBaseEntity entity) { + String internalPropStr = entity.getInternalProperties(); + Map res = new HashMap<>(); + if (internalPropStr == null) { + return res; + } + return deserializeProperties(callCtx, internalPropStr); + } + + /** {@link #loadCachedEntryById(PolarisCallContext, long, long)} */ + private @NotNull CachedEntryResult loadCachedEntryById( + @NotNull PolarisCallContext callCtx, + @NotNull PolarisMetaStoreSession ms, + long entityCatalogId, + long entityId) { + + // load that entity + PolarisBaseEntity entity = ms.lookupEntity(callCtx, entityCatalogId, entityId); + + // if entity not found, return null + if (entity == null) { + return new CachedEntryResult(ReturnStatus.ENTITY_NOT_FOUND, null); + } + + // load the grant records + final List grantRecords; + if (entity.getType().isGrantee()) { + grantRecords = + new ArrayList<>(ms.loadAllGrantRecordsOnGrantee(callCtx, entityCatalogId, entityId)); + grantRecords.addAll(ms.loadAllGrantRecordsOnSecurable(callCtx, entityCatalogId, entityId)); + } else { + grantRecords = ms.loadAllGrantRecordsOnSecurable(callCtx, entityCatalogId, entityId); + } + + // return the result + return new CachedEntryResult(entity, entity.getGrantRecordsVersion(), grantRecords); + } + + /** {@inheritDoc} */ + @Override + public @NotNull CachedEntryResult loadCachedEntryById( + @NotNull PolarisCallContext callCtx, long entityCatalogId, long entityId) { + // get metastore we should be using + PolarisMetaStoreSession ms = callCtx.getMetaStore(); + + // need to run inside a read transaction + return ms.runInReadTransaction( + callCtx, () -> this.loadCachedEntryById(callCtx, ms, entityCatalogId, entityId)); + } + + /** {@link #loadCachedEntryById(PolarisCallContext, long, long)} */ + private @NotNull PolarisMetaStoreManager.CachedEntryResult loadCachedEntryByName( + @NotNull PolarisCallContext callCtx, + @NotNull PolarisMetaStoreSession ms, + long entityCatalogId, + long parentId, + @NotNull PolarisEntityType entityType, + @NotNull String entityName) { + + // load that entity + PolarisEntitiesActiveKey entityActiveKey = + new PolarisEntitiesActiveKey(entityCatalogId, parentId, entityType.getCode(), entityName); + PolarisBaseEntity entity = this.lookupEntityByName(callCtx, ms, entityActiveKey); + + // null if entity not found + if (entity == null) { + return new CachedEntryResult(ReturnStatus.ENTITY_NOT_FOUND, null); + } + + // load the grant records + final List grantRecords; + if (entity.getType().isGrantee()) { + grantRecords = + new ArrayList<>( + ms.loadAllGrantRecordsOnGrantee(callCtx, entityCatalogId, entity.getId())); + grantRecords.addAll( + ms.loadAllGrantRecordsOnSecurable(callCtx, entityCatalogId, entity.getId())); + } else { + grantRecords = ms.loadAllGrantRecordsOnSecurable(callCtx, entityCatalogId, entity.getId()); + } + + // return the result + return new CachedEntryResult(entity, entity.getGrantRecordsVersion(), grantRecords); + } + + /** {@inheritDoc} */ + @Override + public @NotNull CachedEntryResult loadCachedEntryByName( + @NotNull PolarisCallContext callCtx, + long entityCatalogId, + long parentId, + @NotNull PolarisEntityType entityType, + @NotNull String entityName) { + // get metastore we should be using + PolarisMetaStoreSession ms = callCtx.getMetaStore(); + + // need to run inside a read transaction + CachedEntryResult result = + ms.runInReadTransaction( + callCtx, + () -> + this.loadCachedEntryByName( + callCtx, ms, entityCatalogId, parentId, entityType, entityName)); + if (PolarisEntityConstants.getRootContainerName().equals(entityName) + && entityType == PolarisEntityType.ROOT + && !result.isSuccess()) { + // Backfill rootContainer if needed. + ms.runActionInTransaction( + callCtx, + () -> { + PolarisBaseEntity rootContainer = + new PolarisBaseEntity( + PolarisEntityConstants.getNullId(), + PolarisEntityConstants.getRootEntityId(), + PolarisEntityType.ROOT, + PolarisEntitySubType.NULL_SUBTYPE, + PolarisEntityConstants.getRootEntityId(), + PolarisEntityConstants.getRootContainerName()); + EntityResult backfillResult = + this.createEntityIfNotExists(callCtx, ms, null, rootContainer); + if (backfillResult.isSuccess()) { + PolarisEntitiesActiveKey serviceAdminRoleKey = + new PolarisEntitiesActiveKey( + 0L, + 0L, + PolarisEntityType.PRINCIPAL_ROLE.getCode(), + PolarisEntityConstants.getNameOfPrincipalServiceAdminRole()); + PolarisBaseEntity serviceAdminRole = + this.lookupEntityByName(callCtx, ms, serviceAdminRoleKey); + if (serviceAdminRole != null) { + this.persistNewGrantRecord( + callCtx, + ms, + rootContainer, + serviceAdminRole, + PolarisPrivilege.SERVICE_MANAGE_ACCESS); + } + } + }); + + // Redo the lookup in a separate read transaction. + result = + ms.runInReadTransaction( + callCtx, + () -> + this.loadCachedEntryByName( + callCtx, ms, entityCatalogId, parentId, entityType, entityName)); + } + return result; + } + + /** {@inheritDoc} */ + private @NotNull CachedEntryResult refreshCachedEntity( + @NotNull PolarisCallContext callCtx, + @NotNull PolarisMetaStoreSession ms, + int entityVersion, + int entityGrantRecordsVersion, + @NotNull PolarisEntityType entityType, + long entityCatalogId, + long entityId) { + + // load version information + PolarisChangeTrackingVersions entityVersions = + ms.lookupEntityVersions(callCtx, List.of(new PolarisEntityId(entityCatalogId, entityId))) + .get(0); + + // if null, the entity has been purged + if (entityVersions == null) { + return new CachedEntryResult(ReturnStatus.ENTITY_NOT_FOUND, null); + } + + // load the entity if something changed + final PolarisBaseEntity entity; + if (entityVersion != entityVersions.getEntityVersion()) { + entity = ms.lookupEntity(callCtx, entityCatalogId, entityId); + + // if not found, return null + if (entity == null) { + return new CachedEntryResult(ReturnStatus.ENTITY_NOT_FOUND, null); + } + } else { + // entity has not changed, no need to reload it + entity = null; + } + + // load the grant records if required + final List grantRecords; + if (entityVersions.getGrantRecordsVersion() != entityGrantRecordsVersion) { + if (entityType.isGrantee()) { + grantRecords = + new ArrayList<>(ms.loadAllGrantRecordsOnGrantee(callCtx, entityCatalogId, entityId)); + grantRecords.addAll(ms.loadAllGrantRecordsOnSecurable(callCtx, entityCatalogId, entityId)); + } else { + grantRecords = ms.loadAllGrantRecordsOnSecurable(callCtx, entityCatalogId, entityId); + } + } else { + grantRecords = null; + } + + // return the result + return new CachedEntryResult(entity, entityVersions.getGrantRecordsVersion(), grantRecords); + } + + /** {@inheritDoc} */ + @Override + public @NotNull PolarisMetaStoreManager.CachedEntryResult refreshCachedEntity( + @NotNull PolarisCallContext callCtx, + int entityVersion, + int entityGrantRecordsVersion, + @NotNull PolarisEntityType entityType, + long entityCatalogId, + long entityId) { + // get metastore we should be using + PolarisMetaStoreSession ms = callCtx.getMetaStore(); + + // need to run inside a read transaction + return ms.runInReadTransaction( + callCtx, + () -> + this.refreshCachedEntity( + callCtx, + ms, + entityVersion, + entityGrantRecordsVersion, + entityType, + entityCatalogId, + entityId)); + } +} diff --git a/polaris-core/src/main/java/io/polaris/core/persistence/PolarisMetaStoreSession.java b/polaris-core/src/main/java/io/polaris/core/persistence/PolarisMetaStoreSession.java new file mode 100644 index 0000000000..6e48198fd1 --- /dev/null +++ b/polaris-core/src/main/java/io/polaris/core/persistence/PolarisMetaStoreSession.java @@ -0,0 +1,509 @@ +package io.polaris.core.persistence; + +import io.polaris.core.PolarisCallContext; +import io.polaris.core.entity.PolarisBaseEntity; +import io.polaris.core.entity.PolarisChangeTrackingVersions; +import io.polaris.core.entity.PolarisEntitiesActiveKey; +import io.polaris.core.entity.PolarisEntityActiveRecord; +import io.polaris.core.entity.PolarisEntityCore; +import io.polaris.core.entity.PolarisEntityId; +import io.polaris.core.entity.PolarisEntityType; +import io.polaris.core.entity.PolarisGrantRecord; +import io.polaris.core.entity.PolarisPrincipalSecrets; +import io.polaris.core.storage.PolarisStorageConfigurationInfo; +import io.polaris.core.storage.PolarisStorageIntegration; +import java.util.List; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Interface to the Polaris metadata store, allows to persist and retrieve all Polaris metadata like + * metadata for Polaris entities and metadata about grants between these entities which is the + * foundation of our role base access control model. + * + *

Note that APIs to the actual persistence store are very basic, often point read or write to + * the underlying data store. The goal is to make it really easy to back this using databases like + * Postgres or simpler KV store. + */ +public interface PolarisMetaStoreSession { + + /** + * Run the specified transaction code (a Supplier lambda type) in a database read/write + * transaction. If the code of the transaction does not throw any exception and returns normally, + * the transaction will be committed, else the transaction will be automatically rolled-back on + * error. The result of the supplier lambda is returned if success, else the error will be + * re-thrown. + * + * @param callCtx call context + * @param transactionCode code of the transaction being executed, a supplier lambda + */ + T runInTransaction(@NotNull PolarisCallContext callCtx, @NotNull Supplier transactionCode); + + /** + * Run the specified transaction code (a runnable lambda type) in a database read/write + * transaction. If the code of the transaction does not throw any exception and returns normally, + * the transaction will be committed, else the transaction will be automatically rolled-back on + * error. + * + * @param callCtx call context + * @param transactionCode code of the transaction being executed, a runnable lambda + */ + void runActionInTransaction( + @NotNull PolarisCallContext callCtx, @NotNull Runnable transactionCode); + + /** + * Run the specified transaction code (a Supplier lambda type) in a database read transaction. If + * the code of the transaction does not throw any exception and returns normally, the transaction + * will be committed, else the transaction will be automatically rolled-back on error. The result + * of the supplier lambda is returned if success, else the error will be re-thrown. + * + * @param callCtx call context + * @param transactionCode code of the transaction being executed, a supplier lambda + */ + T runInReadTransaction( + @NotNull PolarisCallContext callCtx, @NotNull Supplier transactionCode); + + /** + * Run the specified transaction code (a runnable lambda type) in a database read transaction. If + * the code of the transaction does not throw any exception and returns normally, the transaction + * will be committed, else the transaction will be automatically rolled-back on error. + * + * @param callCtx call context + * @param transactionCode code of the transaction being executed, a runnable lambda + */ + void runActionInReadTransaction( + @NotNull PolarisCallContext callCtx, @NotNull Runnable transactionCode); + + /** + * @param callCtx call context + * @return new unique entity identifier + */ + long generateNewId(@NotNull PolarisCallContext callCtx); + + /** + * Write the base entity to the entities table. If there is a conflict (existing record with the + * same id), all attributes of the new record will replace the existing one. + * + * @param callCtx call context + * @param entity entity record to write, potentially replacing an existing entity record with the + * same key + */ + void writeToEntities(@NotNull PolarisCallContext callCtx, @NotNull PolarisBaseEntity entity); + + /** + * Write the base entity to the entities_active table. If there is a conflict (existing record + * with the same PK), all attributes of the new record will replace the existing one. + * + * @param callCtx call context + * @param entity entity record to write, potentially replacing an existing entity record with the + * same key + */ + void writeToEntitiesActive( + @NotNull PolarisCallContext callCtx, @NotNull PolarisBaseEntity entity); + + /** + * Write the base entity to the entities_dropped table. If there is a conflict (existing record + * with the same PK), all attributes of the new record will replace the existing one. + * + * @param callCtx call context + * @param entity entity record to write, potentially replacing an existing entity record with the + * same key + */ + void writeToEntitiesDropped( + @NotNull PolarisCallContext callCtx, @NotNull PolarisBaseEntity entity); + + /** + * Write the base entity to the entities change tracking table. If there is a conflict (existing + * record with the same id), all attributes of the new record will replace the existing one. + * + * @param callCtx call context + * @param entity entity record to write, potentially replacing an existing entity record with the + * same key + */ + void writeToEntitiesChangeTracking( + @NotNull PolarisCallContext callCtx, @NotNull PolarisBaseEntity entity); + + /** + * Write the specified grantRecord to the grant_records table. If there is a conflict (existing + * record with the same PK), all attributes of the new record will replace the existing one. + * + * @param callCtx call context + * @param grantRec entity record to write, potentially replacing an existing entity record with + * the same key + */ + void writeToGrantRecords( + @NotNull PolarisCallContext callCtx, @NotNull PolarisGrantRecord grantRec); + + /** + * Delete the base entity from the entities table. + * + * @param callCtx call context + * @param entity entity record to delete + */ + void deleteFromEntities(@NotNull PolarisCallContext callCtx, @NotNull PolarisEntityCore entity); + + /** + * Delete the base entity from the entities_active table. + * + * @param callCtx call context + * @param entity entity record to delete + */ + void deleteFromEntitiesActive( + @NotNull PolarisCallContext callCtx, @NotNull PolarisEntityCore entity); + + /** + * Delete the base entity to the entities_dropped table + * + * @param callCtx call context + * @param entity entity record to delete + */ + void deleteFromEntitiesDropped( + @NotNull PolarisCallContext callCtx, @NotNull PolarisBaseEntity entity); + + /** + * Delete the base entity from the entities change tracking table + * + * @param callCtx call context + * @param entity entity record to delete + */ + void deleteFromEntitiesChangeTracking( + @NotNull PolarisCallContext callCtx, @NotNull PolarisEntityCore entity); + + /** + * Delete the specified grantRecord to the grant_records table. + * + * @param callCtx call context + * @param grantRec entity record to delete. + */ + void deleteFromGrantRecords( + @NotNull PolarisCallContext callCtx, @NotNull PolarisGrantRecord grantRec); + + /** + * Delete the all grant records in the grant_records table for the specified entity. This method + * will delete all grant records on that securable entity and also all grants to that grantee + * entity assuming that the entity is a grantee (catalog role, principal role or principal). + * + * @param callCtx call context + * @param entity entity whose grant records to and from should be deleted + * @param grantsOnGrantee all grants to that grantee entity. Empty list if that entity is not a + * grantee + * @param grantsOnSecurable all grants on that securable entity + */ + void deleteAllEntityGrantRecords( + @NotNull PolarisCallContext callCtx, + @NotNull PolarisEntityCore entity, + @NotNull List grantsOnGrantee, + @NotNull List grantsOnSecurable); + + /** + * Delete Polaris entity and grant record metadata from all tables. This is used during metadata + * bootstrap to reset all tables to their original state + * + * @param callCtx call context + */ + void deleteAll(@NotNull PolarisCallContext callCtx); + + /** + * Lookup an entity given its catalog id (which can be NULL_ID for top-level entities) and its + * unique id. + * + * @param callCtx call context + * @param catalogId catalog id or NULL_ID + * @param entityId unique entity id + * @return NULL if the entity was not found, else the base entity. + */ + @Nullable + PolarisBaseEntity lookupEntity( + @NotNull PolarisCallContext callCtx, long catalogId, long entityId); + + /** + * Lookup a set of entities given their catalog id/entity id unique identifier + * + * @param callCtx call context + * @param entityIds list of entity ids + * @return list of polaris base entities, parallel to the input list of ids. An entity in the list + * will be null if the corresponding entity could not be found. + */ + @NotNull + List lookupEntities( + @NotNull PolarisCallContext callCtx, List entityIds); + + /** + * Lookup in the entities_change_tracking table the current version of an entity given its catalog + * id (which can be NULL_ID for top-level entities) and its unique id. Will return 0 if the entity + * does not exist. + * + * @param callCtx call context + * @param catalogId catalog id or NULL_ID + * @param entityId unique entity id + * @return current version for that entity or 0 if entity was not found. + */ + int lookupEntityVersion(@NotNull PolarisCallContext callCtx, long catalogId, long entityId); + + /** + * Get change tracking versions for all specified entity ids. + * + * @param callCtx call context + * @param entityIds list of entity id + * @return list parallel to the input list of entity versions. If an entity cannot be found, the + * corresponding element in the list will be null + */ + @NotNull + List lookupEntityVersions( + @NotNull PolarisCallContext callCtx, List entityIds); + + /** + * Lookup in the entities_active table to determine if the specified entity exists. Return the + * result of that lookup + * + * @param callCtx call context + * @param entityActiveKey key in the ENTITIES_ACTIVE table + * @return null if the specified entity does not exist or has been dropped. + */ + @Nullable + PolarisEntityActiveRecord lookupEntityActive( + @NotNull PolarisCallContext callCtx, @NotNull PolarisEntitiesActiveKey entityActiveKey); + + /** + * Lookup in the entities_active table to determine if the specified set of entities exist. Return + * the result, a parallel list of active records. A record in that list will be null if its + * associated lookup failed + * + * @return the list of entities_active records for the specified lookup operation + */ + @NotNull + List lookupEntityActiveBatch( + @NotNull PolarisCallContext callCtx, List entityActiveKeys); + + /** + * List all active entities of the specified type which are child entities of the specified parent + * + * @param callCtx call context + * @param catalogId catalog id for that entity, NULL_ID if the entity is top-level + * @param parentId id of the parent, can be the special 0 value representing the root entity + * @param entityType type of entities to list + * @return the list of entities_active records for the specified list operation + */ + @NotNull + List listActiveEntities( + @NotNull PolarisCallContext callCtx, + long catalogId, + long parentId, + @NotNull PolarisEntityType entityType); + + /** + * List active entities where some predicate returns true + * + * @param callCtx call context + * @param catalogId catalog id for that entity, NULL_ID if the entity is top-level + * @param parentId id of the parent, can be the special 0 value representing the root entity + * @param entityType type of entities to list + * @param entityFilter the filter to be applied to each entity. Only entities where the predicate + * returns true are returned in the list + * @return the list of entities for which the predicate returns true + */ + @NotNull + List listActiveEntities( + @NotNull PolarisCallContext callCtx, + long catalogId, + long parentId, + @NotNull PolarisEntityType entityType, + @NotNull Predicate entityFilter); + + /** + * List active entities where some predicate returns true and transform the entities with a + * function + * + * @param callCtx call context + * @param catalogId catalog id for that entity, NULL_ID if the entity is top-level + * @param parentId id of the parent, can be the special 0 value representing the root entity + * @param entityType type of entities to list + * @param limit the max number of items to return + * @param entityFilter the filter to be applied to each entity. Only entities where the predicate + * returns true are returned in the list + * @param transformer the transformation function applied to the {@link PolarisBaseEntity} before + * returning + * @return the list of entities for which the predicate returns true + */ + @NotNull + List listActiveEntities( + @NotNull PolarisCallContext callCtx, + long catalogId, + long parentId, + @NotNull PolarisEntityType entityType, + int limit, + @NotNull Predicate entityFilter, + @NotNull Function transformer); + + /** + * Lookup in the entities_change_tracking table the current version of the grant records for this + * entity. That version is changed everytime a grant record is added or removed on a base + * securable or added to a grantee. + * + * @param callCtx call context + * @param catalogId catalog id or NULL_ID + * @param entityId unique entity id + * @return current grant records version for that entity. + */ + int lookupEntityGrantRecordsVersion( + @NotNull PolarisCallContext callCtx, long catalogId, long entityId); + + /** + * Lookup the specified grant record from the grant_records table. Return NULL if not found + * + * @param callCtx call context + * @param securableCatalogId catalog id of the securable entity, NULL_ID if the entity is + * top-level + * @param securableId id of the securable entity + * @param granteeCatalogId catalog id of the grantee entity, NULL_ID if the entity is top-level + * @param granteeId id of the grantee entity + * @param privilegeCode code for the privilege we are looking up + * @return the grant record if found, NULL if not found + */ + @Nullable + PolarisGrantRecord lookupGrantRecord( + @NotNull PolarisCallContext callCtx, + long securableCatalogId, + long securableId, + long granteeCatalogId, + long granteeId, + int privilegeCode); + + /** + * Get all grant records on the specified securable entity. + * + * @param callCtx call context + * @param securableCatalogId catalog id of the securable entity, NULL_ID if the entity is + * top-level + * @param securableId id of the securable entity + * @return the list of grant records for the specified securable + */ + @NotNull + List loadAllGrantRecordsOnSecurable( + @NotNull PolarisCallContext callCtx, long securableCatalogId, long securableId); + + /** + * Get all grant records granted to the specified grantee entity. + * + * @param callCtx call context + * @param granteeCatalogId catalog id of the grantee entity, NULL_ID if the entity is top-level + * @param granteeId id of the grantee entity + * @return the list of grant records for the specified grantee + */ + @NotNull + List loadAllGrantRecordsOnGrantee( + @NotNull PolarisCallContext callCtx, long granteeCatalogId, long granteeId); + + /** + * Allows to retrieve to the secrets of a principal given its unique client id + * + * @param callCtx call context + * @param clientId principal client id + * @return the secrets + */ + @Nullable + PolarisPrincipalSecrets loadPrincipalSecrets( + @NotNull PolarisCallContext callCtx, @NotNull String clientId); + + /** + * generate and store a client id and associated secrets for a newly created principal entity + * + * @param callCtx call context + * @param principalName name of the principal + * @param principalId principal id + */ + @NotNull + PolarisPrincipalSecrets generateNewPrincipalSecrets( + @NotNull PolarisCallContext callCtx, @NotNull String principalName, long principalId); + + /** + * Rotate the secrets of a principal entity, i.e. make the specified main secrets the secondary + * and generate a new main secret + * + * @param callCtx call context + * @param clientId principal client id + * @param principalId principal id + * @param mainSecretToRotate main secret for comparison with the current entity version + * @param reset true if the principal secrets should be disabled and replaced with a one-time + * password + */ + @Nullable + PolarisPrincipalSecrets rotatePrincipalSecrets( + @NotNull PolarisCallContext callCtx, + @NotNull String clientId, + long principalId, + @NotNull String mainSecretToRotate, + boolean reset); + + /** + * When dropping a principal, we also need to drop the secrets of that principal + * + * @param callCtx the call context + * @param clientId principal client id + * @param principalId the id of the principal whose secrets are dropped + */ + void deletePrincipalSecrets( + @NotNull PolarisCallContext callCtx, @NotNull String clientId, long principalId); + + /** + * Create an in-memory storage integration + * + * @param callCtx the polaris calllctx + * @param catalogId the catalog id + * @param entityId the entity id + * @param polarisStorageConfigurationInfo the storage configuration information + * @return a storage integration object + */ + @Nullable + PolarisStorageIntegration createStorageIntegration( + @NotNull PolarisCallContext callCtx, + long catalogId, + long entityId, + PolarisStorageConfigurationInfo polarisStorageConfigurationInfo); + + /** + * Persist a storage integration in the metastore + * + * @param callContext the polaris call context + * @param entity the entity of the object + * @param storageIntegration the storage integration to persist + */ + void persistStorageIntegrationIfNeeded( + @NotNull PolarisCallContext callContext, + @NotNull PolarisBaseEntity entity, + @Nullable PolarisStorageIntegration storageIntegration); + + /** + * Load the polaris storage integration for a polaris entity (Catalog,Namespace,Table,View) + * + * @param callContext the polaris call context + * @param entity the polaris entity + * @return a polaris storage integration + */ + @Nullable + + PolarisStorageIntegration loadPolarisStorageIntegration( + @NotNull PolarisCallContext callContext, @NotNull PolarisBaseEntity entity); + + /** + * Check if the specified parent entity has children. + * + * @param callContext the polaris call context + * @param optionalEntityType if not null, only check for the specified type, else check for all + * types of children entities + * @param catalogId id of the catalog + * @param parentId id of the parent, either a namespace or a catalog + * @return true if the parent entity has children + */ + boolean hasChildren( + @NotNull PolarisCallContext callContext, + @Nullable PolarisEntityType optionalEntityType, + long catalogId, + long parentId); + + /** Rollback the current transaction */ + void rollback(); +} diff --git a/polaris-core/src/main/java/io/polaris/core/persistence/PolarisObjectMapperUtil.java b/polaris-core/src/main/java/io/polaris/core/persistence/PolarisObjectMapperUtil.java new file mode 100644 index 0000000000..41aea3c205 --- /dev/null +++ b/polaris-core/src/main/java/io/polaris/core/persistence/PolarisObjectMapperUtil.java @@ -0,0 +1,170 @@ +package io.polaris.core.persistence; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.polaris.core.PolarisCallContext; +import io.polaris.core.entity.PolarisBaseEntity; +import io.polaris.core.entity.PolarisTaskConstants; +import java.io.IOException; +import java.util.Map; +import org.apache.iceberg.rest.RESTSerializers; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** A mapper to serialize/deserialize polaris objects. */ +public class PolarisObjectMapperUtil { + /** mapper, allows to serialize/deserialize properties to/from JSON */ + private static final ObjectMapper MAPPER = configureMapper(); + + private static ObjectMapper configureMapper() { + ObjectMapper mapper = new ObjectMapper(); + mapper.configure(DeserializationFeature.FAIL_ON_IGNORED_PROPERTIES, false); + RESTSerializers.registerAll(mapper); + return mapper; + } + + /** + * Given the internal property as a map of key/value pairs, serialize it to a String + * + * @param properties a map of key/value pairs + * @return a String, the JSON representation of the map + */ + public static String serializeProperties( + PolarisCallContext callCtx, Map properties) { + + String jsonString = null; + try { + // Deserialize the JSON string to a Map + jsonString = MAPPER.writeValueAsString(properties); + } catch (JsonProcessingException ex) { + callCtx.getDiagServices().fail("got_json_processing_exception", ex.getMessage()); + } + + return jsonString; + } + + public static String serialize(PolarisCallContext callCtx, Object object) { + try { + return MAPPER.writeValueAsString(object); + } catch (JsonProcessingException e) { + callCtx.getDiagServices().fail("got_json_processing_exception", e.getMessage()); + } + return ""; + } + + public static T deserialize(PolarisCallContext callCtx, String text, Class klass) { + try { + return MAPPER.readValue(text, klass); + } catch (JsonProcessingException e) { + callCtx.getDiagServices().fail("got_json_processing_exception", e.getMessage()); + } + return null; + } + + /** + * Given the serialized properties, deserialize those to a Map + * + * @param properties a JSON string representing the set of properties + * @return a Map of string + */ + public static Map deserializeProperties( + PolarisCallContext callCtx, String properties) { + + Map retProperties = null; + try { + // Deserialize the JSON string to a Map + retProperties = MAPPER.readValue(properties, new TypeReference<>() {}); + } catch (JsonMappingException ex) { + callCtx + .getDiagServices() + .fail("got_json_mapping_exception", "properties={}, ex={}", properties, ex); + } catch (JsonProcessingException ex) { + callCtx + .getDiagServices() + .fail("got_json_processing_exception", "properties={}, ex={}", properties, ex); + } + + return retProperties; + } + + static class TaskExecutionState { + final String executor; + final long lastAttemptStartTime; + final int attemptCount; + + TaskExecutionState(String executor, long lastAttemptStartTime, int attemptCount) { + this.executor = executor; + this.lastAttemptStartTime = lastAttemptStartTime; + this.attemptCount = attemptCount; + } + + public String getExecutor() { + return executor; + } + + public long getLastAttemptStartTime() { + return lastAttemptStartTime; + } + + public int getAttemptCount() { + return attemptCount; + } + } + + /** + * Parse a task entity's properties field in order to find the current {@link TaskExecutionState}. + * Avoids parsing most of the data in the properties field, so we can look at just the fields we + * need. + * + * @param entity entity + * @return TaskExecutionState + */ + static @Nullable TaskExecutionState parseTaskState(PolarisBaseEntity entity) { + JsonFactory jfactory = new JsonFactory(); + try (JsonParser jParser = jfactory.createParser(entity.getProperties())) { + String executorId = null; + long lastAttemptStartTime = 0; + int attemptCount = 0; + while (jParser.nextToken() != JsonToken.END_OBJECT) { + if (jParser.getCurrentToken() == JsonToken.FIELD_NAME) { + String fieldName = jParser.currentName(); + if (fieldName.equals(PolarisTaskConstants.LAST_ATTEMPT_EXECUTOR_ID)) { + jParser.nextToken(); + executorId = jParser.getText(); + } else if (fieldName.equals(PolarisTaskConstants.LAST_ATTEMPT_START_TIME)) { + jParser.nextToken(); + lastAttemptStartTime = Long.parseLong(jParser.getText()); + } else if (fieldName.equals(PolarisTaskConstants.ATTEMPT_COUNT)) { + jParser.nextToken(); + attemptCount = Integer.parseInt(jParser.getText()); + } else { + JsonToken next = jParser.nextToken(); + if (next == JsonToken.START_OBJECT || next == JsonToken.START_ARRAY) { + jParser.skipChildren(); + } + } + } + } + return new TaskExecutionState(executorId, lastAttemptStartTime, attemptCount); + } catch (IOException e) { + Logger logger = LoggerFactory.getLogger(PolarisObjectMapperUtil.class); + logger + .atWarn() + .addKeyValue("json", entity.getProperties()) + .addKeyValue("error", e.getMessage()) + .log("Unable to parse task properties"); + return null; + } + } + + long now() { + return 0; + } +} diff --git a/polaris-core/src/main/java/io/polaris/core/persistence/PolarisResolvedPathWrapper.java b/polaris-core/src/main/java/io/polaris/core/persistence/PolarisResolvedPathWrapper.java new file mode 100644 index 0000000000..fb7d408b44 --- /dev/null +++ b/polaris-core/src/main/java/io/polaris/core/persistence/PolarisResolvedPathWrapper.java @@ -0,0 +1,66 @@ +package io.polaris.core.persistence; + +import io.polaris.core.entity.PolarisEntity; +import java.util.List; + +/** + * Holds fully-resolved path of PolarisEntities representing the targetEntity with all its grants + * and grant records. + */ +public class PolarisResolvedPathWrapper { + private final List resolvedPath; + + // TODO: Distinguish between whether parentPath had a null in the chain or whether only + // the leaf element was null. + public PolarisResolvedPathWrapper(List resolvedPath) { + this.resolvedPath = resolvedPath; + } + + public ResolvedPolarisEntity getResolvedLeafEntity() { + if (resolvedPath == null || resolvedPath.isEmpty()) { + return null; + } + return resolvedPath.get(resolvedPath.size() - 1); + } + + public PolarisEntity getRawLeafEntity() { + ResolvedPolarisEntity resolvedEntity = getResolvedLeafEntity(); + if (resolvedEntity != null) { + return resolvedEntity.getEntity(); + } + return null; + } + + public List getResolvedFullPath() { + return resolvedPath; + } + + public List getRawFullPath() { + if (resolvedPath == null) { + return null; + } + return resolvedPath.stream().map(resolved -> resolved.getEntity()).toList(); + } + + public List getResolvedParentPath() { + if (resolvedPath == null) { + return null; + } + return resolvedPath.subList(0, resolvedPath.size() - 1); + } + + public List getRawParentPath() { + if (resolvedPath == null) { + return null; + } + return getResolvedParentPath().stream().map(resolved -> resolved.getEntity()).toList(); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("resolvedPath:"); + sb.append(resolvedPath); + return sb.toString(); + } +} diff --git a/polaris-core/src/main/java/io/polaris/core/persistence/PolarisTreeMapMetaStoreSessionImpl.java b/polaris-core/src/main/java/io/polaris/core/persistence/PolarisTreeMapMetaStoreSessionImpl.java new file mode 100644 index 0000000000..8127b1b612 --- /dev/null +++ b/polaris-core/src/main/java/io/polaris/core/persistence/PolarisTreeMapMetaStoreSessionImpl.java @@ -0,0 +1,553 @@ +package io.polaris.core.persistence; + +import com.google.common.base.Predicates; +import io.polaris.core.PolarisCallContext; +import io.polaris.core.entity.PolarisBaseEntity; +import io.polaris.core.entity.PolarisChangeTrackingVersions; +import io.polaris.core.entity.PolarisEntitiesActiveKey; +import io.polaris.core.entity.PolarisEntityActiveRecord; +import io.polaris.core.entity.PolarisEntityCore; +import io.polaris.core.entity.PolarisEntityId; +import io.polaris.core.entity.PolarisEntityType; +import io.polaris.core.entity.PolarisGrantRecord; +import io.polaris.core.entity.PolarisPrincipalSecrets; +import io.polaris.core.storage.PolarisStorageConfigurationInfo; +import io.polaris.core.storage.PolarisStorageIntegration; +import io.polaris.core.storage.PolarisStorageIntegrationProvider; +import java.util.List; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class PolarisTreeMapMetaStoreSessionImpl implements PolarisMetaStoreSession { + + // the TreeMap store to use + private final PolarisTreeMapStore store; + private final PolarisStorageIntegrationProvider storageIntegrationProvider; + + public PolarisTreeMapMetaStoreSessionImpl( + @NotNull PolarisTreeMapStore store, + @NotNull PolarisStorageIntegrationProvider storageIntegrationProvider) { + + // init store + this.store = store; + this.storageIntegrationProvider = storageIntegrationProvider; + } + + /** {@inheritDoc} */ + @Override + public T runInTransaction( + @NotNull PolarisCallContext callCtx, @NotNull Supplier transactionCode) { + + // run transaction on our underlying store + return store.runInTransaction(callCtx, transactionCode); + } + + /** {@inheritDoc} */ + @Override + public void runActionInTransaction( + @NotNull PolarisCallContext callCtx, @NotNull Runnable transactionCode) { + + // run transaction on our underlying store + store.runActionInTransaction(callCtx, transactionCode); + } + + /** {@inheritDoc} */ + @Override + public T runInReadTransaction( + @NotNull PolarisCallContext callCtx, @NotNull Supplier transactionCode) { + // run transaction on our underlying store + return store.runInReadTransaction(callCtx, transactionCode); + } + + /** {@inheritDoc} */ + @Override + public void runActionInReadTransaction( + @NotNull PolarisCallContext callCtx, @NotNull Runnable transactionCode) { + + // run transaction on our underlying store + store.runActionInReadTransaction(callCtx, transactionCode); + } + + /** + * @return new unique entity identifier + */ + public long generateNewId(@NotNull PolarisCallContext callCtx) { + return this.store.getNextSequence(); + } + + /** {@inheritDoc} */ + @Override + public void writeToEntities( + @NotNull PolarisCallContext callCtx, @NotNull PolarisBaseEntity entity) { + // write it + this.store.getSliceEntities().write(entity); + } + + /** {@inheritDoc} */ + @Override + public void persistStorageIntegrationIfNeeded( + @NotNull PolarisCallContext callContext, + @NotNull PolarisBaseEntity entity, + @Nullable PolarisStorageIntegration storageIntegration) { + // not implemented for in-memory store + } + + /** {@inheritDoc} */ + @Override + public void writeToEntitiesActive( + @NotNull PolarisCallContext callCtx, @NotNull PolarisBaseEntity entity) { + // write it + this.store.getSliceEntitiesActive().write(entity); + } + + /** {@inheritDoc} */ + @Override + public void writeToEntitiesDropped( + @NotNull PolarisCallContext callCtx, @NotNull PolarisBaseEntity entity) { + // write it + this.store.getSliceEntitiesDropped().write(entity); + this.store.getSliceEntitiesDroppedToPurge().write(entity); + } + + /** {@inheritDoc} */ + @Override + public void writeToEntitiesChangeTracking( + @NotNull PolarisCallContext callCtx, @NotNull PolarisBaseEntity entity) { + // write it + this.store.getSliceEntitiesChangeTracking().write(entity); + } + + /** {@inheritDoc} */ + @Override + public void writeToGrantRecords( + @NotNull PolarisCallContext callCtx, @NotNull PolarisGrantRecord grantRec) { + // write it + this.store.getSliceGrantRecords().write(grantRec); + this.store.getSliceGrantRecordsByGrantee().write(grantRec); + } + + /** {@inheritDoc} */ + @Override + public void deleteFromEntities( + @NotNull PolarisCallContext callCtx, @NotNull PolarisEntityCore entity) { + + // delete it + this.store.getSliceEntities().delete(this.store.buildEntitiesKey(entity)); + } + + /** {@inheritDoc} */ + @Override + public void deleteFromEntitiesActive( + @NotNull PolarisCallContext callCtx, @NotNull PolarisEntityCore entity) { + // delete it + this.store.getSliceEntitiesActive().delete(this.store.buildEntitiesActiveKey(entity)); + } + + /** {@inheritDoc} */ + @Override + public void deleteFromEntitiesDropped( + @NotNull PolarisCallContext callCtx, @NotNull PolarisBaseEntity entity) { + // delete it + this.store.getSliceEntitiesDropped().delete(entity); + this.store.getSliceEntitiesDroppedToPurge().delete(entity); + } + + /** + * {@inheritDoc} + * + * @param callCtx + * @param entity entity record to delete + */ + @Override + public void deleteFromEntitiesChangeTracking( + @NotNull PolarisCallContext callCtx, @NotNull PolarisEntityCore entity) { + // delete it + this.store.getSliceEntitiesChangeTracking().delete(this.store.buildEntitiesKey(entity)); + } + + /** {@inheritDoc} */ + @Override + public void deleteFromGrantRecords( + @NotNull PolarisCallContext callCtx, @NotNull PolarisGrantRecord grantRec) { + + // delete it + this.store.getSliceGrantRecords().delete(grantRec); + this.store.getSliceGrantRecordsByGrantee().delete(grantRec); + } + + /** {@inheritDoc} */ + @Override + public void deleteAllEntityGrantRecords( + @NotNull PolarisCallContext callCtx, + @NotNull PolarisEntityCore entity, + @NotNull List grantsOnGrantee, + @NotNull List grantsOnSecurable) { + + // build composite prefix key and delete grant records on the indexed side of each grant table + String prefix = this.store.buildPrefixKeyComposite(entity.getCatalogId(), entity.getId()); + this.store.getSliceGrantRecords().deleteRange(prefix); + this.store.getSliceGrantRecordsByGrantee().deleteRange(prefix); + + // also delete the other side. We need to delete these grants one at a time versus doing a + // range delete + grantsOnGrantee.forEach(gr -> this.store.getSliceGrantRecords().delete(gr)); + grantsOnSecurable.forEach(gr -> this.store.getSliceGrantRecordsByGrantee().delete(gr)); + } + + /** {@inheritDoc} */ + @Override + public void deleteAll(@NotNull PolarisCallContext callCtx) { + // clear all slices + this.store.deleteAll(); + } + + /** {@inheritDoc} */ + @Override + public @Nullable PolarisBaseEntity lookupEntity( + @NotNull PolarisCallContext callCtx, long catalogId, long entityId) { + return this.store.getSliceEntities().read(this.store.buildKeyComposite(catalogId, entityId)); + } + + /** {@inheritDoc} */ + @Override + public @NotNull List lookupEntities( + @NotNull PolarisCallContext callCtx, List entityIds) { + // allocate return list + return entityIds.stream() + .map( + id -> + this.store + .getSliceEntities() + .read(this.store.buildKeyComposite(id.getCatalogId(), id.getId()))) + .collect(Collectors.toList()); + } + + /** {@inheritDoc} */ + @Override + public int lookupEntityVersion( + @NotNull PolarisCallContext callCtx, long catalogId, long entityId) { + PolarisBaseEntity baseEntity = + this.store + .getSliceEntitiesChangeTracking() + .read(this.store.buildKeyComposite(catalogId, entityId)); + + return baseEntity == null ? 0 : baseEntity.getEntityVersion(); + } + + /** {@inheritDoc} */ + @Override + public @NotNull List lookupEntityVersions( + @NotNull PolarisCallContext callCtx, List entityIds) { + // allocate return list + return entityIds.stream() + .map( + id -> + this.store + .getSliceEntitiesChangeTracking() + .read(this.store.buildKeyComposite(id.getCatalogId(), id.getId()))) + .map( + entity -> + (entity != null) + ? new PolarisChangeTrackingVersions( + entity.getEntityVersion(), entity.getGrantRecordsVersion()) + : null) + .collect(Collectors.toList()); + } + + /** {@inheritDoc} */ + @Override + @Nullable + public PolarisEntityActiveRecord lookupEntityActive( + @NotNull PolarisCallContext callCtx, @NotNull PolarisEntitiesActiveKey entityActiveKey) { + // lookup the active entity slice + PolarisBaseEntity entity = + this.store + .getSliceEntitiesActive() + .read( + this.store.buildKeyComposite( + entityActiveKey.getCatalogId(), + entityActiveKey.getParentId(), + entityActiveKey.getTypeCode(), + entityActiveKey.getName())); + + // return record + return (entity == null) + ? null + : new PolarisEntityActiveRecord( + entity.getCatalogId(), + entity.getId(), + entity.getParentId(), + entity.getName(), + entity.getTypeCode(), + entity.getSubTypeCode()); + } + + /** {@inheritDoc} */ + @Override + @NotNull + public List lookupEntityActiveBatch( + @NotNull PolarisCallContext callCtx, + @NotNull List entityActiveKeys) { + // now build a list to quickly verify that nothing has changed + return entityActiveKeys.stream() + .map(entityActiveKey -> this.lookupEntityActive(callCtx, entityActiveKey)) + .collect(Collectors.toList()); + } + + /** {@inheritDoc} */ + @Override + public @NotNull List listActiveEntities( + @NotNull PolarisCallContext callCtx, + long catalogId, + long parentId, + @NotNull PolarisEntityType entityType) { + return listActiveEntities(callCtx, catalogId, parentId, entityType, Predicates.alwaysTrue()); + } + + @Override + public @NotNull List listActiveEntities( + @NotNull PolarisCallContext callCtx, + long catalogId, + long parentId, + @NotNull PolarisEntityType entityType, + @NotNull Predicate entityFilter) { + // full range scan under the parent for that type + return listActiveEntities( + callCtx, + catalogId, + parentId, + entityType, + Integer.MAX_VALUE, + entityFilter, + entity -> + new PolarisEntityActiveRecord( + entity.getCatalogId(), + entity.getId(), + entity.getParentId(), + entity.getName(), + entity.getTypeCode(), + entity.getSubTypeCode())); + } + + @Override + public @NotNull List listActiveEntities( + @NotNull PolarisCallContext callCtx, + long catalogId, + long parentId, + @NotNull PolarisEntityType entityType, + int limit, + @NotNull Predicate entityFilter, + @NotNull Function transformer) { + // full range scan under the parent for that type + return this.store + .getSliceEntitiesActive() + .readRange(this.store.buildPrefixKeyComposite(catalogId, parentId, entityType.getCode())) + .stream() + .filter(entityFilter) + .limit(limit) + .map(transformer) + .collect(Collectors.toList()); + } + + /** {@inheritDoc} */ + public boolean hasChildren( + @NotNull PolarisCallContext callContext, + @Nullable PolarisEntityType entityType, + long catalogId, + long parentId) { + // determine key prefix, add type if one is passed-in + String prefixKey = + entityType == null + ? this.store.buildPrefixKeyComposite(catalogId, parentId) + : this.store.buildPrefixKeyComposite(catalogId, parentId, entityType.getCode()); + // check if it has children + return !this.store.getSliceEntitiesActive().readRange(prefixKey).isEmpty(); + } + + /** {@inheritDoc} */ + @Override + public int lookupEntityGrantRecordsVersion( + @NotNull PolarisCallContext callCtx, long catalogId, long entityId) { + PolarisBaseEntity entity = + this.store + .getSliceEntitiesChangeTracking() + .read(this.store.buildKeyComposite(catalogId, entityId)); + + // does not exist, 0 + return entity == null ? 0 : entity.getGrantRecordsVersion(); + } + + /** {@inheritDoc} */ + @Override + public @Nullable PolarisGrantRecord lookupGrantRecord( + @NotNull PolarisCallContext callCtx, + long securableCatalogId, + long securableId, + long granteeCatalogId, + long granteeId, + int privilegeCode) { + // lookup the grants records slice to find the usage role + return this.store + .getSliceGrantRecords() + .read( + this.store.buildKeyComposite( + securableCatalogId, securableId, granteeCatalogId, granteeId, privilegeCode)); + } + + /** {@inheritDoc} */ + @Override + public @NotNull List loadAllGrantRecordsOnSecurable( + @NotNull PolarisCallContext callCtx, long securableCatalogId, long securableId) { + // now fetch all grants for this securable + return this.store + .getSliceGrantRecords() + .readRange(this.store.buildPrefixKeyComposite(securableCatalogId, securableId)); + } + + /** {@inheritDoc} */ + @Override + public @NotNull List loadAllGrantRecordsOnGrantee( + @NotNull PolarisCallContext callCtx, long granteeCatalogId, long granteeId) { + // now fetch all grants assigned to this grantee + return this.store + .getSliceGrantRecordsByGrantee() + .readRange(this.store.buildPrefixKeyComposite(granteeCatalogId, granteeId)); + } + + /** {@inheritDoc} */ + @Override + public @Nullable PolarisPrincipalSecrets loadPrincipalSecrets( + @NotNull PolarisCallContext callCtx, @NotNull String clientId) { + return this.store.getSlicePrincipalSecrets().read(clientId); + } + + /** {@inheritDoc} */ + @Override + public @NotNull PolarisPrincipalSecrets generateNewPrincipalSecrets( + @NotNull PolarisCallContext callCtx, @NotNull String principalName, long principalId) { + // ensure principal client id is unique + PolarisPrincipalSecrets principalSecrets; + PolarisPrincipalSecrets lookupPrincipalSecrets; + do { + // generate new random client id and secrets + principalSecrets = new PolarisPrincipalSecrets(principalId); + + // load the existing secrets + lookupPrincipalSecrets = + this.store.getSlicePrincipalSecrets().read(principalSecrets.getPrincipalClientId()); + } while (lookupPrincipalSecrets != null); + + // write new principal secrets + this.store.getSlicePrincipalSecrets().write(principalSecrets); + + // if not found, return null + return principalSecrets; + } + + /** {@inheritDoc} */ + @Override + public @NotNull PolarisPrincipalSecrets rotatePrincipalSecrets( + @NotNull PolarisCallContext callCtx, + @NotNull String clientId, + long principalId, + @NotNull String mainSecretToRotate, + boolean reset) { + + // load the existing secrets + PolarisPrincipalSecrets principalSecrets = this.store.getSlicePrincipalSecrets().read(clientId); + + // should be found + callCtx + .getDiagServices() + .checkNotNull( + principalSecrets, + "cannot_find_secrets", + "client_id={} principalId={}", + clientId, + principalId); + + // ensure principal id is matching + callCtx + .getDiagServices() + .check( + principalId == principalSecrets.getPrincipalId(), + "principal_id_mismatch", + "expectedId={} id={}", + principalId, + principalSecrets.getPrincipalId()); + + // rotate the secrets + principalSecrets.rotateSecrets(mainSecretToRotate); + if (reset) { + principalSecrets.rotateSecrets(principalSecrets.getMainSecret()); + } + + // write back new secrets + this.store.getSlicePrincipalSecrets().write(principalSecrets); + + // return those + return principalSecrets; + } + + /** {@inheritDoc} */ + @Override + public void deletePrincipalSecrets( + @NotNull PolarisCallContext callCtx, @NotNull String clientId, long principalId) { + // load the existing secrets + PolarisPrincipalSecrets principalSecrets = this.store.getSlicePrincipalSecrets().read(clientId); + + // should be found + callCtx + .getDiagServices() + .checkNotNull( + principalSecrets, + "cannot_find_secrets", + "client_id={} principalId={}", + clientId, + principalId); + + // ensure principal id is matching + callCtx + .getDiagServices() + .check( + principalId == principalSecrets.getPrincipalId(), + "principal_id_mismatch", + "expectedId={} id={}", + principalId, + principalSecrets.getPrincipalId()); + + // delete these secrets + this.store.getSlicePrincipalSecrets().delete(clientId); + } + + /** {@inheritDoc} */ + @Override + public @Nullable + PolarisStorageIntegration createStorageIntegration( + @NotNull PolarisCallContext callCtx, + long catalogId, + long entityId, + PolarisStorageConfigurationInfo polarisStorageConfigurationInfo) { + return storageIntegrationProvider.getStorageIntegrationForConfig( + polarisStorageConfigurationInfo); + } + + /** {@inheritDoc} */ + @Override + public @Nullable + PolarisStorageIntegration loadPolarisStorageIntegration( + @NotNull PolarisCallContext callCtx, @NotNull PolarisBaseEntity entity) { + PolarisStorageConfigurationInfo storageConfig = + PolarisMetaStoreManagerImpl.readStorageConfiguration(callCtx, entity); + return storageIntegrationProvider.getStorageIntegrationForConfig(storageConfig); + } + + @Override + public void rollback() { + this.store.rollback(); + } +} diff --git a/polaris-core/src/main/java/io/polaris/core/persistence/PolarisTreeMapStore.java b/polaris-core/src/main/java/io/polaris/core/persistence/PolarisTreeMapStore.java new file mode 100644 index 0000000000..43c7427994 --- /dev/null +++ b/polaris-core/src/main/java/io/polaris/core/persistence/PolarisTreeMapStore.java @@ -0,0 +1,540 @@ +package io.polaris.core.persistence; + +import io.polaris.core.PolarisCallContext; +import io.polaris.core.PolarisDiagnostics; +import io.polaris.core.entity.PolarisBaseEntity; +import io.polaris.core.entity.PolarisEntityCore; +import io.polaris.core.entity.PolarisGrantRecord; +import io.polaris.core.entity.PolarisPrincipalSecrets; +import java.util.ArrayList; +import java.util.List; +import java.util.TreeMap; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Function; +import java.util.function.Supplier; +import org.jetbrains.annotations.NotNull; + +/** Implements a simple in-memory store for Polaris, using tree-map */ +public class PolarisTreeMapStore { + + /** Slice of data, simple KV store. */ + public class Slice { + // main KV slice + private final TreeMap slice; + + // if we need to rollback + private final TreeMap undoSlice; + + // the key builder + private final Function buildKey; + + // the key builder + private final Function copyRecord; + + private Slice(Function buildKey, Function copyRecord) { + this.slice = new TreeMap<>(); + this.undoSlice = new TreeMap<>(); + this.buildKey = buildKey; + this.copyRecord = copyRecord; + } + + public String buildKey(T value) { + return this.buildKey.apply(value); + } + + /** + * read a value in the slice, will return null if not found + * + *

TODO: return a copy of each object to avoid mutating the records + * + * @param key key for that value + */ + public T read(String key) { + PolarisTreeMapStore.this.ensureReadTr(); + T value = this.slice.getOrDefault(key, null); + return (value != null) ? this.copyRecord.apply(value) : null; + } + + /** + * read a range of values in the slice corresponding to a key prefix + * + * @param prefix key prefix + */ + public List readRange(String prefix) { + PolarisTreeMapStore.this.ensureReadTr(); + // end of the key + String endKey = + prefix.substring(0, prefix.length() - 1) + + (char) (prefix.charAt(prefix.length() - 1) + 1); + + // Get the sub-map with keys in the range [prefix, endKey) + return new ArrayList<>(slice.subMap(prefix, true, endKey, false).values()); + } + + /** + * write a value in the slice + * + * @param value value to write + */ + public void write(T value) { + PolarisTreeMapStore.this.ensureReadWriteTr(); + T valueToWrite = (value != null) ? this.copyRecord.apply(value) : null; + String key = this.buildKey(valueToWrite); + // write undo if needs be + if (!this.undoSlice.containsKey(key)) { + this.undoSlice.put(key, this.slice.getOrDefault(key, null)); + } + this.slice.put(key, valueToWrite); + } + + /** + * delete the specified record from the slice + * + * @param key key for the record to remove + */ + public void delete(String key) { + PolarisTreeMapStore.this.ensureReadWriteTr(); + if (slice.containsKey(key)) { + // write undo if needs be + if (!this.undoSlice.containsKey(key)) { + this.undoSlice.put(key, this.slice.getOrDefault(key, null)); + } + this.slice.remove(key); + } + } + + /** + * delete range of values + * + * @param prefix key prefix for the record to remove + */ + public void deleteRange(String prefix) { + PolarisTreeMapStore.this.ensureReadWriteTr(); + List elements = this.readRange(prefix); + for (T element : elements) { + this.delete(element); + } + } + + void deleteAll() { + PolarisTreeMapStore.this.ensureReadWriteTr(); + slice.clear(); + undoSlice.clear(); + } + + /** + * delete the specified record from the slice + * + * @param value value to remove + */ + public void delete(T value) { + this.delete(this.buildKey(value)); + } + + /** Rollback all changes made to this slice since transaction started */ + private void rollback() { + PolarisTreeMapStore.this.ensureReadWriteTr(); + undoSlice.forEach( + (key, value) -> { + if (value == null) { + slice.remove(key); + } else { + slice.put(key, value); + } + }); + } + + private void startWriteTransaction() { + undoSlice.clear(); + } + } + + /** Transaction on the tree-map store */ + private static class Transaction { + // if true, we have open a read/write transaction + private final boolean isWrite; + + /** Constructor */ + private Transaction(boolean isWrite) { + this.isWrite = isWrite; + } + + public boolean isWrite() { + return isWrite; + } + } + + // synchronization lock to ensure that only one transaction can be started + private final Object lock; + + // transaction which was started, will be null if no transaction started + private Transaction tr; + + // diagnostic services + private PolarisDiagnostics diagnosticServices; + + // all entities + private final Slice sliceEntities; + + // all entities + private final Slice sliceEntitiesActive; + + // all entities dropped + private final Slice sliceEntitiesDropped; + + // all entities dropped + private final Slice sliceEntitiesDroppedToPurge; + + // all entities dropped + private final Slice sliceEntitiesChangeTracking; + + // all grant records indexed by securable + private final Slice sliceGrantRecords; + + // all grant records indexed by grantees + private final Slice sliceGrantRecordsByGrantee; + + // slice to store principal secrets + private final Slice slicePrincipalSecrets; + + // next id generator + private final AtomicLong nextId = new AtomicLong(); + + /** + * Constructor, allocate everything at once + * + * @param diagnostics diagnostic services + */ + public PolarisTreeMapStore(@NotNull PolarisDiagnostics diagnostics) { + + // the entities slice + this.sliceEntities = + new Slice<>( + entity -> String.format("%d::%d", entity.getCatalogId(), entity.getId()), + PolarisBaseEntity::new); + + // the entities active slice + this.sliceEntitiesActive = new Slice<>(this::buildEntitiesActiveKey, PolarisBaseEntity::new); + + // the entities active slice + this.sliceEntitiesDropped = + new Slice<>( + entity -> + String.format( + "%d::%d::%s::%d::%d::%d", + entity.getCatalogId(), + entity.getParentId(), + entity.getName(), + entity.getTypeCode(), + entity.getSubTypeCode(), + entity.getDropTimestamp()), + PolarisBaseEntity::new); + + // the entities active slice + this.sliceEntitiesDroppedToPurge = + new Slice<>( + entity -> + String.format( + "%d::%d::%s", + entity.getToPurgeTimestamp(), entity.getCatalogId(), entity.getId()), + PolarisBaseEntity::new); + + // change tracking + this.sliceEntitiesChangeTracking = + new Slice<>( + entity -> String.format("%d::%d", entity.getCatalogId(), entity.getId()), + PolarisBaseEntity::new); + + // grant records by securable + this.sliceGrantRecords = + new Slice<>( + grantRecord -> + String.format( + "%d::%d::%d::%d::%d", + grantRecord.getSecurableCatalogId(), + grantRecord.getSecurableId(), + grantRecord.getGranteeCatalogId(), + grantRecord.getGranteeId(), + grantRecord.getPrivilegeCode()), + PolarisGrantRecord::new); + + // grant records by securable + this.sliceGrantRecordsByGrantee = + new Slice<>( + grantRecord -> + String.format( + "%d::%d::%d::%d::%d", + grantRecord.getGranteeCatalogId(), + grantRecord.getGranteeId(), + grantRecord.getSecurableCatalogId(), + grantRecord.getSecurableId(), + grantRecord.getPrivilegeCode()), + PolarisGrantRecord::new); + + // principal secrets + slicePrincipalSecrets = + new Slice<>( + principalSecrets -> String.format("%s", principalSecrets.getPrincipalClientId()), + PolarisPrincipalSecrets::new); + + // no transaction open yet + this.diagnosticServices = diagnostics; + this.tr = null; + this.lock = new Object(); + } + + /** + * Key for the entities_active slice + * + * @param coreEntity core entity + * @return the key + */ + String buildEntitiesActiveKey(PolarisEntityCore coreEntity) { + return String.format( + "%d::%d::%d::%s", + coreEntity.getCatalogId(), + coreEntity.getParentId(), + coreEntity.getTypeCode(), + coreEntity.getName()); + } + + /** + * Key for the entities slice + * + * @param coreEntity core entity + * @return the key + */ + String buildEntitiesKey(PolarisEntityCore coreEntity) { + return String.format("%d::%d", coreEntity.getCatalogId(), coreEntity.getId()); + } + + /** + * Build key from a set of value pairs + * + * @param keys string/long/integer values + * @return unique string identifier + */ + String buildKeyComposite(Object... keys) { + StringBuilder result = new StringBuilder(); + for (Object key : keys) { + if (result.length() != 0) { + result.append("::"); + } + result.append(key.toString()); + } + return result.toString(); + } + + /** + * Build prefix key from a set of value pairs; prefix key will end with the key separator + * + * @param keys string/long/integer values + * @return unique string identifier + */ + String buildPrefixKeyComposite(Object... keys) { + StringBuilder result = new StringBuilder(); + for (Object key : keys) { + result.append(key.toString()); + result.append("::"); + } + return result.toString(); + } + + /** Start a read transaction */ + private void startReadTransaction() { + this.diagnosticServices.check(this.tr == null, "cannot nest transaction"); + this.tr = new Transaction(false); + } + + /** Start a write transaction */ + private void startWriteTransaction() { + this.diagnosticServices.check(this.tr == null, "cannot nest transaction"); + this.tr = new Transaction(true); + this.sliceEntities.startWriteTransaction(); + this.sliceEntitiesActive.startWriteTransaction(); + this.sliceEntitiesDropped.startWriteTransaction(); + this.sliceEntitiesDroppedToPurge.startWriteTransaction(); + this.sliceEntitiesChangeTracking.startWriteTransaction(); + this.sliceGrantRecords.startWriteTransaction(); + this.sliceGrantRecordsByGrantee.startWriteTransaction(); + this.slicePrincipalSecrets.startWriteTransaction(); + } + + /** Rollback transaction */ + void rollback() { + this.sliceEntities.rollback(); + this.sliceEntitiesActive.rollback(); + this.sliceEntitiesDropped.rollback(); + this.sliceEntitiesDroppedToPurge.rollback(); + this.sliceEntitiesChangeTracking.rollback(); + this.sliceGrantRecords.rollback(); + this.sliceGrantRecordsByGrantee.rollback(); + this.slicePrincipalSecrets.rollback(); + } + + /** Ensure that a read/write FDB transaction has been started */ + public void ensureReadWriteTr() { + this.diagnosticServices.check( + this.tr != null && this.tr.isWrite(), "no_write_transaction_started"); + } + + /** Ensure that a read FDB transaction has been started */ + private void ensureReadTr() { + this.diagnosticServices.checkNotNull(this.tr, "no_read_transaction_started"); + } + + /** + * Run inside a read/write transaction + * + * @param callCtx call context to use + * @param transactionCode transaction code + * @return the result of the execution + */ + public T runInTransaction( + @NotNull PolarisCallContext callCtx, @NotNull Supplier transactionCode) { + + synchronized (lock) { + // execute transaction + try { + // init diagnostic services + this.diagnosticServices = callCtx.getDiagServices(); + this.startWriteTransaction(); + return transactionCode.get(); + } catch (Throwable e) { + this.rollback(); + throw e; + } finally { + this.tr = null; + this.diagnosticServices = null; + } + } + } + + /** + * Run inside a read/write transaction + * + * @param callCtx call context to use + * @param transactionCode transaction code + */ + public void runActionInTransaction( + @NotNull PolarisCallContext callCtx, @NotNull Runnable transactionCode) { + + synchronized (lock) { + + // execute transaction + try { + // init diagnostic services + this.diagnosticServices = callCtx.getDiagServices(); + this.startWriteTransaction(); + transactionCode.run(); + } catch (Throwable e) { + this.rollback(); + throw e; + } finally { + this.tr = null; + this.diagnosticServices = null; + } + } + } + + /** + * Run inside a read only transaction + * + * @param callCtx call context to use + * @param transactionCode transaction code + * @return the result of the execution + */ + public T runInReadTransaction( + @NotNull PolarisCallContext callCtx, @NotNull Supplier transactionCode) { + synchronized (lock) { + + // execute transaction + try { + // init diagnostic services + this.diagnosticServices = callCtx.getDiagServices(); + this.startReadTransaction(); + return transactionCode.get(); + } finally { + this.tr = null; + this.diagnosticServices = null; + } + } + } + + /** + * Run inside a read only transaction + * + * @param callCtx call context to use + * @param transactionCode transaction code + */ + public void runActionInReadTransaction( + @NotNull PolarisCallContext callCtx, @NotNull Runnable transactionCode) { + synchronized (lock) { + + // execute transaction + try { + // init diagnostic services + this.diagnosticServices = callCtx.getDiagServices(); + this.startReadTransaction(); + transactionCode.run(); + } finally { + this.tr = null; + this.diagnosticServices = null; + } + } + } + + public Slice getSliceEntities() { + return sliceEntities; + } + + public Slice getSliceEntitiesActive() { + return sliceEntitiesActive; + } + + public Slice getSliceEntitiesDropped() { + return sliceEntitiesDropped; + } + + public Slice getSliceEntitiesDroppedToPurge() { + return sliceEntitiesDroppedToPurge; + } + + public Slice getSliceEntitiesChangeTracking() { + return sliceEntitiesChangeTracking; + } + + public Slice getSliceGrantRecords() { + return sliceGrantRecords; + } + + public Slice getSliceGrantRecordsByGrantee() { + return sliceGrantRecordsByGrantee; + } + + public Slice getSlicePrincipalSecrets() { + return slicePrincipalSecrets; + } + + /** + * Next sequence number generator + * + * @return next id, must be in a read/write transaction + */ + public long getNextSequence() { + return this.nextId.incrementAndGet(); + } + + /** Clear all slices from data */ + void deleteAll() { + this.ensureReadWriteTr(); + this.sliceEntities.deleteAll(); + this.sliceEntitiesActive.deleteAll(); + this.sliceEntitiesDropped.deleteAll(); + this.sliceEntitiesDroppedToPurge.deleteAll(); + this.sliceEntitiesChangeTracking.deleteAll(); + this.sliceGrantRecordsByGrantee.deleteAll(); + this.sliceGrantRecords.deleteAll(); + this.slicePrincipalSecrets.deleteAll(); + } +} diff --git a/polaris-core/src/main/java/io/polaris/core/persistence/ResolvedPolarisEntity.java b/polaris-core/src/main/java/io/polaris/core/persistence/ResolvedPolarisEntity.java new file mode 100644 index 0000000000..bf451d4679 --- /dev/null +++ b/polaris-core/src/main/java/io/polaris/core/persistence/ResolvedPolarisEntity.java @@ -0,0 +1,63 @@ +package io.polaris.core.persistence; + +import com.google.common.collect.ImmutableList; +import io.polaris.core.entity.PolarisEntity; +import io.polaris.core.entity.PolarisGrantRecord; +import io.polaris.core.persistence.cache.EntityCacheEntry; +import java.util.List; + +public class ResolvedPolarisEntity { + private final PolarisEntity entity; + + // only non-empty if this entity can be a grantee; these are the grants on other + // roles/securables granted to this entity. + private final List grantRecordsAsGrantee; + + // grants associated to this entity as the securable; for a principal role or catalog role + // these may be ROLE_USAGE or other permission-management privileges. For a catalog securable, + // these are the grants like TABLE_READ_PROPERTIES, NAMESPACE_LIST, etc. + private final List grantRecordsAsSecurable; + + public ResolvedPolarisEntity( + PolarisEntity entity, + List grantRecordsAsGrantee, + List grantRecordsAsSecurable) { + this.entity = entity; + // TODO: Precondition checks that grantee or securable ids in grant records match entity as + // expected. + this.grantRecordsAsGrantee = grantRecordsAsGrantee; + this.grantRecordsAsSecurable = grantRecordsAsSecurable; + } + + public ResolvedPolarisEntity(EntityCacheEntry cacheEntry) { + this.entity = PolarisEntity.of(cacheEntry.getEntity()); + this.grantRecordsAsGrantee = ImmutableList.copyOf(cacheEntry.getGrantRecordsAsGrantee()); + this.grantRecordsAsSecurable = ImmutableList.copyOf(cacheEntry.getGrantRecordsAsSecurable()); + } + + public PolarisEntity getEntity() { + return entity; + } + + /** The grant records associated with this entity being the grantee of the record. */ + public List getGrantRecordsAsGrantee() { + return grantRecordsAsGrantee; + } + + /** The grant records associated with this entity being the securable of the record. */ + public List getGrantRecordsAsSecurable() { + return grantRecordsAsSecurable; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("entity:"); + sb.append(entity); + sb.append(";grantRecordsAsGrantee:"); + sb.append(grantRecordsAsGrantee); + sb.append(";grantRecordsAsSecurable:"); + sb.append(grantRecordsAsSecurable); + return sb.toString(); + } +} diff --git a/polaris-core/src/main/java/io/polaris/core/persistence/RetryOnConcurrencyException.java b/polaris-core/src/main/java/io/polaris/core/persistence/RetryOnConcurrencyException.java new file mode 100644 index 0000000000..f5a6187742 --- /dev/null +++ b/polaris-core/src/main/java/io/polaris/core/persistence/RetryOnConcurrencyException.java @@ -0,0 +1,20 @@ +package io.polaris.core.persistence; + +import com.google.errorprone.annotations.FormatMethod; + +/** Exception raised when the data is accessed concurrently with conflict. */ +public class RetryOnConcurrencyException extends RuntimeException { + @FormatMethod + public RetryOnConcurrencyException(String message, Object... args) { + super(String.format(message, args)); + } + + @FormatMethod + public RetryOnConcurrencyException(Throwable cause, String message, Object... args) { + super(String.format(message, args), cause); + } + + public RetryOnConcurrencyException(Throwable cause) { + super(cause); + } +} diff --git a/polaris-core/src/main/java/io/polaris/core/persistence/cache/EntityCache.java b/polaris-core/src/main/java/io/polaris/core/persistence/cache/EntityCache.java new file mode 100644 index 0000000000..eaf6f6e75f --- /dev/null +++ b/polaris-core/src/main/java/io/polaris/core/persistence/cache/EntityCache.java @@ -0,0 +1,452 @@ +package io.polaris.core.persistence.cache; + +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.RemovalListener; +import io.polaris.core.PolarisCallContext; +import io.polaris.core.entity.PolarisBaseEntity; +import io.polaris.core.entity.PolarisEntityType; +import io.polaris.core.entity.PolarisGrantRecord; +import io.polaris.core.persistence.PolarisMetaStoreManager; +import java.util.AbstractMap; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** The entity cache, can be private or shared */ +public class EntityCache { + + // cache mode + private EntityCacheMode cacheMode; + + // the meta store manager + private final PolarisMetaStoreManager metaStoreManager; + + // Caffeine cache to keep entries by id + private final Cache byId; + + // index by name + private final AbstractMap byName; + + /** + * Constructor. Cache can be private or shared + * + * @param metaStoreManager the meta store manager implementation + */ + public EntityCache(@NotNull PolarisMetaStoreManager metaStoreManager) { + + // by name cache + this.byName = new ConcurrentHashMap<>(); + + // When an entry is removed, we simply remove it from the byName map + RemovalListener removalListener = + (key, value, cause) -> { + if (value != null) { + // compute name key + EntityCacheByNameKey nameKey = new EntityCacheByNameKey(value.getEntity()); + + // if it is still active, remove it from the name key + this.byName.remove(nameKey, value); + } + }; + + // use a Caffeine cache to purge entries when those have not been used for a long time. + // Assuming 1KB per entry, 100K entries is about 100MB. + this.byId = + Caffeine.newBuilder() + .maximumSize(100_000) // Set maximum size to 100,000 elements + .expireAfterAccess(1, TimeUnit.HOURS) // Expire entries after 1 hour of no access + .removalListener(removalListener) // Set the removal listener + .build(); + + // remember the meta store manager + this.metaStoreManager = metaStoreManager; + + // enabled by default + this.cacheMode = EntityCacheMode.ENABLE; + } + + /** + * Remove the specified cache entry from the cache + * + * @param cacheEntry cache entry to remove + */ + public void removeCacheEntry(@NotNull EntityCacheEntry cacheEntry) { + // compute name key + EntityCacheByNameKey nameKey = new EntityCacheByNameKey(cacheEntry.getEntity()); + + // remove this old entry, this will immediately remove the named entry + this.byId.asMap().remove(cacheEntry.getEntity().getId(), cacheEntry); + + // remove it from the name key + this.byName.remove(nameKey, cacheEntry); + } + + /** + * Cache new entry + * + * @param cacheEntry new cache entry + */ + private void cacheNewEntry(@NotNull EntityCacheEntry cacheEntry) { + + // compute name key + EntityCacheByNameKey nameKey = new EntityCacheByNameKey(cacheEntry.getEntity()); + + // get old value if one exist + EntityCacheEntry oldCacheEntry = this.byId.getIfPresent(cacheEntry.getEntity().getId()); + + // put new entry, only if really newer one + this.byId + .asMap() + .merge( + cacheEntry.getEntity().getId(), + cacheEntry, + (oldValue, newValue) -> this.isNewer(newValue, oldValue) ? newValue : oldValue); + + // only update the name key if this entity was not dropped + if (!cacheEntry.getEntity().isDropped()) { + // here we don't really care about concurrent update to the key. Basically if we are + // pointing to the wrong entry, we will detect this and fix the issue + this.byName.put(nameKey, cacheEntry); + } + + // remove old name if it has changed + if (oldCacheEntry != null) { + // old name + EntityCacheByNameKey oldNameKey = new EntityCacheByNameKey(oldCacheEntry.getEntity()); + if (!oldNameKey.equals(nameKey)) { + this.byName.remove(oldNameKey, oldCacheEntry); + } + } + } + + /** + * Determine if the newer value is really newer + * + * @param newValue new cache entry + * @param oldValue old cache entry + * @return true if the newer cache entry + */ + private boolean isNewer(EntityCacheEntry newValue, EntityCacheEntry oldValue) { + return (newValue.getEntity().getEntityVersion() > oldValue.getEntity().getEntityVersion() + || newValue.getEntity().getGrantRecordsVersion() + > oldValue.getEntity().getGrantRecordsVersion()); + } + + /** + * Replace an old entry with a new one + * + * @param oldCacheEntry old entry + * @param newCacheEntry new entry + */ + private void replaceCacheEntry( + @Nullable EntityCacheEntry oldCacheEntry, @NotNull EntityCacheEntry newCacheEntry) { + + // need to remove old? + if (oldCacheEntry != null) { + // only replace if there is a difference + if (this.entityNameKeyMismatch(oldCacheEntry.getEntity(), newCacheEntry.getEntity()) + || oldCacheEntry.getEntity().getEntityVersion() + < newCacheEntry.getEntity().getEntityVersion() + || oldCacheEntry.getEntity().getGrantRecordsVersion() + < newCacheEntry.getEntity().getGrantRecordsVersion()) { + // write new one + this.cacheNewEntry(newCacheEntry); + + // delete the old one assuming it has not been replaced by the above new entry + this.removeCacheEntry(oldCacheEntry); + } else { + oldCacheEntry.updateLastAccess(); + } + } else { + // write new one + this.cacheNewEntry(newCacheEntry); + } + } + + /** + * Check if two entities have different cache keys (either by id or by name) + * + * @param entity the entity + * @param otherEntity the other entity + * @return true if there is a mismatch + */ + private boolean entityNameKeyMismatch( + @NotNull PolarisBaseEntity entity, @NotNull PolarisBaseEntity otherEntity) { + return entity.getId() != otherEntity.getId() + || entity.getParentId() != otherEntity.getParentId() + || !entity.getName().equals(otherEntity.getName()) + || entity.getTypeCode() != otherEntity.getTypeCode(); + } + + /** + * Get the current cache mode + * + * @return the cache mode + */ + public EntityCacheMode getCacheMode() { + return cacheMode; + } + + /** + * Allows to change the caching mode for testing + * + * @param cacheMode the cache mode + */ + public void setCacheMode(EntityCacheMode cacheMode) { + this.cacheMode = cacheMode; + } + + /** + * Get a cache entity entry given the id of the entity + * + * @param entityId entity id + * @return the cache entry or null if not found + */ + public @Nullable EntityCacheEntry getEntityById(long entityId) { + return byId.getIfPresent(entityId); + } + + /** + * Get a cache entity entry given the name key of the entity + * + * @param entityNameKey entity name key + * @return the cache entry or null if not found + */ + public @Nullable EntityCacheEntry getEntityByName(@NotNull EntityCacheByNameKey entityNameKey) { + return byName.get(entityNameKey); + } + + /** + * Refresh the cache if needs be with a version of the entity/grant records matching the minimum + * specified version. + * + * @param callContext the Polaris call context + * @param entityToValidate copy of the entity held by the caller to validate + * @param entityMinVersion minimum expected version. Should be reloaded if found in a cache with a + * version less than this one + * @param entityGrantRecordsMinVersion minimum grant records version which is expected, grants + * records should be reloaded if needed + * @return the cache entry for the entity or null if the specified entity does not exist + */ + public @Nullable EntityCacheEntry getAndRefreshIfNeeded( + @NotNull PolarisCallContext callContext, + @NotNull PolarisBaseEntity entityToValidate, + int entityMinVersion, + int entityGrantRecordsMinVersion) { + long entityCatalogId = entityToValidate.getCatalogId(); + long entityId = entityToValidate.getId(); + PolarisEntityType entityType = entityToValidate.getType(); + + // first lookup the cache to find the existing cache entry + EntityCacheEntry existingCacheEntry = this.getEntityById(entityId); + + // the caller's fetched entity may have come from a stale lookup byName; we should consider + // the existingCacheEntry to be the older of the two for purposes of invalidation to make + // sure when we replaceCacheEntry we're also removing the old name if it's no longer valid + EntityCacheByNameKey nameKey = new EntityCacheByNameKey(entityToValidate); + EntityCacheEntry existingCacheEntryByName = this.getEntityByName(nameKey); + if (existingCacheEntryByName != null + && existingCacheEntry != null + && isNewer(existingCacheEntry, existingCacheEntryByName)) { + existingCacheEntry = existingCacheEntryByName; + } + + // the new one to be returned + final EntityCacheEntry newCacheEntry; + + // see if we need to load or refresh that entity + if (existingCacheEntry == null + || existingCacheEntry.getEntity().getEntityVersion() < entityMinVersion + || existingCacheEntry.getEntity().getGrantRecordsVersion() < entityGrantRecordsMinVersion) { + + // the refreshed entity + final PolarisMetaStoreManager.CachedEntryResult refreshedCacheEntry; + + // was not found in the cache? + final PolarisBaseEntity entity; + final List grantRecords; + final int grantRecordsVersion; + if (existingCacheEntry == null) { + // try to load it + refreshedCacheEntry = + this.metaStoreManager.loadCachedEntryById(callContext, entityCatalogId, entityId); + if (refreshedCacheEntry.isSuccess()) { + entity = refreshedCacheEntry.getEntity(); + grantRecords = refreshedCacheEntry.getEntityGrantRecords(); + grantRecordsVersion = refreshedCacheEntry.getGrantRecordsVersion(); + } else { + return null; + } + } else { + // refresh it + refreshedCacheEntry = + this.metaStoreManager.refreshCachedEntity( + callContext, + existingCacheEntry.getEntity().getEntityVersion(), + existingCacheEntry.getEntity().getGrantRecordsVersion(), + entityType, + entityCatalogId, + entityId); + if (refreshedCacheEntry.isSuccess()) { + entity = + (refreshedCacheEntry.getEntity() != null) + ? refreshedCacheEntry.getEntity() + : existingCacheEntry.getEntity(); + if (refreshedCacheEntry.getEntityGrantRecords() != null) { + grantRecords = refreshedCacheEntry.getEntityGrantRecords(); + grantRecordsVersion = refreshedCacheEntry.getGrantRecordsVersion(); + } else { + grantRecords = existingCacheEntry.getAllGrantRecords(); + grantRecordsVersion = existingCacheEntry.getEntity().getGrantRecordsVersion(); + } + } else { + // entity has been purged, remove it + this.removeCacheEntry(existingCacheEntry); + return null; + } + } + + // assert that entity, grant records and version are all set + callContext.getDiagServices().checkNotNull(entity, "unexpected_null_entity"); + callContext.getDiagServices().checkNotNull(grantRecords, "unexpected_null_grant_records"); + callContext + .getDiagServices() + .check(grantRecordsVersion > 0, "unexpected_null_grant_records_version"); + + // create new cache entry + newCacheEntry = + new EntityCacheEntry( + callContext.getDiagServices(), + existingCacheEntry == null + ? System.nanoTime() + : existingCacheEntry.getCreatedOnNanoTimestamp(), + entity, + grantRecords, + grantRecordsVersion); + + // insert cache entry + this.replaceCacheEntry(existingCacheEntry, newCacheEntry); + } else { + // found it in the cache and it is up-to-date, simply return it + existingCacheEntry.updateLastAccess(); + newCacheEntry = existingCacheEntry; + } + + return newCacheEntry; + } + + /** + * Get the specified entity by name and load it if it is not found. + * + * @param callContext the Polaris call context + * @param entityCatalogId id of the catalog where this entity resides or NULL_ID if top-level + * @param entityId id of the entity to lookup + * @return null if the entity does not exist or was dropped. Else return the entry for that + * entity, either as found in the cache or loaded from the backend + */ + public @Nullable EntityCacheLookupResult getOrLoadEntityById( + @NotNull PolarisCallContext callContext, long entityCatalogId, long entityId) { + + // if it exists, we are set + EntityCacheEntry entry = this.getEntityById(entityId); + final boolean cacheHit; + + // we need to load it if it does not exist + if (entry == null) { + // this is a miss + cacheHit = false; + + // load it + PolarisMetaStoreManager.CachedEntryResult result = + metaStoreManager.loadCachedEntryById(callContext, entityCatalogId, entityId); + + // not found, exit + if (!result.isSuccess()) { + return null; + } + + // if found, setup entry + callContext.getDiagServices().checkNotNull(result.getEntity(), "entity_should_loaded"); + callContext + .getDiagServices() + .checkNotNull(result.getEntityGrantRecords(), "entity_grant_records_should_loaded"); + entry = + new EntityCacheEntry( + callContext.getDiagServices(), + System.nanoTime(), + result.getEntity(), + result.getEntityGrantRecords(), + result.getGrantRecordsVersion()); + + // the above loading could take a long time so check again if the entry exists and only + this.cacheNewEntry(entry); + } else { + cacheHit = true; + } + + // return what we found + return new EntityCacheLookupResult(entry, cacheHit); + } + + /** + * Get the specified entity by name and load it if it is not found. + * + * @param callContext the Polaris call context + * @param entityNameKey name of the entity to load + * @return null if the entity does not exist or was dropped. Else return the entry for that + * entity, either as found in the cache or loaded from the backend + */ + public @Nullable EntityCacheLookupResult getOrLoadEntityByName( + @NotNull PolarisCallContext callContext, @NotNull EntityCacheByNameKey entityNameKey) { + + // if it exists, we are set + EntityCacheEntry entry = this.getEntityByName(entityNameKey); + final boolean cacheHit; + + // we need to load it if it does not exist + if (entry == null) { + // this is a miss + cacheHit = false; + + // load it + PolarisMetaStoreManager.CachedEntryResult result = + metaStoreManager.loadCachedEntryByName( + callContext, + entityNameKey.getCatalogId(), + entityNameKey.getParentId(), + entityNameKey.getType(), + entityNameKey.getName()); + + // not found, exit + if (!result.isSuccess()) { + return null; + } + + // validate return + callContext.getDiagServices().checkNotNull(result.getEntity(), "entity_should_loaded"); + callContext + .getDiagServices() + .checkNotNull(result.getEntityGrantRecords(), "entity_grant_records_should_loaded"); + + // if found, setup entry + entry = + new EntityCacheEntry( + callContext.getDiagServices(), + System.nanoTime(), + result.getEntity(), + result.getEntityGrantRecords(), + result.getGrantRecordsVersion()); + + // the above loading could take a long time so check again if the entry exists and only + this.cacheNewEntry(entry); + } else { + cacheHit = true; + } + + // return what we found + return new EntityCacheLookupResult(entry, cacheHit); + } +} diff --git a/polaris-core/src/main/java/io/polaris/core/persistence/cache/EntityCacheByNameKey.java b/polaris-core/src/main/java/io/polaris/core/persistence/cache/EntityCacheByNameKey.java new file mode 100644 index 0000000000..d829d60b3d --- /dev/null +++ b/polaris-core/src/main/java/io/polaris/core/persistence/cache/EntityCacheByNameKey.java @@ -0,0 +1,97 @@ +package io.polaris.core.persistence.cache; + +import io.polaris.core.entity.PolarisBaseEntity; +import io.polaris.core.entity.PolarisEntityConstants; +import io.polaris.core.entity.PolarisEntityType; +import java.util.Objects; + +/** Key on the name of an entity */ +public class EntityCacheByNameKey { + + // id of the catalog where this entity resides + private final long catalogId; + + // id of the parent of that entity + private final long parentId; + + // entity type code + private final int typeCode; + + // entity name + private final String name; + + /** + * Constructor for a top-level service entity (principal, principal role or catalog) + * + * @param type entity type + * @param name name of that entity + */ + public EntityCacheByNameKey(PolarisEntityType type, String name) { + this.catalogId = PolarisEntityConstants.getNullId(); + this.parentId = PolarisEntityConstants.getRootEntityId(); + this.typeCode = type.getCode(); + this.name = name; + } + + /** + * Constructor for a non-top-level entity + * + * @param catalogId id of the catalog where this entity is located + * @param parentId id of the parent of this entity + * @param type entity type + * @param name name of that entity + */ + public EntityCacheByNameKey(long catalogId, long parentId, PolarisEntityType type, String name) { + this.catalogId = catalogId; + this.parentId = parentId; + this.typeCode = type.getCode(); + this.name = name; + } + + /** + * Constructor of a key from an existing base entity + * + * @param baseEntity base entity + */ + public EntityCacheByNameKey(PolarisBaseEntity baseEntity) { + this.catalogId = baseEntity.getCatalogId(); + this.parentId = baseEntity.getParentId(); + this.typeCode = baseEntity.getTypeCode(); + this.name = baseEntity.getName(); + } + + public long getCatalogId() { + return catalogId; + } + + public long getParentId() { + return parentId; + } + + public int getTypeCode() { + return typeCode; + } + + public PolarisEntityType getType() { + return PolarisEntityType.fromCode(typeCode); + } + + public String getName() { + return name; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + EntityCacheByNameKey that = (EntityCacheByNameKey) o; + return parentId == that.parentId + && typeCode == that.typeCode + && Objects.equals(name, that.name); + } + + @Override + public int hashCode() { + return Objects.hash(parentId, typeCode, name); + } +} diff --git a/polaris-core/src/main/java/io/polaris/core/persistence/cache/EntityCacheEntry.java b/polaris-core/src/main/java/io/polaris/core/persistence/cache/EntityCacheEntry.java new file mode 100644 index 0000000000..279ace675e --- /dev/null +++ b/polaris-core/src/main/java/io/polaris/core/persistence/cache/EntityCacheEntry.java @@ -0,0 +1,108 @@ +package io.polaris.core.persistence.cache; + +import com.google.common.collect.ImmutableList; +import io.polaris.core.PolarisDiagnostics; +import io.polaris.core.entity.PolarisBaseEntity; +import io.polaris.core.entity.PolarisGrantRecord; +import java.util.List; +import org.jetbrains.annotations.NotNull; + +/** An entry in our entity cache. Note, this is fully immutable */ +public class EntityCacheEntry { + + // epoch time (ns) when the cache entry was added to the cache + private long createdOnNanoTimestamp; + + // epoch time (ns) when the cache entry was added to the cache + private long lastAccessedNanoTimestamp; + + // the entity which have been cached. + private PolarisBaseEntity entity; + + // grants associated to this entity, for a principal, a principal role, or a catalog role these + // are role usage + // grants on that entity. For a catalog securable (i.e. a catalog, namespace, or table_like + // securable), these are + // the grants on this securable. + private List grantRecords; + + /** + * Constructor used when an entry is initially created after loading the entity and its grants + * from the backend. + * + * @param diagnostics diagnostic services + * @param createdOnNanoTimestamp when the entity was created + * @param entity the entity which has just been loaded + * @param grantRecords associated grant records, including grants for this entity as a securable + * as well as grants for this entity as a grantee if applicable + * @param grantsVersion version of the grants when they were loaded + */ + EntityCacheEntry( + @NotNull PolarisDiagnostics diagnostics, + long createdOnNanoTimestamp, + @NotNull PolarisBaseEntity entity, + @NotNull List grantRecords, + int grantsVersion) { + // validate not null + diagnostics.checkNotNull(entity, "entity_null"); + diagnostics.checkNotNull(grantRecords, "grant_records_null"); + + // when this entry has been created + this.createdOnNanoTimestamp = createdOnNanoTimestamp; + + // last accessed time is now + this.lastAccessedNanoTimestamp = System.nanoTime(); + + // we copy all attributes of the entity to avoid any contamination + this.entity = new PolarisBaseEntity(entity); + + // if only the grant records have been reloaded because they were changed, the entity will + // have an old version for those. Patch the entity if this is the case, as if we had reloaded it + if (this.entity.getGrantRecordsVersion() != grantsVersion) { + // remember the grants versions. For now grants should be loaded after the entity, so expect + // grants version to be same or higher + diagnostics.check( + this.entity.getGrantRecordsVersion() <= grantsVersion, + "grants_version_going_backward", + "entity={} grantsVersion={}", + entity, + grantsVersion); + + // patch grant records version + this.entity.setGrantRecordsVersion(grantsVersion); + } + + // the grants + this.grantRecords = ImmutableList.copyOf(grantRecords); + } + + public long getCreatedOnNanoTimestamp() { + return createdOnNanoTimestamp; + } + + public long getLastAccessedNanoTimestamp() { + return lastAccessedNanoTimestamp; + } + + public @NotNull PolarisBaseEntity getEntity() { + return entity; + } + + public @NotNull List getAllGrantRecords() { + return grantRecords; + } + + public @NotNull List getGrantRecordsAsGrantee() { + return grantRecords.stream().filter(record -> record.getGranteeId() == entity.getId()).toList(); + } + + public @NotNull List getGrantRecordsAsSecurable() { + return grantRecords.stream() + .filter(record -> record.getSecurableId() == entity.getId()) + .toList(); + } + + public void updateLastAccess() { + this.lastAccessedNanoTimestamp = System.nanoTime(); + } +} diff --git a/polaris-core/src/main/java/io/polaris/core/persistence/cache/EntityCacheLookupResult.java b/polaris-core/src/main/java/io/polaris/core/persistence/cache/EntityCacheLookupResult.java new file mode 100644 index 0000000000..e551144618 --- /dev/null +++ b/polaris-core/src/main/java/io/polaris/core/persistence/cache/EntityCacheLookupResult.java @@ -0,0 +1,27 @@ +package io.polaris.core.persistence.cache; + +import org.jetbrains.annotations.Nullable; + +/** Result of a lookup operation */ +public class EntityCacheLookupResult { + + // if not null, we found the entity and this is the entry. If not found, the entity was dropped or + // does not exist + private final @Nullable EntityCacheEntry cacheEntry; + + // true if the entity was found in the cache + private final boolean cacheHit; + + public EntityCacheLookupResult(@Nullable EntityCacheEntry cacheEntry, boolean cacheHit) { + this.cacheEntry = cacheEntry; + this.cacheHit = cacheHit; + } + + public @Nullable EntityCacheEntry getCacheEntry() { + return cacheEntry; + } + + public boolean isCacheHit() { + return cacheHit; + } +} diff --git a/polaris-core/src/main/java/io/polaris/core/persistence/cache/EntityCacheMode.java b/polaris-core/src/main/java/io/polaris/core/persistence/cache/EntityCacheMode.java new file mode 100644 index 0000000000..0d20383b09 --- /dev/null +++ b/polaris-core/src/main/java/io/polaris/core/persistence/cache/EntityCacheMode.java @@ -0,0 +1,13 @@ +package io.polaris.core.persistence.cache; + +/** Cache mode, the default is ENABLE. */ +public enum EntityCacheMode { + // bypass the cache, always load + BYPASS, + // enable the cache, this is the default + ENABLE, + // enable but verify that the cache content is consistent. Used in QA mode to detect when + // versioning information is + // not properly maintained + ENABLE_BUT_VERIFY +} diff --git a/polaris-core/src/main/java/io/polaris/core/persistence/models/ModelEntity.java b/polaris-core/src/main/java/io/polaris/core/persistence/models/ModelEntity.java new file mode 100644 index 0000000000..e96ea5c853 --- /dev/null +++ b/polaris-core/src/main/java/io/polaris/core/persistence/models/ModelEntity.java @@ -0,0 +1,290 @@ +/* + * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. + */ + +package io.polaris.core.persistence.models; + +import io.polaris.core.entity.PolarisBaseEntity; +import io.polaris.core.entity.PolarisEntitySubType; +import io.polaris.core.entity.PolarisEntityType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.Version; + +/** + * Entity model representing all attributes of a Polaris Entity. This is used to exchange full + * entity information with ENTITIES table + */ +@Entity +@Table(name = "ENTITIES") +public class ModelEntity { + // the id of the catalog associated to that entity. NULL_ID if this entity is top-level like + // a catalog + @Id private long catalogId; + + // the id of the entity which was resolved + @Id private long id; + + // the id of the parent of this entity, use 0 for a top-level entity whose parent is the account + private long parentId; + + // the type of the entity when it was resolved + private int typeCode; + + // the name that this entity had when it was resolved + private String name; + + // the version that this entity had when it was resolved + private int entityVersion; + + public static final String EMPTY_MAP_STRING = "{}"; + // the type of the entity when it was resolved + private int subTypeCode; + + // timestamp when this entity was created + private long createTimestamp; + + // when this entity was dropped. Null if was never dropped + private long dropTimestamp; + + // when did we start purging this entity. When not null, un-drop is no longer possible + private long purgeTimestamp; + + // when should we start purging this entity + private long toPurgeTimestamp; + + // last time this entity was updated, only for troubleshooting + private long lastUpdateTimestamp; + + // properties, serialized as a JSON string + @Column(length = 65535) + private String properties; + + // internal properties, serialized as a JSON string + @Column(length = 65535) + private String internalProperties; + + // current version for that entity, will be monotonically incremented + private int grantRecordsVersion; + + // Used for Optimistic Locking to handle concurrent reads and updates + @Version private long version; + + public long getId() { + return id; + } + + public long getParentId() { + return parentId; + } + + public int getTypeCode() { + return typeCode; + } + + public String getName() { + return name; + } + + public int getEntityVersion() { + return entityVersion; + } + + public long getCatalogId() { + return catalogId; + } + + public int getSubTypeCode() { + return subTypeCode; + } + + public long getCreateTimestamp() { + return createTimestamp; + } + + public long getDropTimestamp() { + return dropTimestamp; + } + + public long getPurgeTimestamp() { + return purgeTimestamp; + } + + public long getToPurgeTimestamp() { + return toPurgeTimestamp; + } + + public long getLastUpdateTimestamp() { + return lastUpdateTimestamp; + } + + public String getProperties() { + return properties != null ? properties : EMPTY_MAP_STRING; + } + + public String getInternalProperties() { + return internalProperties != null ? internalProperties : EMPTY_MAP_STRING; + } + + public int getGrantRecordsVersion() { + return grantRecordsVersion; + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private final ModelEntity entity; + + private Builder() { + entity = new ModelEntity(); + } + + public Builder catalogId(long catalogId) { + entity.catalogId = catalogId; + return this; + } + + public Builder id(long id) { + entity.id = id; + return this; + } + + public Builder parentId(long parentId) { + entity.parentId = parentId; + return this; + } + + public Builder typeCode(int typeCode) { + entity.typeCode = typeCode; + return this; + } + + public Builder name(String name) { + entity.name = name; + return this; + } + + public Builder entityVersion(int entityVersion) { + entity.entityVersion = entityVersion; + return this; + } + + public Builder subTypeCode(int subTypeCode) { + entity.subTypeCode = subTypeCode; + return this; + } + + public Builder createTimestamp(long createTimestamp) { + entity.createTimestamp = createTimestamp; + return this; + } + + public Builder dropTimestamp(long dropTimestamp) { + entity.dropTimestamp = dropTimestamp; + return this; + } + + public Builder purgeTimestamp(long purgeTimestamp) { + entity.purgeTimestamp = purgeTimestamp; + return this; + } + + public Builder toPurgeTimestamp(long toPurgeTimestamp) { + entity.toPurgeTimestamp = toPurgeTimestamp; + return this; + } + + public Builder lastUpdateTimestamp(long lastUpdateTimestamp) { + entity.lastUpdateTimestamp = lastUpdateTimestamp; + return this; + } + + public Builder properties(String properties) { + entity.properties = properties; + return this; + } + + public Builder internalProperties(String internalProperties) { + entity.internalProperties = internalProperties; + return this; + } + + public Builder grantRecordsVersion(int grantRecordsVersion) { + entity.grantRecordsVersion = grantRecordsVersion; + return this; + } + + public ModelEntity build() { + return entity; + } + } + + public static ModelEntity fromEntity(PolarisBaseEntity entity) { + return ModelEntity.builder() + .catalogId(entity.getCatalogId()) + .id(entity.getId()) + .parentId(entity.getParentId()) + .typeCode(entity.getTypeCode()) + .name(entity.getName()) + .entityVersion(entity.getEntityVersion()) + .subTypeCode(entity.getSubTypeCode()) + .createTimestamp(entity.getCreateTimestamp()) + .dropTimestamp(entity.getDropTimestamp()) + .purgeTimestamp(entity.getPurgeTimestamp()) + .toPurgeTimestamp(entity.getToPurgeTimestamp()) + .lastUpdateTimestamp(entity.getLastUpdateTimestamp()) + .properties(entity.getProperties()) + .internalProperties(entity.getInternalProperties()) + .grantRecordsVersion(entity.getGrantRecordsVersion()) + .build(); + } + + public static PolarisBaseEntity toEntity(ModelEntity model) { + if (model == null) { + return null; + } + + var entity = + new PolarisBaseEntity( + model.getCatalogId(), + model.getId(), + PolarisEntityType.fromCode(model.getTypeCode()), + PolarisEntitySubType.fromCode(model.getSubTypeCode()), + model.getParentId(), + model.getName()); + entity.setEntityVersion(model.getEntityVersion()); + entity.setCreateTimestamp(model.getCreateTimestamp()); + entity.setDropTimestamp(model.getDropTimestamp()); + entity.setPurgeTimestamp(model.getPurgeTimestamp()); + entity.setToPurgeTimestamp(model.getToPurgeTimestamp()); + entity.setLastUpdateTimestamp(model.getLastUpdateTimestamp()); + entity.setProperties(model.getProperties()); + entity.setInternalProperties(model.getInternalProperties()); + entity.setGrantRecordsVersion(model.getGrantRecordsVersion()); + return entity; + } + + public void update(PolarisBaseEntity entity) { + if (entity == null) return; + + this.catalogId = entity.getCatalogId(); + this.id = entity.getId(); + this.parentId = entity.getParentId(); + this.typeCode = entity.getTypeCode(); + this.name = entity.getName(); + this.entityVersion = entity.getEntityVersion(); + this.subTypeCode = entity.getSubTypeCode(); + this.createTimestamp = entity.getCreateTimestamp(); + this.dropTimestamp = entity.getDropTimestamp(); + this.purgeTimestamp = entity.getPurgeTimestamp(); + this.toPurgeTimestamp = entity.getToPurgeTimestamp(); + this.lastUpdateTimestamp = entity.getLastUpdateTimestamp(); + this.properties = entity.getProperties(); + this.internalProperties = entity.getInternalProperties(); + this.grantRecordsVersion = entity.getGrantRecordsVersion(); + } +} diff --git a/polaris-core/src/main/java/io/polaris/core/persistence/models/ModelEntityActive.java b/polaris-core/src/main/java/io/polaris/core/persistence/models/ModelEntityActive.java new file mode 100644 index 0000000000..732581eacc --- /dev/null +++ b/polaris-core/src/main/java/io/polaris/core/persistence/models/ModelEntityActive.java @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. + */ + +package io.polaris.core.persistence.models; + +import io.polaris.core.entity.PolarisEntityActiveRecord; +import io.polaris.core.entity.PolarisEntitySubType; +import io.polaris.core.entity.PolarisEntityType; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +/** + * EntityActive model representing some attributes of a Polaris Entity. This is used to exchange + * entity information with ENTITIES_ACTIVE table + */ +@Entity +@Table(name = "ENTITIES_ACTIVE") +public class ModelEntityActive { + // entity catalog id + @Id private long catalogId; + + // id of the entity + @Id private long id; + + // parent id of the entity + @Id private long parentId; + + // name of the entity + private String name; + + // code representing the type of that entity + @Id private int typeCode; + + // code representing the subtype of that entity + private int subTypeCode; + + public long getCatalogId() { + return catalogId; + } + + public long getId() { + return id; + } + + public long getParentId() { + return parentId; + } + + public String getName() { + return name; + } + + public int getTypeCode() { + return typeCode; + } + + public PolarisEntityType getType() { + return PolarisEntityType.fromCode(this.typeCode); + } + + public int getSubTypeCode() { + return subTypeCode; + } + + public PolarisEntitySubType getSubType() { + return PolarisEntitySubType.fromCode(this.subTypeCode); + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private final ModelEntityActive entity; + + private Builder() { + entity = new ModelEntityActive(); + } + + public Builder catalogId(long catalogId) { + entity.catalogId = catalogId; + return this; + } + + public Builder id(long id) { + entity.id = id; + return this; + } + + public Builder parentId(long parentId) { + entity.parentId = parentId; + return this; + } + + public Builder typeCode(int typeCode) { + entity.typeCode = typeCode; + return this; + } + + public Builder name(String name) { + entity.name = name; + return this; + } + + public Builder subTypeCode(int subTypeCode) { + entity.subTypeCode = subTypeCode; + return this; + } + + public ModelEntityActive build() { + return entity; + } + } + + public static ModelEntityActive fromEntityActive(PolarisEntityActiveRecord record) { + return ModelEntityActive.builder() + .catalogId(record.getCatalogId()) + .id(record.getId()) + .parentId(record.getParentId()) + .name(record.getName()) + .typeCode(record.getTypeCode()) + .subTypeCode(record.getSubTypeCode()) + .build(); + } + + public static PolarisEntityActiveRecord toEntityActive(ModelEntityActive model) { + if (model == null) { + return null; + } + + return new PolarisEntityActiveRecord( + model.catalogId, model.id, model.parentId, model.name, model.typeCode, model.subTypeCode); + } +} diff --git a/polaris-core/src/main/java/io/polaris/core/persistence/models/ModelEntityChangeTracking.java b/polaris-core/src/main/java/io/polaris/core/persistence/models/ModelEntityChangeTracking.java new file mode 100644 index 0000000000..f7d31860f9 --- /dev/null +++ b/polaris-core/src/main/java/io/polaris/core/persistence/models/ModelEntityChangeTracking.java @@ -0,0 +1,61 @@ +package io.polaris.core.persistence.models; + +import io.polaris.core.entity.PolarisBaseEntity; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.Version; + +/** + * EntityChangeTracking model representing some attributes of a Polaris Entity. This is used to + * exchange entity information with ENTITIES_CHANGE_TRACKING table + */ +@Entity +@Table(name = "ENTITIES_CHANGE_TRACKING") +public class ModelEntityChangeTracking { + // the id of the catalog associated to that entity. NULL_ID if this entity is top-level like + // a catalog + @Id private long catalogId; + + // the id of the entity which was resolved + @Id private long id; + + // the version that this entity had when it was resolved + private int entityVersion; + + // current version for that entity, will be monotonically incremented + private int grantRecordsVersion; + + // Used for Optimistic Locking to handle concurrent reads and updates + @Version private long version; + + public ModelEntityChangeTracking() {} + + public ModelEntityChangeTracking(PolarisBaseEntity entity) { + this.catalogId = entity.getCatalogId(); + this.id = entity.getId(); + this.entityVersion = entity.getEntityVersion(); + this.grantRecordsVersion = entity.getGrantRecordsVersion(); + } + + public long getCatalogId() { + return catalogId; + } + + public long getId() { + return id; + } + + public int getEntityVersion() { + return entityVersion; + } + + public int getGrantRecordsVersion() { + return grantRecordsVersion; + } + + public void update(PolarisBaseEntity entity) { + this.entityVersion = entity.getEntityVersion(); + this.grantRecordsVersion = entity.getGrantRecordsVersion(); + } +} diff --git a/polaris-core/src/main/java/io/polaris/core/persistence/models/ModelEntityDropped.java b/polaris-core/src/main/java/io/polaris/core/persistence/models/ModelEntityDropped.java new file mode 100644 index 0000000000..504ef42a7a --- /dev/null +++ b/polaris-core/src/main/java/io/polaris/core/persistence/models/ModelEntityDropped.java @@ -0,0 +1,146 @@ +package io.polaris.core.persistence.models; + +import io.polaris.core.entity.PolarisBaseEntity; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.Version; + +/** + * EntityDropped model representing some attributes of a Polaris Entity. This is used to exchange + * entity information with ENTITIES_DROPPED table + */ +@Entity +@Table(name = "ENTITIES_DROPPED") +public class ModelEntityDropped { + // the id of the catalog associated to that entity. NULL_ID if this entity is top-level like + // a catalog + @Id private long catalogId; + + // the id of the entity which was resolved + private long id; + + // the id of the parent of this entity, use 0 for a top-level entity whose parent is the account + @Id private long parentId; + + // the type of the entity when it was resolved + @Id private int typeCode; + + // the name that this entity had when it was resolved + @Id private String name; + + // the type of the entity when it was resolved + @Id private int subTypeCode; + + // when this entity was dropped. Null if was never dropped + @Id private long dropTimestamp; + + // when should we start purging this entity + private long toPurgeTimestamp; + + // Used for Optimistic Locking to handle concurrent reads and updates + @Version private long version; + + public long getCatalogId() { + return catalogId; + } + + public long getId() { + return id; + } + + public long getParentId() { + return parentId; + } + + public int getTypeCode() { + return typeCode; + } + + public String getName() { + return name; + } + + public int getSubTypeCode() { + return subTypeCode; + } + + public long getDropTimestamp() { + return dropTimestamp; + } + + public long getToPurgeTimestamp() { + return toPurgeTimestamp; + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private final ModelEntityDropped entity; + + private Builder() { + entity = new ModelEntityDropped(); + } + + public Builder catalogId(long catalogId) { + entity.catalogId = catalogId; + return this; + } + + public Builder id(long id) { + entity.id = id; + return this; + } + + public Builder parentId(long parentId) { + entity.parentId = parentId; + return this; + } + + public Builder typeCode(int typeCode) { + entity.typeCode = typeCode; + return this; + } + + public Builder name(String name) { + entity.name = name; + return this; + } + + public Builder subTypeCode(int subTypeCode) { + entity.subTypeCode = subTypeCode; + return this; + } + + public Builder dropTimestamp(long dropTimestamp) { + entity.dropTimestamp = dropTimestamp; + return this; + } + + public Builder toPurgeTimestamp(long toPurgeTimestamp) { + entity.toPurgeTimestamp = toPurgeTimestamp; + return this; + } + + public ModelEntityDropped build() { + return entity; + } + } + + public static ModelEntityDropped fromEntity(PolarisBaseEntity entity) { + if (entity == null) return null; + + return ModelEntityDropped.builder() + .catalogId(entity.getCatalogId()) + .id(entity.getId()) + .parentId(entity.getParentId()) + .typeCode(entity.getTypeCode()) + .name(entity.getName()) + .subTypeCode(entity.getSubTypeCode()) + .dropTimestamp(entity.getDropTimestamp()) + .toPurgeTimestamp(entity.getToPurgeTimestamp()) + .build(); + } +} diff --git a/polaris-core/src/main/java/io/polaris/core/persistence/models/ModelGrantRecord.java b/polaris-core/src/main/java/io/polaris/core/persistence/models/ModelGrantRecord.java new file mode 100644 index 0000000000..022d8334b2 --- /dev/null +++ b/polaris-core/src/main/java/io/polaris/core/persistence/models/ModelGrantRecord.java @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. + */ + +package io.polaris.core.persistence.models; + +import io.polaris.core.entity.PolarisGrantRecord; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Index; +import jakarta.persistence.Table; +import jakarta.persistence.Version; + +/** + * GrantRecord model representing a privilege record of a securable granted to grantee. This is used + * to exchange the information with GRANT_RECORDS table + */ +@Entity +@Table( + name = "GRANT_RECORDS", + indexes = { + @Index( + name = "GRANT_RECORDS_BY_GRANTEE_INDEX", + columnList = "granteeCatalogId,granteeId,securableCatalogId,securableId,privilegeCode") + }) +public class ModelGrantRecord { + + // id of the catalog where the securable entity resides, NULL_ID if this entity is a top-level + // account entity + @Id private long securableCatalogId; + + // id of the securable + @Id private long securableId; + + // id of the catalog where the grantee entity resides, NULL_ID if this entity is a top-level + // account entity + @Id private long granteeCatalogId; + + // id of the grantee + @Id private long granteeId; + + // id associated to the privilege + @Id private int privilegeCode; + + // Used for Optimistic Locking to handle concurrent reads and updates + @Version private long version; + + public long getSecurableCatalogId() { + return securableCatalogId; + } + + public long getSecurableId() { + return securableId; + } + + public long getGranteeCatalogId() { + return granteeCatalogId; + } + + public long getGranteeId() { + return granteeId; + } + + public int getPrivilegeCode() { + return privilegeCode; + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private final ModelGrantRecord grantRecord; + + private Builder() { + grantRecord = new ModelGrantRecord(); + } + + public Builder securableCatalogId(long securableCatalogId) { + grantRecord.securableCatalogId = securableCatalogId; + return this; + } + + public Builder securableId(long securableId) { + grantRecord.securableId = securableId; + return this; + } + + public Builder granteeCatalogId(long granteeCatalogId) { + grantRecord.granteeCatalogId = granteeCatalogId; + return this; + } + + public Builder granteeId(long granteeId) { + grantRecord.granteeId = granteeId; + return this; + } + + public Builder privilegeCode(int privilegeCode) { + grantRecord.privilegeCode = privilegeCode; + return this; + } + + public ModelGrantRecord build() { + return grantRecord; + } + } + + public static ModelGrantRecord fromGrantRecord(PolarisGrantRecord record) { + if (record == null) return null; + + return ModelGrantRecord.builder() + .securableCatalogId(record.getSecurableCatalogId()) + .securableId(record.getSecurableId()) + .granteeCatalogId(record.getGranteeCatalogId()) + .granteeId(record.getGranteeId()) + .privilegeCode(record.getPrivilegeCode()) + .build(); + } + + public static PolarisGrantRecord toGrantRecord(ModelGrantRecord model) { + if (model == null) return null; + + return new PolarisGrantRecord( + model.getSecurableCatalogId(), + model.getSecurableId(), + model.getGranteeCatalogId(), + model.getGranteeId(), + model.getPrivilegeCode()); + } +} diff --git a/polaris-core/src/main/java/io/polaris/core/persistence/models/ModelPrincipalSecrets.java b/polaris-core/src/main/java/io/polaris/core/persistence/models/ModelPrincipalSecrets.java new file mode 100644 index 0000000000..b7658c9186 --- /dev/null +++ b/polaris-core/src/main/java/io/polaris/core/persistence/models/ModelPrincipalSecrets.java @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. + */ + +package io.polaris.core.persistence.models; + +import io.polaris.core.entity.PolarisPrincipalSecrets; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.Version; + +/** + * PrincipalSecrets model representing the secrets used to authenticate a catalog principal. This is + * used to exchange the information with PRINCIPAL_SECRETS table + */ +@Entity +@Table(name = "PRINCIPAL_SECRETS") +public class ModelPrincipalSecrets { + // the id of the principal + private long principalId; + + // the client id for that principal + @Id private String principalClientId; + + // the main secret for that principal + private String mainSecret; + + // the secondary secret for that principal + private String secondarySecret; + + // Used for Optimistic Locking to handle concurrent reads and updates + @Version private long version; + + public long getPrincipalId() { + return principalId; + } + + public String getPrincipalClientId() { + return principalClientId; + } + + public String getMainSecret() { + return mainSecret; + } + + public String getSecondarySecret() { + return secondarySecret; + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private final ModelPrincipalSecrets principalSecrets; + + private Builder() { + principalSecrets = new ModelPrincipalSecrets(); + } + + public Builder principalId(long principalId) { + principalSecrets.principalId = principalId; + return this; + } + + public Builder principalClientId(String principalClientId) { + principalSecrets.principalClientId = principalClientId; + return this; + } + + public Builder mainSecret(String mainSecret) { + principalSecrets.mainSecret = mainSecret; + return this; + } + + public Builder secondarySecret(String secondarySecret) { + principalSecrets.secondarySecret = secondarySecret; + return this; + } + + public ModelPrincipalSecrets build() { + return principalSecrets; + } + } + + public static ModelPrincipalSecrets fromPrincipalSecrets(PolarisPrincipalSecrets record) { + if (record == null) return null; + + return ModelPrincipalSecrets.builder() + .principalId(record.getPrincipalId()) + .principalClientId(record.getPrincipalClientId()) + .mainSecret(record.getMainSecret()) + .secondarySecret(record.getSecondarySecret()) + .build(); + } + + public static PolarisPrincipalSecrets toPrincipalSecrets(ModelPrincipalSecrets model) { + if (model == null) return null; + + return new PolarisPrincipalSecrets( + model.getPrincipalId(), + model.getPrincipalClientId(), + model.getMainSecret(), + model.getSecondarySecret()); + } + + public void update(PolarisPrincipalSecrets principalSecrets) { + if (principalSecrets == null) return; + + this.principalId = principalSecrets.getPrincipalId(); + this.principalClientId = principalSecrets.getPrincipalClientId(); + this.mainSecret = principalSecrets.getMainSecret(); + this.secondarySecret = principalSecrets.getSecondarySecret(); + } +} diff --git a/polaris-core/src/main/java/io/polaris/core/persistence/models/ModelSequenceId.java b/polaris-core/src/main/java/io/polaris/core/persistence/models/ModelSequenceId.java new file mode 100644 index 0000000000..a48fefefb9 --- /dev/null +++ b/polaris-core/src/main/java/io/polaris/core/persistence/models/ModelSequenceId.java @@ -0,0 +1,21 @@ +package io.polaris.core.persistence.models; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.SequenceGenerator; +import jakarta.persistence.Table; + +@Entity +@Table(name = "POLARIS_SEQUENCE") +public class ModelSequenceId { + @Id + @SequenceGenerator( + name = "sequenceGen", + sequenceName = "POLARIS_SEQ", + initialValue = 1000, + allocationSize = 25) + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGen") + private Long id; +} diff --git a/polaris-core/src/main/java/io/polaris/core/persistence/resolver/PolarisResolutionManifest.java b/polaris-core/src/main/java/io/polaris/core/persistence/resolver/PolarisResolutionManifest.java new file mode 100644 index 0000000000..8287cbe6d4 --- /dev/null +++ b/polaris-core/src/main/java/io/polaris/core/persistence/resolver/PolarisResolutionManifest.java @@ -0,0 +1,394 @@ +package io.polaris.core.persistence.resolver; + +import com.google.common.collect.HashMultimap; +import com.google.common.collect.Multimap; +import io.polaris.core.PolarisDiagnostics; +import io.polaris.core.auth.AuthenticatedPolarisPrincipal; +import io.polaris.core.context.CallContext; +import io.polaris.core.entity.PolarisBaseEntity; +import io.polaris.core.entity.PolarisEntityConstants; +import io.polaris.core.entity.PolarisEntitySubType; +import io.polaris.core.entity.PolarisEntityType; +import io.polaris.core.entity.PrincipalRoleEntity; +import io.polaris.core.persistence.PolarisEntityManager; +import io.polaris.core.persistence.PolarisResolvedPathWrapper; +import io.polaris.core.persistence.ResolvedPolarisEntity; +import io.polaris.core.persistence.cache.EntityCacheEntry; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Holds a collection of related resolved PolarisEntity and associated grants including caller + * Principal/PrincipalRoles/CatalogRoles and target securables that will participate in any given + * operation. + * + *

Implemented as a wrapper around a Resolver with helper methods and book-keeping to better + * function as a lookup manifest for downstream callers. + */ +public class PolarisResolutionManifest implements PolarisResolutionManifestCatalogView { + private static final Logger LOG = LoggerFactory.getLogger(PolarisResolutionManifest.class); + + private final PolarisEntityManager entityManager; + private final CallContext callContext; + private final AuthenticatedPolarisPrincipal authenticatedPrincipal; + private final String catalogName; + private final Resolver primaryResolver; + private final PolarisDiagnostics diagnostics; + + private final Map pathLookup = new HashMap<>(); + private final List addedPaths = new ArrayList<>(); + private final Multimap addedTopLevelNames = HashMultimap.create(); + + private final Map passthroughPaths = new HashMap<>(); + + // For applicable operations, this represents the topmost root entity which services as an + // authorization parent for all other entities that reside at the root level, such as + // Catalog, Principal, and PrincipalRole. + // This simulated entity will be used if the actual resolver fails to resolve the rootContainer + // on the backend due to compatibility mismatches. + private ResolvedPolarisEntity simulatedResolvedRootContainerEntity = null; + + private int currentPathIndex = 0; + + // Set when resolveAll is called + private ResolverStatus primaryResolverStatus = null; + + public PolarisResolutionManifest( + CallContext callContext, + PolarisEntityManager entityManager, + AuthenticatedPolarisPrincipal authenticatedPrincipal, + String catalogName) { + this.entityManager = entityManager; + this.callContext = callContext; + this.authenticatedPrincipal = authenticatedPrincipal; + this.catalogName = catalogName; + this.primaryResolver = + entityManager.prepareResolver(callContext, authenticatedPrincipal, catalogName); + this.diagnostics = callContext.getPolarisCallContext().getDiagServices(); + + // TODO: Make the rootContainer lookup no longer optional in the persistence store. + // For now, we'll try to resolve the rootContainer as "optional", and only if we fail to find + // it, we'll use the "simulated" rootContainer entity. + addTopLevelName(PolarisEntityConstants.getRootContainerName(), PolarisEntityType.ROOT, true); + } + + /** Adds a name of a top-level entity (Catalog, Principal, PrincipalRole) to be resolved. */ + public void addTopLevelName(String entityName, PolarisEntityType entityType, boolean isOptional) { + addedTopLevelNames.put(entityName, entityType); + if (isOptional) { + primaryResolver.addOptionalEntityByName(entityType, entityName); + } else { + primaryResolver.addEntityByName(entityType, entityName); + } + } + + /** + * Adds a path that will be statically resolved with the primary Resolver when resolveAll() is + * called, and which contributes to the resolution status of whether all paths have successfully + * resolved. + * + * @param key the friendly lookup key for retrieving resolvedPaths after resolveAll(); typically + * might be a Namespace or TableIdentifier object. + */ + public void addPath(ResolverPath path, Object key) { + primaryResolver.addPath(path); + pathLookup.put(key, currentPathIndex); + addedPaths.add(path); + ++currentPathIndex; + } + + /** + * Adds a path that is allowed to be dynamically resolved with a new Resolver when + * getPassthroughResolvedPath is called. These paths are also included in the primary static + * resolution set resolved during resolveAll(). + */ + public void addPassthroughPath(ResolverPath path, Object key) { + addPath(path, key); + passthroughPaths.put(key, path); + } + + public ResolverStatus resolveAll() { + primaryResolverStatus = primaryResolver.resolveAll(); + // TODO: This could be a race condition where a Principal is dropped after initial authn + // but before the resolution attempt; consider whether 403 forbidden is more appropriate. + diagnostics.check( + primaryResolverStatus.getStatus() + != ResolverStatus.StatusEnum.CALLER_PRINCIPAL_DOES_NOT_EXIST, + "caller_principal_does_not_exist_at_resolution_time"); + + // activated principal roles are known, add them to the call context + if (primaryResolverStatus.getStatus() == ResolverStatus.StatusEnum.SUCCESS) { + List activatedPrincipalRoles = + primaryResolver.getResolvedCallerPrincipalRoles().stream() + .map(ce -> PrincipalRoleEntity.of(ce.getEntity())) + .collect(Collectors.toList()); + this.authenticatedPrincipal.setActivatedPrincipalRoles(activatedPrincipalRoles); + } + return primaryResolverStatus; + } + + @Override + public PolarisResolvedPathWrapper getResolvedReferenceCatalogEntity() { + return getResolvedReferenceCatalogEntity(false); + } + + /** + * @param key the key associated with the path to retrieve that was specified in addPath + * @return null if the path resolved for {@code key} isn't fully-resolved when specified as + * "optional" + */ + @Override + public PolarisResolvedPathWrapper getResolvedPath(Object key) { + return getResolvedPath(key, false); + } + + /** + * @return null if the path resolved for {@code key} isn't fully-resolved when specified as + * "optional", or if it was resolved but the subType doesn't match the specified subType. + */ + @Override + public PolarisResolvedPathWrapper getResolvedPath(Object key, PolarisEntitySubType subType) { + return getResolvedPath(key, subType, false); + } + + /** + * @param key the key associated with the path to retrieve that was specified in addPath + * @return null if the path resolved for {@code key} isn't fully-resolved when specified as + * "optional" + */ + @Override + public PolarisResolvedPathWrapper getPassthroughResolvedPath(Object key) { + diagnostics.check( + passthroughPaths.containsKey(key), + "invalid_key_for_passthrough_resolved_path", + "key={} passthroughPaths={}", + key, + passthroughPaths); + ResolverPath requestedPath = passthroughPaths.get(key); + + // Run a single-use Resolver for this path. + Resolver passthroughResolver = + entityManager.prepareResolver(callContext, authenticatedPrincipal, catalogName); + passthroughResolver.addPath(requestedPath); + ResolverStatus status = passthroughResolver.resolveAll(); + + if (status.getStatus() != ResolverStatus.StatusEnum.SUCCESS) { + LOG.debug("Returning null for key {} due to resolver status {}", key, status.getStatus()); + return null; + } + + List resolvedPath = passthroughResolver.getResolvedPath(); + if (requestedPath.isOptional()) { + if (resolvedPath.size() != requestedPath.getEntityNames().size()) { + LOG.debug( + "Returning null for key {} due to size mismatch from getPassthroughResolvedPath " + + "resolvedPath: {}, requestedPath.getEntityNames(): {}", + key, + resolvedPath.stream().map(ResolvedPolarisEntity::new).toList(), + requestedPath.getEntityNames()); + return null; + } + } + + List resolvedEntities = new ArrayList<>(); + resolvedEntities.add( + new ResolvedPolarisEntity(passthroughResolver.getResolvedReferenceCatalog())); + resolvedPath.stream() + .forEach(cacheEntry -> resolvedEntities.add(new ResolvedPolarisEntity(cacheEntry))); + LOG.debug("Returning resolvedEntities from getPassthroughResolvedPath: {}", resolvedEntities); + return new PolarisResolvedPathWrapper(resolvedEntities); + } + + /** + * @return null if the path resolved for {@code key} isn't fully-resolved when specified as + * "optional", or if it was resolved but the subType doesn't match the specified subType. + */ + @Override + public PolarisResolvedPathWrapper getPassthroughResolvedPath( + Object key, PolarisEntitySubType subType) { + PolarisResolvedPathWrapper resolvedPath = getPassthroughResolvedPath(key); + if (resolvedPath == null) { + return null; + } + if (resolvedPath.getRawLeafEntity() != null + && subType != PolarisEntitySubType.ANY_SUBTYPE + && resolvedPath.getRawLeafEntity().getSubType() != subType) { + return null; + } + return resolvedPath; + } + + public Set getAllActivatedCatalogRoleAndPrincipalRoleIds() { + Set activatedIds = new HashSet<>(); + primaryResolver.getResolvedCallerPrincipalRoles().stream() + .map(EntityCacheEntry::getEntity) + .map(PolarisBaseEntity::getId) + .forEach(activatedIds::add); + if (primaryResolver.getResolvedCatalogRoles() != null) { + primaryResolver.getResolvedCatalogRoles().values().stream() + .map(EntityCacheEntry::getEntity) + .map(PolarisBaseEntity::getId) + .forEach(activatedIds::add); + } + return activatedIds; + } + + public Set getAllActivatedPrincipalRoleIds() { + Set activatedIds = new HashSet<>(); + primaryResolver.getResolvedCallerPrincipalRoles().stream() + .map(EntityCacheEntry::getEntity) + .map(PolarisBaseEntity::getId) + .forEach(activatedIds::add); + return activatedIds; + } + + public void setSimulatedResolvedRootContainerEntity( + ResolvedPolarisEntity simulatedResolvedRootContainerEntity) { + this.simulatedResolvedRootContainerEntity = simulatedResolvedRootContainerEntity; + } + + private ResolvedPolarisEntity getResolvedRootContainerEntity() { + if (primaryResolverStatus.getStatus() != ResolverStatus.StatusEnum.SUCCESS) { + return null; + } + EntityCacheEntry resolvedCacheEntry = + primaryResolver.getResolvedEntity( + PolarisEntityType.ROOT, PolarisEntityConstants.getRootContainerName()); + if (resolvedCacheEntry == null) { + LOG.debug("Failed to find rootContainer, so using simulated rootContainer instead."); + return simulatedResolvedRootContainerEntity; + } + return new ResolvedPolarisEntity(resolvedCacheEntry); + } + + public PolarisResolvedPathWrapper getResolvedRootContainerEntityAsPath() { + return new PolarisResolvedPathWrapper(List.of(getResolvedRootContainerEntity())); + } + + public PolarisResolvedPathWrapper getResolvedReferenceCatalogEntity( + boolean prependRootContainer) { + // This is a server error instead of being able to legitimately return null, since this means + // a callsite failed to incorporate a reference catalog into its authorization flow but is + // still trying to perform operations on the (nonexistence) reference catalog. + diagnostics.checkNotNull(catalogName, "null_catalog_name_for_resolved_reference_catalog"); + EntityCacheEntry resolvedCachedCatalog = primaryResolver.getResolvedReferenceCatalog(); + if (resolvedCachedCatalog == null) { + return null; + } + if (prependRootContainer) { + // Operations directly on Catalogs also consider the root container to be a parent of its + // authorization chain. + // TODO: Throw appropriate Catalog NOT_FOUND exception before any call to + // getResolvedReferenceCatalogEntity(). + return new PolarisResolvedPathWrapper( + List.of( + getResolvedRootContainerEntity(), new ResolvedPolarisEntity(resolvedCachedCatalog))); + } else { + return new PolarisResolvedPathWrapper( + List.of(new ResolvedPolarisEntity(resolvedCachedCatalog))); + } + } + + public PolarisEntitySubType getLeafSubType(Object key) { + diagnostics.check( + pathLookup.containsKey(key), + "never_registered_key_for_resolved_path", + "key={} pathLookup={}", + key, + pathLookup); + int index = pathLookup.get(key); + List resolved = primaryResolver.getResolvedPaths().get(index); + if (resolved.size() == 0) { + return PolarisEntitySubType.NULL_SUBTYPE; + } + return resolved.get(resolved.size() - 1).getEntity().getSubType(); + } + + /** + * @param key the key associated with the path to retrieve that was specified in addPath + * @param prependRootContainer if true, also includes the rootContainer as the first element of + * the path; otherwise, the first element begins with the referenceCatalog. + * @return null if the path resolved for {@code key} isn't fully-resolved when specified as + * "optional" + */ + public PolarisResolvedPathWrapper getResolvedPath(Object key, boolean prependRootContainer) { + diagnostics.check( + pathLookup.containsKey(key), + "never_registered_key_for_resolved_path", + "key={} pathLookup={}", + key, + pathLookup); + + if (primaryResolverStatus.getStatus() != ResolverStatus.StatusEnum.SUCCESS) { + return null; + } + int index = pathLookup.get(key); + + // Return null for a partially-resolved "optional" path. + ResolverPath requestedPath = addedPaths.get(index); + List resolvedPath = primaryResolver.getResolvedPaths().get(index); + if (requestedPath.isOptional()) { + if (resolvedPath.size() != requestedPath.getEntityNames().size()) { + return null; + } + } + + List resolvedEntities = new ArrayList<>(); + if (prependRootContainer) { + resolvedEntities.add(getResolvedRootContainerEntity()); + } + resolvedEntities.add(new ResolvedPolarisEntity(primaryResolver.getResolvedReferenceCatalog())); + resolvedPath.stream() + .forEach(cacheEntry -> resolvedEntities.add(new ResolvedPolarisEntity(cacheEntry))); + return new PolarisResolvedPathWrapper(resolvedEntities); + } + + /** + * @return null if the path resolved for {@code key} isn't fully-resolved when specified as + * "optional", or if it was resolved but the subType doesn't match the specified subType. + */ + public PolarisResolvedPathWrapper getResolvedPath( + Object key, PolarisEntitySubType subType, boolean prependRootContainer) { + PolarisResolvedPathWrapper resolvedPath = getResolvedPath(key, prependRootContainer); + if (resolvedPath == null) { + return null; + } + if (resolvedPath.getRawLeafEntity() != null + && subType != PolarisEntitySubType.ANY_SUBTYPE + && resolvedPath.getRawLeafEntity().getSubType() != subType) { + return null; + } + return resolvedPath; + } + + public PolarisResolvedPathWrapper getResolvedTopLevelEntity( + String entityName, PolarisEntityType entityType) { + // For now, all top-level entities will have the root container prepended so we don't have + // a variation of this method that allows specifying whether to prepend the root container. + diagnostics.check( + addedTopLevelNames.containsEntry(entityName, entityType), + "never_registered_top_level_name_and_type_for_resolved_entity", + "entityName={} entityType={} addedTopLevelNames={}", + entityName, + entityType, + addedTopLevelNames); + + if (primaryResolverStatus.getStatus() != ResolverStatus.StatusEnum.SUCCESS) { + return null; + } + + EntityCacheEntry resolvedCacheEntry = primaryResolver.getResolvedEntity(entityType, entityName); + if (resolvedCacheEntry == null) { + return null; + } + return new PolarisResolvedPathWrapper( + List.of(getResolvedRootContainerEntity(), new ResolvedPolarisEntity(resolvedCacheEntry))); + } +} diff --git a/polaris-core/src/main/java/io/polaris/core/persistence/resolver/PolarisResolutionManifestCatalogView.java b/polaris-core/src/main/java/io/polaris/core/persistence/resolver/PolarisResolutionManifestCatalogView.java new file mode 100644 index 0000000000..335a9726be --- /dev/null +++ b/polaris-core/src/main/java/io/polaris/core/persistence/resolver/PolarisResolutionManifestCatalogView.java @@ -0,0 +1,20 @@ +package io.polaris.core.persistence.resolver; + +import io.polaris.core.entity.PolarisEntitySubType; +import io.polaris.core.persistence.PolarisResolvedPathWrapper; + +/** + * Defines the methods by which a Catalog is expected to access resolved catalog-path entities, + * typically backed by a PolarisResolutionManifest. + */ +public interface PolarisResolutionManifestCatalogView { + PolarisResolvedPathWrapper getResolvedReferenceCatalogEntity(); + + PolarisResolvedPathWrapper getResolvedPath(Object key); + + PolarisResolvedPathWrapper getResolvedPath(Object key, PolarisEntitySubType subType); + + PolarisResolvedPathWrapper getPassthroughResolvedPath(Object key); + + PolarisResolvedPathWrapper getPassthroughResolvedPath(Object key, PolarisEntitySubType subType); +} diff --git a/polaris-core/src/main/java/io/polaris/core/persistence/resolver/Resolver.java b/polaris-core/src/main/java/io/polaris/core/persistence/resolver/Resolver.java new file mode 100644 index 0000000000..6ed75e4296 --- /dev/null +++ b/polaris-core/src/main/java/io/polaris/core/persistence/resolver/Resolver.java @@ -0,0 +1,968 @@ +package io.polaris.core.persistence.resolver; + +import io.polaris.core.PolarisCallContext; +import io.polaris.core.PolarisDiagnostics; +import io.polaris.core.entity.PolarisBaseEntity; +import io.polaris.core.entity.PolarisChangeTrackingVersions; +import io.polaris.core.entity.PolarisEntityConstants; +import io.polaris.core.entity.PolarisEntityId; +import io.polaris.core.entity.PolarisEntityType; +import io.polaris.core.entity.PolarisGrantRecord; +import io.polaris.core.entity.PolarisPrivilege; +import io.polaris.core.persistence.PolarisMetaStoreManager; +import io.polaris.core.persistence.cache.EntityCache; +import io.polaris.core.persistence.cache.EntityCacheByNameKey; +import io.polaris.core.persistence.cache.EntityCacheEntry; +import io.polaris.core.persistence.cache.EntityCacheLookupResult; +import java.util.AbstractSet; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * REST request resolver, allows to resolve all entities referenced directly or indirectly by in + * incoming rest request, Once resolved, the request can be authorized. + */ +public class Resolver { + + // we stash the Polaris call context here + private final @NotNull PolarisCallContext polarisCallContext; + + // the diagnostic services + private final @NotNull PolarisDiagnostics diagnostics; + + // the polaris metastore manager + private final @NotNull PolarisMetaStoreManager metaStoreManager; + + // the cache of entities + private final @NotNull EntityCache cache; + + // the id of the principal making the call or 0 if unknown + private final long callerPrincipalId; + + // the name of the principal making the call or null if unknown. If 0, the principal name will be + // not null + private final String callerPrincipalName; + + // reference catalog name for name resolution + private final String referenceCatalogName; + + // if not null, subset of principal roles to activate + private final @Nullable Set callerPrincipalRoleNamesScope; + + // set of entities to resolve given their name. This does not include namespaces or table_like + // entities which are + // part of a path + private final AbstractSet entitiesToResolve; + + // list of paths to resolve + private final List pathsToResolve; + + // caller principal + private EntityCacheEntry resolvedCallerPrincipal; + + // all principal roles which have been resolved + private List resolvedCallerPrincipalRoles; + + // catalog to use as the reference catalog for role activation + private EntityCacheEntry resolvedReferenceCatalog; + + // all catalog roles which have been activated + private final Map resolvedCatalogRoles; + + // all resolved paths + private List> resolvedPaths; + + // all entities which have been successfully resolved, by name + private final Map resolvedEntriesByName; + + // all entities which have been fully resolved, by id + private final Map resolvedEntriesById; + + private ResolverStatus resolverStatus; + + /** + * Constructor, effectively starts an entity resolver session + * + * @param polarisCallContext the polaris call context + * @param metaStoreManager meta store manager + * @param callerPrincipalId if not 0, the id of the principal calling the service + * @param callerPrincipalName if callerPrincipalId is 0, the name of the principal calling the + * service + * @param callerPrincipalRoleNamesScope if not null, scope principal roles + * @param cache shared entity cache + * @param referenceCatalogName if not null, specifies the name of the reference catalog. The + * reference catalog is the catalog used to resolve catalog roles and catalog path. Also, if a + * catalog reference is added, we will determine all catalog roles which are activated by the + * caller. Note that when a catalog name needs to be resolved because the principal creates or + * drop a catalog, it should not be specified here. Instead, it should be resolved by calling + * {@link #addEntityByName(PolarisEntityType, String)}. Generally, any DDL executed as a + * service admin should use null for that parameter. + */ + public Resolver( + @NotNull PolarisCallContext polarisCallContext, + @NotNull PolarisMetaStoreManager metaStoreManager, + long callerPrincipalId, + @Nullable String callerPrincipalName, + @Nullable Set callerPrincipalRoleNamesScope, + @NotNull EntityCache cache, + @Nullable String referenceCatalogName) { + this.polarisCallContext = polarisCallContext; + this.diagnostics = polarisCallContext.getDiagServices(); + this.metaStoreManager = metaStoreManager; + this.cache = cache; + this.callerPrincipalName = callerPrincipalName; + this.callerPrincipalId = callerPrincipalId; + this.referenceCatalogName = referenceCatalogName; + + // scoped principal role names + this.callerPrincipalRoleNamesScope = callerPrincipalRoleNamesScope; + + // validate inputs + this.diagnostics.checkNotNull(metaStoreManager, "unexpected_null_metaStoreManager"); + this.diagnostics.checkNotNull(cache, "unexpected_null_cache"); + this.diagnostics.check( + callerPrincipalId != 0 || callerPrincipalName != null, "principal_must_be_specified"); + + // paths to resolve + this.pathsToResolve = new ArrayList<>(); + this.resolvedPaths = new ArrayList<>(); + + // all entities we need to resolve by name + this.entitiesToResolve = new HashSet<>(); + + // will contain all principal roles which we were able to resolve + this.resolvedCallerPrincipalRoles = new ArrayList<>(); + + // remember if a reference catalog name was specified + if (referenceCatalogName != null) { + this.resolvedCatalogRoles = new HashMap<>(); + } else { + this.resolvedCatalogRoles = null; + } + + // all resolved entities, by name and by if + this.resolvedEntriesByName = new HashMap<>(); + resolvedEntriesById = new HashMap<>(); + + // the resolver has not yet been called + this.resolverStatus = null; + } + + /** + * Add a top-level entity to resolve. If the entity type is a catalog role, we also expect that a + * reference catalog entity was specified at creation time, else we will assert. That catalog role + * entity will be resolved from there. We will fail the entire resolution process if that entity + * cannot be resolved. If this is not expected, use addOptionalEntityByName() instead. + * + * @param entityType the type of the entity, either a principal, a principal role, a catalog or a + * catalog role. + * @param entityName the name of the entity + */ + public void addEntityByName(@NotNull PolarisEntityType entityType, @NotNull String entityName) { + diagnostics.checkNotNull(entityType, "entity_type_is_null"); + diagnostics.checkNotNull(entityName, "entity_name_is_null"); + // can only be called if the resolver has not yet been called + this.diagnostics.check(resolverStatus == null, "resolver_called"); + this.addEntityByName(entityType, entityName, false); + } + + /** + * Add an optional top-level entity to resolve. If the entity type is a catalog role, we also + * expect that a reference catalog entity was specified at creation time, else we will assert. + * That catalog role entity will be resolved from there. If the entity cannot be resolved, we will + * not fail the resolution process + * + * @param entityType the type of the entity, either a principal, a principal role, a catalog or a + * catalog role. + * @param entityName the name of the entity + */ + public void addOptionalEntityByName( + @NotNull PolarisEntityType entityType, @NotNull String entityName) { + diagnostics.checkNotNull(entityType, "entity_type_is_null"); + diagnostics.checkNotNull(entityName, "entity_name_is_null"); + // can only be called if the resolver has not yet been called + this.diagnostics.check(resolverStatus == null, "resolver_called"); + this.addEntityByName(entityType, entityName, true); + } + + /** + * Add a path to resolve + * + * @param path path to resolve + */ + public void addPath(@NotNull ResolverPath path) { + // can only be called if the resolver has not yet been called + this.diagnostics.check(resolverStatus == null, "resolver_called"); + diagnostics.checkNotNull(path, "unexpected_null_entity_path"); + this.pathsToResolve.add(path); + } + + /** + * Run the resolution process and return the status, either an error or success + * + *

+   * resolution might be working using multiple passes when using the cache since anything we find in the cache might
+   * have changed in the backend store.
+   * For each pass we will
+   *    -  go over all entities and call EntityCache.getOrLoad...() on these entities, including all paths.
+   *    -  split these entities into 3 groups:
+   *          - dropped or purged. We will return an error for these.
+   *          - to be validated entities, they were found in the cache. For those we need to ensure that the
+   *            entity id, its name and parent id has not changed. If yes we need to perform another pass.
+   *          - reloaded from backend, so the entity is validated. Validated entities will not be validated again
+   * 
+ * + * @return the status of the resolver. If success, all entities have been resolved and the + * getResolvedXYZ() method can be called. + */ + public ResolverStatus resolveAll() { + // can only be called if the resolver has not yet been called + this.diagnostics.check(resolverStatus == null, "resolver_called"); + + // retry until a pass terminates, or we reached the maximum iteration count. Note that we should + // finish normally in no more than few passes so the 1000 limit is really to avoid spinning + // forever if there is a bug. + int count = 0; + ResolverStatus status; + do { + status = runResolvePass(); + count++; + } while (status == null && ++count < 1000); + + // assert if status is null + this.diagnostics.checkNotNull(status, "cannot_resolve_all_entities"); + + // remember the resolver status + this.resolverStatus = status; + + // all has been resolved + return status; + } + + /** + * @return the principal we resolved + */ + public @NotNull EntityCacheEntry getResolvedCallerPrincipal() { + // can only be called if the resolver has been called and was success + this.diagnostics.checkNotNull(resolverStatus, "resolver_must_be_called_first"); + this.diagnostics.check( + resolverStatus.getStatus() == ResolverStatus.StatusEnum.SUCCESS, + "resolver_must_be_successful"); + + return resolvedCallerPrincipal; + } + + /** + * @return all principal roles which were activated. The list can be empty + */ + public @NotNull List getResolvedCallerPrincipalRoles() { + // can only be called if the resolver has been called and was success + this.diagnostics.checkNotNull(resolverStatus, "resolver_must_be_called_first"); + this.diagnostics.check( + resolverStatus.getStatus() == ResolverStatus.StatusEnum.SUCCESS, + "resolver_must_be_successful"); + + return resolvedCallerPrincipalRoles; + } + + /** + * @return the reference catalog which has been resolved. Will be null if null was passed in for + * the parameter referenceCatalogName when the Resolver was constructed. + */ + public @Nullable EntityCacheEntry getResolvedReferenceCatalog() { + // can only be called if the resolver has been called and was success + this.diagnostics.checkNotNull(resolverStatus, "resolver_must_be_called_first"); + this.diagnostics.check( + resolverStatus.getStatus() == ResolverStatus.StatusEnum.SUCCESS, + "resolver_must_be_successful"); + + return resolvedReferenceCatalog; + } + + /** + * Empty map if no catalog was resolved. Else the list of catalog roles which are activated by the + * caller + * + * @return map of activated catalog roles or null if no referenceCatalogName was specified + */ + public @Nullable Map getResolvedCatalogRoles() { + // can only be called if the resolver has been called and was success + this.diagnostics.checkNotNull(resolverStatus, "resolver_must_be_called_first"); + this.diagnostics.check( + resolverStatus.getStatus() == ResolverStatus.StatusEnum.SUCCESS, + "resolver_must_be_successful"); + + return resolvedCatalogRoles; + } + + /** + * Get path which has been resolved, should be used only when a single path was added to the + * resolver. If the path to resolve was optional, only the prefix that was resolved will be + * returned. + * + * @return single resolved path + */ + public @NotNull List getResolvedPath() { + // can only be called if the resolver has been called and was success + this.diagnostics.checkNotNull(resolverStatus, "resolver_must_be_called_first"); + this.diagnostics.check( + resolverStatus.getStatus() == ResolverStatus.StatusEnum.SUCCESS, + "resolver_must_be_successful"); + this.diagnostics.check(this.resolvedPaths.size() == 1, "only_if_single"); + + return resolvedPaths.getFirst(); + } + + /** + * One of more resolved path, in the order they were added to the resolver. + * + * @return list of resolved path + */ + public @NotNull List> getResolvedPaths() { + // can only be called if the resolver has been called and was success + this.diagnostics.checkNotNull(resolverStatus, "resolver_must_be_called_first"); + this.diagnostics.check( + resolverStatus.getStatus() == ResolverStatus.StatusEnum.SUCCESS, + "resolver_must_be_successful"); + this.diagnostics.check(!this.resolvedPaths.isEmpty(), "no_path_resolved"); + + return resolvedPaths; + } + + /** + * Get resolved entity associated to the specified type and name or null if not found + * + * @param entityType type of the entity, cannot be a NAMESPACE or a TABLE_LIKE entity. If it is a + * top-level catalog entity (i.e. CATALOG_ROLE), a reference catalog must have been specified + * at construction time. + * @param entityName name of the entity. + * @return the entity which has been resolved or null if that entity does not exist + */ + public @Nullable EntityCacheEntry getResolvedEntity( + @NotNull PolarisEntityType entityType, @NotNull String entityName) { + // can only be called if the resolver has been called and was success + this.diagnostics.checkNotNull(resolverStatus, "resolver_must_be_called_first"); + this.diagnostics.check( + resolverStatus.getStatus() == ResolverStatus.StatusEnum.SUCCESS, + "resolver_must_be_successful"); + + // validate input + diagnostics.check( + entityType != PolarisEntityType.NAMESPACE && entityType != PolarisEntityType.TABLE_LIKE, + "cannot_be_path"); + diagnostics.check( + entityType.isTopLevel() || this.referenceCatalogName != null, "reference_catalog_expected"); + + if (entityType.isTopLevel()) { + return this.resolvedEntriesByName.get(new EntityCacheByNameKey(entityType, entityName)); + } else { + long catalogId = this.resolvedReferenceCatalog.getEntity().getId(); + return this.resolvedEntriesByName.get( + new EntityCacheByNameKey(catalogId, catalogId, entityType, entityName)); + } + } + + /** + * Execute one resolve pass on all entities + * + * @return status of the resolve pass + */ + private ResolverStatus runResolvePass() { + + // we will resolve those again + this.resolvedCallerPrincipal = null; + this.resolvedReferenceCatalog = null; + if (this.resolvedCatalogRoles != null) { + this.resolvedCatalogRoles.clear(); + } + this.resolvedCallerPrincipalRoles.clear(); + this.resolvedPaths.clear(); + + // all entries we found in the cache but that we need to validate since they might be stale + List toValidate = new ArrayList<>(); + + // first resolve the principal and determine the set of activated principal roles + ResolverStatus status = + this.resolveCallerPrincipalAndPrincipalRoles( + toValidate, + this.callerPrincipalId, + this.callerPrincipalName, + this.callerPrincipalRoleNamesScope); + + // if success, continue resolving + if (status.getStatus() == ResolverStatus.StatusEnum.SUCCESS) { + // then resolve the reference catalog if one was specified + if (this.referenceCatalogName != null) { + status = this.resolveReferenceCatalog(toValidate, this.referenceCatalogName); + } + + // if success, continue resolving + if (status.getStatus() == ResolverStatus.StatusEnum.SUCCESS) { + // then resolve all the additional entities we were asked to resolve + status = this.resolveEntities(toValidate, this.entitiesToResolve); + + // if success, continue resolving + if (status.getStatus() == ResolverStatus.StatusEnum.SUCCESS + && this.referenceCatalogName != null) { + // finally, resolve all paths we need to resolve + status = this.resolvePaths(toValidate, this.pathsToResolve); + } + } + } + + // all the above resolution was optimistic i.e. when we probe the cache and find an entity, we + // don't validate if this entity has been changed in the backend. So validate now all these + // entities in one single + // go, + boolean validationSuccess = this.bulkValidate(toValidate); + + if (validationSuccess) { + this.updateResolved(); + } + + // if success, we are done, simply return the status. + return validationSuccess ? status : null; + } + + /** + * Update all entities which have been resolved since after validation, some might have changed + */ + private void updateResolved() { + + // if success, we need to get the validated entries + // we will resolve those again + this.resolvedCallerPrincipal = this.getResolved(this.resolvedCallerPrincipal); + + // update all principal roles with latest + if (!this.resolvedCallerPrincipalRoles.isEmpty()) { + List refreshedResolvedCallerPrincipalRoles = + new ArrayList<>(this.resolvedCallerPrincipalRoles.size()); + this.resolvedCallerPrincipalRoles.forEach( + ce -> refreshedResolvedCallerPrincipalRoles.add(this.getResolved(ce))); + this.resolvedCallerPrincipalRoles = refreshedResolvedCallerPrincipalRoles; + } + + // update referenced catalog + this.resolvedReferenceCatalog = this.getResolved(this.resolvedReferenceCatalog); + + // update all resolved catalog roles + if (this.resolvedCatalogRoles != null) { + for (EntityCacheEntry catalogCacheEntry : this.resolvedCatalogRoles.values()) { + this.resolvedCatalogRoles.put( + catalogCacheEntry.getEntity().getId(), this.getResolved(catalogCacheEntry)); + } + } + + // update all resolved paths + if (!this.resolvedPaths.isEmpty()) { + List> refreshedResolvedPaths = + new ArrayList<>(this.resolvedPaths.size()); + this.resolvedPaths.forEach( + rp -> { + List refreshedRp = new ArrayList<>(rp.size()); + rp.forEach(ce -> refreshedRp.add(this.getResolved(ce))); + refreshedResolvedPaths.add(refreshedRp); + }); + this.resolvedPaths = refreshedResolvedPaths; + } + } + + /** + * Get the fully resolved cache entry for the specified cache entry + * + * @param cacheEntry input cache entry + * @return the fully resolved cached entry which will often be the same + */ + private EntityCacheEntry getResolved(EntityCacheEntry cacheEntry) { + final EntityCacheEntry refreshedEntry; + if (cacheEntry == null) { + refreshedEntry = null; + } else { + // the latest refreshed entry + refreshedEntry = this.resolvedEntriesById.get(cacheEntry.getEntity().getId()); + this.diagnostics.checkNotNull( + refreshedEntry, "cache_entry_should_be_resolved", "entity={}", cacheEntry.getEntity()); + } + return refreshedEntry; + } + + /** + * Bulk validate now the set of entities we didn't validate when we were accessing the entity + * cache + * + * @param toValidate entities to validate + * @return true if none of the entities in the cache has changed + */ + private boolean bulkValidate(List toValidate) { + // assume everything is good + boolean validationStatus = true; + + // bulk validate + if (!toValidate.isEmpty()) { + List entityIds = + toValidate.stream() + .map( + cacheEntry -> + new PolarisEntityId( + cacheEntry.getEntity().getCatalogId(), cacheEntry.getEntity().getId())) + .collect(Collectors.toList()); + + // now get the current backend versions of all these entities + PolarisMetaStoreManager.ChangeTrackingResult changeTrackingResult = + this.metaStoreManager.loadEntitiesChangeTracking(this.polarisCallContext, entityIds); + + // refresh any entity which is not fresh. If an entity is missing, reload it + Iterator entityIterator = toValidate.iterator(); + Iterator versionIterator = + changeTrackingResult.getChangeTrackingVersions().iterator(); + + // determine the ones we need to reload or refresh and the ones which are up-to-date + while (entityIterator.hasNext()) { + // get cache entry and associated versions + EntityCacheEntry cacheEntry = entityIterator.next(); + PolarisChangeTrackingVersions versions = versionIterator.next(); + + // entity we found in the cache + PolarisBaseEntity entity = cacheEntry.getEntity(); + + // refresh cache entry if the entity or grant records version is different + final EntityCacheEntry refreshedCacheEntry; + if (versions == null + || entity.getEntityVersion() != versions.getEntityVersion() + || entity.getGrantRecordsVersion() != versions.getGrantRecordsVersion()) { + // if null version we need to invalidate the cached entry since it has probably been + // dropped + if (versions == null) { + this.cache.removeCacheEntry(cacheEntry); + refreshedCacheEntry = null; + } else { + // refresh that entity. If versions is null, it has been dropped + refreshedCacheEntry = + this.cache.getAndRefreshIfNeeded( + this.polarisCallContext, + entity, + versions.getEntityVersion(), + versions.getGrantRecordsVersion()); + } + + // get the refreshed entity + PolarisBaseEntity refreshedEntity = + (refreshedCacheEntry == null) ? null : refreshedCacheEntry.getEntity(); + + // if the entity has been removed, or its name has changed, or it was re-parented, or it + // was dropped, we will have to perform another pass + if (refreshedEntity == null + || refreshedEntity.getParentId() != entity.getParentId() + || refreshedEntity.isDropped() != entity.isDropped() + || !refreshedEntity.getName().equals(entity.getName())) { + validationStatus = false; + } + + // special cases: the set of principal roles or catalog roles which have been + // activated might change if usage grants to a principal or a principal role have + // changed. Hence, force another pass if we are in that scenario + if (entity.getTypeCode() == PolarisEntityType.PRINCIPAL.getCode() + || entity.getTypeCode() == PolarisEntityType.PRINCIPAL_ROLE.getCode()) { + validationStatus = false; + } + } else { + // no need to refresh, it is up-to-date + refreshedCacheEntry = cacheEntry; + } + + // if it was found, it has been resolved, so if there is another pass, we will not have to + // resolve it again + if (refreshedCacheEntry != null) { + this.addToResolved(refreshedCacheEntry); + } + } + } + + // done, return final validation status + return validationStatus; + } + + /** + * Resolve a set of top-level service or catalog entities + * + * @param toValidate all entities we have resolved from the cache, hence we will have to verify + * that these entities have not changed in the backend + * @param entitiesToResolve the set of entities to resolve + * @return the status of resolution + */ + private ResolverStatus resolveEntities( + List toValidate, AbstractSet entitiesToResolve) { + // resolve each + for (ResolverEntityName entityName : entitiesToResolve) { + // resolve that entity + EntityCacheEntry resolvedEntity = + this.resolveByName(toValidate, entityName.getEntityType(), entityName.getEntityName()); + + // if not found, we can exit unless the entity is optional + if (!entityName.isOptional() + && (resolvedEntity == null || resolvedEntity.getEntity().isDropped())) { + return new ResolverStatus(entityName.getEntityType(), entityName.getEntityName()); + } + } + + // complete success + return new ResolverStatus(ResolverStatus.StatusEnum.SUCCESS); + } + + /** + * Resolve a set of path inside the referenced catalog + * + * @param toValidate all entities we have resolved from the cache, hence we will have to verify + * that these entities have not changed in the backend + * @param pathsToResolve the set of paths to resolve + * @return the status of resolution + */ + private ResolverStatus resolvePaths( + List toValidate, List pathsToResolve) { + + // id of the catalog for all these paths + final long catalogId = this.resolvedReferenceCatalog.getEntity().getId(); + + // resolve each path + for (ResolverPath path : pathsToResolve) { + + // path we are resolving + List resolvedPath = new ArrayList<>(); + + // initial parent id is the catalog itself + long parentId = catalogId; + + // resolve each segment + Iterator pathIt = path.getEntityNames().iterator(); + for (int segmentIndex = 0; segmentIndex < path.getEntityNames().size(); segmentIndex++) { + // get segment name + String segmentName = pathIt.next(); + + // determine the segment type + PolarisEntityType segmentType = + pathIt.hasNext() ? PolarisEntityType.NAMESPACE : path.getLastEntityType(); + + // resolve that entity + EntityCacheEntry segment = + this.resolveByName(toValidate, catalogId, segmentType, parentId, segmentName); + + // if not found, abort + if (segment == null || segment.getEntity().isDropped()) { + if (path.isOptional()) { + // we have resolved as much as what we could have + break; + } else { + return new ResolverStatus(path, segmentIndex); + } + } + + // this is the parent of the next segment + parentId = segment.getEntity().getId(); + + // add it to the path we are resolving + resolvedPath.add(segment); + } + + // one more path has been resolved + this.resolvedPaths.add(resolvedPath); + } + + // complete success + return new ResolverStatus(ResolverStatus.StatusEnum.SUCCESS); + } + + /** + * Resolve the principal and determine which principal roles are activated. Resolved those. + * + * @param toValidate all entities we have resolved from the cache, hence we will have to verify + * that these entities have not changed in the backend + * @param callerPrincipalId the id of the principal which made the call + * @param callerPrincipalRoleNamesScope if not null, subset of roles activated by this call + * @return the status of resolution + */ + private ResolverStatus resolveCallerPrincipalAndPrincipalRoles( + List toValidate, + long callerPrincipalId, + String callerPrincipalName, + Set callerPrincipalRoleNamesScope) { + + // resolve the principal, by name or id + this.resolvedCallerPrincipal = + (callerPrincipalId != PolarisEntityConstants.getNullId()) + ? this.resolveById( + toValidate, + PolarisEntityType.PRINCIPAL, + PolarisEntityConstants.getNullId(), + callerPrincipalId) + : this.resolveByName(toValidate, PolarisEntityType.PRINCIPAL, callerPrincipalName); + + // if the principal was not found, we can end right there + if (this.resolvedCallerPrincipal == null + || this.resolvedCallerPrincipal.getEntity().isDropped()) { + return new ResolverStatus(ResolverStatus.StatusEnum.CALLER_PRINCIPAL_DOES_NOT_EXIST); + } + + // activate all principal roles which still exist + for (PolarisGrantRecord grantRecord : this.resolvedCallerPrincipal.getGrantRecordsAsGrantee()) { + if (grantRecord.getPrivilegeCode() == PolarisPrivilege.PRINCIPAL_ROLE_USAGE.getCode()) { + + // resolve principal role granted to that principal + EntityCacheEntry principalRole = + this.resolveById( + toValidate, + PolarisEntityType.PRINCIPAL_ROLE, + PolarisEntityConstants.getNullId(), + grantRecord.getSecurableId()); + + // skip if purged or has been dropped + if (principalRole != null && !principalRole.getEntity().isDropped()) { + // add it to the activated list if no scoped principal role or this principal role is + // activated + if (callerPrincipalRoleNamesScope == null + || callerPrincipalRoleNamesScope.contains(principalRole.getEntity().getName())) { + // this principal role is activated + this.resolvedCallerPrincipalRoles.add(principalRole); + } + } + } + } + + // total success + return new ResolverStatus(ResolverStatus.StatusEnum.SUCCESS); + } + + /** + * Resolve the reference catalog and determine all activated role. The principal and principal + * roles should have already been resolved + * + * @param toValidate all entities we have resolved from the cache, hence we will have to verify + * that these entities have not changed in the backend + * @param referenceCatalogName name of the reference catalog to resolve, along with all catalog + * roles which are activated + * @return the status of resolution + */ + private ResolverStatus resolveReferenceCatalog( + @NotNull List toValidate, @NotNull String referenceCatalogName) { + // resolve the catalog + this.resolvedReferenceCatalog = + this.resolveByName(toValidate, PolarisEntityType.CATALOG, referenceCatalogName); + + // error out if we couldn't find it + if (this.resolvedReferenceCatalog == null + || this.resolvedReferenceCatalog.getEntity().isDropped()) { + return new ResolverStatus(PolarisEntityType.CATALOG, this.referenceCatalogName); + } + + // determine the set of catalog roles which have been activated + long catalogId = this.resolvedReferenceCatalog.getEntity().getId(); + for (EntityCacheEntry principalRole : resolvedCallerPrincipalRoles) { + for (PolarisGrantRecord grantRecord : principalRole.getGrantRecordsAsGrantee()) { + // the securable is a catalog role belonging to + if (grantRecord.getPrivilegeCode() == PolarisPrivilege.CATALOG_ROLE_USAGE.getCode() + && grantRecord.getSecurableCatalogId() == catalogId) { + // the id of the catalog role + long catalogRoleId = grantRecord.getSecurableId(); + + // skip if it has already been added + if (!this.resolvedCatalogRoles.containsKey(catalogRoleId)) { + // see if this catalog can be resolved + EntityCacheEntry catalogRole = + this.resolveById( + toValidate, PolarisEntityType.CATALOG_ROLE, catalogId, catalogRoleId); + + // if found and not dropped, add it to the list of activated catalog roles + if (catalogRole != null && !catalogRole.getEntity().isDropped()) { + this.resolvedCatalogRoles.put(catalogRoleId, catalogRole); + } + } + } + } + } + + // all good + return new ResolverStatus(ResolverStatus.StatusEnum.SUCCESS); + } + + /** + * Add a cache entry to the set of resolved entities + * + * @param refreshedCacheEntry refreshed cache entry + */ + private void addToResolved(EntityCacheEntry refreshedCacheEntry) { + // underlying entity + PolarisBaseEntity entity = refreshedCacheEntry.getEntity(); + + // add it by ID + this.resolvedEntriesById.put(entity.getId(), refreshedCacheEntry); + + // in the by name map, only add it if it has not been dropped + if (!entity.isDropped()) { + this.resolvedEntriesByName.put( + new EntityCacheByNameKey( + entity.getCatalogId(), entity.getParentId(), entity.getType(), entity.getName()), + refreshedCacheEntry); + } + } + + /** + * Add a top-level entity to resolve. If the entity type is a catalog role, we also expect that a + * reference catalog entity was specified at creation time, else we will assert. That catalog role + * entity will be resolved from there. We will fail the entire resolution process if that entity + * cannot be resolved. If this is not expected, use addOptionalEntityByName() instead. + * + * @param entityType the type of the entity, either a principal, a principal role, a catalog or a + * catalog role. + * @param entityName the name of the entity + * @param optional if true, the entity is optional + */ + private void addEntityByName( + @NotNull PolarisEntityType entityType, @NotNull String entityName, boolean optional) { + + // can only be called if the resolver has not yet been called + this.diagnostics.check(resolverStatus == null, "resolver_called"); + + // ensure everything was specified + diagnostics.checkNotNull(entityType, "unexpected_null_entity_type"); + diagnostics.checkNotNull(entityName, "unexpected_null_entity_name"); + + // ensure that a reference catalog has been specified if this entity is a catalog role + diagnostics.check( + entityType != PolarisEntityType.CATALOG_ROLE || this.referenceCatalogName != null, + "reference_catalog_must_be_specified"); + + // one more to resolve + this.entitiesToResolve.add(new ResolverEntityName(entityType, entityName, optional)); + } + + /** + * Resolve a top-level entity by name + * + * @param toValidate set of entries we will have to validate + * @param entityType entity type + * @param entityName name of the entity to resolve + * @return cache entry created for that entity + */ + private EntityCacheEntry resolveByName( + List toValidate, PolarisEntityType entityType, String entityName) { + if (entityType.isTopLevel()) { + return this.resolveByName( + toValidate, + PolarisEntityConstants.getNullId(), + entityType, + PolarisEntityConstants.getNullId(), + entityName); + } else { + // only top-level catalog entity + long catalogId = this.resolvedReferenceCatalog.getEntity().getId(); + this.diagnostics.check(entityType == PolarisEntityType.CATALOG_ROLE, "catalog_role_expected"); + return this.resolveByName(toValidate, catalogId, entityType, catalogId, entityName); + } + } + + /** + * Resolve a top-level entity by name + * + * @param toValidate (IN/OUT) list of entities we will have to validate + * @param entityType entity type + * @param entityName name of the entity to resolve + * @return the resolve entity. Potentially update the toValidate list if we will have to validate + * that this entity is up-to-date + */ + private EntityCacheEntry resolveByName( + @NotNull List toValidate, + long catalogId, + @NotNull PolarisEntityType entityType, + long parentId, + @NotNull String entityName) { + + // key for that entity + EntityCacheByNameKey nameKey = + new EntityCacheByNameKey(catalogId, parentId, entityType, entityName); + + // first check if this entity has not yet been resolved + EntityCacheEntry cacheEntry = this.resolvedEntriesByName.get(nameKey); + if (cacheEntry != null) { + return cacheEntry; + } + + // then check if it does not exist in the toValidate list. The same entity might be resolved + // several times with multi-path resolution + for (EntityCacheEntry ce : toValidate) { + PolarisBaseEntity entity = ce.getEntity(); + if (entity.getCatalogId() == catalogId + && entity.getParentId() == parentId + && entity.getType() == entityType + && entity.getName().equals(entityName)) { + return ce; + } + } + + // get or load by name + EntityCacheLookupResult lookupResult = + this.cache.getOrLoadEntityByName( + this.polarisCallContext, + new EntityCacheByNameKey(catalogId, parentId, entityType, entityName)); + + // if not found + if (lookupResult == null) { + // not found + return null; + } else if (lookupResult.isCacheHit()) { + // found in the cache, we will have to validate this entity + toValidate.add(lookupResult.getCacheEntry()); + } else { + // entry cannot be null + this.diagnostics.checkNotNull(lookupResult.getCacheEntry(), "cache_entry_is_null"); + // if not found in cache, it was loaded from backend, hence it has been resolved + this.addToResolved(lookupResult.getCacheEntry()); + } + + // return the cache entry + return lookupResult.getCacheEntry(); + } + + /** + * Resolve an entity by id + * + * @param toValidate (IN/OUT) list of entities we will have to validate + * @param entityType type of the entity to resolve + * @param catalogId entity catalog id + * @param entityId entity id + * @return the resolve entity. Potentially update the toValidate list if we will have to validate + * that this entity is up-to-date + */ + private EntityCacheEntry resolveById( + @NotNull List toValidate, + @NotNull PolarisEntityType entityType, + long catalogId, + long entityId) { + // get or load by name + EntityCacheLookupResult lookupResult = + this.cache.getOrLoadEntityById(this.polarisCallContext, catalogId, entityId); + + // if not found, return null + if (lookupResult == null) { + return null; + } else if (lookupResult.isCacheHit()) { + // found in the cache, we will have to validate this entity + toValidate.add(lookupResult.getCacheEntry()); + } else { + // entry cannot be null + this.diagnostics.checkNotNull(lookupResult.getCacheEntry(), "cache_entry_is_null"); + + // if not found in cache, it was loaded from backend, hence it has been resolved + this.addToResolved(lookupResult.getCacheEntry()); + } + + // return the cache entry + return lookupResult.getCacheEntry(); + } +} diff --git a/polaris-core/src/main/java/io/polaris/core/persistence/resolver/ResolverEntityName.java b/polaris-core/src/main/java/io/polaris/core/persistence/resolver/ResolverEntityName.java new file mode 100644 index 0000000000..e808e14d99 --- /dev/null +++ b/polaris-core/src/main/java/io/polaris/core/persistence/resolver/ResolverEntityName.java @@ -0,0 +1,49 @@ +package io.polaris.core.persistence.resolver; + +import io.polaris.core.entity.PolarisEntityType; +import java.util.Objects; + +/** Simple class to represent the name of an entity to resolve */ +public class ResolverEntityName { + + // type of the entity + private final PolarisEntityType entityType; + + // the name of the entity + private final String entityName; + + // true if we should not fail while resolving this entity + private final boolean isOptional; + + public ResolverEntityName(PolarisEntityType entityType, String entityName, boolean isOptional) { + this.entityType = entityType; + this.entityName = entityName; + this.isOptional = isOptional; + } + + public PolarisEntityType getEntityType() { + return entityType; + } + + public String getEntityName() { + return entityName; + } + + public boolean isOptional() { + return isOptional; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ResolverEntityName that = (ResolverEntityName) o; + return getEntityType() == that.getEntityType() + && Objects.equals(getEntityName(), that.getEntityName()); + } + + @Override + public int hashCode() { + return Objects.hash(getEntityType(), getEntityName()); + } +} diff --git a/polaris-core/src/main/java/io/polaris/core/persistence/resolver/ResolverPath.java b/polaris-core/src/main/java/io/polaris/core/persistence/resolver/ResolverPath.java new file mode 100644 index 0000000000..66518fc3c4 --- /dev/null +++ b/polaris-core/src/main/java/io/polaris/core/persistence/resolver/ResolverPath.java @@ -0,0 +1,70 @@ +package io.polaris.core.persistence.resolver; + +import com.google.common.collect.ImmutableList; +import io.polaris.core.entity.PolarisEntityType; +import java.util.List; + +/** Simple class to represent a path within a catalog */ +public class ResolverPath { + + // name of the entities in that path. The parent of the first named entity is the path is the + // catalog + private final List entityNames; + + // all entities in a path are namespaces except the last one which can be a table_like entity + // versus a namespace + private final PolarisEntityType lastEntityType; + + // true if this path is optional, i.e. failing to fully resolve it is not an error + private final boolean isOptional; + + /** + * Constructor for an optional path + * + * @param entityNames set of entity names, all are namespaces except the last one which is either + * a namespace or a table_like entity + * @param lastEntityType type of the last entity, either namespace or table_like + */ + public ResolverPath(List entityNames, PolarisEntityType lastEntityType) { + this(entityNames, lastEntityType, false); + } + + /** + * Constructor for an optional path + * + * @param entityNames set of entity names, all are namespaces except the last one which is either + * a namespace or a table_like entity + * @param lastEntityType type of the last entity, either namespace or table_like + * @param isOptional true if optional + */ + public ResolverPath( + List entityNames, PolarisEntityType lastEntityType, boolean isOptional) { + this.entityNames = ImmutableList.copyOf(entityNames); + this.lastEntityType = lastEntityType; + this.isOptional = isOptional; + } + + public List getEntityNames() { + return entityNames; + } + + public PolarisEntityType getLastEntityType() { + return lastEntityType; + } + + public boolean isOptional() { + return isOptional; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("entityNames:"); + sb.append(entityNames.toString()); + sb.append(";lastEntityType:"); + sb.append(lastEntityType.toString()); + sb.append(";isOptional:"); + sb.append(isOptional); + return sb.toString(); + } +} diff --git a/polaris-core/src/main/java/io/polaris/core/persistence/resolver/ResolverPrincipalRole.java b/polaris-core/src/main/java/io/polaris/core/persistence/resolver/ResolverPrincipalRole.java new file mode 100644 index 0000000000..415b4b3620 --- /dev/null +++ b/polaris-core/src/main/java/io/polaris/core/persistence/resolver/ResolverPrincipalRole.java @@ -0,0 +1,8 @@ +package io.polaris.core.persistence.resolver; + +/** Expected principal type for the principal. Expectation depends on the REST request type */ +public enum ResolverPrincipalRole { + ANY_PRINCIPAL, + CATALOG_ADMIN_PRINCIPAL, + SERVICE_ADMIN_PRINCIPAL +} diff --git a/polaris-core/src/main/java/io/polaris/core/persistence/resolver/ResolverStatus.java b/polaris-core/src/main/java/io/polaris/core/persistence/resolver/ResolverStatus.java new file mode 100644 index 0000000000..d144cb01f2 --- /dev/null +++ b/polaris-core/src/main/java/io/polaris/core/persistence/resolver/ResolverStatus.java @@ -0,0 +1,88 @@ +package io.polaris.core.persistence.resolver; + +import io.polaris.core.entity.PolarisEntityType; + +public class ResolverStatus { + + /** + * Status code for the caller to know if all entities were resolved successfully or if resolution + * failed. Anything but success is a failure + */ + public enum StatusEnum { + // success + SUCCESS, + + // error, principal making the call does not exist + CALLER_PRINCIPAL_DOES_NOT_EXIST, + + // error, the path could not be resolved. The payload of the status will provide the path and + // the index in that + // path for the segment of the path which could not be resolved + PATH_COULD_NOT_BE_FULLY_RESOLVED, + + // error, an entity could not be resolved + ENTITY_COULD_NOT_BE_RESOLVED, + }; + + private final StatusEnum status; + + // if status is ENTITY_COULD_NOT_BE_RESOLVED, will be set to the entity type which couldn't be + // resolved + private final PolarisEntityType failedToResolvedEntityType; + + // if status is ENTITY_COULD_NOT_BE_RESOLVED, will be set to the entity name which couldn't be + // resolved + private final String failedToResolvedEntityName; + + // if status is PATH_COULD_NOT_BE_FULLY_RESOLVED, path which we failed to resolve + private final ResolverPath failedToResolvePath; + + // if status is PATH_COULD_NOT_BE_FULLY_RESOLVED, index in the path which we failed to + // resolve + private final int failedToResolvedEntityIndex; + + public ResolverStatus(StatusEnum status) { + this.status = status; + this.failedToResolvedEntityType = null; + this.failedToResolvedEntityName = null; + this.failedToResolvePath = null; + this.failedToResolvedEntityIndex = 0; + } + + public ResolverStatus( + PolarisEntityType failedToResolvedEntityType, String failedToResolvedEntityName) { + this.status = StatusEnum.ENTITY_COULD_NOT_BE_RESOLVED; + this.failedToResolvedEntityType = failedToResolvedEntityType; + this.failedToResolvedEntityName = failedToResolvedEntityName; + this.failedToResolvePath = null; + this.failedToResolvedEntityIndex = 0; + } + + public ResolverStatus(ResolverPath failedToResolvePath, int failedToResolvedEntityIndex) { + this.status = StatusEnum.PATH_COULD_NOT_BE_FULLY_RESOLVED; + this.failedToResolvedEntityType = null; + this.failedToResolvedEntityName = null; + this.failedToResolvePath = failedToResolvePath; + this.failedToResolvedEntityIndex = failedToResolvedEntityIndex; + } + + public StatusEnum getStatus() { + return status; + } + + public PolarisEntityType getFailedToResolvedEntityType() { + return failedToResolvedEntityType; + } + + public String getFailedToResolvedEntityName() { + return failedToResolvedEntityName; + } + + public ResolverPath getFailedToResolvePath() { + return failedToResolvePath; + } + + public int getFailedToResolvedEntityIndex() { + return failedToResolvedEntityIndex; + } +} diff --git a/polaris-core/src/main/java/io/polaris/core/storage/FileStorageConfigurationInfo.java b/polaris-core/src/main/java/io/polaris/core/storage/FileStorageConfigurationInfo.java new file mode 100644 index 0000000000..f8b7f1c0cb --- /dev/null +++ b/polaris-core/src/main/java/io/polaris/core/storage/FileStorageConfigurationInfo.java @@ -0,0 +1,38 @@ +package io.polaris.core.storage; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import org.jetbrains.annotations.NotNull; + +/** + * Support for file:// URLs in storage configuration. This is pretty-much only used for testing. + * Supports URLs that start with file:// or /, but also supports wildcard (*) to support certain + * test cases. + */ +public class FileStorageConfigurationInfo extends PolarisStorageConfigurationInfo { + + public FileStorageConfigurationInfo( + @JsonProperty(value = "allowedLocations", required = true) @NotNull + List allowedLocations) { + super(StorageType.FILE, allowedLocations); + } + + @Override + public String getFileIoImplClassName() { + return "org.apache.iceberg.hadoop.HadoopFileIO"; + } + + @Override + public void validatePrefixForStorageType() { + this.allowedLocations.forEach( + loc -> { + if (!loc.startsWith(storageType.getPrefix()) + && !loc.startsWith("/") + && !loc.equals("*")) { + throw new IllegalArgumentException( + String.format( + "Location prefix not allowed: '%s', expected prefix: file:// or / or *", loc)); + } + }); + } +} diff --git a/polaris-core/src/main/java/io/polaris/core/storage/InMemoryStorageIntegration.java b/polaris-core/src/main/java/io/polaris/core/storage/InMemoryStorageIntegration.java new file mode 100644 index 0000000000..7d389c69c2 --- /dev/null +++ b/polaris-core/src/main/java/io/polaris/core/storage/InMemoryStorageIntegration.java @@ -0,0 +1,128 @@ +package io.polaris.core.storage; + +import io.polaris.core.context.CallContext; +import java.util.HashMap; +import java.util.Map; +import java.util.NavigableSet; +import java.util.Optional; +import java.util.Set; +import java.util.TreeSet; +import java.util.function.Function; +import java.util.stream.Collectors; +import org.jetbrains.annotations.NotNull; + +/** + * Base class for in-memory implementations of {@link PolarisStorageIntegration}. A basic + * implementation of {@link #validateAccessToLocations(PolarisStorageConfigurationInfo, Set, Set)} + * is provided that checks to see that the list of locations being accessed is among the list of + * {@link PolarisStorageConfigurationInfo#allowedLocations}. Locations being accessed must be equal + * to or a subdirectory of at least one of the allowed locations. + * + * @param + */ +public abstract class InMemoryStorageIntegration + extends PolarisStorageIntegration { + + public InMemoryStorageIntegration(String identifierOrId) { + super(identifierOrId); + } + + /** + * Check that the locations being accessed are all equal to or subdirectories of at least one of + * the {@link PolarisStorageConfigurationInfo#allowedLocations}. + * + * @param storageConfig + * @param actions a set of operation actions to validate, like LIST/READ/DELETE/WRITE/ALL + * @param locations a set of locations to get access to + * @return a map of location to a validation result for each action passed in. In this + * implementation, all actions have the same validation result, as we only verify the + * locations are equal to or subdirectories of the allowed locations. + */ + public static Map> + validateSubpathsOfAllowedLocations( + @NotNull PolarisStorageConfigurationInfo storageConfig, + @NotNull Set actions, + @NotNull Set locations) { + // trim trailing / from allowed locations so that locations missing the trailing slash still + // match + // TODO: Canonicalize with URI and compare scheme/authority/path components separately + TreeSet allowedLocations = + storageConfig.getAllowedLocations().stream() + .map( + str -> { + if (str.endsWith("/") && str.length() > 1) { + return str.substring(0, str.length() - 1); + } else { + return str; + } + }) + .map(str -> str.replace("file:///", "file:/")) + .collect(Collectors.toCollection(TreeSet::new)); + boolean allowWildcardLocation = + Optional.ofNullable(CallContext.getCurrentContext()) + .flatMap(c -> Optional.ofNullable(c.getPolarisCallContext())) + .map( + pc -> + pc.getConfigurationStore() + .getConfiguration(pc, "ALLOW_WILDCARD_LOCATION", false)) + .orElse(false); + + if (allowWildcardLocation && allowedLocations.contains("*")) { + return locations.stream() + .collect( + Collectors.toMap( + Function.identity(), + loc -> + actions.stream() + .collect( + Collectors.toMap( + Function.identity(), + a -> + new ValidationResult( + true, loc + " in the list of allowed locations"))))); + } + Map> resultMap = new HashMap<>(); + for (String rawLocation : locations) { + String location = rawLocation.replace("file:///", "file:/"); + StringBuilder builder = new StringBuilder(); + NavigableSet prefixes = allowedLocations; + boolean validLocation = false; + for (char c : location.toCharArray()) { + builder.append(c); + prefixes = allowedLocations.tailSet(builder.toString(), true); + if (prefixes.isEmpty()) { + break; + } else if (prefixes.first().equals(builder.toString())) { + validLocation = true; + break; + } + } + final boolean isValidLocation = validLocation; + Map locationResult = + actions.stream() + .collect( + Collectors.toMap( + Function.identity(), + a -> + new ValidationResult( + isValidLocation, + rawLocation + + " is " + + (isValidLocation ? "" : "not ") + + "in the list of allowed locations: " + + allowedLocations))); + + resultMap.put(rawLocation, locationResult); + } + return resultMap; + } + + @Override + @NotNull + public Map> validateAccessToLocations( + @NotNull T storageConfig, + @NotNull Set actions, + @NotNull Set locations) { + return validateSubpathsOfAllowedLocations(storageConfig, actions, locations); + } +} diff --git a/polaris-core/src/main/java/io/polaris/core/storage/PolarisCredentialProperty.java b/polaris-core/src/main/java/io/polaris/core/storage/PolarisCredentialProperty.java new file mode 100644 index 0000000000..61f53967b2 --- /dev/null +++ b/polaris-core/src/main/java/io/polaris/core/storage/PolarisCredentialProperty.java @@ -0,0 +1,43 @@ +package io.polaris.core.storage; + +/** Enum of polaris supported credential properties */ +public enum PolarisCredentialProperty { + AWS_KEY_ID(String.class, "s3.access-key-id", "the aws access key id"), + AWS_SECRET_KEY(String.class, "s3.secret-access-key", "the aws access key secret"), + AWS_TOKEN(String.class, "s3.session-token", "the aws scoped access token"), + + GCS_ACCESS_TOKEN(String.class, "gcs.oauth2.token", "the gcs scoped access token"), + GCS_ACCESS_TOKEN_EXPIRES_AT( + String.class, + "gcs.oauth2.token-expires-at", + "the time the gcs access token expires, in milliseconds"), + + // Currently not using ACCESS TOKEN as the ResolvingFileIO is using ADLSFileIO for azure case and + // it expects for SAS + AZURE_ACCESS_TOKEN(String.class, "", "the azure scoped access token"), + AZURE_SAS_TOKEN(String.class, "adls.sas-token.", "an azure shared access signature token"), + AZURE_ACCOUNT_HOST( + String.class, + "the azure storage account host", + "the azure account name + endpoint that will append to the ADLS_SAS_TOKEN_PREFIX"), + EXPIRATION_TIME(Long.class, "", "the expiration time for the access token, in milliseconds"); + + private final Class valueType; + private final String propertyName; + private final String description; + + /* + s3.access-key-id`: id for for credentials that provide access to the data in S3 + - `s3.secret-access-key`: secret for credentials that provide access to data in S3 + - `s3.session-token + */ + PolarisCredentialProperty(Class valueType, String propertyName, String description) { + this.valueType = valueType; + this.propertyName = propertyName; + this.description = description; + } + + public String getPropertyName() { + return propertyName; + } +} diff --git a/polaris-core/src/main/java/io/polaris/core/storage/PolarisStorageActions.java b/polaris-core/src/main/java/io/polaris/core/storage/PolarisStorageActions.java new file mode 100644 index 0000000000..a367c9b95b --- /dev/null +++ b/polaris-core/src/main/java/io/polaris/core/storage/PolarisStorageActions.java @@ -0,0 +1,20 @@ +package io.polaris.core.storage; + +public enum PolarisStorageActions { + READ, + WRITE, + LIST, + DELETE, + ALL, + ; + + /** check if the provided string is a valid action. */ + public static boolean isValidAction(String s) { + for (PolarisStorageActions action : PolarisStorageActions.values()) { + if (action.name().equalsIgnoreCase(s)) { + return true; + } + } + return false; + } +} diff --git a/polaris-core/src/main/java/io/polaris/core/storage/PolarisStorageConfigurationInfo.java b/polaris-core/src/main/java/io/polaris/core/storage/PolarisStorageConfigurationInfo.java new file mode 100644 index 0000000000..bc25d1febd --- /dev/null +++ b/polaris-core/src/main/java/io/polaris/core/storage/PolarisStorageConfigurationInfo.java @@ -0,0 +1,147 @@ +package io.polaris.core.storage; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.polaris.core.PolarisDiagnostics; +import io.polaris.core.storage.aws.AwsStorageConfigurationInfo; +import io.polaris.core.storage.azure.AzureStorageConfigurationInfo; +import io.polaris.core.storage.gcp.GcpStorageConfigurationInfo; +import java.util.List; +import org.jetbrains.annotations.NotNull; + +/** + * The polaris storage configuration information, is part of a polaris entity's internal property, + * that holds necessary information including + * + *
+ * 1. locations that allows polaris to get access to
+ * 2. cloud identity info that a service principle can request access token to the locations
+ * 
+ */
+@JsonTypeInfo(use = JsonTypeInfo.Id.NAME)
+@JsonSubTypes({
+  @JsonSubTypes.Type(value = AwsStorageConfigurationInfo.class),
+  @JsonSubTypes.Type(value = AzureStorageConfigurationInfo.class),
+  @JsonSubTypes.Type(value = GcpStorageConfigurationInfo.class),
+  @JsonSubTypes.Type(value = FileStorageConfigurationInfo.class),
+})
+public abstract class PolarisStorageConfigurationInfo {
+
+  // a list of allowed locations
+  public List allowedLocations;
+
+  // storage type
+  public StorageType storageType;
+
+  public PolarisStorageConfigurationInfo(
+      @JsonProperty(value = "storageType", required = true) @NotNull StorageType storageType,
+      @JsonProperty(value = "allowedLocations", required = true) @NotNull
+          List allowedLocations) {
+    this.allowedLocations = allowedLocations;
+    this.storageType = storageType;
+    this.validatePrefixForStorageType();
+  }
+
+  public List getAllowedLocations() {
+    return allowedLocations;
+  }
+
+  public StorageType getStorageType() {
+    return storageType;
+  }
+
+  private static final ObjectMapper DEFAULT_MAPPER;
+
+  static {
+    DEFAULT_MAPPER = new ObjectMapper();
+    DEFAULT_MAPPER.setSerializationInclusion(JsonInclude.Include.NON_NULL);
+    DEFAULT_MAPPER.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
+  }
+
+  public String serialize() {
+    try {
+      return DEFAULT_MAPPER.writeValueAsString(this);
+    } catch (JsonProcessingException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  /**
+   * Deserialize a json string into a PolarisStorageConfiguration object
+   *
+   * @param diagnostics the diagnostics instance
+   * @param jsonStr a json string
+   * @return the PolarisStorageConfiguration object
+   */
+  public static PolarisStorageConfigurationInfo deserialize(
+      @NotNull PolarisDiagnostics diagnostics, final @NotNull String jsonStr) {
+    try {
+      return DEFAULT_MAPPER.readValue(jsonStr, PolarisStorageConfigurationInfo.class);
+    } catch (JsonProcessingException exception) {
+      diagnostics.fail(
+          "fail_to_deserialize_storage_configuration", exception, "jsonStr={}", jsonStr);
+    }
+    return null;
+  }
+
+  /** Subclasses must provide the Iceberg FileIO impl associated with their type in this method. */
+  public abstract String getFileIoImplClassName();
+
+  /** Validate if the provided allowed locations are valid for the storage type */
+  public void validatePrefixForStorageType() {
+    this.allowedLocations.forEach(
+        loc -> {
+          if (!loc.toLowerCase().startsWith(storageType.prefix)) {
+            throw new IllegalArgumentException(
+                String.format(
+                    "Location prefix not allowed: '%s', expected prefix: '%s'",
+                    loc, storageType.prefix));
+          }
+        });
+  }
+
+  /** Validate the number of allowed locations not exceeding the max value. */
+  public void validateMaxAllowedLocations(int maxAllowedLocations) {
+    if (allowedLocations.size() > maxAllowedLocations) {
+      throw new IllegalArgumentException(
+          "Number of allowed locations exceeds " + maxAllowedLocations);
+    }
+  }
+
+  /** Polaris' storage type, each has a fixed prefix for its location */
+  public enum StorageType {
+    S3("s3://"),
+    AZURE("abfs"), // abfs or abfss
+    GCS("gs://"),
+    FILE("file://"),
+    ;
+
+    final String prefix;
+
+    StorageType(String prefix) {
+      this.prefix = prefix;
+    }
+
+    public String getPrefix() {
+      return prefix;
+    }
+  }
+
+  /** Enum property for describe storage integration for config purpose. */
+  public enum DescribeProperty {
+    STORAGE_PROVIDER,
+    STORAGE_ALLOWED_LOCATIONS,
+    STORAGE_AWS_ROLE_ARN,
+    STORAGE_AWS_IAM_USER_ARN,
+    STORAGE_AWS_EXTERNAL_ID,
+    STORAGE_GCP_SERVICE_ACCOUNT,
+    AZURE_TENANT_ID,
+    AZURE_CONSENT_URL,
+    AZURE_MULTI_TENANT_APP_NAME,
+  }
+}
diff --git a/polaris-core/src/main/java/io/polaris/core/storage/PolarisStorageIntegration.java b/polaris-core/src/main/java/io/polaris/core/storage/PolarisStorageIntegration.java
new file mode 100644
index 0000000000..7f83b13149
--- /dev/null
+++ b/polaris-core/src/main/java/io/polaris/core/storage/PolarisStorageIntegration.java
@@ -0,0 +1,131 @@
+package io.polaris.core.storage;
+
+import io.polaris.core.PolarisDiagnostics;
+import java.util.EnumMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Abstract of Polaris Storage Integration. It holds the reference to an object that having the
+ * service principle information
+ *
+ * @param  the concrete type of {@link PolarisStorageConfigurationInfo} this integration supports
+ */
+public abstract class PolarisStorageIntegration {
+
+  private final String integrationIdentifierOrId;
+
+  public PolarisStorageIntegration(String identifierOrId) {
+    this.integrationIdentifierOrId = identifierOrId;
+  }
+
+  public String getStorageIdentifierOrId() {
+    return integrationIdentifierOrId;
+  }
+
+  /**
+   * Subscope the creds against the allowed read and write locations.
+   *
+   * @param diagnostics the diagnostics service
+   * @param storageConfig storage configuration
+   * @param allowListOperation whether to allow LIST on all the provided allowed read/write
+   *     locations
+   * @param allowedReadLocations a set of allowed to read locations
+   * @param allowedWriteLocations a set of allowed to write locations
+   * @return An enum map including the scoped credentials
+   */
+  public abstract EnumMap getSubscopedCreds(
+      @NotNull PolarisDiagnostics diagnostics,
+      @NotNull T storageConfig,
+      boolean allowListOperation,
+      @NotNull Set allowedReadLocations,
+      @NotNull Set allowedWriteLocations);
+
+  /**
+   * Describe the configuration for the current storage integration.
+   *
+   * @param storageConfigInfo the configuration info provided by the user.
+   * @return an enum map
+   */
+  public abstract EnumMap
+      descPolarisStorageConfiguration(@NotNull PolarisStorageConfigurationInfo storageConfigInfo);
+
+  /**
+   * Validate access for the provided operation actions and locations.
+   *
+   * @param actions a set of operation actions to validate, like LIST/READ/DELETE/WRITE/ALL
+   * @param locations a set of locations to get access to
+   * @return A Map of string, representing the result of validation, the key value is . A validate result looks like this
+   *     
+   * {
+   *   "status" : "failure",
+   *   "actions" : {
+   *     "READ" : {
+   *       "message" : "The specified file was not found",
+   *       "status" : "failure"
+   *     },
+   *     "DELETE" : {
+   *       "message" : "One or more objects could not be deleted (Status Code: 200; Error Code: null)",
+   *       "status" : "failure"
+   *     },
+   *     "LIST" : {
+   *       "status" : "success"
+   *     },
+   *     "WRITE" : {
+   *       "message" : "Access Denied (Status Code: 403; Error Code: AccessDenied)",
+   *       "status" : "failure"
+   *     }
+   *   },
+   *   "message" : "Some of the integration checks failed. Check the Snowflake documentation for more information."
+   * }
+   * 
+ */ + @NotNull + public abstract Map> + validateAccessToLocations( + @NotNull T storageConfig, + @NotNull Set actions, + @NotNull Set locations); + + /** + * Result of calling {@link #validateAccessToLocations(PolarisStorageConfigurationInfo, Set, Set)} + */ + public static final class ValidationResult { + private final boolean success; + private final String message; + + public ValidationResult(boolean success, String message) { + this.success = success; + this.message = message; + } + + public boolean isSuccess() { + return success; + } + + public String getMessage() { + return message; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof ValidationResult)) return false; + ValidationResult that = (ValidationResult) o; + return success == that.success; + } + + @Override + public int hashCode() { + return Objects.hashCode(success); + } + + @Override + public String toString() { + return "ValidationResult{" + "success=" + success + ", message='" + message + '\'' + '}'; + } + } +} diff --git a/polaris-core/src/main/java/io/polaris/core/storage/PolarisStorageIntegrationProvider.java b/polaris-core/src/main/java/io/polaris/core/storage/PolarisStorageIntegrationProvider.java new file mode 100644 index 0000000000..1e9fd85893 --- /dev/null +++ b/polaris-core/src/main/java/io/polaris/core/storage/PolarisStorageIntegrationProvider.java @@ -0,0 +1,14 @@ +package io.polaris.core.storage; + +import org.jetbrains.annotations.Nullable; + +/** + * Factory interface that knows how to construct a {@link PolarisStorageIntegration} given a {@link + * PolarisStorageConfigurationInfo}. + */ +public interface PolarisStorageIntegrationProvider { + @SuppressWarnings("unchecked") + @Nullable + PolarisStorageIntegration getStorageIntegrationForConfig( + PolarisStorageConfigurationInfo polarisStorageConfigurationInfo); +} diff --git a/polaris-core/src/main/java/io/polaris/core/storage/aws/AwsCredentialsStorageIntegration.java b/polaris-core/src/main/java/io/polaris/core/storage/aws/AwsCredentialsStorageIntegration.java new file mode 100644 index 0000000000..fe53a3f587 --- /dev/null +++ b/polaris-core/src/main/java/io/polaris/core/storage/aws/AwsCredentialsStorageIntegration.java @@ -0,0 +1,171 @@ +package io.polaris.core.storage.aws; + +import io.polaris.core.PolarisDiagnostics; +import io.polaris.core.storage.InMemoryStorageIntegration; +import io.polaris.core.storage.PolarisCredentialProperty; +import io.polaris.core.storage.PolarisStorageConfigurationInfo; +import java.net.URI; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Stream; +import org.jetbrains.annotations.NotNull; +import software.amazon.awssdk.policybuilder.iam.IamConditionOperator; +import software.amazon.awssdk.policybuilder.iam.IamEffect; +import software.amazon.awssdk.policybuilder.iam.IamPolicy; +import software.amazon.awssdk.policybuilder.iam.IamResource; +import software.amazon.awssdk.policybuilder.iam.IamStatement; +import software.amazon.awssdk.services.sts.StsClient; +import software.amazon.awssdk.services.sts.model.AssumeRoleRequest; +import software.amazon.awssdk.services.sts.model.AssumeRoleResponse; + +/** Credential vendor that supports generating */ +public class AwsCredentialsStorageIntegration + extends InMemoryStorageIntegration { + private final StsClient stsClient; + + public AwsCredentialsStorageIntegration(StsClient stsClient) { + super(AwsCredentialsStorageIntegration.class.getName()); + this.stsClient = stsClient; + } + + /** {@inheritDoc} */ + @Override + public EnumMap getSubscopedCreds( + @NotNull PolarisDiagnostics diagnostics, + @NotNull AwsStorageConfigurationInfo storageConfig, + boolean allowListOperation, + @NotNull Set allowedReadLocations, + @NotNull Set allowedWriteLocations) { + AssumeRoleResponse response = + stsClient.assumeRole( + AssumeRoleRequest.builder() + .externalId(storageConfig.getExternalId()) + .roleArn(storageConfig.getRoleARN()) + .roleSessionName("PolarisAwsCredentialsStorageIntegration") + .policy( + policyString( + storageConfig.getRoleARN(), + allowListOperation, + allowedReadLocations, + allowedWriteLocations) + .toJson()) + .build()); + EnumMap credentialMap = + new EnumMap<>(PolarisCredentialProperty.class); + credentialMap.put(PolarisCredentialProperty.AWS_KEY_ID, response.credentials().accessKeyId()); + credentialMap.put( + PolarisCredentialProperty.AWS_SECRET_KEY, response.credentials().secretAccessKey()); + credentialMap.put(PolarisCredentialProperty.AWS_TOKEN, response.credentials().sessionToken()); + return credentialMap; + } + + /** + * generate an IamPolicy from the input readLocations and writeLocations, optionally with list + * support. Credentials will be scoped to exactly the resources provided. If read and write + * locations are empty, a non-empty policy will be generated that grants GetObject and (optionally + * ListBucket privileges with no resources. This prevents us from sending an empty policy to AWS + * and just assuming the role with full privileges. + * + * @param roleArn + * @param allowList + * @param readLocations + * @param writeLocations + * @return + */ + // TODO - add KMS key access + private IamPolicy policyString( + String roleArn, boolean allowList, Set readLocations, Set writeLocations) { + IamPolicy.Builder policyBuilder = IamPolicy.builder(); + IamStatement.Builder allowGetObjectStatementBuilder = + IamStatement.builder() + .effect(IamEffect.ALLOW) + .addAction("s3:GetObject") + .addAction("s3:GetObjectVersion"); + Map bucketListStatmentBuilder = new HashMap<>(); + + String arnPrefix = getArnPrefixFor(roleArn); + Stream.concat(readLocations.stream(), writeLocations.stream()) + .distinct() + .forEach( + location -> { + URI uri = URI.create(location); + allowGetObjectStatementBuilder.addResource( + // TODO add support for CN and GOV + IamResource.create(arnPrefix + parseS3Path(uri) + "/*")); + if (allowList) { + bucketListStatmentBuilder + .computeIfAbsent( + arnPrefix + uri.getHost(), + (String key) -> + IamStatement.builder() + .effect(IamEffect.ALLOW) + .addAction("s3:ListBucket") + .addResource(key)) + .addCondition( + IamConditionOperator.STRING_LIKE, + "s3:prefix", + trimLeadingSlash(uri.getPath()) + "/*"); + } + }); + + if (!writeLocations.isEmpty()) { + IamStatement.Builder allowPutObjectStatementBuilder = + IamStatement.builder() + .effect(IamEffect.ALLOW) + .addAction("s3:PutObject") + .addAction("s3:DeleteObject"); + writeLocations.forEach( + location -> { + URI uri = URI.create(location); + // TODO add support for CN and GOV + allowPutObjectStatementBuilder.addResource( + IamResource.create(arnPrefix + parseS3Path(uri) + "/*")); + }); + policyBuilder.addStatement(allowPutObjectStatementBuilder.build()); + } + if (!bucketListStatmentBuilder.isEmpty()) { + bucketListStatmentBuilder + .values() + .forEach(statementBuilder -> policyBuilder.addStatement(statementBuilder.build())); + } else if (allowList) { + // add list privilege with 0 resources + policyBuilder.addStatement( + IamStatement.builder().effect(IamEffect.ALLOW).addAction("s3:ListBucket").build()); + } + return policyBuilder.addStatement(allowGetObjectStatementBuilder.build()).build(); + } + + private String getArnPrefixFor(String roleArn) { + if (roleArn.contains("aws-cn")) { + return "arn:aws-cn:s3:::"; + } else if (roleArn.contains("aws-us-gov")) { + return "arn:aws-us-gov:s3:::"; + } else { + return "arn:aws:s3:::"; + } + } + + private static @NotNull String parseS3Path(URI uri) { + String bucket = uri.getHost(); + String path = trimLeadingSlash(uri.getPath()); + return String.join( + "/", Stream.of(bucket, path).filter(Objects::nonNull).toArray(String[]::new)); + } + + private static @NotNull String trimLeadingSlash(String path) { + if (path.startsWith("/")) { + path = path.substring(1); + } + return path; + } + + // FIXME - we don't need this method in the interface + @Override + public EnumMap + descPolarisStorageConfiguration(@NotNull PolarisStorageConfigurationInfo storageConfigInfo) { + return null; + } +} diff --git a/polaris-core/src/main/java/io/polaris/core/storage/aws/AwsStorageConfigurationInfo.java b/polaris-core/src/main/java/io/polaris/core/storage/aws/AwsStorageConfigurationInfo.java new file mode 100644 index 0000000000..5cfed26840 --- /dev/null +++ b/polaris-core/src/main/java/io/polaris/core/storage/aws/AwsStorageConfigurationInfo.java @@ -0,0 +1,103 @@ +package io.polaris.core.storage.aws; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.base.MoreObjects; +import io.polaris.core.storage.PolarisStorageConfigurationInfo; +import java.util.List; +import java.util.regex.Pattern; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** Aws Polaris Storage Configuration information */ +public class AwsStorageConfigurationInfo extends PolarisStorageConfigurationInfo { + + // 5 is the approximate max allowed locations for the size of AccessPolicy when LIST is required + // for allowed read and write locations for subscoping creds. + @JsonIgnore private static final int MAX_ALLOWED_LOCATIONS = 5; + + // Technically, it should be ^arn:(aws|aws-cn|aws-us-gov):iam::\d{12}:role/.+$, + @JsonIgnore public static String ROLE_ARN_PATTERN = "^arn:aws:iam::\\d{12}:role/.+$"; + + // AWS role to be assumed + private final @NotNull String roleARN; + + // AWS external ID, optional + @JsonProperty(value = "externalId", required = false) + private @Nullable String externalId = null; + + /** User ARN for the service principal */ + @JsonProperty(value = "userARN", required = false) + private @Nullable String userARN = null; + + @JsonCreator + public AwsStorageConfigurationInfo( + @JsonProperty(value = "storageType", required = true) @NotNull StorageType storageType, + @JsonProperty(value = "allowedLocations", required = true) @NotNull + List allowedLocations, + @JsonProperty(value = "roleARN", required = true) @NotNull String roleARN) { + this(storageType, allowedLocations, roleARN, null); + } + + public AwsStorageConfigurationInfo( + @NotNull StorageType storageType, + @NotNull List allowedLocations, + @NotNull String roleARN, + @Nullable String externalId) { + super(storageType, allowedLocations); + this.roleARN = roleARN; + this.externalId = externalId; + validateMaxAllowedLocations(MAX_ALLOWED_LOCATIONS); + } + + @Override + public String getFileIoImplClassName() { + return "org.apache.iceberg.aws.s3.S3FileIO"; + } + + public void validateArn(String arn) { + if (arn == null || arn.isEmpty()) { + throw new IllegalArgumentException("ARN cannot be null or empty"); + } + // specifically throw errors for China and Gov + if (arn.contains("aws-cn") || arn.contains("aws-us-gov")) { + throw new IllegalArgumentException("AWS China or Gov Cloud are temporarily not supported"); + } + if (!Pattern.matches(ROLE_ARN_PATTERN, arn)) { + throw new IllegalArgumentException("Invalid role ARN format"); + } + } + + public @NotNull String getRoleARN() { + return roleARN; + } + + public @Nullable String getExternalId() { + return externalId; + } + + public void setExternalId(String externalId) { + this.externalId = externalId; + } + + public @Nullable String getUserARN() { + return userARN; + } + + public void setUserARN(@Nullable String userARN) { + this.userARN = userARN; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("storageType", storageType) + .add("storageType", storageType.name()) + .add("roleARN", roleARN) + .add("userARN", userARN) + .add("externalId", externalId) + .add("allowedLocation", allowedLocations) + .toString(); + } +} diff --git a/polaris-core/src/main/java/io/polaris/core/storage/aws/PolarisS3FileIOClientFactory.java b/polaris-core/src/main/java/io/polaris/core/storage/aws/PolarisS3FileIOClientFactory.java new file mode 100644 index 0000000000..f250bcc3f7 --- /dev/null +++ b/polaris-core/src/main/java/io/polaris/core/storage/aws/PolarisS3FileIOClientFactory.java @@ -0,0 +1,51 @@ +package io.polaris.core.storage.aws; + +import java.util.Map; +import org.apache.iceberg.aws.AwsClientProperties; +import org.apache.iceberg.aws.HttpClientProperties; +import org.apache.iceberg.aws.s3.S3FileIOAwsClientFactory; +import org.apache.iceberg.aws.s3.S3FileIOProperties; +import software.amazon.awssdk.services.s3.S3Client; + +/** + * A S3FileIOAwsClientFactory that will be used by the S3FileIO to initialize S3 client. The + * difference of this factory and DefaultS3FileIOAwsClientFactory is that this one enables cross + * region access. The S3FileIO is not supporting cross region access due to the issue described here + * https://github.com/apache/iceberg/issues/9785 + */ +public class PolarisS3FileIOClientFactory implements S3FileIOAwsClientFactory { + private S3FileIOProperties s3FileIOProperties; + private HttpClientProperties httpClientProperties; + private AwsClientProperties awsClientProperties; + + PolarisS3FileIOClientFactory() { + this.s3FileIOProperties = new S3FileIOProperties(); + this.httpClientProperties = new HttpClientProperties(); + this.awsClientProperties = new AwsClientProperties(); + } + + @Override + public void initialize(Map properties) { + this.s3FileIOProperties = new S3FileIOProperties(properties); + this.awsClientProperties = new AwsClientProperties(properties); + this.httpClientProperties = new HttpClientProperties(properties); + } + + @Override + public S3Client s3() { + return S3Client.builder() + .applyMutation(awsClientProperties::applyClientRegionConfiguration) + .applyMutation(httpClientProperties::applyHttpClientConfigurations) + .applyMutation(s3FileIOProperties::applyEndpointConfigurations) + .applyMutation(s3FileIOProperties::applyServiceConfigurations) + .applyMutation( + s3ClientBuilder -> { + s3FileIOProperties.applyCredentialConfigurations( + awsClientProperties, s3ClientBuilder); + }) + .applyMutation(s3FileIOProperties::applySignerConfiguration) + .applyMutation(s3FileIOProperties::applyS3AccessGrantsConfigurations) + .applyMutation(s3ClientBuilder -> s3ClientBuilder.crossRegionAccessEnabled(true)) + .build(); + } +} diff --git a/polaris-core/src/main/java/io/polaris/core/storage/azure/AzureCredentialsStorageIntegration.java b/polaris-core/src/main/java/io/polaris/core/storage/azure/AzureCredentialsStorageIntegration.java new file mode 100644 index 0000000000..dc342c0e7f --- /dev/null +++ b/polaris-core/src/main/java/io/polaris/core/storage/azure/AzureCredentialsStorageIntegration.java @@ -0,0 +1,270 @@ +package io.polaris.core.storage.azure; + +import com.azure.core.credential.AccessToken; +import com.azure.core.credential.TokenRequestContext; +import com.azure.identity.DefaultAzureCredential; +import com.azure.identity.DefaultAzureCredentialBuilder; +import com.azure.storage.blob.BlobContainerClientBuilder; +import com.azure.storage.blob.BlobServiceClient; +import com.azure.storage.blob.BlobServiceClientBuilder; +import com.azure.storage.blob.models.BlobStorageException; +import com.azure.storage.blob.models.UserDelegationKey; +import com.azure.storage.blob.sas.BlobSasPermission; +import com.azure.storage.blob.sas.BlobServiceSasSignatureValues; +import com.azure.storage.file.datalake.DataLakeFileSystemClientBuilder; +import com.azure.storage.file.datalake.DataLakeServiceClient; +import com.azure.storage.file.datalake.DataLakeServiceClientBuilder; +import com.azure.storage.file.datalake.models.DataLakeStorageException; +import com.azure.storage.file.datalake.sas.DataLakeServiceSasSignatureValues; +import com.azure.storage.file.datalake.sas.PathSasPermission; +import io.polaris.core.PolarisDiagnostics; +import io.polaris.core.storage.InMemoryStorageIntegration; +import io.polaris.core.storage.PolarisCredentialProperty; +import io.polaris.core.storage.PolarisStorageConfigurationInfo; +import java.time.Instant; +import java.time.OffsetDateTime; +import java.time.Period; +import java.time.ZoneOffset; +import java.time.temporal.ChronoUnit; +import java.util.EnumMap; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Mono; + +/** Azure credential vendor that supports generating SAS token */ +public class AzureCredentialsStorageIntegration + extends InMemoryStorageIntegration { + + private final Logger LOGGER = LoggerFactory.getLogger(AzureCredentialsStorageIntegration.class); + + final DefaultAzureCredential defaultAzureCredential; + + public AzureCredentialsStorageIntegration() { + super(AzureCredentialsStorageIntegration.class.getName()); + // The DefaultAzureCredential will by default load the environment variables for client id, + // client secret, tenant id + defaultAzureCredential = new DefaultAzureCredentialBuilder().build(); + } + + @Override + public EnumMap getSubscopedCreds( + @NotNull PolarisDiagnostics diagnostics, + @NotNull AzureStorageConfigurationInfo storageConfig, + boolean allowListOperation, + @NotNull Set allowedReadLocations, + @NotNull Set allowedWriteLocations) { + EnumMap credentialMap = + new EnumMap<>(PolarisCredentialProperty.class); + String loc = + !allowedWriteLocations.isEmpty() + ? allowedWriteLocations.stream().findAny().orElse(null) + : allowedReadLocations.stream().findAny().orElse(null); + if (loc == null) { + throw new IllegalArgumentException("Expect valid location"); + } + // schema://@./ + AzureLocation location = new AzureLocation(loc); + validateAccountAndContainer(location, allowedReadLocations, allowedWriteLocations); + + String storageDnsName = location.getStorageAccount() + "." + location.getEndpoint(); + String endpoint = "https://" + storageDnsName; + String filePath = location.getFilePath(); + + BlobSasPermission blobSasPermission = new BlobSasPermission(); + // pathSasPermission is for Data lake storage + PathSasPermission pathSasPermission = new PathSasPermission(); + + if (allowListOperation) { + // container level + blobSasPermission.setListPermission(true); + pathSasPermission.setListPermission(true); + } + if (!allowedReadLocations.isEmpty()) { + blobSasPermission.setReadPermission(true); + pathSasPermission.setReadPermission(true); + } + if (!allowedWriteLocations.isEmpty()) { + blobSasPermission.setAddPermission(true); + blobSasPermission.setWritePermission(true); + blobSasPermission.setDeletePermission(true); + pathSasPermission.setAddPermission(true); + pathSasPermission.setWritePermission(true); + pathSasPermission.setDeletePermission(true); + } + + Instant start = Instant.now(); + OffsetDateTime expiry = + OffsetDateTime.ofInstant( + start.plusSeconds(3600), ZoneOffset.UTC); // 1 hr to sync with AWS and GCP Access token + + AccessToken accessToken = getAccessToken(storageConfig.getTenantId()); + // Get user delegation key. + // Set the new generated user delegation key expiry to 7 days and minute 1 min + // Azure strictly requires the end time to be <= 7 days from the current time, -1 min to avoid + // clock skew between the client and server, + OffsetDateTime startTime = start.truncatedTo(ChronoUnit.SECONDS).atOffset(ZoneOffset.UTC); + OffsetDateTime sanitizedEndTime = + start.plus(Period.ofDays(7)).minusSeconds(60).atOffset(ZoneOffset.UTC); + LOGGER + .atDebug() + .addKeyValue("allowedListAction", allowListOperation) + .addKeyValue("allowedReadLoc", allowedReadLocations) + .addKeyValue("allowedWriteLoc", allowedWriteLocations) + .addKeyValue("location", loc) + .addKeyValue("storageAccount", location.getStorageAccount()) + .addKeyValue("endpoint", location.getEndpoint()) + .addKeyValue("container", location.getContainer()) + .addKeyValue("filePath", filePath) + .log("Subscope Azure SAS"); + String sasToken = ""; + if (location.getEndpoint().equalsIgnoreCase(AzureLocation.BLOB_ENDPOINT)) { + sasToken = + getBlobUserDelegationSas( + startTime, + sanitizedEndTime, + expiry, + storageDnsName, + location.getContainer(), + blobSasPermission, + Mono.just(accessToken)); + } else if (location.getEndpoint().equalsIgnoreCase(AzureLocation.ADLS_ENDPOINT)) { + sasToken = + getAdlsUserDelegationSas( + startTime, + sanitizedEndTime, + expiry, + storageDnsName, + location.getContainer(), + pathSasPermission, + Mono.just(accessToken)); + } else { + throw new RuntimeException( + String.format("Endpoint %s not supported", location.getEndpoint())); + } + credentialMap.put(PolarisCredentialProperty.AZURE_SAS_TOKEN, sasToken); + credentialMap.put(PolarisCredentialProperty.AZURE_ACCOUNT_HOST, storageDnsName); + return credentialMap; + } + + private String getBlobUserDelegationSas( + OffsetDateTime startTime, + OffsetDateTime keyEndtime, + OffsetDateTime sasExpiry, + String storageDnsName, + String container, + BlobSasPermission blobSasPermission, + Mono accessTokenMono) { + String endpoint = "https://" + storageDnsName; + try { + BlobServiceClient serviceClient = + new BlobServiceClientBuilder() + .endpoint(endpoint) + .credential(c -> accessTokenMono) + .buildClient(); + UserDelegationKey userDelegationKey = + serviceClient.getUserDelegationKey(startTime, keyEndtime); + BlobServiceSasSignatureValues sigValues = + new BlobServiceSasSignatureValues(sasExpiry, blobSasPermission); + // scoped to the container + return new BlobContainerClientBuilder() + .endpoint(endpoint) + .containerName(container) + .buildClient() + .generateUserDelegationSas(sigValues, userDelegationKey); + } catch (BlobStorageException ex) { + LOGGER.debug( + "Azure DataLakeStorageException for getBlobUserDelegationSas. keyStart={} keyEnd={}, storageDns={}, container={}", + startTime, + keyEndtime, + storageDnsName, + container, + ex); + throw ex; + } + } + + private String getAdlsUserDelegationSas( + OffsetDateTime startTime, + OffsetDateTime endTime, + OffsetDateTime sasExpiry, + String storageDnsName, + String fileSystemNameOrContainer, + PathSasPermission pathSasPermission, + Mono accessTokenMono) { + String endpoint = "https://" + storageDnsName; + try { + DataLakeServiceClient dataLakeServiceClient = + new DataLakeServiceClientBuilder() + .endpoint(endpoint) + .credential(c -> accessTokenMono) + .buildClient(); + com.azure.storage.file.datalake.models.UserDelegationKey userDelegationKey = + dataLakeServiceClient.getUserDelegationKey(startTime, endTime); + + DataLakeServiceSasSignatureValues signatureValues = + new DataLakeServiceSasSignatureValues(sasExpiry, pathSasPermission); + + return new DataLakeFileSystemClientBuilder() + .endpoint(endpoint) + .fileSystemName(fileSystemNameOrContainer) + .buildClient() + .generateUserDelegationSas(signatureValues, userDelegationKey); + } catch (DataLakeStorageException ex) { + LOGGER.debug( + "Azure DataLakeStorageException for getAdlsUserDelegationSas. keyStart={} keyEnd={}, storageDns={}, fileSystemName={}", + startTime, + endTime, + storageDnsName, + fileSystemNameOrContainer, + ex); + throw ex; + } + } + + /** + * Verify that storage accounts, containers and endpoint are the same + * + * @param target + * @param readLocations + * @param writeLocations + */ + private void validateAccountAndContainer( + AzureLocation target, Set readLocations, Set writeLocations) { + Set allLocations = new HashSet<>(); + allLocations.addAll(readLocations); + allLocations.addAll(writeLocations); + allLocations.forEach( + loc -> { + AzureLocation location = new AzureLocation(loc); + if (!Objects.equals(location.getStorageAccount(), target.getStorageAccount()) + || !Objects.equals(location.getContainer(), target.getContainer()) + || !Objects.equals(location.getEndpoint(), target.getEndpoint())) { + throw new RuntimeException( + "Expect allowed read write locations belong to the same storage account and container"); + } + }); + } + + private AccessToken getAccessToken(String tenantId) { + String scope = "https://storage.azure.com/.default"; + AccessToken accessToken = + defaultAzureCredential + .getToken(new TokenRequestContext().addScopes(scope).setTenantId(tenantId)) + .blockOptional() + .orElse(null); + if (accessToken == null) { + throw new RuntimeException("No access token fetched!"); + } + return accessToken; + } + + @Override + public EnumMap + descPolarisStorageConfiguration(@NotNull PolarisStorageConfigurationInfo storageConfigInfo) { + return null; + } +} diff --git a/polaris-core/src/main/java/io/polaris/core/storage/azure/AzureLocation.java b/polaris-core/src/main/java/io/polaris/core/storage/azure/AzureLocation.java new file mode 100644 index 0000000000..847c38a0b2 --- /dev/null +++ b/polaris-core/src/main/java/io/polaris/core/storage/azure/AzureLocation.java @@ -0,0 +1,77 @@ +package io.polaris.core.storage.azure; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.jetbrains.annotations.NotNull; + +/** This class represents all information for a azure location */ +public class AzureLocation { + /** The pattern only allows abfs[s] now because the ResovlingFileIO only accept ADLSFileIO */ + private static final Pattern URI_PATTERN = Pattern.compile("^abfss?://([^/?#]+)(.*)?$"); + + public static final String ADLS_ENDPOINT = "dfs.core.windows.net"; + + public static final String BLOB_ENDPOINT = "blob.core.windows.net"; + + private final String storageAccount; + private final String container; + + private final String endpoint; + private final String filePath; + + /** + * Construct an Azure location object from a location uri, it should follow this pattern: + * + *
 abfs[s]://[@]/ 
+ * + * @param location a uri + */ + public AzureLocation(@NotNull String location) { + Matcher matcher = URI_PATTERN.matcher(location); + if (!matcher.matches()) { + throw new IllegalArgumentException("Invalid azure adls location uri " + location); + } + String authority = matcher.group(1); + // look for @ + String[] parts = authority.split("@", -1); + + // expect container and account both exist + if (parts.length != 2) { + throw new IllegalArgumentException("container and account name must be both provided"); + } + this.container = parts[0]; + String accountHost = parts[1]; + String[] hostParts = accountHost.split("\\.", 2); + if (hostParts.length != 2) { + throw new IllegalArgumentException("storage account and endpoint must be both provided"); + } + this.storageAccount = hostParts[0]; + this.endpoint = hostParts[1]; + String path = matcher.group(2); + filePath = path == null ? "" : path.startsWith("/") ? path.substring(1) : path; + } + + /** + * Get the storage account + * + * @return + */ + public String getStorageAccount() { + return storageAccount; + } + + /** Get the container name */ + public String getContainer() { + return container; + } + + /** Get the endpoint, for example: blob.core.windows.net */ + public String getEndpoint() { + return endpoint; + } + + /** Get the file path */ + public String getFilePath() { + return filePath; + } +} diff --git a/polaris-core/src/main/java/io/polaris/core/storage/azure/AzureStorageConfigurationInfo.java b/polaris-core/src/main/java/io/polaris/core/storage/azure/AzureStorageConfigurationInfo.java new file mode 100644 index 0000000000..4c50562994 --- /dev/null +++ b/polaris-core/src/main/java/io/polaris/core/storage/azure/AzureStorageConfigurationInfo.java @@ -0,0 +1,79 @@ +package io.polaris.core.storage.azure; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.base.MoreObjects; +import io.polaris.core.storage.PolarisStorageConfigurationInfo; +import java.util.List; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** Azure storage configuration information. */ +public class AzureStorageConfigurationInfo extends PolarisStorageConfigurationInfo { + // technically there is no limitation since expectation for Azure locations are for the same + // storage account and same container + @JsonIgnore private static final int MAX_ALLOWED_LOCATIONS = 20; + + // Azure tenant id + private final @NotNull String tenantId; + + /** The multi tenant app name for the service principal */ + @JsonProperty(value = "multiTenantAppName", required = false) + private @Nullable String multiTenantAppName = null; + + /** The consent url to the Azure permissions request page */ + @JsonProperty(value = "consentUrl", required = false) + private @Nullable String consentUrl = null; + + @JsonCreator + public AzureStorageConfigurationInfo( + @JsonProperty(value = "allowedLocations", required = true) @NotNull + List allowedLocations, + @JsonProperty(value = "tenantId", required = true) @NotNull String tenantId) { + super(StorageType.AZURE, allowedLocations); + this.tenantId = tenantId; + validateMaxAllowedLocations(MAX_ALLOWED_LOCATIONS); + } + + @Override + public String getFileIoImplClassName() { + return "org.apache.iceberg.azure.adlsv2.ADLSFileIO"; + } + + public @NotNull String getTenantId() { + return tenantId; + } + + public String getMultiTenantAppName() { + return multiTenantAppName; + } + + public void setMultiTenantAppName(String multiTenantAppName) { + this.multiTenantAppName = multiTenantAppName; + } + + public String getConsentUrl() { + return consentUrl; + } + + public void setConsentUrl(String consentUrl) { + this.consentUrl = consentUrl; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("storageType", storageType) + .add("tenantId", tenantId) + .add("allowedLocation", allowedLocations) + .add("multiTenantAppName", multiTenantAppName) + .add("consentUrl", consentUrl) + .toString(); + } + + @Override + public void validatePrefixForStorageType() { + this.allowedLocations.forEach(AzureLocation::new); + } +} diff --git a/polaris-core/src/main/java/io/polaris/core/storage/cache/StorageCredentialCache.java b/polaris-core/src/main/java/io/polaris/core/storage/cache/StorageCredentialCache.java new file mode 100644 index 0000000000..45723daae9 --- /dev/null +++ b/polaris-core/src/main/java/io/polaris/core/storage/cache/StorageCredentialCache.java @@ -0,0 +1,153 @@ +package io.polaris.core.storage.cache; + +import com.github.benmanes.caffeine.cache.CacheLoader; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.Expiry; +import com.github.benmanes.caffeine.cache.LoadingCache; +import io.polaris.core.PolarisCallContext; +import io.polaris.core.entity.PolarisEntity; +import io.polaris.core.entity.PolarisEntityType; +import io.polaris.core.persistence.PolarisMetaStoreManager; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import org.apache.iceberg.exceptions.UnprocessableEntityException; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.VisibleForTesting; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** Storage subscoped credential cache. */ +public class StorageCredentialCache { + + private static final Logger LOGGER = LoggerFactory.getLogger(StorageCredentialCache.class); + + private static final long CACHE_MAX_DURATION_MS = 30 * 60 * 1000L; // 30 minutes + private static final long CACHE_MAX_NUMBER_OF_ENTRIES = 10_000L; + private final LoadingCache cache; + + /** Initialize the creds cache, max cache duration is half an hr. */ + public StorageCredentialCache() { + cache = + Caffeine.newBuilder() + .maximumSize(CACHE_MAX_NUMBER_OF_ENTRIES) + .expireAfter( + new Expiry() { + @Override + public long expireAfterCreate( + StorageCredentialCacheKey key, + StorageCredentialCacheEntry entry, + long currentTime) { + long expireAfterMillis = + Math.max( + 0, + Math.min( + (entry.getExpirationTime() - System.currentTimeMillis()) / 2, + CACHE_MAX_DURATION_MS)); + return TimeUnit.MILLISECONDS.toNanos(expireAfterMillis); + } + + @Override + public long expireAfterUpdate( + StorageCredentialCacheKey key, + StorageCredentialCacheEntry entry, + long currentTime, + long currentDuration) { + return currentDuration; + } + + @Override + public long expireAfterRead( + StorageCredentialCacheKey key, + StorageCredentialCacheEntry entry, + long currentTime, + long currentDuration) { + return currentDuration; + } + }) + .build( + new CacheLoader() { + @Override + public StorageCredentialCacheEntry load(StorageCredentialCacheKey key) { + // the load happen at getOrGenerateSubScopeCreds() + return null; + } + }); + } + + /** + * Either get from the cache or generate a new entry for a scoped creds + * + * @param metaStoreManager the meta storage manager used to generate a new scoped creds if needed + * @param callCtx the call context + * @param polarisEntity the polaris entity that is going to scoped creds + * @param allowListOperation whether allow list action on the provided read and write locations + * @param allowedReadLocations a set of allowed to read locations + * @param allowedWriteLocations a set of allowed to write locations. + * @return the a map of string containing the scoped creds information + */ + public Map getOrGenerateSubScopeCreds( + @NotNull PolarisMetaStoreManager metaStoreManager, + @NotNull PolarisCallContext callCtx, + @NotNull PolarisEntity polarisEntity, + boolean allowListOperation, + @NotNull Set allowedReadLocations, + @NotNull Set allowedWriteLocations) { + if (!isTypeSupported(polarisEntity.getType())) { + callCtx + .getDiagServices() + .fail("entity_type_not_suppported_to_scope_creds", "type={}", polarisEntity.getType()); + } + StorageCredentialCacheKey key = + new StorageCredentialCacheKey( + polarisEntity, + allowListOperation, + allowedReadLocations, + allowedWriteLocations, + callCtx); + LOGGER.atDebug().addKeyValue("key", key).log("subscopedCredsCache"); + Function loader = + k -> { + LOGGER.atDebug().log("StorageCredentialCache::load"); + PolarisMetaStoreManager.ScopedCredentialsResult scopedCredentialsResult = + metaStoreManager.getSubscopedCredsForEntity( + k.getCallContext(), + k.getCatalogId(), + k.getEntityId(), + k.isAllowedListAction(), + k.getAllowedReadLocations(), + k.getAllowedWriteLocations()); + if (scopedCredentialsResult.isSuccess()) { + return new StorageCredentialCacheEntry(scopedCredentialsResult); + } + LOGGER + .atDebug() + .addKeyValue("errorMessage", scopedCredentialsResult.getExtraInformation()) + .log("Failed to get subscoped credentials"); + throw new UnprocessableEntityException( + "Failed to get subscoped credentials: " + + scopedCredentialsResult.getExtraInformation()); + }; + return cache.get(key, loader).convertToMapOfString(); + } + + public Map getIfPresent(StorageCredentialCacheKey key) { + return Optional.ofNullable(cache.getIfPresent(key)) + .map(value -> value.convertToMapOfString()) + .orElse(null); + } + + private boolean isTypeSupported(PolarisEntityType type) { + return type == PolarisEntityType.CATALOG + || type == PolarisEntityType.NAMESPACE + || type == PolarisEntityType.TABLE_LIKE + || type == PolarisEntityType.TASK; + } + + @VisibleForTesting + public long getEstimatedSize() { + return this.cache.estimatedSize(); + } +} diff --git a/polaris-core/src/main/java/io/polaris/core/storage/cache/StorageCredentialCacheEntry.java b/polaris-core/src/main/java/io/polaris/core/storage/cache/StorageCredentialCacheEntry.java new file mode 100644 index 0000000000..0d0f841343 --- /dev/null +++ b/polaris-core/src/main/java/io/polaris/core/storage/cache/StorageCredentialCacheEntry.java @@ -0,0 +1,61 @@ +package io.polaris.core.storage.cache; + +import io.polaris.core.persistence.PolarisMetaStoreManager; +import io.polaris.core.storage.PolarisCredentialProperty; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.Map; + +/** A storage credential cached entry. */ +public class StorageCredentialCacheEntry { + /** The scoped creds map that is fetched from a creds vending service */ + public final EnumMap credsMap; + + private final PolarisMetaStoreManager.ScopedCredentialsResult scopedCredentialsResult; + + public StorageCredentialCacheEntry( + PolarisMetaStoreManager.ScopedCredentialsResult scopedCredentialsResult) { + this.scopedCredentialsResult = scopedCredentialsResult; + this.credsMap = scopedCredentialsResult.getCredentials(); + } + + /** + * Get the expiration time in millisecond for the cached entry + * + * @return + */ + public long getExpirationTime() { + if (credsMap.containsKey(PolarisCredentialProperty.GCS_ACCESS_TOKEN_EXPIRES_AT)) { + return Long.parseLong(credsMap.get(PolarisCredentialProperty.GCS_ACCESS_TOKEN_EXPIRES_AT)); + } + if (credsMap.containsKey(PolarisCredentialProperty.EXPIRATION_TIME)) { + return Long.parseLong(credsMap.get(PolarisCredentialProperty.EXPIRATION_TIME)); + } + return Long.MAX_VALUE; + } + + /** + * Get the map of string creds that is needed for the query engine. + * + * @return a map of string representing the subscoped creds info. + */ + public Map convertToMapOfString() { + Map resCredsMap = new HashMap<>(); + if (!credsMap.isEmpty()) { + credsMap.forEach( + (key, value) -> { + // only Azure needs special handle, the target key is dynamically with storageaccount + // endpoint appended + if (key.equals(PolarisCredentialProperty.AZURE_SAS_TOKEN)) { + resCredsMap.put( + key.getPropertyName() + + credsMap.get(PolarisCredentialProperty.AZURE_ACCOUNT_HOST), + value); + } else if (!key.equals(PolarisCredentialProperty.AZURE_ACCOUNT_HOST)) { + resCredsMap.put(key.getPropertyName(), value); + } + }); + } + return resCredsMap; + } +} diff --git a/polaris-core/src/main/java/io/polaris/core/storage/cache/StorageCredentialCacheKey.java b/polaris-core/src/main/java/io/polaris/core/storage/cache/StorageCredentialCacheKey.java new file mode 100644 index 0000000000..559425f802 --- /dev/null +++ b/polaris-core/src/main/java/io/polaris/core/storage/cache/StorageCredentialCacheKey.java @@ -0,0 +1,124 @@ +package io.polaris.core.storage.cache; + +import io.polaris.core.PolarisCallContext; +import io.polaris.core.context.CallContext; +import io.polaris.core.entity.PolarisEntity; +import io.polaris.core.entity.PolarisEntityConstants; +import java.util.Objects; +import java.util.Set; +import org.jetbrains.annotations.Nullable; + +public class StorageCredentialCacheKey { + + private final long catalogId; + + /** The serialized string of the storage config. */ + private final String storageConfigSerializedStr; + + /** + * The entity id is passed to be used to fetch subscoped creds, but is not used to do hash/equals + * as part of the cache key. + */ + private final long entityId; + + private final boolean allowedListAction; + private final Set allowedReadLocations; + + private final Set allowedWriteLocations; + + /** + * The callContext is passed to be used to fetch subscoped creds, but is not used to hash/equals + * as part of the cache key. + */ + private @Nullable PolarisCallContext callContext; + + public StorageCredentialCacheKey( + PolarisEntity entity, + boolean allowedListAction, + Set allowedReadLocations, + Set allowedWriteLocations, + PolarisCallContext callContext) { + this.catalogId = entity.getCatalogId(); + this.storageConfigSerializedStr = + entity + .getInternalPropertiesAsMap() + .get(PolarisEntityConstants.getStorageConfigInfoPropertyName()); + this.entityId = entity.getId(); + this.allowedListAction = allowedListAction; + this.allowedReadLocations = allowedReadLocations; + this.allowedWriteLocations = allowedWriteLocations; + this.callContext = callContext; + if (this.callContext == null) { + this.callContext = CallContext.getCurrentContext().getPolarisCallContext(); + } + } + + public long getCatalogId() { + return catalogId; + } + + public String getStorageConfigSerializedStr() { + return storageConfigSerializedStr; + } + + public long getEntityId() { + return entityId; + } + + public boolean isAllowedListAction() { + return allowedListAction; + } + + public Set getAllowedReadLocations() { + return allowedReadLocations; + } + + public Set getAllowedWriteLocations() { + return allowedWriteLocations; + } + + public PolarisCallContext getCallContext() { + return callContext; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + StorageCredentialCacheKey cacheKey = (StorageCredentialCacheKey) o; + return catalogId == cacheKey.getCatalogId() + && Objects.equals(storageConfigSerializedStr, cacheKey.getStorageConfigSerializedStr()) + && allowedListAction == cacheKey.allowedListAction + && Objects.equals(allowedReadLocations, cacheKey.allowedReadLocations) + && Objects.equals(allowedWriteLocations, cacheKey.allowedWriteLocations); + } + + @Override + public int hashCode() { + return Objects.hash( + catalogId, + storageConfigSerializedStr, + allowedListAction, + allowedReadLocations, + allowedWriteLocations); + } + + @Override + public String toString() { + return "StorageCredentialCacheKey{" + + "catalogId=" + + catalogId + + ", storageConfigSerializedStr='" + + storageConfigSerializedStr + + '\'' + + ", entityId=" + + entityId + + ", allowedListAction=" + + allowedListAction + + ", allowedReadLocations=" + + allowedReadLocations + + ", allowedWriteLocations=" + + allowedWriteLocations + + '}'; + } +} diff --git a/polaris-core/src/main/java/io/polaris/core/storage/gcp/GcpCredentialsStorageIntegration.java b/polaris-core/src/main/java/io/polaris/core/storage/gcp/GcpCredentialsStorageIntegration.java new file mode 100644 index 0000000000..7a6ca1fdec --- /dev/null +++ b/polaris-core/src/main/java/io/polaris/core/storage/gcp/GcpCredentialsStorageIntegration.java @@ -0,0 +1,201 @@ +package io.polaris.core.storage.gcp; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.auth.http.HttpTransportFactory; +import com.google.auth.oauth2.AccessToken; +import com.google.auth.oauth2.CredentialAccessBoundary; +import com.google.auth.oauth2.DownscopedCredentials; +import com.google.auth.oauth2.GoogleCredentials; +import io.polaris.core.PolarisDiagnostics; +import io.polaris.core.storage.InMemoryStorageIntegration; +import io.polaris.core.storage.PolarisCredentialProperty; +import io.polaris.core.storage.PolarisStorageConfigurationInfo; +import io.polaris.core.storage.PolarisStorageIntegration; +import java.io.IOException; +import java.net.URI; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Stream; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.VisibleForTesting; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * GCS implementation of {@link PolarisStorageIntegration} with support for scoping credentials for + * input read/write locations + */ +public class GcpCredentialsStorageIntegration + extends InMemoryStorageIntegration { + public static final String TOKEN_URL = "https://sts.googleapis.com/v1/token"; + private final Logger LOGGER = LoggerFactory.getLogger(GcpCredentialsStorageIntegration.class); + + private final GoogleCredentials sourceCredentials; + private final HttpTransportFactory transportFactory; + public static final Set TOKEN_ENDPOINT_RETRYABLE_STATUS_CODES = + new HashSet<>(Arrays.asList(500, 503, 408, 429)); + + public GcpCredentialsStorageIntegration( + GoogleCredentials sourceCredentials, HttpTransportFactory transportFactory) { + super(GcpCredentialsStorageIntegration.class.getName()); + // Needed for when environment variable GOOGLE_APPLICATION_CREDENTIALS points to google service + // account key json + this.sourceCredentials = + sourceCredentials.createScoped("https://www.googleapis.com/auth/cloud-platform"); + this.transportFactory = transportFactory; + } + + @Override + public EnumMap getSubscopedCreds( + @NotNull PolarisDiagnostics diagnostics, + @NotNull GcpStorageConfigurationInfo storageConfig, + boolean allowListOperation, + @NotNull Set allowedReadLocations, + @NotNull Set allowedWriteLocations) { + try { + sourceCredentials.refresh(); + } catch (IOException e) { + throw new RuntimeException("Unable to refresh GCP credentials", e); + } + AccessToken sourceCredentialsAccessToken = this.sourceCredentials.getAccessToken(); + + CredentialAccessBoundary accessBoundary = + generateAccessBoundaryRules( + allowListOperation, allowedReadLocations, allowedWriteLocations); + DownscopedCredentials credentials = + DownscopedCredentials.newBuilder() + .setHttpTransportFactory(transportFactory) + .setSourceCredential(sourceCredentials) + .setCredentialAccessBoundary(accessBoundary) + .build(); + AccessToken token; + try { + token = credentials.refreshAccessToken(); + } catch (IOException e) { + LOGGER + .atError() + .addKeyValue("readLocations", allowedReadLocations) + .addKeyValue("writeLocations", allowedWriteLocations) + .addKeyValue("includesList", allowListOperation) + .addKeyValue("accessBoundary", convertToString(accessBoundary)) + .log("Unable to refresh access credentials", e); + throw new RuntimeException("Unable to fetch access credentials " + e.getMessage()); + } + + // If expires_in missing, use source credential's expire time, which require another api call to + // get. + EnumMap propertyMap = + new EnumMap<>(PolarisCredentialProperty.class); + propertyMap.put(PolarisCredentialProperty.GCS_ACCESS_TOKEN, token.getTokenValue()); + propertyMap.put( + PolarisCredentialProperty.GCS_ACCESS_TOKEN_EXPIRES_AT, + String.valueOf(token.getExpirationTime().getTime())); + return propertyMap; + } + + private String convertToString(CredentialAccessBoundary accessBoundary) { + try { + return new ObjectMapper().writeValueAsString(accessBoundary); + } catch (JsonProcessingException e) { + LOGGER.warn("Unable to convert access boundary to json", e); + return Objects.toString(accessBoundary); + } + } + + @VisibleForTesting + public static CredentialAccessBoundary generateAccessBoundaryRules( + boolean allowListOperation, + @NotNull Set allowedReadLocations, + @NotNull Set allowedWriteLocations) { + Map> readConditionsMap = new HashMap<>(); + Map> writeConditionsMap = new HashMap<>(); + + HashSet readBuckets = new HashSet<>(); + HashSet writeBuckets = new HashSet<>(); + Stream.concat(allowedReadLocations.stream(), allowedWriteLocations.stream()) + .distinct() + .forEach( + location -> { + URI uri = URI.create(location); + String bucket = uri.getHost(); + readBuckets.add(bucket); + String path = uri.getPath().substring(1); + List resourceExpressions = + readConditionsMap.computeIfAbsent(bucket, key -> new ArrayList<>()); + resourceExpressions.add( + String.format( + "resource.name.startsWith('projects/_/buckets/%s/objects/%s')", + bucket, path)); + if (allowListOperation) { + resourceExpressions.add( + String.format( + "api.getAttribute('storage.googleapis.com/objectListPrefix', '').startsWith('%s')", + path)); + } + if (allowedWriteLocations.contains(location)) { + writeBuckets.add(bucket); + List writeExpressions = + writeConditionsMap.computeIfAbsent(bucket, key -> new ArrayList<>()); + writeExpressions.add( + String.format( + "resource.name.startsWith('projects/_/buckets/%s/objects/%s')", + bucket, path)); + } + }); + CredentialAccessBoundary.Builder accessBoundaryBuilder = CredentialAccessBoundary.newBuilder(); + readBuckets.forEach( + bucket -> { + List readConditions = readConditionsMap.get(bucket); + if (readConditions == null || readConditions.isEmpty()) { + return; + } + CredentialAccessBoundary.AccessBoundaryRule.Builder builder = + CredentialAccessBoundary.AccessBoundaryRule.newBuilder(); + builder.setAvailableResource(bucketResource(bucket)); + builder.setAvailabilityCondition( + CredentialAccessBoundary.AccessBoundaryRule.AvailabilityCondition.newBuilder() + .setExpression(String.join(" || ", readConditions)) + .build()); + builder.setAvailablePermissions(List.of("inRole:roles/storage.legacyObjectReader")); + if (allowListOperation) { + builder.addAvailablePermission("inRole:roles/storage.objectViewer"); + } + accessBoundaryBuilder.addRule(builder.build()); + }); + writeBuckets.forEach( + bucket -> { + List writeConditions = writeConditionsMap.get(bucket); + if (writeConditions == null || writeConditions.isEmpty()) { + return; + } + CredentialAccessBoundary.AccessBoundaryRule.Builder builder = + CredentialAccessBoundary.AccessBoundaryRule.newBuilder(); + builder.setAvailableResource(bucketResource(bucket)); + builder.setAvailabilityCondition( + CredentialAccessBoundary.AccessBoundaryRule.AvailabilityCondition.newBuilder() + .setExpression(String.join(" || ", writeConditions)) + .build()); + builder.setAvailablePermissions(List.of("inRole:roles/storage.legacyBucketWriter")); + accessBoundaryBuilder.addRule(builder.build()); + }); + return accessBoundaryBuilder.build(); + } + + private static String bucketResource(String bucket) { + return "//storage.googleapis.com/projects/_/buckets/" + bucket; + } + + @Override + public EnumMap + descPolarisStorageConfiguration(@NotNull PolarisStorageConfigurationInfo storageConfigInfo) { + return null; + } +} diff --git a/polaris-core/src/main/java/io/polaris/core/storage/gcp/GcpStorageConfigurationInfo.java b/polaris-core/src/main/java/io/polaris/core/storage/gcp/GcpStorageConfigurationInfo.java new file mode 100644 index 0000000000..4cff55f82b --- /dev/null +++ b/polaris-core/src/main/java/io/polaris/core/storage/gcp/GcpStorageConfigurationInfo.java @@ -0,0 +1,53 @@ +package io.polaris.core.storage.gcp; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.base.MoreObjects; +import io.polaris.core.storage.PolarisStorageConfigurationInfo; +import java.util.List; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** Gcp storage storage configuration information. */ +public class GcpStorageConfigurationInfo extends PolarisStorageConfigurationInfo { + + // 8 is an experimental result from generating GCP accessBoundaryRules when subscoping creds, + // when the rule is too large, GCS only returns error: 400 bad request "Invalid arguments + // provided in the request" + @JsonIgnore private static final int MAX_ALLOWED_LOCATIONS = 8; + + /** The gcp service account */ + @JsonProperty(value = "gcpServiceAccount", required = false) + private @Nullable String gcpServiceAccount = null; + + @JsonCreator + public GcpStorageConfigurationInfo( + @JsonProperty(value = "allowedLocations", required = true) @NotNull + List allowedLocations) { + super(StorageType.GCS, allowedLocations); + validateMaxAllowedLocations(MAX_ALLOWED_LOCATIONS); + } + + @Override + public String getFileIoImplClassName() { + return "org.apache.iceberg.gcp.gcs.GCSFileIO"; + } + + public void setGcpServiceAccount(String gcpServiceAccount) { + this.gcpServiceAccount = gcpServiceAccount; + } + + public String getGcpServiceAccount() { + return gcpServiceAccount; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("storageType", storageType) + .add("allowedLocation", allowedLocations) + .add("gcpServiceAccount", gcpServiceAccount) + .toString(); + } +} diff --git a/polaris-core/src/test/java/io/polaris/core/persistence/EntityCacheTest.java b/polaris-core/src/test/java/io/polaris/core/persistence/EntityCacheTest.java new file mode 100644 index 0000000000..98627e29dd --- /dev/null +++ b/polaris-core/src/test/java/io/polaris/core/persistence/EntityCacheTest.java @@ -0,0 +1,448 @@ +package io.polaris.core.persistence; + +import io.polaris.core.PolarisCallContext; +import io.polaris.core.PolarisDefaultDiagServiceImpl; +import io.polaris.core.PolarisDiagnostics; +import io.polaris.core.entity.PolarisBaseEntity; +import io.polaris.core.entity.PolarisEntitySubType; +import io.polaris.core.entity.PolarisEntityType; +import io.polaris.core.entity.PolarisGrantRecord; +import io.polaris.core.entity.PolarisPrivilege; +import io.polaris.core.persistence.cache.EntityCache; +import io.polaris.core.persistence.cache.EntityCacheByNameKey; +import io.polaris.core.persistence.cache.EntityCacheEntry; +import io.polaris.core.persistence.cache.EntityCacheLookupResult; +import java.util.List; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +/** Unit testing of the entity cache */ +public class EntityCacheTest { + + // diag services + private final PolarisDiagnostics diagServices; + + // the entity store, use treemap implementation + private final PolarisTreeMapStore store; + + // to interact with the metastore + private final PolarisMetaStoreSession metaStore; + + // polaris call context + private final PolarisCallContext callCtx; + + // utility to bootstrap the mata store + private final PolarisTestMetaStoreManager tm; + + // the meta store manager + private final PolarisMetaStoreManager metaStoreManager; + + /** + * Initialize and create the test metadata + * + *
+   * - test
+   * - (N1/N2/T1)
+   * - (N1/N2/T2)
+   * - (N1/N2/V1)
+   * - (N1/N3/T3)
+   * - (N1/N3/V2)
+   * - (N1/T4)
+   * - (N1/N4)
+   * - N5/N6/T5
+   * - N5/N6/T6
+   * - R1(TABLE_READ on N1/N2, VIEW_CREATE on C, TABLE_LIST on N2, TABLE_DROP on N5/N6/T5)
+   * - R2(TABLE_WRITE_DATA on N5, VIEW_LIST on C)
+   * - PR1(R1, R2)
+   * - PR2(R2)
+   * - P1(PR1, PR2)
+   * - P2(PR2)
+   * 
+ */ + public EntityCacheTest() { + diagServices = new PolarisDefaultDiagServiceImpl(); + store = new PolarisTreeMapStore(diagServices); + metaStore = new PolarisTreeMapMetaStoreSessionImpl(store, Mockito.mock()); + callCtx = new PolarisCallContext(metaStore, diagServices); + metaStoreManager = new PolarisMetaStoreManagerImpl(); + + // bootstrap the mata store with our test schema + tm = new PolarisTestMetaStoreManager(metaStoreManager, callCtx); + tm.testCreateTestCatalog(); + } + + /** + * @return new cache for the entity store + */ + EntityCache allocateNewCache() { + return new EntityCache(this.metaStoreManager); + } + + @Test + void testGetOrLoadEntityByName() { + // get a new cache + EntityCache cache = this.allocateNewCache(); + + // should exist and no cache hit + EntityCacheLookupResult lookup = + cache.getOrLoadEntityByName( + this.callCtx, new EntityCacheByNameKey(PolarisEntityType.CATALOG, "test")); + Assertions.assertNotNull(lookup); + Assertions.assertFalse(lookup.isCacheHit()); + Assertions.assertNotNull(lookup.getCacheEntry()); + + // validate the cache entry + PolarisBaseEntity catalog = lookup.getCacheEntry().getEntity(); + Assertions.assertNotNull(catalog); + Assertions.assertEquals(PolarisEntityType.CATALOG, catalog.getType()); + + // do it again, should be found in the cache + lookup = + cache.getOrLoadEntityByName( + this.callCtx, new EntityCacheByNameKey(PolarisEntityType.CATALOG, "test")); + Assertions.assertNotNull(lookup); + Assertions.assertTrue(lookup.isCacheHit()); + + // do it again by id, should be found in the cache + lookup = cache.getOrLoadEntityById(this.callCtx, catalog.getCatalogId(), catalog.getId()); + Assertions.assertNotNull(lookup); + Assertions.assertTrue(lookup.isCacheHit()); + Assertions.assertNotNull(lookup.getCacheEntry()); + Assertions.assertNotNull(lookup.getCacheEntry().getEntity()); + Assertions.assertNotNull(lookup.getCacheEntry().getGrantRecordsAsSecurable()); + + // get N1 + PolarisBaseEntity N1 = + this.tm.ensureExistsByName(List.of(catalog), PolarisEntityType.NAMESPACE, "N1"); + + // get it directly from the cache, should not be there + EntityCacheByNameKey N1_name = + new EntityCacheByNameKey( + catalog.getId(), catalog.getId(), PolarisEntityType.NAMESPACE, "N1"); + EntityCacheEntry cacheEntry = cache.getEntityByName(N1_name); + Assertions.assertNull(cacheEntry); + + // try to find it in the cache by id. Should not be there, i.e. no cache hit + lookup = cache.getOrLoadEntityById(this.callCtx, N1.getCatalogId(), N1.getId()); + Assertions.assertNotNull(lookup); + Assertions.assertFalse(lookup.isCacheHit()); + + // should be there now, by name + cacheEntry = cache.getEntityByName(N1_name); + Assertions.assertNotNull(cacheEntry); + Assertions.assertNotNull(cacheEntry.getEntity()); + Assertions.assertNotNull(cacheEntry.getGrantRecordsAsSecurable()); + + // should be there now, by id + cacheEntry = cache.getEntityById(N1.getId()); + Assertions.assertNotNull(cacheEntry); + Assertions.assertNotNull(cacheEntry.getEntity()); + Assertions.assertNotNull(cacheEntry.getGrantRecordsAsSecurable()); + + // lookup N1 + EntityCacheEntry N1_entry = cache.getEntityById(N1.getId()); + Assertions.assertNotNull(N1_entry); + Assertions.assertNotNull(N1_entry.getEntity()); + Assertions.assertNotNull(N1_entry.getGrantRecordsAsSecurable()); + + // negative tests, load an entity which does not exist + lookup = cache.getOrLoadEntityById(this.callCtx, N1.getCatalogId(), 10000); + Assertions.assertNull(lookup); + lookup = + cache.getOrLoadEntityByName( + this.callCtx, + new EntityCacheByNameKey(PolarisEntityType.CATALOG, "non_existant_catalog")); + Assertions.assertNull(lookup); + + // lookup N2 to validate grants + EntityCacheByNameKey N2_name = + new EntityCacheByNameKey(catalog.getId(), N1.getId(), PolarisEntityType.NAMESPACE, "N2"); + lookup = cache.getOrLoadEntityByName(callCtx, N2_name); + Assertions.assertNotNull(lookup); + EntityCacheEntry cacheEntry_N1 = lookup.getCacheEntry(); + Assertions.assertNotNull(cacheEntry_N1); + Assertions.assertNotNull(cacheEntry_N1.getEntity()); + Assertions.assertNotNull(cacheEntry_N1.getGrantRecordsAsSecurable()); + + // lookup catalog role R1 + EntityCacheByNameKey R1_name = + new EntityCacheByNameKey( + catalog.getId(), catalog.getId(), PolarisEntityType.CATALOG_ROLE, "R1"); + lookup = cache.getOrLoadEntityByName(callCtx, R1_name); + Assertions.assertNotNull(lookup); + EntityCacheEntry cacheEntry_R1 = lookup.getCacheEntry(); + Assertions.assertNotNull(cacheEntry_R1); + Assertions.assertNotNull(cacheEntry_R1.getEntity()); + Assertions.assertNotNull(cacheEntry_R1.getGrantRecordsAsSecurable()); + Assertions.assertNotNull(cacheEntry_R1.getGrantRecordsAsGrantee()); + + // we expect one TABLE_READ grant on that securable granted to the catalog role R1 + Assertions.assertEquals(1, cacheEntry_N1.getGrantRecordsAsSecurable().size()); + PolarisGrantRecord gr = cacheEntry_N1.getGrantRecordsAsSecurable().get(0); + + // securable is N1, grantee is R1 + Assertions.assertEquals(cacheEntry_R1.getEntity().getId(), gr.getGranteeId()); + Assertions.assertEquals(cacheEntry_R1.getEntity().getCatalogId(), gr.getGranteeCatalogId()); + Assertions.assertEquals(cacheEntry_N1.getEntity().getId(), gr.getSecurableId()); + Assertions.assertEquals(cacheEntry_N1.getEntity().getCatalogId(), gr.getSecurableCatalogId()); + Assertions.assertEquals(PolarisPrivilege.TABLE_READ_DATA.getCode(), gr.getPrivilegeCode()); + + // R1 should have 4 privileges granted to it + Assertions.assertEquals(4, cacheEntry_R1.getGrantRecordsAsGrantee().size()); + List matchPriv = + cacheEntry_R1.getGrantRecordsAsGrantee().stream() + .filter( + grantRecord -> + grantRecord.getPrivilegeCode() == PolarisPrivilege.TABLE_READ_DATA.getCode()) + .toList(); + Assertions.assertEquals(1, matchPriv.size()); + gr = matchPriv.getFirst(); + Assertions.assertEquals(cacheEntry_R1.getEntity().getId(), gr.getGranteeId()); + Assertions.assertEquals(cacheEntry_R1.getEntity().getCatalogId(), gr.getGranteeCatalogId()); + Assertions.assertEquals(cacheEntry_N1.getEntity().getId(), gr.getSecurableId()); + Assertions.assertEquals(cacheEntry_N1.getEntity().getCatalogId(), gr.getSecurableCatalogId()); + Assertions.assertEquals(PolarisPrivilege.TABLE_READ_DATA.getCode(), gr.getPrivilegeCode()); + + // lookup principal role PR1 + EntityCacheByNameKey PR1_name = + new EntityCacheByNameKey(PolarisEntityType.PRINCIPAL_ROLE, "PR1"); + lookup = cache.getOrLoadEntityByName(callCtx, PR1_name); + Assertions.assertNotNull(lookup); + EntityCacheEntry cacheEntry_PR1 = lookup.getCacheEntry(); + Assertions.assertNotNull(cacheEntry_PR1); + Assertions.assertNotNull(cacheEntry_PR1.getEntity()); + Assertions.assertNotNull(cacheEntry_PR1.getGrantRecordsAsSecurable()); + Assertions.assertNotNull(cacheEntry_PR1.getGrantRecordsAsGrantee()); + + // R1 should have 1 CATALOG_ROLE_USAGE privilege granted *on* it to PR1 + Assertions.assertEquals(1, cacheEntry_R1.getGrantRecordsAsSecurable().size()); + gr = cacheEntry_R1.getGrantRecordsAsSecurable().get(0); + Assertions.assertEquals(cacheEntry_R1.getEntity().getId(), gr.getSecurableId()); + Assertions.assertEquals(cacheEntry_R1.getEntity().getCatalogId(), gr.getSecurableCatalogId()); + Assertions.assertEquals(cacheEntry_PR1.getEntity().getId(), gr.getGranteeId()); + Assertions.assertEquals(cacheEntry_PR1.getEntity().getCatalogId(), gr.getGranteeCatalogId()); + Assertions.assertEquals(PolarisPrivilege.CATALOG_ROLE_USAGE.getCode(), gr.getPrivilegeCode()); + + // PR1 should have 1 grant on it to P1. + Assertions.assertEquals(1, cacheEntry_PR1.getGrantRecordsAsSecurable().size()); + Assertions.assertEquals( + PolarisPrivilege.PRINCIPAL_ROLE_USAGE.getCode(), + cacheEntry_PR1.getGrantRecordsAsSecurable().get(0).getPrivilegeCode()); + + // PR1 should have 2 grants to it, on R1 and R2 + Assertions.assertEquals(2, cacheEntry_PR1.getGrantRecordsAsGrantee().size()); + Assertions.assertEquals( + PolarisPrivilege.CATALOG_ROLE_USAGE.getCode(), + cacheEntry_PR1.getGrantRecordsAsGrantee().get(0).getPrivilegeCode()); + Assertions.assertEquals( + PolarisPrivilege.CATALOG_ROLE_USAGE.getCode(), + cacheEntry_PR1.getGrantRecordsAsGrantee().get(1).getPrivilegeCode()); + } + + @Test + void testRefresh() { + // allocate a new cache + EntityCache cache = this.allocateNewCache(); + + // should exist and no cache hit + EntityCacheLookupResult lookup = + cache.getOrLoadEntityByName( + this.callCtx, new EntityCacheByNameKey(PolarisEntityType.CATALOG, "test")); + Assertions.assertNotNull(lookup); + Assertions.assertFalse(lookup.isCacheHit()); + + // the catalog + Assertions.assertNotNull(lookup.getCacheEntry()); + PolarisBaseEntity catalog = lookup.getCacheEntry().getEntity(); + Assertions.assertNotNull(catalog); + Assertions.assertEquals(PolarisEntityType.CATALOG, catalog.getType()); + + // find table N5/N6/T6 + PolarisBaseEntity N5 = + this.tm.ensureExistsByName(List.of(catalog), PolarisEntityType.NAMESPACE, "N5"); + PolarisBaseEntity N5_N6 = + this.tm.ensureExistsByName(List.of(catalog, N5), PolarisEntityType.NAMESPACE, "N6"); + PolarisBaseEntity T6v1 = + this.tm.ensureExistsByName( + List.of(catalog, N5, N5_N6), + PolarisEntityType.TABLE_LIKE, + PolarisEntitySubType.TABLE, + "T6"); + Assertions.assertNotNull(T6v1); + + // that table is not in the cache + EntityCacheEntry cacheEntry = cache.getEntityById(T6v1.getId()); + Assertions.assertNull(cacheEntry); + + // now load that table in the cache + cacheEntry = + cache.getAndRefreshIfNeeded( + this.callCtx, T6v1, T6v1.getEntityVersion(), T6v1.getGrantRecordsVersion()); + Assertions.assertNotNull(cacheEntry); + Assertions.assertNotNull(cacheEntry.getEntity()); + Assertions.assertNotNull(cacheEntry.getGrantRecordsAsSecurable()); + PolarisBaseEntity table = cacheEntry.getEntity(); + Assertions.assertEquals(T6v1.getId(), table.getId()); + Assertions.assertEquals(T6v1.getEntityVersion(), table.getEntityVersion()); + Assertions.assertEquals(T6v1.getGrantRecordsVersion(), table.getGrantRecordsVersion()); + + // update the entity + PolarisBaseEntity T6v2 = + this.tm.updateEntity( + List.of(catalog, N5, N5_N6), + T6v1, + "{\"v2_properties\": \"some value\"}", + "{\"v2_internal_properties\": \"internal value\"}"); + Assertions.assertNotNull(T6v2); + + // now refresh that entity. But because we don't change the versions, nothing should be reloaded + cacheEntry = + cache.getAndRefreshIfNeeded( + this.callCtx, T6v1, T6v1.getEntityVersion(), T6v1.getGrantRecordsVersion()); + Assertions.assertNotNull(cacheEntry); + Assertions.assertNotNull(cacheEntry.getEntity()); + Assertions.assertNotNull(cacheEntry.getGrantRecordsAsSecurable()); + table = cacheEntry.getEntity(); + Assertions.assertEquals(T6v1.getId(), table.getId()); + Assertions.assertEquals(T6v1.getEntityVersion(), table.getEntityVersion()); + Assertions.assertEquals(T6v1.getGrantRecordsVersion(), table.getGrantRecordsVersion()); + + // now refresh again, this time with the new versions. Should be reloaded + cacheEntry = + cache.getAndRefreshIfNeeded( + this.callCtx, T6v2, T6v2.getEntityVersion(), T6v2.getGrantRecordsVersion()); + Assertions.assertNotNull(cacheEntry); + Assertions.assertNotNull(cacheEntry.getEntity()); + Assertions.assertNotNull(cacheEntry.getGrantRecordsAsSecurable()); + table = cacheEntry.getEntity(); + Assertions.assertEquals(T6v2.getId(), table.getId()); + Assertions.assertEquals(T6v2.getEntityVersion(), table.getEntityVersion()); + Assertions.assertEquals(T6v2.getGrantRecordsVersion(), table.getGrantRecordsVersion()); + + // update it again + PolarisBaseEntity T6v3 = + this.tm.updateEntity( + List.of(catalog, N5, N5_N6), + T6v2, + "{\"v3_properties\": \"some value\"}", + "{\"v3_internal_properties\": \"internal value\"}"); + Assertions.assertNotNull(T6v3); + + // the two catalog roles + PolarisBaseEntity R1 = + this.tm.ensureExistsByName(List.of(catalog), PolarisEntityType.CATALOG_ROLE, "R1"); + PolarisBaseEntity N1 = + this.tm.ensureExistsByName(List.of(catalog), PolarisEntityType.NAMESPACE, "N1"); + PolarisBaseEntity N2 = + this.tm.ensureExistsByName(List.of(catalog, N1), PolarisEntityType.NAMESPACE, "N2"); + + // load that namespace + cacheEntry = + cache.getAndRefreshIfNeeded( + this.callCtx, N2, N2.getEntityVersion(), N2.getGrantRecordsVersion()); + + // should have one single grant + Assertions.assertNotNull(cacheEntry); + Assertions.assertNotNull(cacheEntry.getGrantRecordsAsSecurable()); + Assertions.assertEquals(1, cacheEntry.getGrantRecordsAsSecurable().size()); + + // perform an additional grant to R1 + this.tm.grantPrivilege(R1, List.of(catalog, N1), N2, PolarisPrivilege.NAMESPACE_FULL_METADATA); + + // now reload N2, grant records version should have changed + PolarisBaseEntity N2v2 = + this.tm.ensureExistsByName(List.of(catalog, N1), PolarisEntityType.NAMESPACE, "N2"); + + // same entity version but different grant records + Assertions.assertNotNull(N2v2); + Assertions.assertEquals(N2.getGrantRecordsVersion() + 1, N2v2.getGrantRecordsVersion()); + + // the cache is outdated now + lookup = + cache.getOrLoadEntityByName( + this.callCtx, + new EntityCacheByNameKey( + catalog.getId(), N1.getId(), PolarisEntityType.NAMESPACE, "N2")); + Assertions.assertNotNull(lookup); + cacheEntry = lookup.getCacheEntry(); + Assertions.assertNotNull(cacheEntry); + Assertions.assertNotNull(cacheEntry.getEntity()); + Assertions.assertNotNull(cacheEntry.getGrantRecordsAsSecurable()); + Assertions.assertEquals(1, cacheEntry.getGrantRecordsAsSecurable().size()); + Assertions.assertEquals( + N2.getGrantRecordsVersion(), cacheEntry.getEntity().getGrantRecordsVersion()); + + // now refresh + cacheEntry = + cache.getAndRefreshIfNeeded( + this.callCtx, N2, N2v2.getEntityVersion(), N2v2.getGrantRecordsVersion()); + Assertions.assertNotNull(cacheEntry); + Assertions.assertNotNull(cacheEntry.getEntity()); + Assertions.assertNotNull(cacheEntry.getGrantRecordsAsSecurable()); + Assertions.assertEquals(2, cacheEntry.getGrantRecordsAsSecurable().size()); + Assertions.assertEquals( + N2v2.getGrantRecordsVersion(), cacheEntry.getEntity().getGrantRecordsVersion()); + } + + @Test + void testRenameAndCacheDestinationBeforeLoadingSource() { + // get a new cache + EntityCache cache = this.allocateNewCache(); + + EntityCacheLookupResult lookup = + cache.getOrLoadEntityByName( + this.callCtx, new EntityCacheByNameKey(PolarisEntityType.CATALOG, "test")); + Assertions.assertNotNull(lookup); + Assertions.assertNotNull(lookup.getCacheEntry()); + PolarisBaseEntity catalog = lookup.getCacheEntry().getEntity(); + + PolarisBaseEntity N1 = + this.tm.ensureExistsByName(List.of(catalog), PolarisEntityType.NAMESPACE, "N1"); + lookup = cache.getOrLoadEntityById(this.callCtx, N1.getCatalogId(), N1.getId()); + Assertions.assertNotNull(lookup); + + EntityCacheByNameKey T4_name = + new EntityCacheByNameKey(N1.getCatalogId(), N1.getId(), PolarisEntityType.TABLE_LIKE, "T4"); + lookup = cache.getOrLoadEntityByName(callCtx, T4_name); + Assertions.assertNotNull(lookup); + EntityCacheEntry cacheEntry_T4 = lookup.getCacheEntry(); + Assertions.assertNotNull(cacheEntry_T4); + Assertions.assertNotNull(cacheEntry_T4.getEntity()); + Assertions.assertNotNull(cacheEntry_T4.getGrantRecordsAsSecurable()); + + PolarisBaseEntity T4 = cacheEntry_T4.getEntity(); + + this.tm.renameEntity(List.of(catalog, N1), T4, null, "T4_renamed"); + + // load the renamed entity into cache + EntityCacheByNameKey T4_renamed = + new EntityCacheByNameKey( + N1.getCatalogId(), N1.getId(), PolarisEntityType.TABLE_LIKE, "T4_renamed"); + lookup = cache.getOrLoadEntityByName(callCtx, T4_renamed); + Assertions.assertNotNull(lookup); + EntityCacheEntry cacheEntry_T4_renamed = lookup.getCacheEntry(); + Assertions.assertNotNull(cacheEntry_T4_renamed); + PolarisBaseEntity T4_renamed_entity = cacheEntry_T4_renamed.getEntity(); + + // new entry if lookup by id + EntityCacheLookupResult lookupResult = + cache.getOrLoadEntityById(callCtx, T4.getCatalogId(), T4.getId()); + Assertions.assertNotNull(lookupResult); + Assertions.assertNotNull(lookupResult.getCacheEntry()); + Assertions.assertEquals("T4_renamed", lookupResult.getCacheEntry().getEntity().getName()); + + // old name is gone, replaced by new name + // Assertions.assertNull(cache.getOrLoadEntityByName(callCtx, T4_name)); + + // refreshing should return null since our current held T4 is outdated + cache.getAndRefreshIfNeeded( + callCtx, + T4, + T4_renamed_entity.getEntityVersion(), + T4_renamed_entity.getGrantRecordsVersion()); + + // now the loading by the old name should return null + Assertions.assertNull(cache.getOrLoadEntityByName(callCtx, T4_name)); + } +} diff --git a/polaris-core/src/test/java/io/polaris/core/persistence/PolarisObjectMapperUtilTest.java b/polaris-core/src/test/java/io/polaris/core/persistence/PolarisObjectMapperUtilTest.java new file mode 100644 index 0000000000..27b13ea4ce --- /dev/null +++ b/polaris-core/src/test/java/io/polaris/core/persistence/PolarisObjectMapperUtilTest.java @@ -0,0 +1,59 @@ +package io.polaris.core.persistence; + +import io.polaris.core.entity.PolarisBaseEntity; +import io.polaris.core.entity.PolarisEntitySubType; +import io.polaris.core.entity.PolarisEntityType; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +class PolarisObjectMapperUtilTest { + + @Test + public void testParseTaskState() { + PolarisBaseEntity entity = + new PolarisBaseEntity( + 0L, 1L, PolarisEntityType.TASK, PolarisEntitySubType.NULL_SUBTYPE, 0L, "task"); + entity.setProperties( + "{\"name\": \"my name\", \"lastAttemptExecutorId\": \"the_executor\", \"data\": {\"nestedFields\": " + + "{\"further_nesting\": \"astring\", \"anArray\": [1, 2, 3, 4]}, \"anotherNestedField\": \"simple string\"}, " + + "\"lastAttemptStartTime\": \"100\", \"attemptCount\": \"9\"}"); + PolarisObjectMapperUtil.TaskExecutionState state = + PolarisObjectMapperUtil.parseTaskState(entity); + Assertions.assertThat(state) + .isNotNull() + .returns(100L, PolarisObjectMapperUtil.TaskExecutionState::getLastAttemptStartTime) + .returns(9, PolarisObjectMapperUtil.TaskExecutionState::getAttemptCount) + .returns("the_executor", PolarisObjectMapperUtil.TaskExecutionState::getExecutor); + } + + @Test + public void testParseTaskStateWithMissingFields() { + PolarisBaseEntity entity = + new PolarisBaseEntity( + 0L, 1L, PolarisEntityType.TASK, PolarisEntitySubType.NULL_SUBTYPE, 0L, "task"); + entity.setProperties( + "{\"name\": \"my name\", \"data\": {\"nestedFields\": " + + "{\"further_nesting\": \"astring\", \"anArray\": [1, 2, 3, 4]}, \"anotherNestedField\": \"simple string\"}, " + + "\"attemptCount\": \"5\"}"); + PolarisObjectMapperUtil.TaskExecutionState state = + PolarisObjectMapperUtil.parseTaskState(entity); + Assertions.assertThat(state) + .isNotNull() + .returns(0L, PolarisObjectMapperUtil.TaskExecutionState::getLastAttemptStartTime) + .returns(5, PolarisObjectMapperUtil.TaskExecutionState::getAttemptCount) + .returns(null, PolarisObjectMapperUtil.TaskExecutionState::getExecutor); + } + + @Test + public void testParseTaskStateWithInvalidJson() { + PolarisBaseEntity entity = + new PolarisBaseEntity( + 0L, 1L, PolarisEntityType.TASK, PolarisEntitySubType.NULL_SUBTYPE, 0L, "task"); + entity.setProperties( + "{\"name\": \"my name\", \"data\": {\"nestedFields\": " + + "{\"further_nesting\": \"astring\", \"anArray\": , : \"simple string\"}, "); + PolarisObjectMapperUtil.TaskExecutionState state = + PolarisObjectMapperUtil.parseTaskState(entity); + Assertions.assertThat(state).isNull(); + } +} diff --git a/polaris-core/src/test/java/io/polaris/core/persistence/PolarisTreeMapMetaStoreManagerTest.java b/polaris-core/src/test/java/io/polaris/core/persistence/PolarisTreeMapMetaStoreManagerTest.java new file mode 100644 index 0000000000..36df7b5d7a --- /dev/null +++ b/polaris-core/src/test/java/io/polaris/core/persistence/PolarisTreeMapMetaStoreManagerTest.java @@ -0,0 +1,24 @@ +package io.polaris.core.persistence; + +import io.polaris.core.PolarisCallContext; +import io.polaris.core.PolarisConfigurationStore; +import io.polaris.core.PolarisDefaultDiagServiceImpl; +import io.polaris.core.PolarisDiagnostics; +import java.time.ZoneId; +import org.mockito.Mockito; + +public class PolarisTreeMapMetaStoreManagerTest extends PolarisMetaStoreManagerTest { + @Override + public PolarisTestMetaStoreManager createPolarisTestMetaStoreManager() { + PolarisDiagnostics diagServices = new PolarisDefaultDiagServiceImpl(); + PolarisTreeMapStore store = new PolarisTreeMapStore(diagServices); + PolarisCallContext callCtx = + new PolarisCallContext( + new PolarisTreeMapMetaStoreSessionImpl(store, Mockito.mock()), + diagServices, + new PolarisConfigurationStore() {}, + timeSource.withZone(ZoneId.systemDefault())); + + return new PolarisTestMetaStoreManager(new PolarisMetaStoreManagerImpl(), callCtx); + } +} diff --git a/polaris-core/src/test/java/io/polaris/core/persistence/ResolverTest.java b/polaris-core/src/test/java/io/polaris/core/persistence/ResolverTest.java new file mode 100644 index 0000000000..f409751db3 --- /dev/null +++ b/polaris-core/src/test/java/io/polaris/core/persistence/ResolverTest.java @@ -0,0 +1,923 @@ +package io.polaris.core.persistence; + +import io.polaris.core.PolarisCallContext; +import io.polaris.core.PolarisDefaultDiagServiceImpl; +import io.polaris.core.PolarisDiagnostics; +import io.polaris.core.entity.PolarisBaseEntity; +import io.polaris.core.entity.PolarisEntityCore; +import io.polaris.core.entity.PolarisEntitySubType; +import io.polaris.core.entity.PolarisEntityType; +import io.polaris.core.entity.PolarisGrantRecord; +import io.polaris.core.entity.PolarisPrivilege; +import io.polaris.core.persistence.cache.EntityCache; +import io.polaris.core.persistence.cache.EntityCacheEntry; +import io.polaris.core.persistence.resolver.Resolver; +import io.polaris.core.persistence.resolver.ResolverPath; +import io.polaris.core.persistence.resolver.ResolverStatus; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +public class ResolverTest { + + // diag services + private final PolarisDiagnostics diagServices; + + // the entity store, use treemap implementation + private final PolarisTreeMapStore store; + + // to interact with the metastore + private final PolarisMetaStoreSession metaStore; + + // polaris call context + private final PolarisCallContext callCtx; + + // utility to bootstrap the mata store + private final PolarisTestMetaStoreManager tm; + + // the meta store manager + private final PolarisMetaStoreManager metaStoreManager; + + // Principal P1 + private final PolarisBaseEntity P1; + + // cache we are using + private EntityCache cache; + + /** + * Initialize and create the test metadata + * + *
+   * - test
+   * - (N1/N2/T1)
+   * - (N1/N2/T2)
+   * - (N1/N2/V1)
+   * - (N1/N3/T3)
+   * - (N1/N3/V2)
+   * - (N1/T4)
+   * - (N1/N4)
+   * - N5/N6/T5
+   * - N5/N6/T6
+   * - R1(TABLE_READ on N1/N2, VIEW_CREATE on C, TABLE_LIST on N2, TABLE_DROP on N5/N6/T5)
+   * - R2(TABLE_WRITE_DATA on N5, VIEW_LIST on C)
+   * - PR1(R1, R2)
+   * - PR2(R2)
+   * - P1(PR1, PR2)
+   * - P2(PR1)
+   * 
+ */ + public ResolverTest() { + diagServices = new PolarisDefaultDiagServiceImpl(); + store = new PolarisTreeMapStore(diagServices); + metaStore = new PolarisTreeMapMetaStoreSessionImpl(store, Mockito.mock()); + callCtx = new PolarisCallContext(metaStore, diagServices); + metaStoreManager = new PolarisMetaStoreManagerImpl(); + + // bootstrap the mata store with our test schema + tm = new PolarisTestMetaStoreManager(metaStoreManager, callCtx); + tm.testCreateTestCatalog(); + + // principal P1 + this.P1 = tm.ensureExistsByName(null, PolarisEntityType.PRINCIPAL, "P1"); + } + + /** This test resolver for a create-principal scenario */ + @Test + void testResolvePrincipal() { + + // resolve a principal which does not exist, but make it optional so will succeed + this.resolveDriver(null, null, "P3", true, null, null); + + // resolve same principal but now make it non optional, so should fail + this.resolveDriver( + null, null, "P3", false, null, ResolverStatus.StatusEnum.ENTITY_COULD_NOT_BE_RESOLVED); + + // then resolve a principal which does exist + this.resolveDriver(null, null, "P2", false, null, null); + + // do it again, but this time using the primed cache + this.resolveDriver(this.cache, null, "P2", false, null, null); + + // now add a principal roles + this.resolveDriver(this.cache, null, "P2", false, "PR1", null); + + // do it again, everything in the cache + this.resolveDriver(this.cache, null, "P2", false, "PR1", null); + + // do it again on a cold cache + this.resolveDriver(this.cache, null, "P2", false, "PR1", null); + } + + /** Test that we can specify a subset of principal role names */ + @Test + void testScopedPrincipalRole() { + + // start without a scope + this.resolveDriver(null, null, "P2", false, "PR1", null); + + // specify various scopes + this.resolveDriver(this.cache, Set.of("PR1"), "P2", false, "PR1", null); + this.resolveDriver(this.cache, Set.of("PR2"), "P2", false, "PR1", null); + this.resolveDriver(this.cache, Set.of("PR2", "PR3"), "P2", false, "PR1", null); + this.resolveDriver(null, Set.of("PR2", "PR3"), "P2", false, "PR1", null); + this.resolveDriver(null, Set.of("PR3"), "P2", false, "PR1", null); + this.resolveDriver(this.cache, Set.of("PR1", "PR2"), "P2", false, "PR1", null); + } + + /** + * Test that the set of catalog roles being activated is correctly inferred, based of a set of + * principal roles + */ + @Test + void testCatalogRolesActivation() { + + // start simple, with both PR1 and PR2, you get R1 and R2 + this.resolveDriver(null, Set.of("PR1", "PR2"), "test", Set.of("R1", "R2")); + + // PR1 itself is enough to activate both R1 and R2 + this.resolveDriver(this.cache, Set.of("PR1"), "test", Set.of("R1", "R2")); + + // PR2 only activates R2 + this.resolveDriver(this.cache, Set.of("PR2"), "test", Set.of("R2")); + + // With a non-existing principal roles, nothing gets activated + this.resolveDriver(this.cache, Set.of("NOT_EXISTING"), "test", Set.of()); + } + + /** Test that paths, one or more, are properly resolved */ + @Test + void testResolvePath() { + // N1 which exists + ResolverPath N1 = new ResolverPath(List.of("N1"), PolarisEntityType.NAMESPACE); + this.resolveDriver(null, "test", N1, null, null); + + // N1/N2 which exists + ResolverPath N1_N2 = new ResolverPath(List.of("N1", "N2"), PolarisEntityType.NAMESPACE); + this.resolveDriver(null, "test", N1_N2, null, null); + + // N1/N2/T1 which exists + ResolverPath N1_N2_T1 = + new ResolverPath(List.of("N1", "N2", "T1"), PolarisEntityType.TABLE_LIKE); + this.resolveDriver(this.cache, "test", N1_N2_T1, null, null); + + // N1/N2/T1 which exists + ResolverPath N1_N2_V1 = + new ResolverPath(List.of("N1", "N2", "V1"), PolarisEntityType.TABLE_LIKE); + this.resolveDriver(this.cache, "test", N1_N2_V1, null, null); + + // N5/N6 which exists + ResolverPath N5_N6 = new ResolverPath(List.of("N5", "N6"), PolarisEntityType.NAMESPACE); + this.resolveDriver(this.cache, "test", N5_N6, null, null); + + // N5/N6/T5 which exists + ResolverPath N5_N6_T5 = + new ResolverPath(List.of("N5", "N6", "T5"), PolarisEntityType.TABLE_LIKE); + this.resolveDriver(this.cache, "test", N5_N6_T5, null, null); + + // Error scenarios: N5/N6/T8 which does not exists + ResolverPath N5_N6_T8 = + new ResolverPath(List.of("N5", "N6", "T8"), PolarisEntityType.TABLE_LIKE); + this.resolveDriver( + this.cache, + "test", + N5_N6_T8, + null, + ResolverStatus.StatusEnum.PATH_COULD_NOT_BE_FULLY_RESOLVED); + + // Error scenarios: N8/N6/T8 which does not exists + ResolverPath N8_N6_T8 = + new ResolverPath(List.of("N8", "N6", "T8"), PolarisEntityType.TABLE_LIKE); + this.resolveDriver( + this.cache, + "test", + N8_N6_T8, + null, + ResolverStatus.StatusEnum.PATH_COULD_NOT_BE_FULLY_RESOLVED); + + // now test multiple paths + this.resolveDriver( + this.cache, "test", null, List.of(N1, N5_N6, N1, N1_N2, N5_N6_T5, N1_N2), null); + this.resolveDriver( + this.cache, + "test", + null, + List.of(N1, N5_N6_T8, N5_N6_T5, N1_N2), + ResolverStatus.StatusEnum.PATH_COULD_NOT_BE_FULLY_RESOLVED); + + // except if the optional flag is specified + N5_N6_T8 = new ResolverPath(List.of("N5", "N6", "T8"), PolarisEntityType.TABLE_LIKE, true); + Resolver resolver = + this.resolveDriver(this.cache, "test", null, List.of(N1, N5_N6_T8, N5_N6_T5, N1_N2), null); + // get all the resolved paths + List> resolvedPath = resolver.getResolvedPaths(); + Assertions.assertEquals(1, resolvedPath.get(0).size()); + Assertions.assertEquals(2, resolvedPath.get(1).size()); + Assertions.assertEquals(3, resolvedPath.get(2).size()); + Assertions.assertEquals(2, resolvedPath.get(3).size()); + } + + /** + * Ensure that if data changes while entities are cached, we will always resolve to the latest + * version + */ + @Test + void testConsistency() { + + // resolve principal "P2" + this.resolveDriver(null, null, "P2", false, null, null); + this.resolveDriver(this.cache, null, "P2", false, null, null); + + // now drop this principal. It is still cached + PolarisBaseEntity P2 = this.tm.ensureExistsByName(null, PolarisEntityType.PRINCIPAL, "P2"); + this.tm.dropEntity(null, P2); + + // now resolve it again. Should fail because the entity was dropped + this.resolveDriver( + this.cache, + null, + "P2", + false, + null, + ResolverStatus.StatusEnum.ENTITY_COULD_NOT_BE_RESOLVED); + + // recreate P2 + this.tm.createPrincipal("P2"); + + // now resolve it again. Should succeed because the entity has been re-created + this.resolveDriver(this.cache, null, "P2", false, null, ResolverStatus.StatusEnum.SUCCESS); + + // resolve existing grants on catalog + this.resolveDriver(this.cache, Set.of("PR1", "PR2"), "test", Set.of("R1", "R2")); + + // with only PR2, we will only activate R2 + Resolver resolver = this.resolveDriver(this.cache, Set.of("PR2"), "test", Set.of("R2")); + + // Now add a new catalog role and see if the changes are reflected + Assertions.assertNotNull(resolver.getResolvedReferenceCatalog()); + PolarisBaseEntity TEST = resolver.getResolvedReferenceCatalog().getEntity(); + PolarisBaseEntity R3 = + this.tm.createEntity(List.of(TEST), PolarisEntityType.CATALOG_ROLE, "R3"); + + // now grant R3 to PR2 + Assertions.assertEquals(1, resolver.getResolvedCallerPrincipalRoles().size()); + PolarisBaseEntity PR2 = resolver.getResolvedCallerPrincipalRoles().getFirst().getEntity(); + this.tm.grantToGrantee(TEST, R3, PR2, PolarisPrivilege.CATALOG_ROLE_USAGE); + + // now resolve again with only PR2 activated, should see the new catalog role R3 + this.resolveDriver(this.cache, Set.of("PR2"), "test", Set.of("R2", "R3")); + + // now drop that role and then recreate it. The new incarnation should be used + this.tm.dropEntity(List.of(TEST), R3); + PolarisBaseEntity R3_NEW = + this.tm.createEntity(List.of(TEST), PolarisEntityType.CATALOG_ROLE, "R3"); + + // now grant R3_NEW to PR2 and resolve it again + this.tm.grantToGrantee(TEST, R3_NEW, PR2, PolarisPrivilege.CATALOG_ROLE_USAGE); + resolver = this.resolveDriver(this.cache, Set.of("PR2"), "test", Set.of("R2", "R3")); + + // ensure that the correct catalog role was resolved + Assertions.assertTrue(resolver.getResolvedCatalogRoles().containsKey(R3_NEW.getId())); + } + + /** Check resolve paths when cache is inconsistent */ + @Test + void testPathConsistency() { + // resolve few paths path + ResolverPath N1_PATH = new ResolverPath(List.of("N1"), PolarisEntityType.NAMESPACE); + this.resolveDriver(null, "test", N1_PATH, null, null); + ResolverPath N1_N2_PATH = new ResolverPath(List.of("N1", "N2"), PolarisEntityType.NAMESPACE); + this.resolveDriver(this.cache, "test", N1_N2_PATH, null, null); + ResolverPath N1_N2_T1_PATH = + new ResolverPath(List.of("N1", "N2", "T1"), PolarisEntityType.TABLE_LIKE); + Resolver resolver = this.resolveDriver(this.cache, "test", N1_N2_T1_PATH, null, null); + + // get the catalog + Assertions.assertNotNull(resolver.getResolvedReferenceCatalog()); + PolarisBaseEntity TEST = resolver.getResolvedReferenceCatalog().getEntity(); + + // get the various entities in the path + Assertions.assertNotNull(resolver.getResolvedPath()); + Assertions.assertEquals(3, resolver.getResolvedPath().size()); + PolarisBaseEntity N1 = resolver.getResolvedPath().getFirst().getEntity(); + PolarisBaseEntity N2 = resolver.getResolvedPath().get(1).getEntity(); + PolarisBaseEntity T1 = resolver.getResolvedPath().get(2).getEntity(); + + // resolve N3 + ResolverPath N1_N3_PATH = new ResolverPath(List.of("N1", "N3"), PolarisEntityType.NAMESPACE); + resolver = this.resolveDriver(this.cache, "test", N1_N3_PATH, null, null); + Assertions.assertNotNull(resolver.getResolvedPath()); + Assertions.assertEquals(2, resolver.getResolvedPath().size()); + PolarisBaseEntity N3 = resolver.getResolvedPath().get(1).getEntity(); + + // now re-parent T1 under N3, keeping the same name + this.tm.renameEntity(List.of(TEST, N1, N2), T1, List.of(TEST, N1, N3), "T1"); + + // now expect to fail resolving T1 under N1/N2 + this.resolveDriver( + this.cache, + "test", + N1_N2_T1_PATH, + null, + ResolverStatus.StatusEnum.PATH_COULD_NOT_BE_FULLY_RESOLVED); + + // but we should be able to resolve it under N1/N3 + ResolverPath N1_N3_T1_PATH = + new ResolverPath(List.of("N1", "N3", "T1"), PolarisEntityType.TABLE_LIKE); + this.resolveDriver(this.cache, "test", N1_N3_T1_PATH, null, null); + } + + /** Resolve catalog roles */ + @Test + void testResolveCatalogRole() { + + // resolve catalog role + this.resolveDriver(null, "test", "R1", null); + + // do it again + this.resolveDriver(this.cache, "test", "R1", null); + this.resolveDriver(this.cache, "test", "R1", null); + + // failure scenario + this.resolveDriver( + this.cache, "test", "R5", ResolverStatus.StatusEnum.ENTITY_COULD_NOT_BE_RESOLVED); + } + + /** + * Create a simple resolver without a reference catalog, any principal roles sub-scope and using + * P1 as the caller principal + * + * @return new resolver to test with + */ + @NotNull + private Resolver allocateResolver() { + return this.allocateResolver(null, null); + } + + /** + * Create a simple resolver without any principal roles sub-scope and using P1 as the caller + * principal + * + * @param referenceCatalogName the reference e catalog name, can be null + * @return new resolver to test with + */ + @NotNull + private Resolver allocateResolver(@Nullable String referenceCatalogName) { + return this.allocateResolver(null, referenceCatalogName); + } + + /** + * Create a simple resolver without any principal roles sub-scope and using P1 as the caller + * principal + * + * @param cache if not null, cache to use, else one will be created + * @return new resolver to test with + */ + @NotNull + private Resolver allocateResolver(@Nullable EntityCache cache) { + return this.allocateResolver(cache, null); + } + + /** + * Create a simple resolver without any principal roles sub-scope and using P1 as the caller + * principal + * + * @param cache if not null, cache to use, else one will be created + * @param referenceCatalogName the reference e catalog name, can be null + * @return new resolver to test with + */ + @NotNull + private Resolver allocateResolver( + @Nullable EntityCache cache, @Nullable String referenceCatalogName) { + return this.allocateResolver(cache, null, referenceCatalogName); + } + + /** + * Create a simple resolver without any principal roles sub-scope and using P1 as the caller + * principal + * + * @param cache if not null, cache to use, else one will be created + * @param principalRolesScope if not null, scoped principal roles + * @param referenceCatalogName the reference e catalog name, can be null + * @return new resolver to test with + */ + @NotNull + private Resolver allocateResolver( + @Nullable EntityCache cache, + Set principalRolesScope, + @Nullable String referenceCatalogName) { + + // create a new cache if needs be + if (cache == null) { + this.cache = new EntityCache(this.metaStoreManager); + } + return new Resolver( + this.callCtx, + this.metaStoreManager, + this.P1.getId(), + null, + principalRolesScope, + this.cache, + referenceCatalogName); + } + + /** + * Resolve a principal and optionally a principal role + * + * @param cache if not null, cache to use + * @param principalName name of the principal name being created + * @param exists true if this principal already exists + * @param principalRoleName name of the principal role, should exist + */ + private void resolvePrincipalAndPrincipalRole( + EntityCache cache, String principalName, boolean exists, String principalRoleName) { + Resolver resolver = allocateResolver(cache); + + // for a principal creation, we simply want to test if the principal we are creating exists + // or not + resolver.addOptionalEntityByName(PolarisEntityType.PRINCIPAL, principalName); + + // add principal role if one passed-in + if (principalRoleName != null) { + resolver.addOptionalEntityByName(PolarisEntityType.PRINCIPAL_ROLE, principalRoleName); + } + + // done, run resolve + ResolverStatus status = resolver.resolveAll(); + + // we expect success + Assertions.assertEquals(ResolverStatus.StatusEnum.SUCCESS, status.getStatus()); + + // the principal does not exist, check that this is the case + if (exists) { + // the principal exist, check that this is the case + this.ensureResolved( + resolver.getResolvedEntity(PolarisEntityType.PRINCIPAL, principalName), + PolarisEntityType.PRINCIPAL, + principalName); + } else { + // not found + Assertions.assertNull(resolver.getResolvedEntity(PolarisEntityType.PRINCIPAL, principalName)); + } + + // validate that we were able to resolve the principal and the two principal roles + this.ensureResolved(resolver.getResolvedCallerPrincipal(), PolarisEntityType.PRINCIPAL, "P1"); + + // validate that the two principal roles have been activated + List principalRolesResolved = resolver.getResolvedCallerPrincipalRoles(); + + // expect two principal roles + Assertions.assertEquals(2, principalRolesResolved.size()); + principalRolesResolved.sort(Comparator.comparing(p -> p.getEntity().getName())); + + // ensure they are PR1 and PR2 + this.ensureResolved(principalRolesResolved.getFirst(), PolarisEntityType.PRINCIPAL_ROLE, "PR1"); + this.ensureResolved(principalRolesResolved.getLast(), PolarisEntityType.PRINCIPAL_ROLE, "PR2"); + + // if a principal role was passed-in, ensure it exists + if (principalRoleName != null) { + this.ensureResolved( + resolver.getResolvedEntity(PolarisEntityType.PRINCIPAL_ROLE, principalRoleName), + PolarisEntityType.PRINCIPAL_ROLE, + principalRoleName); + } + } + + /** + * Main resolve driver + * + * @param cache if not null, cache we can use + * @param principalRolesScope if not null, scoped roles + * @param principalName if not null, name of the principal to resolve + * @param isPrincipalNameOptional if true, the name of the principal is optional + * @param principalRoleName if not null, name of the principal role to resolve + * @param expectedStatus the expected status if not success + * @return resolver we created and which has been validated. + */ + private Resolver resolveDriver( + EntityCache cache, + Set principalRolesScope, + String principalName, + boolean isPrincipalNameOptional, + String principalRoleName, + ResolverStatus.StatusEnum expectedStatus) { + return this.resolveDriver( + cache, + principalRolesScope, + principalName, + isPrincipalNameOptional, + principalRoleName, + null, + null, + null, + null, + expectedStatus, + null); + } + + /** + * Main resolve driver + * + * @param cache if not null, cache we can use + * @param catalogName if not null, name of the catalog to resolve + * @param path if not null, single path in that catalog + * @param paths if not null, set of path in that catalog. Path and paths are mutually exclusive + * @param expectedStatus the expected status if not success activated + * @return resolver we created and which has been validated. + */ + private Resolver resolveDriver( + EntityCache cache, + String catalogName, + ResolverPath path, + List paths, + ResolverStatus.StatusEnum expectedStatus) { + return this.resolveDriver( + cache, null, null, false, null, catalogName, null, path, paths, expectedStatus, null); + } + + /** + * Main resolve driver for testing catalog role activation + * + * @param cache if not null, cache we can use + * @param principalRolesScope if not null, scoped roles + * @param catalogName if not null, name of the catalog to resolve + * @param expectedActivatedCatalogRoles set of catalog role names the caller expects to be + * activated + * @return resolver we created and which has been validated. + */ + private Resolver resolveDriver( + EntityCache cache, + Set principalRolesScope, + String catalogName, + Set expectedActivatedCatalogRoles) { + return this.resolveDriver( + cache, + principalRolesScope, + null, + false, + null, + catalogName, + null, + null, + null, + null, + expectedActivatedCatalogRoles); + } + + /** + * Main resolve driver for resolving catalog roles + * + * @param cache if not null, cache we can use + * @param catalogName if not null, name of the catalog to resolve + * @param catalogRoleName if not null, name of catalog role name to resolve + * @param expectedStatus the expected status if not success + * @return resolver we created and which has been validated. + */ + private Resolver resolveDriver( + EntityCache cache, + String catalogName, + String catalogRoleName, + ResolverStatus.StatusEnum expectedStatus) { + return this.resolveDriver( + cache, + null, + null, + false, + null, + catalogName, + catalogRoleName, + null, + null, + expectedStatus, + null); + } + + /** + * Main resolve driver + * + * @param cache if not null, cache we can use + * @param principalRolesScope if not null, scoped roles + * @param principalName if not null, name of the principal to resolve + * @param isPrincipalNameOptional if true, the name of the principal is optional + * @param principalRoleName if not null, name of the principal role to resolve + * @param catalogName if not null, name of the catalog to resolve + * @param catalogRoleName if not null, name of catalog role name to resolve + * @param path if not null, single path in that catalog + * @param paths if not null, set of path in that catalog. Path and paths are mutually exclusive + * @param expectedStatus the expected status if not success + * @param expectedActivatedCatalogRoles set of catalog role names the caller expects to be + * activated + * @return resolver we created and which has been validated. + */ + private Resolver resolveDriver( + EntityCache cache, + Set principalRolesScope, + String principalName, + boolean isPrincipalNameOptional, + String principalRoleName, + String catalogName, + String catalogRoleName, + ResolverPath path, + List paths, + ResolverStatus.StatusEnum expectedStatus, + Set expectedActivatedCatalogRoles) { + + // if null we expect success + if (expectedStatus == null) { + expectedStatus = ResolverStatus.StatusEnum.SUCCESS; + } + + // allocate resolver + Resolver resolver = allocateResolver(cache, principalRolesScope, catalogName); + + // principal name? + if (principalName != null) { + if (isPrincipalNameOptional) { + resolver.addOptionalEntityByName(PolarisEntityType.PRINCIPAL, principalName); + } else { + resolver.addEntityByName(PolarisEntityType.PRINCIPAL, principalName); + } + } + + // add principal role if one passed-in + if (principalRoleName != null) { + resolver.addEntityByName(PolarisEntityType.PRINCIPAL_ROLE, principalRoleName); + } + + // add catalog role if one passed-in + if (catalogRoleName != null) { + resolver.addEntityByName(PolarisEntityType.CATALOG_ROLE, catalogRoleName); + } + + // add all paths + if (path != null) { + resolver.addPath(path); + } else if (paths != null) { + paths.forEach(resolver::addPath); + } + + // done, run resolve + ResolverStatus status = resolver.resolveAll(); + + // we expect success unless a status + Assertions.assertNotNull(status); + Assertions.assertEquals(expectedStatus, status.getStatus()); + + // validate if status is success + if (status.getStatus() == ResolverStatus.StatusEnum.SUCCESS) { + + // the principal does not exist, check that this is the case + if (principalName != null) { + // see if the principal exists + PolarisMetaStoreManager.EntityResult result = + this.metaStoreManager.readEntityByName( + this.callCtx, + null, + PolarisEntityType.PRINCIPAL, + PolarisEntitySubType.NULL_SUBTYPE, + principalName); + // if found, ensure properly resolved + if (result.getEntity() != null) { + // the principal exist, check that this is the case + this.ensureResolved( + resolver.getResolvedEntity(PolarisEntityType.PRINCIPAL, principalName), + PolarisEntityType.PRINCIPAL, + principalName); + } else { + // principal was optional + Assertions.assertTrue(isPrincipalNameOptional); + // not found + Assertions.assertNull( + resolver.getResolvedEntity(PolarisEntityType.PRINCIPAL, principalName)); + } + } + + // validate that we were able to resolve the caller principal + this.ensureResolved(resolver.getResolvedCallerPrincipal(), PolarisEntityType.PRINCIPAL, "P1"); + + // validate that the correct set if principal roles have been activated + List principalRolesResolved = resolver.getResolvedCallerPrincipalRoles(); + principalRolesResolved.sort(Comparator.comparing(p -> p.getEntity().getName())); + + // expect two principal roles if not scoped + int expectedSize; + if (principalRolesScope != null) { + expectedSize = 0; + for (String pr : principalRolesScope) { + if (pr.equals("PR1") || pr.equals("PR2")) { + expectedSize++; + } + } + } else { + // both PR1 and PR2 + expectedSize = 2; + } + + // ensure the right set of principal roles were activated + Assertions.assertEquals(expectedSize, principalRolesResolved.size()); + + // expect either PR1 and PR2 + for (EntityCacheEntry principalRoleResolved : principalRolesResolved) { + Assertions.assertNotNull(principalRoleResolved); + Assertions.assertNotNull(principalRoleResolved.getEntity()); + String roleName = principalRoleResolved.getEntity().getName(); + + // should be either PR1 or PR2 + Assertions.assertTrue(roleName.equals("PR1") || roleName.equals("PR2")); + + // ensure they are PR1 and PR2 + this.ensureResolved(principalRoleResolved, PolarisEntityType.PRINCIPAL_ROLE, roleName); + } + + // if a principal role was passed-in, ensure it exists + if (principalRoleName != null) { + this.ensureResolved( + resolver.getResolvedEntity(PolarisEntityType.PRINCIPAL_ROLE, principalRoleName), + PolarisEntityType.PRINCIPAL_ROLE, + principalRoleName); + } + + // if a catalog was passed-in, ensure it exists + if (catalogName != null) { + EntityCacheEntry catalogEntry = + resolver.getResolvedEntity(PolarisEntityType.CATALOG, catalogName); + Assertions.assertNotNull(catalogEntry); + this.ensureResolved(catalogEntry, PolarisEntityType.CATALOG, catalogName); + + // if a catalog role was passed-in, ensure that it was properly resolved + if (catalogRoleName != null) { + EntityCacheEntry catalogRoleEntry = + resolver.getResolvedEntity(PolarisEntityType.CATALOG_ROLE, catalogRoleName); + this.ensureResolved( + catalogRoleEntry, + List.of(catalogEntry.getEntity()), + PolarisEntityType.CATALOG_ROLE, + catalogRoleName); + } + + // validate activated catalog roles + Map activatedCatalogs = resolver.getResolvedCatalogRoles(); + + // if there is an expected set, ensure we have the same set + if (expectedActivatedCatalogRoles != null) { + Assertions.assertEquals(expectedActivatedCatalogRoles.size(), activatedCatalogs.size()); + } + + // process each of those + for (EntityCacheEntry resolvedActivatedCatalogEntry : activatedCatalogs.values()) { + // must be in the expected list + Assertions.assertNotNull(resolvedActivatedCatalogEntry); + PolarisBaseEntity activatedCatalogRole = resolvedActivatedCatalogEntry.getEntity(); + Assertions.assertNotNull(activatedCatalogRole); + // ensure well resolved + this.ensureResolved( + resolvedActivatedCatalogEntry, + List.of(catalogEntry.getEntity()), + PolarisEntityType.CATALOG_ROLE, + activatedCatalogRole.getName()); + + // in the set of expected catalog roles + Assertions.assertTrue( + expectedActivatedCatalogRoles == null + || expectedActivatedCatalogRoles.contains(activatedCatalogRole.getName())); + } + + // resolve each path + if (path != null || paths != null) { + // path to validate + List allPathsToCheck = (paths == null) ? List.of(path) : paths; + + // all resolved path + List> allResolvedPaths = resolver.getResolvedPaths(); + + // same size + Assertions.assertEquals(allPathsToCheck.size(), allResolvedPaths.size()); + + // check that each path was properly resolved + int pathCount = 0; + Iterator allPathsToCheckIt = allPathsToCheck.iterator(); + for (List resolvedPath : allResolvedPaths) { + this.ensurePathResolved( + pathCount++, catalogEntry.getEntity(), allPathsToCheckIt.next(), resolvedPath); + } + } + } + } + return resolver; + } + + /** + * Ensure a path has been properly resolved + * + * @param pathCount pathCount + * @param catalog catalog + * @param pathToResolve the path to resolve + * @param resolvedPath resolved path + */ + private void ensurePathResolved( + int pathCount, + PolarisBaseEntity catalog, + ResolverPath pathToResolve, + List resolvedPath) { + + // ensure same cardinality + if (!pathToResolve.isOptional()) { + Assertions.assertEquals(pathToResolve.getEntityNames().size(), resolvedPath.size()); + } + + // catalog path + List catalogPath = new ArrayList<>(); + catalogPath.add(catalog); + + // loop and validate each element + for (int index = 0; index < resolvedPath.size(); index++) { + EntityCacheEntry cacheEntry = resolvedPath.get(index); + String entityName = pathToResolve.getEntityNames().get(index); + PolarisEntityType entityType = + (index == pathToResolve.getEntityNames().size() - 1) + ? pathToResolve.getLastEntityType() + : PolarisEntityType.NAMESPACE; + + // ensure that this entity has been properly resolved + this.ensureResolved(cacheEntry, catalogPath, entityType, entityName); + + // add to the path under construction + catalogPath.add(cacheEntry.getEntity()); + } + } + + /** + * Ensure that an entity has been properly resolved + * + * @param cacheEntry the entity as resolved by the resolver + * @param catalogPath path to that entity, can be null for top-level entities + * @param entityType entity type + * @param entityName entity name + */ + private void ensureResolved( + EntityCacheEntry cacheEntry, + List catalogPath, + PolarisEntityType entityType, + String entityName) { + // everything was resolved + Assertions.assertNotNull(cacheEntry); + PolarisBaseEntity entity = cacheEntry.getEntity(); + Assertions.assertNotNull(entity); + List grantRecords = cacheEntry.getAllGrantRecords(); + Assertions.assertNotNull(grantRecords); + + // reference entity cannot be null + PolarisBaseEntity refEntity = + this.tm.ensureExistsByName( + catalogPath, entityType, PolarisEntitySubType.ANY_SUBTYPE, entityName); + Assertions.assertNotNull(refEntity); + + // reload the cached entry from the backend + PolarisMetaStoreManager.CachedEntryResult refCachedEntry = + this.metaStoreManager.loadCachedEntryById( + this.callCtx, refEntity.getCatalogId(), refEntity.getId()); + + // should exist + Assertions.assertNotNull(refCachedEntry); + + // ensure same entity + refEntity = refCachedEntry.getEntity(); + List refGrantRecords = refCachedEntry.getEntityGrantRecords(); + Assertions.assertNotNull(refEntity); + Assertions.assertNotNull(refGrantRecords); + Assertions.assertEquals(refEntity, entity); + Assertions.assertEquals(refEntity.getEntityVersion(), entity.getEntityVersion()); + + // ensure it has not been dropped + Assertions.assertEquals(0, entity.getDropTimestamp()); + + // same number of grants + Assertions.assertEquals(refGrantRecords.size(), grantRecords.size()); + + // ensure same grant records. The order in the list should be deterministic + Iterator refGrantRecordsIt = refGrantRecords.iterator(); + for (PolarisGrantRecord grantRecord : grantRecords) { + PolarisGrantRecord refGrantRecord = refGrantRecordsIt.next(); + Assertions.assertEquals(refGrantRecord, grantRecord); + } + } + + /** + * Ensure that an entity has been properly resolved + * + * @param cacheEntry the entity as resolved by the resolver + * @param entityType entity type + * @param entityName entity name + */ + private void ensureResolved( + EntityCacheEntry cacheEntry, PolarisEntityType entityType, String entityName) { + this.ensureResolved(cacheEntry, null, entityType, entityName); + } +} diff --git a/polaris-core/src/test/java/io/polaris/core/storage/InMemoryStorageIntegrationTest.java b/polaris-core/src/test/java/io/polaris/core/storage/InMemoryStorageIntegrationTest.java new file mode 100644 index 0000000000..778437be28 --- /dev/null +++ b/polaris-core/src/test/java/io/polaris/core/storage/InMemoryStorageIntegrationTest.java @@ -0,0 +1,189 @@ +package io.polaris.core.storage; + +import io.polaris.core.PolarisCallContext; +import io.polaris.core.PolarisConfigurationStore; +import io.polaris.core.PolarisDefaultDiagServiceImpl; +import io.polaris.core.PolarisDiagnostics; +import io.polaris.core.context.CallContext; +import io.polaris.core.storage.aws.AwsStorageConfigurationInfo; +import java.time.Clock; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.assertj.core.api.Assertions; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +class InMemoryStorageIntegrationTest { + + @Test + public void testValidateAccessToLocations() { + MockInMemoryStorageIntegration storage = new MockInMemoryStorageIntegration(); + Map> result = + storage.validateAccessToLocations( + new AwsStorageConfigurationInfo( + PolarisStorageConfigurationInfo.StorageType.S3, + List.of( + "s3://bucket/path/to/warehouse", + "s3://bucket/anotherpath/to/warehouse", + "s3://bucket2/warehouse/"), + "arn:aws:iam::012345678901:role/jdoe"), + Set.of(PolarisStorageActions.READ), + Set.of( + "s3://bucket/path/to/warehouse/namespace/table", + "s3://bucket2/warehouse", + "s3://arandombucket/path/to/warehouse/namespace/table")); + Assertions.assertThat(result) + .hasSize(3) + .containsEntry( + "s3://bucket/path/to/warehouse/namespace/table", + Map.of( + PolarisStorageActions.READ, + new PolarisStorageIntegration.ValidationResult(true, ""))) + .containsEntry( + "s3://bucket2/warehouse", + Map.of( + PolarisStorageActions.READ, + new PolarisStorageIntegration.ValidationResult(true, ""))) + .containsEntry( + "s3://arandombucket/path/to/warehouse/namespace/table", + Map.of( + PolarisStorageActions.READ, + new PolarisStorageIntegration.ValidationResult(false, ""))); + } + + @Test + public void testValidateAccessToLocationsWithWildcard() { + MockInMemoryStorageIntegration storage = new MockInMemoryStorageIntegration(); + Map config = Map.of("ALLOW_WILDCARD_LOCATION", true); + PolarisCallContext polarisCallContext = + new PolarisCallContext( + Mockito.mock(), + new PolarisDefaultDiagServiceImpl(), + new PolarisConfigurationStore() { + @Override + public @Nullable T getConfiguration(PolarisCallContext ctx, String configName) { + return (T) config.get(configName); + } + }, + Clock.systemUTC()); + try (CallContext cc = + CallContext.setCurrentContext(CallContext.of(() -> "realm", polarisCallContext))) { + Map> result = + storage.validateAccessToLocations( + new FileStorageConfigurationInfo(List.of("file://", "*")), + Set.of(PolarisStorageActions.READ), + Set.of( + "s3://bucket/path/to/warehouse/namespace/table", + "file:///etc/passwd", + "a/relative/subdirectory")); + Assertions.assertThat(result) + .hasSize(3) + .hasEntrySatisfying( + "s3://bucket/path/to/warehouse/namespace/table", + val -> + Assertions.assertThat(val) + .hasSize(1) + .containsKey(PolarisStorageActions.READ) + .extractingByKey(PolarisStorageActions.READ) + .returns(true, PolarisStorageIntegration.ValidationResult::isSuccess)) + .hasEntrySatisfying( + "file:///etc/passwd", + val -> + Assertions.assertThat(val) + .hasSize(1) + .containsKey(PolarisStorageActions.READ) + .extractingByKey(PolarisStorageActions.READ) + .returns(true, PolarisStorageIntegration.ValidationResult::isSuccess)) + .hasEntrySatisfying( + "a/relative/subdirectory", + val -> + Assertions.assertThat(val) + .hasSize(1) + .containsKey(PolarisStorageActions.READ) + .extractingByKey(PolarisStorageActions.READ) + .returns(true, PolarisStorageIntegration.ValidationResult::isSuccess)); + } + } + + @Test + public void testValidateAccessToLocationsNoAllowedLocations() { + MockInMemoryStorageIntegration storage = new MockInMemoryStorageIntegration(); + Map> result = + storage.validateAccessToLocations( + new AwsStorageConfigurationInfo( + PolarisStorageConfigurationInfo.StorageType.S3, + List.of(), + "arn:aws:iam::012345678901:role/jdoe"), + Set.of(PolarisStorageActions.READ), + Set.of( + "s3://bucket/path/to/warehouse/namespace/table", + "s3://bucket2/warehouse/namespace/table", + "s3://arandombucket/path/to/warehouse/namespace/table")); + Assertions.assertThat(result) + .hasSize(3) + .containsEntry( + "s3://bucket/path/to/warehouse/namespace/table", + Map.of( + PolarisStorageActions.READ, + new PolarisStorageIntegration.ValidationResult(false, ""))) + .containsEntry( + "s3://bucket2/warehouse/namespace/table", + Map.of( + PolarisStorageActions.READ, + new PolarisStorageIntegration.ValidationResult(false, ""))) + .containsEntry( + "s3://arandombucket/path/to/warehouse/namespace/table", + Map.of( + PolarisStorageActions.READ, + new PolarisStorageIntegration.ValidationResult(false, ""))); + } + + @Test + public void testValidateAccessToLocationsWithPrefixOfAllowedLocation() { + MockInMemoryStorageIntegration storage = new MockInMemoryStorageIntegration(); + Map> result = + storage.validateAccessToLocations( + new AwsStorageConfigurationInfo( + PolarisStorageConfigurationInfo.StorageType.S3, + List.of("s3://bucket/path/to/warehouse"), + "arn:aws:iam::012345678901:role/jdoe"), + Set.of(PolarisStorageActions.READ), + // trying to read a prefix under the allowed location + Set.of("s3://bucket/path/to")); + Assertions.assertThat(result) + .hasSize(1) + .containsEntry( + "s3://bucket/path/to", + Map.of( + PolarisStorageActions.READ, + new PolarisStorageIntegration.ValidationResult(false, ""))); + } + + private static final class MockInMemoryStorageIntegration + extends InMemoryStorageIntegration { + public MockInMemoryStorageIntegration() { + super(MockInMemoryStorageIntegration.class.getName()); + } + + @Override + public EnumMap getSubscopedCreds( + @NotNull PolarisDiagnostics diagnostics, + @NotNull PolarisStorageConfigurationInfo storageConfig, + boolean allowListOperation, + @NotNull Set allowedReadLocations, + @NotNull Set allowedWriteLocations) { + return null; + } + + @Override + public EnumMap + descPolarisStorageConfiguration( + @NotNull PolarisStorageConfigurationInfo storageConfigInfo) { + return null; + } + } +} diff --git a/polaris-core/src/test/java/io/polaris/core/storage/cache/StorageCredentialCacheTest.java b/polaris-core/src/test/java/io/polaris/core/storage/cache/StorageCredentialCacheTest.java new file mode 100644 index 0000000000..2c5e045950 --- /dev/null +++ b/polaris-core/src/test/java/io/polaris/core/storage/cache/StorageCredentialCacheTest.java @@ -0,0 +1,418 @@ +package io.polaris.core.storage.cache; + +import io.polaris.core.PolarisCallContext; +import io.polaris.core.PolarisDefaultDiagServiceImpl; +import io.polaris.core.PolarisDiagnostics; +import io.polaris.core.entity.PolarisBaseEntity; +import io.polaris.core.entity.PolarisEntity; +import io.polaris.core.entity.PolarisEntityConstants; +import io.polaris.core.entity.PolarisEntitySubType; +import io.polaris.core.entity.PolarisEntityType; +import io.polaris.core.persistence.PolarisMetaStoreManager; +import io.polaris.core.persistence.PolarisMetaStoreManagerImpl; +import io.polaris.core.persistence.PolarisMetaStoreSession; +import io.polaris.core.persistence.PolarisObjectMapperUtil; +import io.polaris.core.persistence.PolarisTreeMapMetaStoreSessionImpl; +import io.polaris.core.persistence.PolarisTreeMapStore; +import io.polaris.core.storage.PolarisCredentialProperty; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.EnumMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.RepeatedTest; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +public class StorageCredentialCacheTest { + + // polaris call context + private final PolarisCallContext callCtx; + + // the meta store manager + private final PolarisMetaStoreManager metaStoreManager; + + StorageCredentialCache storageCredentialCache; + + public StorageCredentialCacheTest() { + // diag services + PolarisDiagnostics diagServices = new PolarisDefaultDiagServiceImpl(); + // the entity store, use treemap implementation + PolarisTreeMapStore store = new PolarisTreeMapStore(diagServices); + // to interact with the metastore + PolarisMetaStoreSession metaStore = + new PolarisTreeMapMetaStoreSessionImpl(store, Mockito.mock()); + callCtx = new PolarisCallContext(metaStore, diagServices); + metaStoreManager = Mockito.mock(PolarisMetaStoreManagerImpl.class); + storageCredentialCache = new StorageCredentialCache(); + } + + @Test + public void testBadResult() { + storageCredentialCache = new StorageCredentialCache(); + PolarisMetaStoreManager.ScopedCredentialsResult badResult = + new PolarisMetaStoreManager.ScopedCredentialsResult( + PolarisMetaStoreManager.ReturnStatus.SUBSCOPE_CREDS_ERROR, "extra_error_info"); + Mockito.when( + metaStoreManager.getSubscopedCredsForEntity( + Mockito.any(), + Mockito.anyLong(), + Mockito.anyLong(), + Mockito.anyBoolean(), + Mockito.anySet(), + Mockito.anySet())) + .thenReturn(badResult); + PolarisEntity polarisEntity = + new PolarisEntity( + new PolarisBaseEntity( + 1, 2, PolarisEntityType.CATALOG, PolarisEntitySubType.TABLE, 0, "name")); + Assertions.assertThrows( + RuntimeException.class, + () -> + storageCredentialCache.getOrGenerateSubScopeCreds( + metaStoreManager, + callCtx, + polarisEntity, + true, + new HashSet<>(Arrays.asList("s3://bucket1/path")), + new HashSet<>(Arrays.asList("s3://bucket3/path")))); + } + + @Test + public void testCacheHit() { + storageCredentialCache = new StorageCredentialCache(); + List mockedScopedCreds = + getFakeScopedCreds(3, /* expireSoon= */ false); + Mockito.when( + metaStoreManager.getSubscopedCredsForEntity( + Mockito.any(), + Mockito.anyLong(), + Mockito.anyLong(), + Mockito.anyBoolean(), + Mockito.anySet(), + Mockito.anySet())) + .thenReturn(mockedScopedCreds.get(0)) + .thenReturn(mockedScopedCreds.get(1)) + .thenReturn(mockedScopedCreds.get(1)); + PolarisBaseEntity baseEntity = + new PolarisBaseEntity( + 1, 2, PolarisEntityType.CATALOG, PolarisEntitySubType.TABLE, 0, "name"); + PolarisEntity polarisEntity = new PolarisEntity(baseEntity); + + // add an item to the cache + storageCredentialCache.getOrGenerateSubScopeCreds( + metaStoreManager, + callCtx, + polarisEntity, + true, + new HashSet<>(Arrays.asList("s3://bucket1/path", "s3://bucket2/path")), + new HashSet<>(Arrays.asList("s3://bucket3/path", "s3://bucket4/path"))); + Assertions.assertEquals(1, storageCredentialCache.getEstimatedSize()); + + // subscope for the same entity and same allowed locations, will hit the cache + storageCredentialCache.getOrGenerateSubScopeCreds( + metaStoreManager, + callCtx, + polarisEntity, + true, + new HashSet<>(Arrays.asList("s3://bucket1/path", "s3://bucket2/path")), + new HashSet<>(Arrays.asList("s3://bucket3/path", "s3://bucket4/path"))); + Assertions.assertEquals(1, storageCredentialCache.getEstimatedSize()); + } + + @RepeatedTest(10) + public void testCacheEvict() throws InterruptedException { + storageCredentialCache = new StorageCredentialCache(); + List mockedScopedCreds = + getFakeScopedCreds(3, /* expireSoon= */ true); + + Mockito.when( + metaStoreManager.getSubscopedCredsForEntity( + Mockito.any(), + Mockito.anyLong(), + Mockito.anyLong(), + Mockito.anyBoolean(), + Mockito.anySet(), + Mockito.anySet())) + .thenReturn(mockedScopedCreds.get(0)) + .thenReturn(mockedScopedCreds.get(1)) + .thenReturn(mockedScopedCreds.get(2)); + PolarisBaseEntity baseEntity = + new PolarisBaseEntity( + 1, 2, PolarisEntityType.CATALOG, PolarisEntitySubType.TABLE, 0, "name"); + PolarisEntity polarisEntity = new PolarisEntity(baseEntity); + StorageCredentialCacheKey cacheKey = + new StorageCredentialCacheKey( + polarisEntity, + true, + new HashSet<>(Arrays.asList("s3://bucket1/path", "s3://bucket2/path")), + new HashSet<>(Arrays.asList("s3://bucket/path")), + callCtx); + + // the entry will be evicted immediately because the token is expired + storageCredentialCache.getOrGenerateSubScopeCreds( + metaStoreManager, + callCtx, + polarisEntity, + true, + new HashSet<>(Arrays.asList("s3://bucket1/path", "s3://bucket2/path")), + new HashSet<>(Arrays.asList("s3://bucket/path"))); + Assertions.assertNull(storageCredentialCache.getIfPresent(cacheKey)); + + storageCredentialCache.getOrGenerateSubScopeCreds( + metaStoreManager, + callCtx, + polarisEntity, + true, + new HashSet<>(Arrays.asList("s3://bucket1/path", "s3://bucket2/path")), + new HashSet<>(Arrays.asList("s3://bucket/path"))); + Assertions.assertNull(storageCredentialCache.getIfPresent(cacheKey)); + + storageCredentialCache.getOrGenerateSubScopeCreds( + metaStoreManager, + callCtx, + polarisEntity, + true, + new HashSet<>(Arrays.asList("s3://bucket1/path", "s3://bucket2/path")), + new HashSet<>(Arrays.asList("s3://bucket/path"))); + Assertions.assertNull(storageCredentialCache.getIfPresent(cacheKey)); + } + + @Test + public void testCacheGenerateNewEntries() { + storageCredentialCache = new StorageCredentialCache(); + List mockedScopedCreds = + getFakeScopedCreds(3, /* expireSoon= */ false); + Mockito.when( + metaStoreManager.getSubscopedCredsForEntity( + Mockito.any(), + Mockito.anyLong(), + Mockito.anyLong(), + Mockito.anyBoolean(), + Mockito.anySet(), + Mockito.anySet())) + .thenReturn(mockedScopedCreds.get(0)) + .thenReturn(mockedScopedCreds.get(1)) + .thenReturn(mockedScopedCreds.get(2)); + List entityList = getPolarisEntities(); + int cacheSize = 0; + // different catalog will generate new cache entries + for (PolarisEntity entity : entityList) { + Map res = + storageCredentialCache.getOrGenerateSubScopeCreds( + metaStoreManager, + callCtx, + entity, + true, + new HashSet<>(Arrays.asList("s3://bucket1/path", "s3://bucket2/path")), + new HashSet<>(Arrays.asList("s3://bucket/path"))); + Assertions.assertEquals(++cacheSize, storageCredentialCache.getEstimatedSize()); + } + // update the entity's storage config, since StorageConfig changed, cache will generate new + // entry + for (PolarisEntity entity : entityList) { + Map internalMap = entity.getPropertiesAsMap(); + internalMap.put( + PolarisEntityConstants.getStorageConfigInfoPropertyName(), "newStorageConfig"); + entity.setInternalProperties( + PolarisObjectMapperUtil.serializeProperties(callCtx, internalMap)); + storageCredentialCache.getOrGenerateSubScopeCreds( + metaStoreManager, + callCtx, + entity, + /* allowedListAction= */ true, + new HashSet<>(Arrays.asList("s3://bucket1/path", "s3://bucket2/path")), + new HashSet<>(Arrays.asList("s3://bucket/path"))); + Assertions.assertEquals(++cacheSize, storageCredentialCache.getEstimatedSize()); + } + // allowedListAction changed to different value FALSE, will generate new entry + for (PolarisEntity entity : entityList) { + storageCredentialCache.getOrGenerateSubScopeCreds( + metaStoreManager, + callCtx, + entity, + /* allowedListAction= */ false, + new HashSet<>(Arrays.asList("s3://bucket1/path", "s3://bucket2/path")), + new HashSet<>(Arrays.asList("s3://bucket/path"))); + Assertions.assertEquals(++cacheSize, storageCredentialCache.getEstimatedSize()); + } + // different allowedWriteLocations, will generate new entry + for (PolarisEntity entity : entityList) { + storageCredentialCache.getOrGenerateSubScopeCreds( + metaStoreManager, + callCtx, + entity, + /* allowedListAction= */ false, + new HashSet<>(Arrays.asList("s3://bucket1/path", "s3://bucket2/path")), + new HashSet<>(Arrays.asList("s3://differentbucket/path"))); + Assertions.assertEquals(++cacheSize, storageCredentialCache.getEstimatedSize()); + } + // different allowedReadLocations, will generate new try + for (PolarisEntity entity : entityList) { + Map internalMap = entity.getPropertiesAsMap(); + internalMap.put( + PolarisEntityConstants.getStorageConfigInfoPropertyName(), "newStorageConfig"); + entity.setInternalProperties( + PolarisObjectMapperUtil.serializeProperties(callCtx, internalMap)); + storageCredentialCache.getOrGenerateSubScopeCreds( + metaStoreManager, + callCtx, + entity, + /* allowedListAction= */ false, + new HashSet<>(Arrays.asList("s3://differentbucket/path", "s3://bucket2/path")), + new HashSet<>(Arrays.asList("s3://bucket/path"))); + Assertions.assertEquals(++cacheSize, storageCredentialCache.getEstimatedSize()); + } + } + + @Test + public void testCacheNotAffectedBy() { + storageCredentialCache = new StorageCredentialCache(); + List mockedScopedCreds = + getFakeScopedCreds(3, /* expireSoon= */ false); + + Mockito.when( + metaStoreManager.getSubscopedCredsForEntity( + Mockito.any(), + Mockito.anyLong(), + Mockito.anyLong(), + Mockito.anyBoolean(), + Mockito.anySet(), + Mockito.anySet())) + .thenReturn(mockedScopedCreds.get(0)) + .thenReturn(mockedScopedCreds.get(1)) + .thenReturn(mockedScopedCreds.get(2)); + List entityList = getPolarisEntities(); + for (PolarisEntity entity : entityList) { + storageCredentialCache.getOrGenerateSubScopeCreds( + metaStoreManager, + callCtx, + entity, + true, + new HashSet<>(Arrays.asList("s3://bucket1/path", "s3://bucket2/path")), + new HashSet<>(Arrays.asList("s3://bucket3/path", "s3://bucket4/path"))); + } + Assertions.assertEquals(entityList.size(), storageCredentialCache.getEstimatedSize()); + + // entity ID does not affect the cache + for (PolarisEntity entity : entityList) { + entity.setId(1234); + storageCredentialCache.getOrGenerateSubScopeCreds( + metaStoreManager, + callCtx, + entity, + true, + new HashSet<>(Arrays.asList("s3://bucket1/path", "s3://bucket2/path")), + new HashSet<>(Arrays.asList("s3://bucket3/path", "s3://bucket4/path"))); + Assertions.assertEquals(entityList.size(), storageCredentialCache.getEstimatedSize()); + } + + // other property changes does not affect the cache + for (PolarisEntity entity : entityList) { + entity.setEntityVersion(5); + storageCredentialCache.getOrGenerateSubScopeCreds( + metaStoreManager, + callCtx, + entity, + true, + new HashSet<>(Arrays.asList("s3://bucket1/path", "s3://bucket2/path")), + new HashSet<>(Arrays.asList("s3://bucket3/path", "s3://bucket4/path"))); + Assertions.assertEquals(entityList.size(), storageCredentialCache.getEstimatedSize()); + } + // order of the allowedReadLocations does not affect the cache + for (PolarisEntity entity : entityList) { + entity.setEntityVersion(5); + storageCredentialCache.getOrGenerateSubScopeCreds( + metaStoreManager, + callCtx, + entity, + true, + new HashSet<>(Arrays.asList("s3://bucket2/path", "s3://bucket1/path")), + new HashSet<>(Arrays.asList("s3://bucket3/path", "s3://bucket4/path"))); + Assertions.assertEquals(entityList.size(), storageCredentialCache.getEstimatedSize()); + } + + // order of the allowedWriteLocations does not affect the cache + for (PolarisEntity entity : entityList) { + entity.setEntityVersion(5); + storageCredentialCache.getOrGenerateSubScopeCreds( + metaStoreManager, + callCtx, + entity, + true, + new HashSet<>(Arrays.asList("s3://bucket2/path", "s3://bucket1/path")), + new HashSet<>(Arrays.asList("s3://bucket4/path", "s3://bucket3/path"))); + Assertions.assertEquals(entityList.size(), storageCredentialCache.getEstimatedSize()); + } + } + + private static List getFakeScopedCreds( + int number, boolean expireSoon) { + List res = new ArrayList<>(); + for (int i = 1; i <= number; i = i + 3) { + int finalI = i; + // NOTE: The default behavior of the Caffeine cache seems to have a bug; if our + // expireAfter definition in the StorageCredentialCache constructor doesn't clip + // the returned time to minimum of 0, and we set the expiration time to more than + // 1 second in the past, it seems the cache fails to remove the expired entries + // no matter how long we wait. This is possibly related to the implementation-specific + // "minimum difference between the scheduled executions" documented in Caffeine.java + // to be 1 second. + String expireTime = + expireSoon + ? String.valueOf(System.currentTimeMillis() - 100) + : String.valueOf(Long.MAX_VALUE); + res.add( + new PolarisMetaStoreManager.ScopedCredentialsResult( + new EnumMap<>(PolarisCredentialProperty.class) { + { + put(PolarisCredentialProperty.AWS_KEY_ID, "key_id_" + finalI); + put(PolarisCredentialProperty.AWS_SECRET_KEY, "key_secret_" + finalI); + put(PolarisCredentialProperty.EXPIRATION_TIME, expireTime); + } + })); + if (res.size() == number) return res; + res.add( + new PolarisMetaStoreManager.ScopedCredentialsResult( + new EnumMap<>(PolarisCredentialProperty.class) { + { + put(PolarisCredentialProperty.AZURE_SAS_TOKEN, "sas_token_" + finalI); + put(PolarisCredentialProperty.AZURE_ACCOUNT_HOST, "account_host"); + put(PolarisCredentialProperty.EXPIRATION_TIME, expireTime); + } + })); + if (res.size() == number) return res; + res.add( + new PolarisMetaStoreManager.ScopedCredentialsResult( + new EnumMap<>(PolarisCredentialProperty.class) { + { + put(PolarisCredentialProperty.GCS_ACCESS_TOKEN, "gcs_token_" + finalI); + put(PolarisCredentialProperty.GCS_ACCESS_TOKEN_EXPIRES_AT, expireTime); + } + })); + } + return res; + } + + @NotNull + private static List getPolarisEntities() { + PolarisEntity polarisEntity1 = + new PolarisEntity( + new PolarisBaseEntity( + 1, 2, PolarisEntityType.CATALOG, PolarisEntitySubType.TABLE, 0, "name")); + PolarisEntity polarisEntity2 = + new PolarisEntity( + new PolarisBaseEntity( + 2, 2, PolarisEntityType.CATALOG, PolarisEntitySubType.TABLE, 0, "name")); + PolarisEntity polarisEntity3 = + new PolarisEntity( + new PolarisBaseEntity( + 3, 2, PolarisEntityType.CATALOG, PolarisEntitySubType.TABLE, 0, "name")); + + List entityList = Arrays.asList(polarisEntity1, polarisEntity2, polarisEntity3); + return entityList; + } +} diff --git a/polaris-core/src/test/java/io/polaris/service/storage/aws/AwsCredentialsStorageIntegrationTest.java b/polaris-core/src/test/java/io/polaris/service/storage/aws/AwsCredentialsStorageIntegrationTest.java new file mode 100644 index 0000000000..1d27614633 --- /dev/null +++ b/polaris-core/src/test/java/io/polaris/service/storage/aws/AwsCredentialsStorageIntegrationTest.java @@ -0,0 +1,449 @@ +package io.polaris.service.storage.aws; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.polaris.core.PolarisDiagnostics; +import io.polaris.core.storage.PolarisCredentialProperty; +import io.polaris.core.storage.PolarisStorageConfigurationInfo; +import io.polaris.core.storage.aws.AwsCredentialsStorageIntegration; +import io.polaris.core.storage.aws.AwsStorageConfigurationInfo; +import java.util.EnumMap; +import java.util.List; +import java.util.Set; +import org.assertj.core.api.InstanceOfAssertFactories; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.Mockito; +import software.amazon.awssdk.policybuilder.iam.IamAction; +import software.amazon.awssdk.policybuilder.iam.IamCondition; +import software.amazon.awssdk.policybuilder.iam.IamConditionOperator; +import software.amazon.awssdk.policybuilder.iam.IamEffect; +import software.amazon.awssdk.policybuilder.iam.IamPolicy; +import software.amazon.awssdk.policybuilder.iam.IamResource; +import software.amazon.awssdk.policybuilder.iam.IamStatement; +import software.amazon.awssdk.services.sts.StsClient; +import software.amazon.awssdk.services.sts.model.AssumeRoleRequest; +import software.amazon.awssdk.services.sts.model.AssumeRoleResponse; +import software.amazon.awssdk.services.sts.model.Credentials; + +class AwsCredentialsStorageIntegrationTest { + + public static final AssumeRoleResponse ASSUME_ROLE_RESPONSE = + AssumeRoleResponse.builder() + .credentials( + Credentials.builder() + .accessKeyId("accessKey") + .secretAccessKey("secretKey") + .sessionToken("sess") + .build()) + .build(); + public static final String AWS_PARTITION = "aws"; + + @Test + public void testGetSubscopedCreds() { + StsClient stsClient = Mockito.mock(StsClient.class); + String roleARN = "arn:aws:iam::012345678901:role/jdoe"; + String externalId = "externalId"; + Mockito.when(stsClient.assumeRole(Mockito.isA(AssumeRoleRequest.class))) + .thenAnswer( + invocation -> { + assertThat(invocation.getArguments()[0]) + .isInstanceOf(AssumeRoleRequest.class) + .asInstanceOf(InstanceOfAssertFactories.type(AssumeRoleRequest.class)) + .returns(externalId, AssumeRoleRequest::externalId) + .returns(roleARN, AssumeRoleRequest::roleArn); + return ASSUME_ROLE_RESPONSE; + }); + String warehouseDir = "s3://bucket/path/to/warehouse"; + EnumMap credentials = + new AwsCredentialsStorageIntegration(stsClient) + .getSubscopedCreds( + Mockito.mock(PolarisDiagnostics.class), + new AwsStorageConfigurationInfo( + PolarisStorageConfigurationInfo.StorageType.S3, + List.of(warehouseDir), + roleARN, + externalId), + true, + Set.of(warehouseDir + "/namespace/table"), + Set.of(warehouseDir + "/namespace/table")); + assertThat(credentials) + .isNotEmpty() + .containsEntry(PolarisCredentialProperty.AWS_TOKEN, "sess") + .containsEntry(PolarisCredentialProperty.AWS_KEY_ID, "accessKey") + .containsEntry(PolarisCredentialProperty.AWS_SECRET_KEY, "secretKey"); + } + + @ParameterizedTest + @ValueSource(strings = {AWS_PARTITION, "aws-cn", "aws-us-gov"}) + public void testGetSubscopedCredsInlinePolicy(String awsPartition) { + PolarisStorageConfigurationInfo.StorageType storageType = + PolarisStorageConfigurationInfo.StorageType.S3; + String roleARN = + switch (awsPartition) { + case AWS_PARTITION -> "arn:aws:iam::012345678901:role/jdoe"; + case "aws-cn" -> "arn:aws-cn:iam::012345678901:role/jdoe"; + case "aws-us-gov" -> "arn:aws-us-gov:iam::012345678901:role/jdoe"; + default -> throw new IllegalArgumentException("Unknown aws partition: " + awsPartition); + }; + StsClient stsClient = Mockito.mock(StsClient.class); + String externalId = "externalId"; + String bucket = "bucket"; + String warehouseKeyPrefix = "path/to/warehouse"; + String firstPath = warehouseKeyPrefix + "/namespace/table"; + String secondPath = warehouseKeyPrefix + "/oldnamespace/table"; + Mockito.when(stsClient.assumeRole(Mockito.isA(AssumeRoleRequest.class))) + .thenAnswer( + invocation -> { + assertThat(invocation.getArguments()[0]) + .isInstanceOf(AssumeRoleRequest.class) + .asInstanceOf(InstanceOfAssertFactories.type(AssumeRoleRequest.class)) + .extracting(AssumeRoleRequest::policy) + .extracting(IamPolicy::fromJson) + .satisfies( + policy -> { + assertThat(policy) + .extracting(IamPolicy::statements) + .asInstanceOf(InstanceOfAssertFactories.list(IamStatement.class)) + .hasSize(3) + .satisfiesExactly( + statement -> + assertThat(statement) + .returns(IamEffect.ALLOW, IamStatement::effect) + .returns( + List.of( + IamResource.create( + s3Arn(awsPartition, bucket, firstPath))), + IamStatement::resources) + .returns( + List.of( + IamAction.create("s3:PutObject"), + IamAction.create("s3:DeleteObject")), + IamStatement::actions), + statement -> + assertThat(statement) + .returns(IamEffect.ALLOW, IamStatement::effect) + .returns( + List.of( + IamResource.create( + s3Arn(awsPartition, bucket, null))), + IamStatement::resources) + .returns( + List.of(IamAction.create("s3:ListBucket")), + IamStatement::actions) + .returns( + List.of( + IamResource.create( + s3Arn(awsPartition, bucket, null))), + IamStatement::resources) + .satisfies( + st -> + assertThat(st.conditions()) + .containsExactlyInAnyOrder( + IamCondition.builder() + .operator( + IamConditionOperator.STRING_LIKE) + .key("s3:prefix") + .value(secondPath + "/*") + .build(), + IamCondition.builder() + .operator( + IamConditionOperator.STRING_LIKE) + .key("s3:prefix") + .value(firstPath + "/*") + .build())), + statement -> + assertThat(statement) + .returns(IamEffect.ALLOW, IamStatement::effect) + .satisfies( + st -> + assertThat(st.resources()) + .containsExactlyInAnyOrder( + IamResource.create( + s3Arn(awsPartition, bucket, firstPath)), + IamResource.create( + s3Arn( + awsPartition, bucket, secondPath)))) + .returns( + List.of( + IamAction.create("s3:GetObject"), + IamAction.create("s3:GetObjectVersion")), + IamStatement::actions)); + }); + return ASSUME_ROLE_RESPONSE; + }); + EnumMap credentials = + new AwsCredentialsStorageIntegration(stsClient) + .getSubscopedCreds( + Mockito.mock(PolarisDiagnostics.class), + new AwsStorageConfigurationInfo( + storageType, + List.of(s3Path(bucket, warehouseKeyPrefix, storageType)), + roleARN, + externalId), + true, + Set.of( + s3Path(bucket, firstPath, storageType), + s3Path(bucket, secondPath, storageType)), + Set.of(s3Path(bucket, firstPath, storageType))); + assertThat(credentials) + .isNotEmpty() + .containsEntry(PolarisCredentialProperty.AWS_TOKEN, "sess") + .containsEntry(PolarisCredentialProperty.AWS_KEY_ID, "accessKey") + .containsEntry(PolarisCredentialProperty.AWS_SECRET_KEY, "secretKey"); + } + + @Test + public void testGetSubscopedCredsInlinePolicyWithoutList() { + StsClient stsClient = Mockito.mock(StsClient.class); + String roleARN = "arn:aws:iam::012345678901:role/jdoe"; + String externalId = "externalId"; + String bucket = "bucket"; + String warehouseKeyPrefix = "path/to/warehouse"; + String firstPath = warehouseKeyPrefix + "/namespace/table"; + String secondPath = warehouseKeyPrefix + "/oldnamespace/table"; + Mockito.when(stsClient.assumeRole(Mockito.isA(AssumeRoleRequest.class))) + .thenAnswer( + invocation -> { + assertThat(invocation.getArguments()[0]) + .isInstanceOf(AssumeRoleRequest.class) + .asInstanceOf(InstanceOfAssertFactories.type(AssumeRoleRequest.class)) + .extracting(AssumeRoleRequest::policy) + .extracting(IamPolicy::fromJson) + .satisfies( + policy -> { + assertThat(policy) + .extracting(IamPolicy::statements) + .asInstanceOf(InstanceOfAssertFactories.list(IamStatement.class)) + .hasSize(2) + .satisfiesExactly( + statement -> + assertThat(statement) + .returns(IamEffect.ALLOW, IamStatement::effect) + .returns( + List.of( + IamResource.create( + s3Arn(AWS_PARTITION, bucket, firstPath))), + IamStatement::resources) + .returns( + List.of( + IamAction.create("s3:PutObject"), + IamAction.create("s3:DeleteObject")), + IamStatement::actions), + statement -> + assertThat(statement) + .returns(IamEffect.ALLOW, IamStatement::effect) + .satisfies( + st -> + assertThat(st.resources()) + .containsExactlyInAnyOrder( + IamResource.create( + s3Arn( + AWS_PARTITION, bucket, firstPath)), + IamResource.create( + s3Arn( + AWS_PARTITION, + bucket, + secondPath)))) + .returns( + List.of( + IamAction.create("s3:GetObject"), + IamAction.create("s3:GetObjectVersion")), + IamStatement::actions)); + }); + return ASSUME_ROLE_RESPONSE; + }); + PolarisStorageConfigurationInfo.StorageType storageType = + PolarisStorageConfigurationInfo.StorageType.S3; + EnumMap credentials = + new AwsCredentialsStorageIntegration(stsClient) + .getSubscopedCreds( + Mockito.mock(PolarisDiagnostics.class), + new AwsStorageConfigurationInfo( + PolarisStorageConfigurationInfo.StorageType.S3, + List.of(s3Path(bucket, warehouseKeyPrefix, storageType)), + roleARN, + externalId), + false, /* allowList = false*/ + Set.of( + s3Path(bucket, firstPath, storageType), + s3Path(bucket, secondPath, storageType)), + Set.of(s3Path(bucket, firstPath, storageType))); + assertThat(credentials) + .isNotEmpty() + .containsEntry(PolarisCredentialProperty.AWS_TOKEN, "sess") + .containsEntry(PolarisCredentialProperty.AWS_KEY_ID, "accessKey") + .containsEntry(PolarisCredentialProperty.AWS_SECRET_KEY, "secretKey"); + } + + @Test + public void testGetSubscopedCredsInlinePolicyWithoutWrites() { + StsClient stsClient = Mockito.mock(StsClient.class); + String roleARN = "arn:aws:iam::012345678901:role/jdoe"; + String externalId = "externalId"; + String bucket = "bucket"; + String warehouseKeyPrefix = "path/to/warehouse"; + String firstPath = warehouseKeyPrefix + "/namespace/table"; + String secondPath = warehouseKeyPrefix + "/oldnamespace/table"; + Mockito.when(stsClient.assumeRole(Mockito.isA(AssumeRoleRequest.class))) + .thenAnswer( + invocation -> { + assertThat(invocation.getArguments()[0]) + .isInstanceOf(AssumeRoleRequest.class) + .asInstanceOf(InstanceOfAssertFactories.type(AssumeRoleRequest.class)) + .extracting(AssumeRoleRequest::policy) + .extracting(IamPolicy::fromJson) + .satisfies( + policy -> { + assertThat(policy) + .extracting(IamPolicy::statements) + .asInstanceOf(InstanceOfAssertFactories.list(IamStatement.class)) + .hasSize(2) + .satisfiesExactly( + statement -> + assertThat(statement) + .returns(IamEffect.ALLOW, IamStatement::effect) + .returns( + List.of( + IamResource.create( + s3Arn(AWS_PARTITION, bucket, null))), + IamStatement::resources) + .returns( + List.of(IamAction.create("s3:ListBucket")), + IamStatement::actions), + statement -> + assertThat(statement) + .returns(IamEffect.ALLOW, IamStatement::effect) + .satisfies( + st -> + assertThat(st.resources()) + .containsExactlyInAnyOrder( + IamResource.create( + s3Arn( + AWS_PARTITION, bucket, firstPath)), + IamResource.create( + s3Arn( + AWS_PARTITION, + bucket, + secondPath)))) + .returns( + List.of( + IamAction.create("s3:GetObject"), + IamAction.create("s3:GetObjectVersion")), + IamStatement::actions)); + }); + return ASSUME_ROLE_RESPONSE; + }); + PolarisStorageConfigurationInfo.StorageType storageType = + PolarisStorageConfigurationInfo.StorageType.S3; + EnumMap credentials = + new AwsCredentialsStorageIntegration(stsClient) + .getSubscopedCreds( + Mockito.mock(PolarisDiagnostics.class), + new AwsStorageConfigurationInfo( + storageType, + List.of(s3Path(bucket, warehouseKeyPrefix, storageType)), + roleARN, + externalId), + true, /* allowList = true */ + Set.of( + s3Path(bucket, firstPath, storageType), + s3Path(bucket, secondPath, storageType)), + Set.of()); + assertThat(credentials) + .isNotEmpty() + .containsEntry(PolarisCredentialProperty.AWS_TOKEN, "sess") + .containsEntry(PolarisCredentialProperty.AWS_KEY_ID, "accessKey") + .containsEntry(PolarisCredentialProperty.AWS_SECRET_KEY, "secretKey"); + } + + @Test + public void testGetSubscopedCredsInlinePolicyWithEmptyReadAndWrite() { + StsClient stsClient = Mockito.mock(StsClient.class); + String roleARN = "arn:aws:iam::012345678901:role/jdoe"; + String externalId = "externalId"; + String bucket = "bucket"; + String warehouseKeyPrefix = "path/to/warehouse"; + String firstPath = warehouseKeyPrefix + "/namespace/table"; + String secondPath = warehouseKeyPrefix + "/oldnamespace/table"; + Mockito.when(stsClient.assumeRole(Mockito.isA(AssumeRoleRequest.class))) + .thenAnswer( + invocation -> { + assertThat(invocation.getArguments()[0]) + .isInstanceOf(AssumeRoleRequest.class) + .asInstanceOf(InstanceOfAssertFactories.type(AssumeRoleRequest.class)) + .extracting(AssumeRoleRequest::policy) + .extracting(IamPolicy::fromJson) + .satisfies( + policy -> { + assertThat(policy) + .extracting(IamPolicy::statements) + .asInstanceOf(InstanceOfAssertFactories.list(IamStatement.class)) + .hasSize(2) + .satisfiesExactly( + statement -> + assertThat(statement) + .returns(IamEffect.ALLOW, IamStatement::effect) + .returns(List.of(), IamStatement::resources) + .returns( + List.of(IamAction.create("s3:ListBucket")), + IamStatement::actions), + statement -> + assertThat(statement) + .returns(IamEffect.ALLOW, IamStatement::effect) + .satisfies( + st -> assertThat(st.resources()).containsExactly()) + .returns( + List.of( + IamAction.create("s3:GetObject"), + IamAction.create("s3:GetObjectVersion")), + IamStatement::actions)); + }); + return ASSUME_ROLE_RESPONSE; + }); + EnumMap credentials = + new AwsCredentialsStorageIntegration(stsClient) + .getSubscopedCreds( + Mockito.mock(PolarisDiagnostics.class), + new AwsStorageConfigurationInfo( + PolarisStorageConfigurationInfo.StorageType.S3, + List.of( + s3Path( + bucket, + warehouseKeyPrefix, + PolarisStorageConfigurationInfo.StorageType.S3)), + roleARN, + externalId), + true, /* allowList = true */ + Set.of(), + Set.of()); + assertThat(credentials) + .isNotEmpty() + .containsEntry(PolarisCredentialProperty.AWS_TOKEN, "sess") + .containsEntry(PolarisCredentialProperty.AWS_KEY_ID, "accessKey") + .containsEntry(PolarisCredentialProperty.AWS_SECRET_KEY, "secretKey"); + } + + private static @NotNull String s3Arn(String partition, String bucket, String keyPrefix) { + String bucketArn = "arn:" + partition + ":s3:::" + bucket; + if (keyPrefix == null) { + return bucketArn; + } + return bucketArn + "/" + keyPrefix + "/*"; + } + + private static @NotNull String s3CnArn(String bucket, String keyPrefix) { + String bucketArn = "arn:aws-cn:s3:::" + bucket; + if (keyPrefix == null) { + return bucketArn; + } + return bucketArn + "/" + keyPrefix + "/*"; + } + + private static @NotNull String s3Path( + String bucket, String keyPrefix, PolarisStorageConfigurationInfo.StorageType storageType) { + return storageType.getPrefix() + bucket + "/" + keyPrefix; + } +} diff --git a/polaris-core/src/test/java/io/polaris/service/storage/azure/AzureCredentialStorageIntegrationTest.java b/polaris-core/src/test/java/io/polaris/service/storage/azure/AzureCredentialStorageIntegrationTest.java new file mode 100644 index 0000000000..2c9200803c --- /dev/null +++ b/polaris-core/src/test/java/io/polaris/service/storage/azure/AzureCredentialStorageIntegrationTest.java @@ -0,0 +1,386 @@ +package io.polaris.service.storage.azure; + +import com.azure.storage.blob.BlobClient; +import com.azure.storage.blob.BlobClientBuilder; +import com.azure.storage.blob.BlobContainerClient; +import com.azure.storage.blob.BlobServiceClient; +import com.azure.storage.blob.BlobServiceClientBuilder; +import com.azure.storage.blob.models.BlobStorageException; +import com.azure.storage.blob.models.ListBlobsOptions; +import com.azure.storage.blob.options.BlobParallelUploadOptions; +import com.azure.storage.common.Utility; +import com.azure.storage.file.datalake.DataLakeFileClient; +import com.azure.storage.file.datalake.DataLakeFileSystemClient; +import com.azure.storage.file.datalake.DataLakeFileSystemClientBuilder; +import com.azure.storage.file.datalake.models.DataLakeStorageException; +import com.azure.storage.file.datalake.models.PathItem; +import io.polaris.core.PolarisDefaultDiagServiceImpl; +import io.polaris.core.storage.PolarisCredentialProperty; +import io.polaris.core.storage.azure.AzureCredentialsStorageIntegration; +import io.polaris.core.storage.azure.AzureStorageConfigurationInfo; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.EnumMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; +import org.assertj.core.util.Strings; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class AzureCredentialStorageIntegrationTest { + + private final Logger LOGGER = + LoggerFactory.getLogger(AzureCredentialStorageIntegrationTest.class); + + private final String clientId = System.getenv("AZURE_CLIENT_ID"); + private final String clientSecret = System.getenv("AZURE_CLIENT_SECRET"); + private final String tenantId = "d479c7c9-2632-445a-b22d-7c19e68774f6"; + + private boolean checkEnvNullVariables() { + if (Strings.isNullOrEmpty(clientId) || Strings.isNullOrEmpty(clientSecret)) { + LOGGER.debug("Null Azure testing environment variables! Skip " + this.getClass().getName()); + return true; + } + return false; + } + + @Test + public void testNegativeCases() { + List differentEndpointList = + Arrays.asList( + "abfss://container@icebergdfsstorageacct.dfs.core.windows.net/polaris-test/", + "abfss://container@icebergdfsstorageacct.blob.core.windows.net/polaris-test/"); + Assertions.assertThrows( + RuntimeException.class, + () -> + subscopedCredsForOperations( + differentEndpointList, /* allowedWriteLoc= */ new ArrayList<>(), true)); + + List differentStorageAccts = + Arrays.asList( + "abfss://container@polarisadls.dfs.core.windows.net/polaris-test/", + "abfss://container@icebergdfsstorageacct.dfs.core.windows.net/polaris-test/"); + Assertions.assertThrows( + RuntimeException.class, + () -> + subscopedCredsForOperations( + differentStorageAccts, /* allowedWriteLoc= */ new ArrayList<>(), true)); + List differentContainers = + Arrays.asList( + "abfss://container1@icebergdfsstorageacct.dfs.core.windows.net/polaris-test/", + "abfss://container2@icebergdfsstorageacct.dfs.core.windows.net/polaris-test/"); + + Assertions.assertThrows( + RuntimeException.class, + () -> + subscopedCredsForOperations( + differentContainers, /* allowedWriteLoc= */ new ArrayList<>(), true)); + } + + @TestWithAzureArgs + public void testGetSubscopedTokenList(boolean allowListAction, String service) { + + if (checkEnvNullVariables()) { + return; + } + boolean isBlobService = service.equals("blob"); + List allowedLoc = + Arrays.asList( + String.format( + "abfss://container@icebergdfsstorageacct.%s.core.windows.net/polaris-test/", + service)); + Map credsMap = + subscopedCredsForOperations( + /* allowedReadLoc= */ allowedLoc, + /* allowedWriteLoc= */ new ArrayList<>(), + allowListAction); + Assertions.assertEquals(2, credsMap.size()); + String sasToken = credsMap.get(PolarisCredentialProperty.AZURE_SAS_TOKEN); + Assertions.assertNotNull(sasToken); + String serviceEndpoint = + String.format("https://icebergdfsstorageacct.%s.core.windows.net", service); + BlobContainerClient containerClient = + createContainerClient(sasToken, serviceEndpoint, "container"); + DataLakeFileSystemClient fileSystemClient = + createDatalakeFileSystemClient(sasToken, serviceEndpoint, "container"); + + if (allowListAction) { + // LIST succeed + Assertions.assertDoesNotThrow( + () -> { + if (isBlobService) { + containerClient + .listBlobs( + new ListBlobsOptions().setPrefix(Utility.urlEncode("polaris-test/")), + Duration.ofSeconds(5)) + .streamByPage() + .findFirst() + .orElse(null); + } else { + fileSystemClient + .getDirectoryClient("polaris-test") + .listPaths() + .forEach(PathItem::getName); + } + }); + } else { + if (isBlobService) { + Assertions.assertThrows( + BlobStorageException.class, + () -> + containerClient + .listBlobs( + new ListBlobsOptions().setPrefix(Utility.urlEncode("polaris-test/")), + Duration.ofSeconds(5)) + .streamByPage() + .findFirst() + .orElse(null)); + } else { + Assertions.assertThrows( + DataLakeStorageException.class, + () -> + fileSystemClient + .getDirectoryClient("polaris-test") + .listPaths() + .forEach(PathItem::getName)); + } + } + } + + @TestWithAzureArgs + public void testGetSubscopedTokenRead(boolean allowListAction, String service) { + if (checkEnvNullVariables()) { + return; + } + String allowedPrefix = "polaris-test"; + String blockedPrefix = "blocked-prefix"; + List allowedLoc = + Arrays.asList( + String.format( + "abfss://container@icebergdfsstorageacct.%s.core.windows.net/%s", + service, allowedPrefix)); + Map credsMap = + subscopedCredsForOperations( + /* allowedReadLoc= */ allowedLoc, + /* allowedWriteLoc= */ new ArrayList<>(), + /* allowListAction= */ false); + + BlobClient blobClient = + createBlobClient( + credsMap.get(PolarisCredentialProperty.AZURE_SAS_TOKEN), + "https://icebergdfsstorageacct.dfs.core.windows.net", + "container", + allowedPrefix); + + // READ succeed + Assertions.assertDoesNotThrow( + () -> + blobClient.downloadStreamWithResponse( + new ByteArrayOutputStream(), null, null, null, false, Duration.ofSeconds(5), null)); + + // read will fail because only READ permission allowed + Assertions.assertThrows( + BlobStorageException.class, + () -> + blobClient.uploadWithResponse( + new BlobParallelUploadOptions( + new ByteArrayInputStream("polaris".getBytes(StandardCharsets.UTF_8))), + Duration.ofSeconds(5), + null)); + + // read fail because container is blocked + BlobClient blobClientReadFail = + createBlobClient( + credsMap.get(PolarisCredentialProperty.AZURE_SAS_TOKEN), + String.format("https://icebergdfsstorageacct.%s.core.windows.net", service), + "regtest", + blockedPrefix); + + Assertions.assertThrows( + BlobStorageException.class, + () -> + blobClientReadFail.downloadStreamWithResponse( + new ByteArrayOutputStream(), null, null, null, false, Duration.ofSeconds(5), null)); + } + + @TestWithAzureArgs + public void testGetSubscopedTokenWrite(boolean allowListAction, String service) { + if (checkEnvNullVariables()) { + return; + } + boolean isBlobService = service.equals("blob"); + String allowedPrefix = "polaris-test/scopedcreds/"; + String blockedPrefix = "blocked-prefix"; + List allowedLoc = + Arrays.asList( + String.format( + "abfss://container@icebergdfsstorageacct.%s.core.windows.net/%s", + service, allowedPrefix)); + Map credsMap = + subscopedCredsForOperations( + /* allowedReadLoc= */ new ArrayList<>(), + /* allowedWriteLoc= */ allowedLoc, + /* allowListAction= */ false); + String serviceEndpoint = + String.format("https://icebergdfsstorageacct.%s.core.windows.net", service); + BlobClient blobClient = + createBlobClient( + credsMap.get(PolarisCredentialProperty.AZURE_SAS_TOKEN), + serviceEndpoint, + "container", + allowedPrefix + "metadata/00000-65ffa17b-fe64-4c38-bcb9-06f9bd12aa2a.metadata.json"); + DataLakeFileClient fileClient = + createDatalakeFileClient( + credsMap.get(PolarisCredentialProperty.AZURE_SAS_TOKEN), + serviceEndpoint, + "container", + "polaris-test/scopedcreds/metadata", + "00000-65ffa17b-fe64-4c38-bcb9-06f9bd12aa2a.metadata.json"); + // upload succeed + ByteArrayInputStream inputStream = + new ByteArrayInputStream("polaris".getBytes(StandardCharsets.UTF_8)); + Assertions.assertDoesNotThrow( + () -> { + if (isBlobService) { + blobClient.uploadWithResponse( + new BlobParallelUploadOptions(inputStream), Duration.ofSeconds(5), null); + } else { + fileClient.upload(inputStream, "polaris".length(), /*override*/ true); + } + }); + ByteArrayOutputStream outStream = new ByteArrayOutputStream(); + // READ not allowed + if (isBlobService) { + Assertions.assertThrows( + BlobStorageException.class, + () -> + blobClient.downloadStreamWithResponse( + outStream, null, null, null, false, Duration.ofSeconds(5), null)); + } else { + Assertions.assertThrows(DataLakeStorageException.class, () -> fileClient.read(outStream)); + } + + // upload fail because container not allowed + String blockedContainer = "regtest"; + BlobClient blobClientWriteFail = + createBlobClient( + credsMap.get(PolarisCredentialProperty.AZURE_SAS_TOKEN), + serviceEndpoint, + blockedContainer, + blockedPrefix); + DataLakeFileClient fileClientFail = + createDatalakeFileClient( + credsMap.get(PolarisCredentialProperty.AZURE_SAS_TOKEN), + serviceEndpoint, + blockedContainer, + "polaris-test/scopedcreds/metadata", + "00000-65ffa17b-fe64-4c38-bcb9-06f9bd12aa2a.metadata.json"); + + if (isBlobService) { + Assertions.assertThrows( + BlobStorageException.class, + () -> + blobClientWriteFail.uploadWithResponse( + new BlobParallelUploadOptions( + new ByteArrayInputStream("polaris".getBytes(StandardCharsets.UTF_8))), + Duration.ofSeconds(5), + null)); + } else { + Assertions.assertThrows( + DataLakeStorageException.class, + () -> fileClientFail.upload(inputStream, "polaris".length())); + } + } + + private Map subscopedCredsForOperations( + List allowedReadLoc, List allowedWriteLoc, boolean allowListAction) { + List allowedLoc = new ArrayList<>(); + allowedLoc.addAll(allowedReadLoc); + allowedLoc.addAll(allowedWriteLoc); + AzureStorageConfigurationInfo azureConfig = + new AzureStorageConfigurationInfo(allowedLoc, tenantId); + AzureCredentialsStorageIntegration azureCredsIntegration = + new AzureCredentialsStorageIntegration(); + EnumMap credsMap = + azureCredsIntegration.getSubscopedCreds( + new PolarisDefaultDiagServiceImpl(), + azureConfig, + allowListAction, + new HashSet<>(allowedReadLoc), + new HashSet<>(allowedWriteLoc)); + return credsMap; + } + + private BlobContainerClient createContainerClient( + String sasToken, String endpoint, String container) { + BlobServiceClient blobServiceClient = + new BlobServiceClientBuilder().sasToken(sasToken).endpoint(endpoint).buildClient(); + return blobServiceClient.getBlobContainerClient(container); + } + + private DataLakeFileSystemClient createDatalakeFileSystemClient( + String sasToken, String endpoint, String containerOrFileSystem) { + return new DataLakeFileSystemClientBuilder() + .sasToken(sasToken) + .endpoint(endpoint) + .fileSystemName(containerOrFileSystem) + .buildClient(); + } + + private BlobClient createBlobClient( + String sasToken, String endpoint, String container, String filePath) { + BlobServiceClient blobServiceClient = + new BlobServiceClientBuilder().sasToken(sasToken).endpoint(endpoint).buildClient(); + return new BlobClientBuilder() + .endpoint(blobServiceClient.getAccountUrl()) + .pipeline(blobServiceClient.getHttpPipeline()) + .containerName(container) + .blobName(filePath) + .buildClient(); + } + + private DataLakeFileClient createDatalakeFileClient( + String sasToken, + String endpoint, + String containerOrFileSystem, + String directory, + String fileName) { + DataLakeFileSystemClient dataLakeFileSystemClient = + createDatalakeFileSystemClient(sasToken, endpoint, containerOrFileSystem); + return dataLakeFileSystemClient.getDirectoryClient(directory).getFileClient(fileName); + } + + @Target({ElementType.METHOD}) + @Retention(RetentionPolicy.RUNTIME) + @ParameterizedTest + @ArgumentsSource(AzureTestArgs.class) + protected @interface TestWithAzureArgs {} + + protected static class AzureTestArgs implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext extensionContext) { + return Stream.of( + Arguments.of(/* allowedList= */ true, "blob"), + Arguments.of(/* allowedList= */ false, "blob"), + Arguments.of(/* allowedList= */ true, "dfs"), + Arguments.of(/* allowedList= */ false, "dfs")); + } + } +} diff --git a/polaris-core/src/test/java/io/polaris/service/storage/azure/AzureLocationTest.java b/polaris-core/src/test/java/io/polaris/service/storage/azure/AzureLocationTest.java new file mode 100644 index 0000000000..00e0b23f20 --- /dev/null +++ b/polaris-core/src/test/java/io/polaris/service/storage/azure/AzureLocationTest.java @@ -0,0 +1,31 @@ +package io.polaris.service.storage.azure; + +import io.polaris.core.storage.azure.AzureLocation; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class AzureLocationTest { + + @Test + public void testLocation() { + String uri = "abfss://container@storageaccount.blob.core.windows.net/myfile"; + AzureLocation azureLocation = new AzureLocation(uri); + Assertions.assertEquals("container", azureLocation.getContainer()); + Assertions.assertEquals("storageaccount", azureLocation.getStorageAccount()); + Assertions.assertEquals("blob.core.windows.net", azureLocation.getEndpoint()); + Assertions.assertEquals("myfile", azureLocation.getFilePath()); + } + + @Test + public void testLocation_negative_cases() { + Assertions.assertThrows( + IllegalArgumentException.class, + () -> new AzureLocation("wasbs://container@storageaccount.blob.core.windows.net/myfile")); + Assertions.assertThrows( + IllegalArgumentException.class, + () -> new AzureLocation("abfss://storageaccount.blob.core.windows.net/myfile")); + Assertions.assertThrows( + IllegalArgumentException.class, + () -> new AzureLocation("abfss://container@storageaccount/myfile")); + } +} diff --git a/polaris-core/src/test/java/io/polaris/service/storage/gcp/GcpCredentialsStorageIntegrationTest.java b/polaris-core/src/test/java/io/polaris/service/storage/gcp/GcpCredentialsStorageIntegrationTest.java new file mode 100644 index 0000000000..580c744ae2 --- /dev/null +++ b/polaris-core/src/test/java/io/polaris/service/storage/gcp/GcpCredentialsStorageIntegrationTest.java @@ -0,0 +1,414 @@ +package io.polaris.service.storage.gcp; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ContainerNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.api.client.http.javanet.NetHttpTransport; +import com.google.auth.http.HttpTransportFactory; +import com.google.auth.oauth2.AccessToken; +import com.google.auth.oauth2.CredentialAccessBoundary; +import com.google.auth.oauth2.GoogleCredentials; +import com.google.cloud.ServiceOptions; +import com.google.cloud.http.HttpTransportOptions; +import com.google.cloud.storage.BlobId; +import com.google.cloud.storage.BlobInfo; +import com.google.cloud.storage.Storage; +import com.google.cloud.storage.StorageException; +import com.google.cloud.storage.StorageOptions; +import io.polaris.core.PolarisDefaultDiagServiceImpl; +import io.polaris.core.storage.PolarisCredentialProperty; +import io.polaris.core.storage.gcp.GcpCredentialsStorageIntegration; +import io.polaris.core.storage.gcp.GcpStorageConfigurationInfo; +import java.io.IOException; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.EnumMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.assertj.core.api.recursive.comparison.RecursiveComparisonConfiguration; +import org.assertj.core.util.Strings; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +class GcpCredentialsStorageIntegrationTest { + + private final String gcsServiceKeyJsonFileLocation = + System.getenv("GOOGLE_APPLICATION_CREDENTIALS"); + + private final Logger LOGGER = LoggerFactory.getLogger(GcpCredentialsStorageIntegrationTest.class); + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void testSubscope(boolean allowedListAction) throws IOException { + if (Strings.isNullOrEmpty(gcsServiceKeyJsonFileLocation)) { + LOGGER.debug( + "Environment variable GOOGLE_APPLICATION_CREDENTIALS not exits, skip test " + + getClass().getName()); + return; + } + List allowedRead = + Arrays.asList( + "gs://sfc-dev1-regtest/polaris-test/subscoped-test/read1/", + "gs://sfc-dev1-regtest/polaris-test/subscoped-test/read2/"); + List allowedWrite = + Arrays.asList( + "gs://sfc-dev1-regtest/polaris-test/subscoped-test/write1/", + "gs://sfc-dev1-regtest/polaris-test/subscoped-test/write2/"); + Storage storageClient = setupStorageClient(allowedRead, allowedWrite, allowedListAction); + BlobInfo blobInfoGoodWrite = + createStorageBlob("sfc-dev1-regtest", "polaris-test/subscoped-test/write1/", "file.txt"); + BlobInfo blobInfoBad = + createStorageBlob("sfc-dev1-regtest", "polaris-test/subscoped-test/write3/", "file.txt"); + BlobInfo blobInfoGoodRead = + createStorageBlob("sfc-dev1-regtest", "polaris-test/subscoped-test/read1/", "file.txt"); + final byte[] fileContent = "hello-polaris".getBytes(); + // GOOD WRITE + Assertions.assertDoesNotThrow(() -> storageClient.create(blobInfoGoodWrite, fileContent)); + + // BAD WROTE + Assertions.assertThrows( + StorageException.class, () -> storageClient.create(blobInfoBad, fileContent)); + + Assertions.assertDoesNotThrow(() -> storageClient.get(blobInfoGoodRead.getBlobId())); + Assertions.assertThrows( + StorageException.class, () -> storageClient.get(blobInfoBad.getBlobId())); + + // LIST + if (allowedListAction) { + Assertions.assertDoesNotThrow( + () -> + storageClient.list( + "sfc-dev1-regtest", + Storage.BlobListOption.prefix("polaris-test/subscoped-test/read1/"))); + } else { + Assertions.assertThrows( + StorageException.class, + () -> + storageClient.list( + "sfc-dev1-regtest", + Storage.BlobListOption.prefix("polaris-test/subscoped-test/read1/"))); + } + // DELETE + List allowedWrite2 = + Arrays.asList( + "gs://sfc-dev1-regtest/polaris-test/subscoped-test/write2/", + "gs://sfc-dev1-regtest/polaris-test/subscoped-test/write3/"); + Storage clientForDelete = setupStorageClient(List.of(), allowedWrite2, allowedListAction); + + // can not delete because it is not in allowed write path for this client + Assertions.assertThrows( + StorageException.class, () -> clientForDelete.delete(blobInfoGoodWrite.getBlobId())); + + // good to delete allowed location + Assertions.assertDoesNotThrow(() -> storageClient.delete(blobInfoGoodWrite.getBlobId())); + } + + private Storage setupStorageClient( + List allowedReadLoc, List allowedWriteLoc, boolean allowListAction) + throws IOException { + Map credsMap = + subscopedCredsForOperations(allowedReadLoc, allowedWriteLoc, allowListAction); + return createStorageClient(credsMap); + } + + BlobInfo createStorageBlob(String bucket, String prefix, String fileName) { + BlobId blobId = BlobId.of(bucket, prefix + fileName); + return BlobInfo.newBuilder(blobId).build(); + } + + private Storage createStorageClient(Map credsMap) { + AccessToken accessToken = + new AccessToken( + credsMap.get(PolarisCredentialProperty.GCS_ACCESS_TOKEN), + new Date( + Long.parseLong( + credsMap.get(PolarisCredentialProperty.GCS_ACCESS_TOKEN_EXPIRES_AT)))); + return StorageOptions.newBuilder() + .setCredentials(GoogleCredentials.create(accessToken)) + .build() + .getService(); + } + + private Map subscopedCredsForOperations( + List allowedReadLoc, List allowedWriteLoc, boolean allowListAction) + throws IOException { + List allowedLoc = new ArrayList<>(); + allowedLoc.addAll(allowedReadLoc); + allowedLoc.addAll(allowedWriteLoc); + GcpStorageConfigurationInfo gcpConfig = new GcpStorageConfigurationInfo(allowedLoc); + GcpCredentialsStorageIntegration gcpCredsIntegration = + new GcpCredentialsStorageIntegration( + GoogleCredentials.getApplicationDefault(), + ServiceOptions.getFromServiceLoader(HttpTransportFactory.class, NetHttpTransport::new)); + EnumMap credsMap = + gcpCredsIntegration.getSubscopedCreds( + new PolarisDefaultDiagServiceImpl(), + gcpConfig, + allowListAction, + new HashSet<>(allowedReadLoc), + new HashSet<>(allowedWriteLoc)); + return credsMap; + } + + @Test + public void testGenerateAccessBoundary() throws IOException { + GcpCredentialsStorageIntegration integration = + new GcpCredentialsStorageIntegration( + GoogleCredentials.newBuilder() + .setAccessToken( + new AccessToken( + "my_token", + new Date(Instant.now().plus(10, ChronoUnit.MINUTES).toEpochMilli()))) + .build(), + new HttpTransportOptions.DefaultHttpTransportFactory()); + CredentialAccessBoundary credentialAccessBoundary = + integration.generateAccessBoundaryRules( + true, Set.of("gs://bucket1/path/to/data"), Set.of("gs://bucket1/path/to/data")); + assertThat(credentialAccessBoundary).isNotNull(); + ObjectMapper mapper = new ObjectMapper(); + JsonNode parsedRules = mapper.convertValue(credentialAccessBoundary, JsonNode.class); + JsonNode refRules = + mapper.readTree( + """ +{ + "accessBoundaryRules": [ + { + "availablePermissions": [ + "inRole:roles/storage.objectViewer" + ], + "availableResource": "//storage.googleapis.com/projects/_/buckets/bucket1", + "availabilityCondition": { + "expression": "resource.name.startsWith('projects/_/buckets/bucket1/objects/path/to/data') || api.getAttribute('storage.googleapis.com/objectListPrefix', '').startsWith('path/to/data')" + } + }, + { + "availablePermissions": [ + "inRole:roles/storage.objectCreator" + ], + "availableResource": "//storage.googleapis.com/projects/_/buckets/bucket1", + "availabilityCondition": { + "expression": "resource.name.startsWith('projects/_/buckets/bucket1/objects/path/to/data')" + } + } + ] +} + """); + assertThat(parsedRules) + .usingRecursiveComparison( + RecursiveComparisonConfiguration.builder() + .withEqualsForType(this::recursiveEquals, ObjectNode.class) + .build()) + .isEqualTo(refRules); + } + + @Test + public void testGenerateAccessBoundaryWithMultipleBuckets() throws IOException { + GcpCredentialsStorageIntegration integration = + new GcpCredentialsStorageIntegration( + GoogleCredentials.newBuilder() + .setAccessToken( + new AccessToken( + "my_token", + new Date(Instant.now().plus(10, ChronoUnit.MINUTES).toEpochMilli()))) + .build(), + new HttpTransportOptions.DefaultHttpTransportFactory()); + CredentialAccessBoundary credentialAccessBoundary = + integration.generateAccessBoundaryRules( + true, + Set.of( + "gs://bucket1/normal/path/to/data", + "gs://bucket1/awesome/path/to/data", + "gs://bucket2/a/super/path/to/data"), + Set.of("gs://bucket1/normal/path/to/data")); + assertThat(credentialAccessBoundary).isNotNull(); + ObjectMapper mapper = new ObjectMapper(); + JsonNode parsedRules = mapper.convertValue(credentialAccessBoundary, JsonNode.class); + JsonNode refRules = + mapper.readTree( + """ +{ + "accessBoundaryRules": [ + { + "availablePermissions": [ + "inRole:roles/storage.objectViewer" + ], + "availableResource": "//storage.googleapis.com/projects/_/buckets/bucket1", + "availabilityCondition": { + "expression": "resource.name.startsWith('projects/_/buckets/bucket1/objects/normal/path/to/data') || api.getAttribute('storage.googleapis.com/objectListPrefix', '').startsWith('normal/path/to/data') || resource.name.startsWith('projects/_/buckets/bucket1/objects/awesome/path/to/data') || api.getAttribute('storage.googleapis.com/objectListPrefix', '').startsWith('awesome/path/to/data')" + } + }, + { + "availablePermissions": [ + "inRole:roles/storage.objectViewer" + ], + "availableResource": "//storage.googleapis.com/projects/_/buckets/bucket2", + "availabilityCondition": { + "expression": "resource.name.startsWith('projects/_/buckets/bucket2/objects/a/super/path/to/data') || api.getAttribute('storage.googleapis.com/objectListPrefix', '').startsWith('a/super/path/to/data')" + } + }, + { + "availablePermissions": [ + "inRole:roles/storage.objectCreator" + ], + "availableResource": "//storage.googleapis.com/projects/_/buckets/bucket1", + "availabilityCondition": { + "expression": "resource.name.startsWith('projects/_/buckets/bucket1/objects/path/to/data')" + } + } + ] +} + """); + assertThat(parsedRules) + .usingRecursiveComparison( + RecursiveComparisonConfiguration.builder() + .withEqualsForType(this::recursiveEquals, ObjectNode.class) + .build()) + .isEqualTo(refRules); + } + + @Test + public void testGenerateAccessBoundaryWithoutList() throws IOException { + GcpCredentialsStorageIntegration integration = + new GcpCredentialsStorageIntegration( + GoogleCredentials.newBuilder() + .setAccessToken( + new AccessToken( + "my_token", + new Date(Instant.now().plus(10, ChronoUnit.MINUTES).toEpochMilli()))) + .build(), + new HttpTransportOptions.DefaultHttpTransportFactory()); + CredentialAccessBoundary credentialAccessBoundary = + integration.generateAccessBoundaryRules( + false, + Set.of("gs://bucket1/path/to/data", "gs://bucket1/another/path/to/data"), + Set.of("gs://bucket1/path/to/data")); + assertThat(credentialAccessBoundary).isNotNull(); + ObjectMapper mapper = new ObjectMapper(); + JsonNode parsedRules = mapper.convertValue(credentialAccessBoundary, JsonNode.class); + JsonNode refRules = + mapper.readTree( + """ +{ + "accessBoundaryRules": [ + { + "availablePermissions": [ + "inRole:roles/storage.objectViewer" + ], + "availableResource": "//storage.googleapis.com/projects/_/buckets/bucket1", + "availabilityCondition": { + "expression": "resource.name.startsWith('projects/_/buckets/bucket1/objects/path/to/data') || resource.name.startsWith('projects/_/buckets/bucket1/objects/another/path/to/data')" + } + }, + { + "availablePermissions": [ + "inRole:roles/storage.objectCreator" + ], + "availableResource": "//storage.googleapis.com/projects/_/buckets/bucket1", + "availabilityCondition": { + "expression": "resource.name.startsWith('projects/_/buckets/bucket1/objects/path/to/data')" + } + } + ] +} + """); + assertThat(parsedRules) + .usingRecursiveComparison( + RecursiveComparisonConfiguration.builder() + .withEqualsForType(this::recursiveEquals, ObjectNode.class) + .build()) + .isEqualTo(refRules); + } + + @Test + public void testGenerateAccessBoundaryWithoutWrites() throws IOException { + GcpCredentialsStorageIntegration integration = + new GcpCredentialsStorageIntegration( + GoogleCredentials.newBuilder() + .setAccessToken( + new AccessToken( + "my_token", + new Date(Instant.now().plus(10, ChronoUnit.MINUTES).toEpochMilli()))) + .build(), + new HttpTransportOptions.DefaultHttpTransportFactory()); + CredentialAccessBoundary credentialAccessBoundary = + integration.generateAccessBoundaryRules( + false, + Set.of("gs://bucket1/normal/path/to/data", "gs://bucket1/awesome/path/to/data"), + Set.of()); + assertThat(credentialAccessBoundary).isNotNull(); + ObjectMapper mapper = new ObjectMapper(); + JsonNode parsedRules = mapper.convertValue(credentialAccessBoundary, JsonNode.class); + JsonNode refRules = + mapper.readTree( + """ +{ + "accessBoundaryRules": [ + { + "availablePermissions": [ + "inRole:roles/storage.objectViewer" + ], + "availableResource": "//storage.googleapis.com/projects/_/buckets/bucket1", + "availabilityCondition": { + "expression": "resource.name.startsWith('projects/_/buckets/bucket1/objects/normal/path/to/data') || api.getAttribute('storage.googleapis.com/objectListPrefix', '').startsWith('normal/path/to/data') || resource.name.startsWith('projects/_/buckets/bucket1/objects/awesome/path/to/data') || api.getAttribute('storage.googleapis.com/objectListPrefix', '').startsWith('awesome/path/to/data')" + } + } + ] +} + """); + assertThat(parsedRules) + .usingRecursiveComparison( + RecursiveComparisonConfiguration.builder() + .withEqualsForType(this::recursiveEquals, ObjectNode.class) + .build()) + .isEqualTo(refRules); + } + + /** + * Custom comparator as ObjectNodes are compared by field indexes as opposed to field names. They + * also don't equate a field that is present and set to null with a field that is omitted + * + * @param on1 + * @param on2 + * @return + */ + private boolean recursiveEquals(ContainerNode on1, ContainerNode on2) { + Set fieldNames = new HashSet<>(); + on1.fieldNames().forEachRemaining(fieldNames::add); + on2.fieldNames().forEachRemaining(fieldNames::add); + for (String fieldName : fieldNames) { + if ((!on1.has(fieldName) || !on2.has(fieldName))) { + if (isNotNull(on1.get(fieldName)) || isNotNull(on2.get(fieldName))) { + return false; + } + } else { + JsonNode fieldValue = on1.get(fieldName); + JsonNode fieldValue2 = on2.get(fieldName); + if (fieldValue.isContainerNode()) { + if (!fieldValue2.isContainerNode() + || !recursiveEquals((ContainerNode) fieldValue, (ContainerNode) fieldValue2)) { + return false; + } + } else if (!fieldValue.equals(fieldValue2)) { + return false; + } + } + } + return true; + } + + private boolean isNotNull(JsonNode node) { + return node != null && !node.isNull(); + } +} diff --git a/polaris-core/src/testFixtures/java/io/polaris/core/persistence/PolarisMetaStoreManagerTest.java b/polaris-core/src/testFixtures/java/io/polaris/core/persistence/PolarisMetaStoreManagerTest.java new file mode 100644 index 0000000000..9006e24863 --- /dev/null +++ b/polaris-core/src/testFixtures/java/io/polaris/core/persistence/PolarisMetaStoreManagerTest.java @@ -0,0 +1,473 @@ +/* + * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. + */ + +package io.polaris.core.persistence; + +import io.polaris.core.PolarisCallContext; +import io.polaris.core.context.CallContext; +import io.polaris.core.entity.AsyncTaskType; +import io.polaris.core.entity.PolarisBaseEntity; +import io.polaris.core.entity.PolarisEntity; +import io.polaris.core.entity.PolarisEntityActiveRecord; +import io.polaris.core.entity.PolarisEntitySubType; +import io.polaris.core.entity.PolarisEntityType; +import io.polaris.core.entity.TaskEntity; +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.time.InstantSource; +import java.time.ZoneId; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.Set; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.assertj.core.api.Assertions; +import org.assertj.core.api.InstanceOfAssertFactories; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Integration test for the polaris persistence layer + * + *
@TODO
+ *   - Update multiple entities in one shot
+ *   - Lookup active: test non existent stuff
+ *   - Failure to resolve, i.e. something has changed
+ *   - better status report
+ * 
+ * + * @author bdagevil + */ +public abstract class PolarisMetaStoreManagerTest { + + protected static final MockInstantSource timeSource = new MockInstantSource(); + + private PolarisTestMetaStoreManager polarisTestMetaStoreManager; + + @BeforeEach + public void setupPolariMetaStoreManager() { + this.polarisTestMetaStoreManager = createPolarisTestMetaStoreManager(); + } + + protected abstract PolarisTestMetaStoreManager createPolarisTestMetaStoreManager(); + + /** validate that the root catalog was properly constructed */ + @Test + void validateBootstrap() { + // allocate test driver + polarisTestMetaStoreManager.validateBootstrap(); + } + + @Test + void testCreateTestCatalog() { + // allocate test driver + polarisTestMetaStoreManager.testCreateTestCatalog(); + } + + @Test + void testCreateTestCatalogWithRetry() { + // allocate test driver + polarisTestMetaStoreManager.forceRetry(); + polarisTestMetaStoreManager.testCreateTestCatalog(); + } + + @Test + void testBrowse() { + // allocate test driver + polarisTestMetaStoreManager.testBrowse(); + } + + @Test + void testCreateEntities() { + PolarisMetaStoreManager metaStoreManager = polarisTestMetaStoreManager.polarisMetaStoreManager; + try (CallContext callCtx = + CallContext.of(() -> "testRealm", polarisTestMetaStoreManager.polarisCallContext)) { + if (CallContext.getCurrentContext() == null) { + CallContext.setCurrentContext(callCtx); + } + TaskEntity task1 = createTask("task1", 100L); + TaskEntity task2 = createTask("task2", 101L); + List createdEntities = + metaStoreManager + .createEntitiesIfNotExist( + polarisTestMetaStoreManager.polarisCallContext, null, List.of(task1, task2)) + .getEntities(); + + Assertions.assertThat(createdEntities) + .isNotNull() + .hasSize(2) + .extracting(PolarisEntity::toCore) + .containsExactly(PolarisEntity.toCore(task1), PolarisEntity.toCore(task2)); + + List listedEntities = + metaStoreManager + .listEntities( + polarisTestMetaStoreManager.polarisCallContext, + null, + PolarisEntityType.TASK, + PolarisEntitySubType.NULL_SUBTYPE) + .getEntities(); + Assertions.assertThat(listedEntities) + .isNotNull() + .hasSize(2) + .containsExactly( + new PolarisEntityActiveRecord( + task1.getCatalogId(), + task1.getId(), + task1.getParentId(), + task1.getName(), + task1.getTypeCode(), + task1.getSubTypeCode()), + new PolarisEntityActiveRecord( + task2.getCatalogId(), + task2.getId(), + task2.getParentId(), + task2.getName(), + task2.getTypeCode(), + task2.getSubTypeCode())); + } + } + + @Test + void testCreateEntitiesAlreadyExisting() { + PolarisMetaStoreManager metaStoreManager = polarisTestMetaStoreManager.polarisMetaStoreManager; + try (CallContext callCtx = + CallContext.of(() -> "testRealm", polarisTestMetaStoreManager.polarisCallContext)) { + if (CallContext.getCurrentContext() == null) { + CallContext.setCurrentContext(callCtx); + } + TaskEntity task1 = createTask("task1", 100L); + TaskEntity task2 = createTask("task2", 101L); + List createdEntities = + metaStoreManager + .createEntitiesIfNotExist( + polarisTestMetaStoreManager.polarisCallContext, null, List.of(task1, task2)) + .getEntities(); + + Assertions.assertThat(createdEntities) + .isNotNull() + .hasSize(2) + .extracting(PolarisEntity::toCore) + .containsExactly(PolarisEntity.toCore(task1), PolarisEntity.toCore(task2)); + + TaskEntity task3 = createTask("task3", 103L); + + // entities task1 and task2 already exist with the same identifier, so the full list is + // returned + createdEntities = + metaStoreManager + .createEntitiesIfNotExist( + polarisTestMetaStoreManager.polarisCallContext, + null, + List.of(task1, task2, task3)) + .getEntities(); + Assertions.assertThat(createdEntities) + .isNotNull() + .hasSize(3) + .extracting(PolarisEntity::toCore) + .containsExactly( + PolarisEntity.toCore(task1), + PolarisEntity.toCore(task2), + PolarisEntity.toCore(task3)); + } + } + + @Test + void testCreateEntitiesWithConflict() { + PolarisMetaStoreManager metaStoreManager = polarisTestMetaStoreManager.polarisMetaStoreManager; + try (CallContext callCtx = + CallContext.of(() -> "testRealm", polarisTestMetaStoreManager.polarisCallContext)) { + if (CallContext.getCurrentContext() == null) { + CallContext.setCurrentContext(callCtx); + } + TaskEntity task1 = createTask("task1", 100L); + TaskEntity task2 = createTask("task2", 101L); + TaskEntity task3 = createTask("task3", 103L); + List createdEntities = + metaStoreManager + .createEntitiesIfNotExist( + polarisTestMetaStoreManager.polarisCallContext, + null, + List.of(task1, task2, task3)) + .getEntities(); + + Assertions.assertThat(createdEntities) + .isNotNull() + .hasSize(3) + .extracting(PolarisEntity::toCore) + .containsExactly( + PolarisEntity.toCore(task1), + PolarisEntity.toCore(task2), + PolarisEntity.toCore(task3)); + + TaskEntity secondTask3 = createTask("task3", 104L); + + TaskEntity task4 = createTask("task4", 105L); + createdEntities = + metaStoreManager + .createEntitiesIfNotExist( + polarisTestMetaStoreManager.polarisCallContext, null, List.of(secondTask3, task4)) + .getEntities(); + Assertions.assertThat(createdEntities).isNull(); + } + } + + private static TaskEntity createTask(String taskName, long id) { + return new TaskEntity.Builder() + .setName(taskName) + .withData("data") + .setId(id) + .withTaskType(AsyncTaskType.FILE_CLEANUP) + .setCreateTimestamp(Instant.now().toEpochMilli()) + .build(); + } + + /** Test that entity updates works well */ + @Test + void testUpdateEntities() { + // allocate test driver + polarisTestMetaStoreManager.testUpdateEntities(); + } + + /** Test that entity drop works well */ + @Test + void testDropEntities() { + // allocate test driver + polarisTestMetaStoreManager.testDropEntities(); + } + + /** Test that granting/revoking privileges works well */ + @Test + void testPrivileges() { + // allocate test driver + polarisTestMetaStoreManager.testPrivileges(); + } + + /** test entity rename */ + @Test + void testRename() { + // allocate test driver + polarisTestMetaStoreManager.testRename(); + } + + /** Test the set of functions for the entity cache */ + @Test + void testEntityCache() { + // allocate test driver + polarisTestMetaStoreManager.testEntityCache(); + } + + @Test + void testLoadTasks() { + for (int i = 0; i < 20; i++) { + polarisTestMetaStoreManager.createEntity( + null, PolarisEntityType.TASK, PolarisEntitySubType.NULL_SUBTYPE, "task_" + i); + } + String executorId = "testExecutor_abc"; + PolarisMetaStoreManager metaStoreManager = polarisTestMetaStoreManager.polarisMetaStoreManager; + PolarisCallContext callCtx = polarisTestMetaStoreManager.polarisCallContext; + List taskList = + metaStoreManager.loadTasks(callCtx, executorId, 5).getEntities(); + Assertions.assertThat(taskList) + .isNotNull() + .isNotEmpty() + .hasSize(5) + .allSatisfy( + entry -> + Assertions.assertThat(entry) + .extracting( + e -> + PolarisObjectMapperUtil.deserializeProperties( + callCtx, e.getProperties())) + .asInstanceOf(InstanceOfAssertFactories.map(String.class, String.class)) + .containsEntry("lastAttemptExecutorId", executorId) + .containsEntry("attemptCount", "1")); + Set firstTasks = + taskList.stream().map(PolarisBaseEntity::getName).collect(Collectors.toSet()); + + // grab a second round of tasks. Assert that none of the original 5 are in the list + List newTaskList = + metaStoreManager.loadTasks(callCtx, executorId, 5).getEntities(); + Assertions.assertThat(newTaskList) + .isNotNull() + .isNotEmpty() + .hasSize(5) + .extracting(PolarisBaseEntity::getName) + .noneMatch(firstTasks::contains); + + Set firstTenTaskNames = + Stream.concat(firstTasks.stream(), newTaskList.stream().map(PolarisBaseEntity::getName)) + .collect(Collectors.toSet()); + + // only 10 tasks are unnassigned. Requesting 20, we should only receive those 10 + List lastTen = + metaStoreManager.loadTasks(callCtx, executorId, 20).getEntities(); + + Assertions.assertThat(lastTen) + .isNotNull() + .isNotEmpty() + .hasSize(10) + .extracting(PolarisBaseEntity::getName) + .noneMatch(firstTenTaskNames::contains); + + Set allTaskNames = + Stream.concat(firstTenTaskNames.stream(), lastTen.stream().map(PolarisBaseEntity::getName)) + .collect(Collectors.toSet()); + + List emtpyList = + metaStoreManager.loadTasks(callCtx, executorId, 20).getEntities(); + + Assertions.assertThat(emtpyList).isNotNull().isEmpty(); + + timeSource.updateClock(Clock.offset(timeSource.currentClock, Duration.ofMinutes(10))); + + // all the tasks are unnassigned. Fetch them all + List allTasks = + metaStoreManager.loadTasks(callCtx, executorId, 20).getEntities(); + + Assertions.assertThat(allTasks) + .isNotNull() + .isNotEmpty() + .hasSize(20) + .extracting(PolarisBaseEntity::getName) + .allMatch(allTaskNames::contains); + + // drop all the tasks. Skip the clock forward and fetch. empty list expected + allTasks.forEach( + entity -> metaStoreManager.dropEntityIfExists(callCtx, null, entity, Map.of(), false)); + timeSource.updateClock(Clock.offset(timeSource.currentClock, Duration.ofMinutes(10))); + + List finalList = + metaStoreManager.loadTasks(callCtx, executorId, 20).getEntities(); + + Assertions.assertThat(finalList).isNotNull().isEmpty(); + } + + @Test + void testLoadTasksInParallel() { + for (int i = 0; i < 100; i++) { + polarisTestMetaStoreManager.createEntity( + null, PolarisEntityType.TASK, PolarisEntitySubType.NULL_SUBTYPE, "task_" + i); + } + PolarisMetaStoreManager metaStoreManager = polarisTestMetaStoreManager.polarisMetaStoreManager; + PolarisCallContext callCtx = polarisTestMetaStoreManager.polarisCallContext; + List>> futureList = new ArrayList<>(); + List> responses; + try (ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor()) { + for (int i = 0; i < 3; i++) { + final String executorId = "taskExecutor_" + i; + + futureList.add( + executorService.submit( + () -> { + Set taskNames = new HashSet<>(); + List taskList = List.of(); + boolean retry = false; + do { + retry = false; + try { + taskList = metaStoreManager.loadTasks(callCtx, executorId, 5).getEntities(); + taskList.stream().map(PolarisBaseEntity::getName).forEach(taskNames::add); + } catch (RetryOnConcurrencyException e) { + retry = true; + } + } while (retry || !taskList.isEmpty()); + return taskNames; + })); + } + responses = + futureList.stream() + .map( + f -> { + try { + return f.get(); + } catch (Exception e) { + throw new RuntimeException(e); + } + }) + .toList(); + } + Assertions.assertThat(responses) + .hasSize(3) + .satisfies(l -> Assertions.assertThat(l.stream().flatMap(Set::stream)).hasSize(100)); + Map taskCounts = + responses.stream() + .flatMap(Set::stream) + .collect(Collectors.toMap(Function.identity(), (val) -> 1, Integer::sum)); + Assertions.assertThat(taskCounts) + .hasSize(100) + .allSatisfy((k, v) -> Assertions.assertThat(v).isEqualTo(1)); + } + + /** Test generateNewEntityId() function that generates unique ids by creating Tasks in parallel */ + @Test + void testCreateTasksInParallel() { + List>> futureList = new ArrayList<>(); + Random rand = new Random(); + try (ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor()) { + for (int threadId = 0; threadId < 10; threadId++) { + Future> future = + executorService.submit( + () -> { + List list = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + var entity = + polarisTestMetaStoreManager.createEntity( + null, + PolarisEntityType.TASK, + PolarisEntitySubType.NULL_SUBTYPE, + "task_" + rand.nextLong() + "" + i); + list.add(entity.getId()); + } + return list; + }); + futureList.add(future); + } + + List> responses = + futureList.stream() + .map( + f -> { + try { + return f.get(); + } catch (Exception e) { + throw new RuntimeException(e); + } + }) + .toList(); + + Assertions.assertThat(responses) + .hasSize(10) + .satisfies(l -> Assertions.assertThat(l.stream().flatMap(List::stream)).hasSize(100)); + Map idCounts = + responses.stream() + .flatMap(List::stream) + .collect(Collectors.toMap(Function.identity(), (val) -> 1, Integer::sum)); + Assertions.assertThat(idCounts) + .hasSize(100) + .allSatisfy((k, v) -> Assertions.assertThat(v).isEqualTo(1)); + } + } + + protected static final class MockInstantSource implements InstantSource { + private Clock currentClock = Clock.system(ZoneId.systemDefault()); + + @Override + public Instant instant() { + return Instant.now(currentClock); + } + + public void updateClock(Clock clock) { + this.currentClock = clock; + } + } +} diff --git a/polaris-core/src/testFixtures/java/io/polaris/core/persistence/PolarisTestMetaStoreManager.java b/polaris-core/src/testFixtures/java/io/polaris/core/persistence/PolarisTestMetaStoreManager.java new file mode 100644 index 0000000000..5ff18af4b5 --- /dev/null +++ b/polaris-core/src/testFixtures/java/io/polaris/core/persistence/PolarisTestMetaStoreManager.java @@ -0,0 +1,2381 @@ +/* + * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. + */ + +package io.polaris.core.persistence; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.polaris.core.PolarisCallContext; +import io.polaris.core.entity.PolarisBaseEntity; +import io.polaris.core.entity.PolarisChangeTrackingVersions; +import io.polaris.core.entity.PolarisEntity; +import io.polaris.core.entity.PolarisEntityActiveRecord; +import io.polaris.core.entity.PolarisEntityConstants; +import io.polaris.core.entity.PolarisEntityCore; +import io.polaris.core.entity.PolarisEntityId; +import io.polaris.core.entity.PolarisEntitySubType; +import io.polaris.core.entity.PolarisEntityType; +import io.polaris.core.entity.PolarisGrantRecord; +import io.polaris.core.entity.PolarisPrincipalSecrets; +import io.polaris.core.entity.PolarisPrivilege; +import io.polaris.core.entity.PolarisTaskConstants; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import org.apache.commons.lang3.tuple.ImmutablePair; +import org.apache.commons.lang3.tuple.Pair; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.Assertions; + +/** Test the Polaris persistence layer */ +public class PolarisTestMetaStoreManager { + + // call context + final PolarisCallContext polarisCallContext; + + // call metastore manager + final PolarisMetaStoreManager polarisMetaStoreManager; + + // the start time + private final long testStartTime = System.currentTimeMillis(); + private final ObjectMapper objectMapper = new ObjectMapper(); + + // if true, simulate retries by client + private boolean doRetry; + + // initialize the test + public PolarisTestMetaStoreManager( + PolarisMetaStoreManager polarisMetaStoreManager, PolarisCallContext polarisCallContext) { + this.polarisCallContext = polarisCallContext; + this.polarisMetaStoreManager = polarisMetaStoreManager; + this.doRetry = false; + + // bootstrap the Polaris service + polarisMetaStoreManager.bootstrapPolarisService(polarisCallContext); + } + + public void forceRetry() { + this.doRetry = true; + } + + /** + * Validate that the specified identity identified by the pair catalogId, id has been properly + * persisted. + * + * @param catalogPath path of that entity in the catalog. If null, this entity is top-level + * @param entityId id + * @param expectedActive true if this entity should be active + * @param expectedName its expected name + * @param expectedType its expected type + * @param expectedSubType its expected subtype + * @return the persisted entity as a DPO + */ + private PolarisBaseEntity ensureExistsById( + List catalogPath, + long entityId, + boolean expectedActive, + String expectedName, + PolarisEntityType expectedType, + PolarisEntitySubType expectedSubType) { + + // derive id of the catalog for that entity as well as its parent id + final long catalogId; + final long parentId; + if (catalogPath == null) { + // top-level entity + catalogId = PolarisEntityConstants.getNullId(); + parentId = PolarisEntityConstants.getRootEntityId(); + } else { + catalogId = catalogPath.get(0).getId(); + parentId = catalogPath.get(catalogPath.size() - 1).getId(); + } + + // make sure this entity was persisted + PolarisBaseEntity entity = + polarisMetaStoreManager + .loadEntity(this.polarisCallContext, catalogId, entityId) + .getEntity(); + + // assert all expected values + Assertions.assertNotNull(entity); + Assertions.assertEquals(expectedName, entity.getName()); + Assertions.assertEquals(parentId, entity.getParentId()); + Assertions.assertEquals(expectedType.getCode(), entity.getTypeCode()); + Assertions.assertEquals(expectedSubType.getCode(), entity.getSubTypeCode()); + + // ensure creation time set + Assertions.assertTrue(entity.getCreateTimestamp() >= this.testStartTime); + Assertions.assertTrue(entity.getLastUpdateTimestamp() >= this.testStartTime); + + // test active + if (expectedActive) { + // make sure any other timestamps are 0 + Assertions.assertEquals(0, entity.getPurgeTimestamp()); + Assertions.assertEquals(0, entity.getDropTimestamp()); + Assertions.assertEquals(0, entity.getPurgeTimestamp()); + + // we should find it + PolarisMetaStoreManager.EntityResult result = + polarisMetaStoreManager.readEntityByName( + this.polarisCallContext, catalogPath, expectedType, expectedSubType, expectedName); + + // should be success, nothing changed + Assertions.assertNotNull(result); + + // should be success + Assertions.assertTrue(result.isSuccess()); + + // same id + Assertions.assertEquals(entity.getId(), result.getEntity().getId()); + } else { + // make sure any other timestamps are 0 + Assertions.assertNotEquals(0, entity.getDropTimestamp()); + + // we should not find it + PolarisMetaStoreManager.EntityResult result = + polarisMetaStoreManager.readEntityByName( + this.polarisCallContext, catalogPath, expectedType, expectedSubType, expectedName); + + // lookup must be success, nothing changed + Assertions.assertNotNull(result); + + // should be success + Assertions.assertTrue(result.isSuccess()); + + // should be null, not found + Assertions.assertNull(result.getEntity()); + } + + return entity; + } + + /** + * Check if the specified grant record exists + * + * @param grantRecords list of grant records + * @param securable the securable + * @param grantee the grantee + * @param priv privilege that was granted + */ + boolean isGrantRecordExists( + List grantRecords, + PolarisEntityCore securable, + PolarisEntityCore grantee, + PolarisPrivilege priv) { + // ensure that this grant record is present + long grantCount = + grantRecords.stream() + .filter( + gr -> + gr.getSecurableCatalogId() == securable.getCatalogId() + && gr.getSecurableId() == securable.getId() + && gr.getGranteeCatalogId() == grantee.getCatalogId() + && gr.getGranteeId() == grantee.getId() + && gr.getPrivilegeCode() == priv.getCode()) + .count(); + return grantCount == 1; + } + + /** + * Ensure that the specified grant record exists + * + * @param grantRecords list of grant records + * @param securable the securable + * @param grantee the grantee + * @param priv privilege that was granted + */ + void checkGrantRecordExists( + List grantRecords, + PolarisEntityCore securable, + PolarisEntityCore grantee, + PolarisPrivilege priv) { + // ensure that this grant record is present + boolean exists = this.isGrantRecordExists(grantRecords, securable, grantee, priv); + Assertions.assertTrue(exists); + } + + /** + * Ensure that the specified grant record has been removed + * + * @param grantRecords list of grant records + * @param securable the securable + * @param grantee the grantee + * @param priv privilege that was granted + */ + void checkGrantRecordRemoved( + List grantRecords, + PolarisEntityCore securable, + PolarisEntityCore grantee, + PolarisPrivilege priv) { + // ensure that this grant record is absent + boolean exists = this.isGrantRecordExists(grantRecords, securable, grantee, priv); + Assertions.assertFalse(exists); + } + + /** + * Ensure that the specified grant record has been properly persisted + * + * @param securable the securable + * @param grantee the grantee + * @param priv privilege that was granted + */ + void ensureGrantRecordExists( + PolarisEntityCore securable, PolarisEntityCore grantee, PolarisPrivilege priv) { + // re-load both entities, ensure not null + securable = + polarisMetaStoreManager + .loadEntity(this.polarisCallContext, securable.getCatalogId(), securable.getId()) + .getEntity(); + Assertions.assertNotNull(securable); + grantee = + polarisMetaStoreManager + .loadEntity(this.polarisCallContext, grantee.getCatalogId(), grantee.getId()) + .getEntity(); + Assertions.assertNotNull(grantee); + + // the grantee better be a grantee + Assertions.assertTrue(grantee.getType().isGrantee()); + + // load all grant records on that securable, should not fail + PolarisMetaStoreManager.LoadGrantsResult loadGrantsOnSecurable = + polarisMetaStoreManager.loadGrantsOnSecurable( + this.polarisCallContext, securable.getCatalogId(), securable.getId()); + // ensure entities for these grant records have been properly loaded + this.validateLoadedGrants(loadGrantsOnSecurable, false); + + // check that the grant record exists in the list + this.checkGrantRecordExists(loadGrantsOnSecurable.getGrantRecords(), securable, grantee, priv); + + // load all grant records on that grantee, should not fail + PolarisMetaStoreManager.LoadGrantsResult loadGrantsOnGrantee = + polarisMetaStoreManager.loadGrantsToGrantee( + this.polarisCallContext, grantee.getCatalogId(), grantee.getId()); + // ensure entities for these grant records have been properly loaded + this.validateLoadedGrants(loadGrantsOnGrantee, true); + + // check that the grant record exists + this.checkGrantRecordExists(loadGrantsOnGrantee.getGrantRecords(), securable, grantee, priv); + } + + /** + * Validate the return of loadGrantsToGrantee() or loadGrantsOnSecurable() + * + * @param loadGrantRecords return from calling loadGrantsToGrantee()/loadGrantsOnSecurable() + * @param isGrantee if true, loadGrantsToGrantee() was called, else loadGrantsOnSecurable() was + * called + */ + private void validateLoadedGrants( + PolarisMetaStoreManager.LoadGrantsResult loadGrantRecords, boolean isGrantee) { + // ensure not null + Assertions.assertNotNull(loadGrantRecords); + + // ensure that entities have been populated + Map entities = loadGrantRecords.getEntitiesAsMap(); + Assertions.assertNotNull(entities); + + // ensure all present + for (PolarisGrantRecord grantRecord : loadGrantRecords.getGrantRecords()) { + + long catalogId = + isGrantee ? grantRecord.getSecurableCatalogId() : grantRecord.getGranteeCatalogId(); + long entityId = isGrantee ? grantRecord.getSecurableId() : grantRecord.getGranteeId(); + + // load that entity + PolarisBaseEntity entity = + polarisMetaStoreManager + .loadEntity(this.polarisCallContext, catalogId, entityId) + .getEntity(); + Assertions.assertNotNull(entity); + Assertions.assertEquals(entity, entities.get(entityId)); + } + } + + /** + * Ensure that the specified grant record has been properly removed + * + * @param securable the securable + * @param grantee the grantee + * @param priv privilege that was granted + */ + void ensureGrantRecordRemoved( + PolarisEntityCore securable, PolarisEntityCore grantee, PolarisPrivilege priv) { + // re-load both entities, ensure not null + securable = + polarisMetaStoreManager + .loadEntity(this.polarisCallContext, securable.getCatalogId(), securable.getId()) + .getEntity(); + Assertions.assertNotNull(securable); + grantee = + polarisMetaStoreManager + .loadEntity(this.polarisCallContext, grantee.getCatalogId(), grantee.getId()) + .getEntity(); + Assertions.assertNotNull(grantee); + + // the grantee better be a grantee + Assertions.assertTrue(grantee.getType().isGrantee()); + + // load all grant records on that securable, should not fail + PolarisMetaStoreManager.LoadGrantsResult loadGrantsOnSecurable = + polarisMetaStoreManager.loadGrantsOnSecurable( + this.polarisCallContext, securable.getCatalogId(), securable.getId()); + // ensure entities for these grant records have been properly loaded + this.validateLoadedGrants(loadGrantsOnSecurable, false); + + // check that the grant record no longer exists + this.checkGrantRecordRemoved(loadGrantsOnSecurable.getGrantRecords(), securable, grantee, priv); + + // load all grant records on that grantee, should not fail + PolarisMetaStoreManager.LoadGrantsResult loadGrantsOnGrantee = + polarisMetaStoreManager.loadGrantsToGrantee( + this.polarisCallContext, grantee.getCatalogId(), grantee.getId()); + this.validateLoadedGrants(loadGrantsOnGrantee, true); + + // check that the grant record has been removed + this.checkGrantRecordRemoved(loadGrantsOnGrantee.getGrantRecords(), securable, grantee, priv); + } + + /** + * Ensure that the specified catalog has been properly created. + * + * @param catalogName name of the catalog + */ + Pair validateCatalogCreated(String catalogName) { + // load all catalogs + List catalogs = + polarisMetaStoreManager + .listEntities( + this.polarisCallContext, + null, + PolarisEntityType.CATALOG, + PolarisEntitySubType.NULL_SUBTYPE) + .getEntities(); + + // cannot be null + Assertions.assertNotNull(catalogs); + + // iterate to find our catalog + PolarisEntityActiveRecord catalogListInfo = null; + for (PolarisEntityActiveRecord cat : catalogs) { + if (cat.getName().equals(catalogName)) { + catalogListInfo = cat; + break; + } + } + + // we must find it + Assertions.assertNotNull(catalogListInfo); + + // now make sure this catalog was properly persisted + PolarisBaseEntity catalog = + this.ensureExistsById( + null, + catalogListInfo.getId(), + true, + catalogName, + PolarisEntityType.CATALOG, + PolarisEntitySubType.NULL_SUBTYPE); + + // build catalog path to our catalog + List catalogPath = new ArrayList<>(); + catalogPath.add(catalog); + + // load all roles + List roles = + polarisMetaStoreManager + .listEntities( + this.polarisCallContext, + catalogPath, + PolarisEntityType.CATALOG_ROLE, + PolarisEntitySubType.NULL_SUBTYPE) + .getEntities(); + + // ensure not null, one element only + Assertions.assertNotNull(roles); + Assertions.assertEquals(1, roles.size()); + + // get catalog list information + PolarisEntityActiveRecord roleListInfo = roles.get(0); + + // now make sure this principal was properly persisted + PolarisBaseEntity role = + this.ensureExistsById( + catalogPath, + roleListInfo.getId(), + true, + PolarisEntityConstants.getNameOfCatalogAdminRole(), + PolarisEntityType.CATALOG_ROLE, + PolarisEntitySubType.NULL_SUBTYPE); + + // ensure that the admin role has been granted CATALOG_MANAGE_ACCESS and + // CATALOG_MANAGE_METADATA priv on the catalog + this.ensureGrantRecordExists(catalog, role, PolarisPrivilege.CATALOG_MANAGE_ACCESS); + this.ensureGrantRecordExists(catalog, role, PolarisPrivilege.CATALOG_MANAGE_METADATA); + + // success, return result + return new ImmutablePair<>(catalog, role); + } + + /** Create a principal */ + PolarisBaseEntity createPrincipal(String name) { + // create new principal identity + PolarisBaseEntity principalEntity = + new PolarisBaseEntity( + PolarisEntityConstants.getNullId(), + polarisMetaStoreManager.generateNewEntityId(this.polarisCallContext).getId(), + PolarisEntityType.PRINCIPAL, + PolarisEntitySubType.NULL_SUBTYPE, + PolarisEntityConstants.getRootEntityId(), + name); + principalEntity.setInternalProperties( + PolarisObjectMapperUtil.serializeProperties( + this.polarisCallContext, + Map.of(PolarisEntityConstants.PRINCIPAL_CREDENTIAL_ROTATION_REQUIRED_STATE, "true"))); + PolarisMetaStoreManager.CreatePrincipalResult createPrincipalResult = + polarisMetaStoreManager.createPrincipal(this.polarisCallContext, principalEntity); + Assertions.assertNotNull(createPrincipalResult); + + // ensure well created + this.ensureExistsById( + null, + createPrincipalResult.getPrincipal().getId(), + true, + name, + PolarisEntityType.PRINCIPAL, + PolarisEntitySubType.NULL_SUBTYPE); + + // the client id + PolarisPrincipalSecrets secrets = createPrincipalResult.getPrincipalSecrets(); + String clientId = secrets.getPrincipalClientId(); + + // ensure secrets are properly populated + Assertions.assertNotNull(secrets.getMainSecret()); + Assertions.assertTrue(secrets.getMainSecret().length() >= 32); + Assertions.assertNotNull(secrets.getSecondarySecret()); + Assertions.assertTrue(secrets.getSecondarySecret().length() >= 32); + + // should be same principal id + Assertions.assertEquals(principalEntity.getId(), secrets.getPrincipalId()); + + // ensure that the secrets have been properly saved and match + PolarisPrincipalSecrets reloadSecrets = + polarisMetaStoreManager + .loadPrincipalSecrets(this.polarisCallContext, clientId) + .getPrincipalSecrets(); + Assertions.assertNotNull(reloadSecrets); + Assertions.assertEquals(secrets.getPrincipalId(), reloadSecrets.getPrincipalId()); + Assertions.assertEquals(secrets.getPrincipalClientId(), reloadSecrets.getPrincipalClientId()); + Assertions.assertEquals(secrets.getMainSecret(), reloadSecrets.getMainSecret()); + Assertions.assertEquals(secrets.getSecondarySecret(), reloadSecrets.getSecondarySecret()); + + Map internalProperties = + PolarisObjectMapperUtil.deserializeProperties( + this.polarisCallContext, createPrincipalResult.getPrincipal().getInternalProperties()); + Assertions.assertNotNull( + internalProperties.get( + PolarisEntityConstants.PRINCIPAL_CREDENTIAL_ROTATION_REQUIRED_STATE)); + + // simulate retry if we are asked to + if (this.doRetry) { + // simulate that we retried + PolarisMetaStoreManager.CreatePrincipalResult newCreatePrincipalResult = + polarisMetaStoreManager.createPrincipal(this.polarisCallContext, principalEntity); + Assertions.assertNotNull(newCreatePrincipalResult); + + // ensure same + Assertions.assertEquals( + createPrincipalResult.getPrincipal().getId(), + newCreatePrincipalResult.getPrincipal().getId()); + PolarisPrincipalSecrets newSecrets = newCreatePrincipalResult.getPrincipalSecrets(); + Assertions.assertEquals(secrets.getPrincipalId(), newSecrets.getPrincipalId()); + Assertions.assertEquals(secrets.getPrincipalClientId(), newSecrets.getPrincipalClientId()); + Assertions.assertEquals(secrets.getMainSecret(), newSecrets.getMainSecret()); + Assertions.assertEquals(secrets.getMainSecret(), newSecrets.getMainSecret()); + } + + secrets = + polarisMetaStoreManager + .rotatePrincipalSecrets( + this.polarisCallContext, + clientId, + principalEntity.getId(), + secrets.getMainSecret(), + false) + .getPrincipalSecrets(); + Assertions.assertNotEquals(reloadSecrets.getMainSecret(), secrets.getMainSecret()); + Assertions.assertNotEquals(reloadSecrets.getMainSecret(), secrets.getMainSecret()); + + PolarisBaseEntity reloadPrincipal = + polarisMetaStoreManager + .loadEntity(this.polarisCallContext, 0L, createPrincipalResult.getPrincipal().getId()) + .getEntity(); + internalProperties = + PolarisObjectMapperUtil.deserializeProperties( + this.polarisCallContext, reloadPrincipal.getInternalProperties()); + Assertions.assertNull( + internalProperties.get( + PolarisEntityConstants.PRINCIPAL_CREDENTIAL_ROTATION_REQUIRED_STATE)); + + // rotate the secrets, twice! + polarisMetaStoreManager.rotatePrincipalSecrets( + this.polarisCallContext, clientId, principalEntity.getId(), secrets.getMainSecret(), false); + polarisMetaStoreManager.rotatePrincipalSecrets( + this.polarisCallContext, clientId, principalEntity.getId(), secrets.getMainSecret(), false); + + // reload and check that now the main should be secondary + reloadSecrets = + polarisMetaStoreManager + .loadPrincipalSecrets(this.polarisCallContext, clientId) + .getPrincipalSecrets(); + Assertions.assertNotNull(reloadSecrets); + Assertions.assertEquals(secrets.getPrincipalId(), reloadSecrets.getPrincipalId()); + Assertions.assertEquals(secrets.getPrincipalClientId(), reloadSecrets.getPrincipalClientId()); + Assertions.assertEquals(secrets.getMainSecret(), reloadSecrets.getSecondarySecret()); + String newMainSecret = reloadSecrets.getMainSecret(); + + // reset - the previous main secret is no longer one of the secrets + polarisMetaStoreManager.rotatePrincipalSecrets( + this.polarisCallContext, + clientId, + principalEntity.getId(), + reloadSecrets.getMainSecret(), + true); + reloadSecrets = + polarisMetaStoreManager + .loadPrincipalSecrets(this.polarisCallContext, clientId) + .getPrincipalSecrets(); + Assertions.assertNotNull(reloadSecrets); + Assertions.assertEquals(secrets.getPrincipalId(), reloadSecrets.getPrincipalId()); + Assertions.assertEquals(secrets.getPrincipalClientId(), reloadSecrets.getPrincipalClientId()); + Assertions.assertNotEquals(newMainSecret, reloadSecrets.getMainSecret()); + Assertions.assertNotEquals(newMainSecret, reloadSecrets.getSecondarySecret()); + + PolarisBaseEntity newPrincipal = + polarisMetaStoreManager + .loadEntity(this.polarisCallContext, 0L, principalEntity.getId()) + .getEntity(); + internalProperties = + PolarisObjectMapperUtil.deserializeProperties( + this.polarisCallContext, newPrincipal.getInternalProperties()); + Assertions.assertNotNull( + internalProperties.get( + PolarisEntityConstants.PRINCIPAL_CREDENTIAL_ROTATION_REQUIRED_STATE)); + + // reset again. we should get new secrets and the CREDENTIAL_ROTATION_REQUIRED flag should be + // gone + polarisMetaStoreManager.rotatePrincipalSecrets( + this.polarisCallContext, + clientId, + principalEntity.getId(), + reloadSecrets.getMainSecret(), + true); + PolarisPrincipalSecrets postResetCredentials = + polarisMetaStoreManager + .loadPrincipalSecrets(this.polarisCallContext, clientId) + .getPrincipalSecrets(); + Assertions.assertNotNull(reloadSecrets); + Assertions.assertEquals(reloadSecrets.getPrincipalId(), postResetCredentials.getPrincipalId()); + Assertions.assertEquals( + reloadSecrets.getPrincipalClientId(), postResetCredentials.getPrincipalClientId()); + Assertions.assertNotEquals(reloadSecrets.getMainSecret(), postResetCredentials.getMainSecret()); + Assertions.assertNotEquals( + reloadSecrets.getSecondarySecret(), postResetCredentials.getSecondarySecret()); + + PolarisBaseEntity finalPrincipal = + polarisMetaStoreManager + .loadEntity(this.polarisCallContext, 0L, principalEntity.getId()) + .getEntity(); + internalProperties = + PolarisObjectMapperUtil.deserializeProperties( + this.polarisCallContext, finalPrincipal.getInternalProperties()); + Assertions.assertNull( + internalProperties.get( + PolarisEntityConstants.PRINCIPAL_CREDENTIAL_ROTATION_REQUIRED_STATE)); + + // return it + return finalPrincipal; + } + + /** Create an entity */ + public PolarisBaseEntity createEntity( + List catalogPath, + PolarisEntityType entityType, + PolarisEntitySubType entitySubType, + String name) { + return createEntity( + catalogPath, + entityType, + entitySubType, + name, + polarisMetaStoreManager.generateNewEntityId(this.polarisCallContext).getId()); + } + + PolarisBaseEntity createEntity( + List catalogPath, + PolarisEntityType entityType, + PolarisEntitySubType entitySubType, + String name, + long entityId) { + long parentId; + long catalogId; + if (catalogPath != null) { + catalogId = catalogPath.get(0).getId(); + parentId = catalogPath.get(catalogPath.size() - 1).getId(); + } else { + catalogId = PolarisEntityConstants.getNullId(); + parentId = PolarisEntityConstants.getRootEntityId(); + } + PolarisBaseEntity newEntity = + new PolarisBaseEntity(catalogId, entityId, entityType, entitySubType, parentId, name); + PolarisBaseEntity entity = + polarisMetaStoreManager + .createEntityIfNotExists(this.polarisCallContext, catalogPath, newEntity) + .getEntity(); + Assertions.assertNotNull(entity); + + // same id + Assertions.assertEquals(newEntity.getId(), entity.getId()); + + // ensure well created + this.ensureExistsById(catalogPath, entity.getId(), true, name, entityType, entitySubType); + + // retry if we are asked to + if (this.doRetry) { + PolarisBaseEntity retryEntity = + polarisMetaStoreManager + .createEntityIfNotExists(this.polarisCallContext, catalogPath, newEntity) + .getEntity(); + Assertions.assertNotNull(retryEntity); + + // same id + Assertions.assertEquals(retryEntity.getId(), entity.getId()); + + // ensure well created + this.ensureExistsById( + catalogPath, retryEntity.getId(), true, name, entityType, entitySubType); + } + + // return it + return entity; + } + + /** + * Create an entity with a null subtype + * + * @return the entity + */ + PolarisBaseEntity createEntity( + List catalogPath, PolarisEntityType entityType, String name) { + return createEntity(catalogPath, entityType, PolarisEntitySubType.NULL_SUBTYPE, name); + } + + /** Drop the entity if it exists. */ + void dropEntity(List catalogPath, PolarisEntityCore entityToDrop) { + // see if the entity exists + final boolean exists; + boolean hasChildren = false; + + // check if it exists + PolarisBaseEntity entity = + polarisMetaStoreManager + .loadEntity(this.polarisCallContext, entityToDrop.getCatalogId(), entityToDrop.getId()) + .getEntity(); + if (entity != null) { + PolarisMetaStoreManager.EntityResult entityFound = + polarisMetaStoreManager.readEntityByName( + this.polarisCallContext, + catalogPath, + entity.getType(), + entity.getSubType(), + entity.getName()); + exists = entityFound.isSuccess(); + + // if exists, see if empty + if (exists + && (entity.getType() == PolarisEntityType.CATALOG + || entity.getType() == PolarisEntityType.NAMESPACE)) { + // build path + List path = new ArrayList<>(); + if (catalogPath != null) { + path.addAll(catalogPath); + } + path.add(entityToDrop); + + // get all children, cannot be null + List children = + polarisMetaStoreManager + .listEntities( + this.polarisCallContext, + path, + PolarisEntityType.NAMESPACE, + PolarisEntitySubType.NULL_SUBTYPE) + .getEntities(); + Assertions.assertNotNull(children); + if (children.isEmpty() && entity.getType() == PolarisEntityType.NAMESPACE) { + children = + polarisMetaStoreManager + .listEntities( + this.polarisCallContext, + path, + PolarisEntityType.TABLE_LIKE, + PolarisEntitySubType.ANY_SUBTYPE) + .getEntities(); + Assertions.assertNotNull(children); + } else if (children.isEmpty()) { + children = + polarisMetaStoreManager + .listEntities( + this.polarisCallContext, + path, + PolarisEntityType.CATALOG_ROLE, + PolarisEntitySubType.ANY_SUBTYPE) + .getEntities(); + Assertions.assertNotNull(children); + // if only one left, it can be dropped. + if (children.size() == 1) { + children.clear(); + } + } + hasChildren = !children.isEmpty(); + } + } else { + exists = false; + } + + // load all the grants to ensure they are properly cleaned + final List granteeEntities; + final List securableEntities; + if (exists) { + granteeEntities = + new ArrayList<>( + polarisMetaStoreManager + .loadGrantsOnSecurable( + this.polarisCallContext, entity.getCatalogId(), entity.getId()) + .getEntities()); + securableEntities = + new ArrayList<>( + polarisMetaStoreManager + .loadGrantsToGrantee( + this.polarisCallContext, entity.getCatalogId(), entity.getId()) + .getEntities()); + } else { + granteeEntities = List.of(); + securableEntities = List.of(); + } + + // now drop it + Map cleanupProperties = + Map.of("taskId", String.valueOf(entity.getId()), "cleanupProperty", "cleanupValue"); + PolarisMetaStoreManager.DropEntityResult dropResult = + polarisMetaStoreManager.dropEntityIfExists( + this.polarisCallContext, catalogPath, entityToDrop, cleanupProperties, true); + + // should have been dropped if exists + if (entityToDrop.cannotBeDroppedOrRenamed()) { + Assertions.assertFalse(dropResult.isSuccess()); + Assertions.assertFalse(dropResult.failedBecauseNotEmpty()); + Assertions.assertTrue(dropResult.isEntityUnDroppable()); + } else if (exists && hasChildren) { + Assertions.assertFalse(dropResult.isSuccess()); + Assertions.assertTrue(dropResult.failedBecauseNotEmpty()); + Assertions.assertFalse(dropResult.isEntityUnDroppable()); + } else { + Assertions.assertEquals(exists, dropResult.isSuccess()); + Assertions.assertFalse(dropResult.failedBecauseNotEmpty()); + Assertions.assertFalse(dropResult.isEntityUnDroppable()); + Assertions.assertNotNull(dropResult.getCleanupTaskId()); + PolarisBaseEntity cleanupTask = + polarisMetaStoreManager + .loadEntity(this.polarisCallContext, 0L, dropResult.getCleanupTaskId()) + .getEntity(); + Assertions.assertNotNull(cleanupTask); + Assertions.assertEquals(PolarisEntityType.TASK, cleanupTask.getType()); + Assertions.assertNotNull(cleanupTask.getInternalProperties()); + Map internalProperties = + PolarisObjectMapperUtil.deserializeProperties( + polarisCallContext, cleanupTask.getInternalProperties()); + Assertions.assertEquals(cleanupProperties, internalProperties); + Map properties = + PolarisObjectMapperUtil.deserializeProperties( + polarisCallContext, cleanupTask.getProperties()); + Assertions.assertNotNull(properties); + Assertions.assertNotNull(properties.get(PolarisTaskConstants.TASK_DATA)); + PolarisBaseEntity droppedEntity = + PolarisObjectMapperUtil.deserialize( + polarisCallContext, + properties.get(PolarisTaskConstants.TASK_DATA), + PolarisBaseEntity.class); + Assertions.assertNotNull(droppedEntity); + Assertions.assertEquals(entity.getId(), droppedEntity.getId()); + } + + // verify gone if it was dropped + if (dropResult.isSuccess()) { + // should be found but deleted + PolarisBaseEntity entityAfterDrop = + polarisMetaStoreManager + .loadEntity( + this.polarisCallContext, entityToDrop.getCatalogId(), entityToDrop.getId()) + .getEntity(); + + // ensure dropped + Assertions.assertNull(entityAfterDrop); + + // should no longer exists + Assertions.assertNotNull(entity); + PolarisMetaStoreManager.EntityResult entityFound = + polarisMetaStoreManager.readEntityByName( + this.polarisCallContext, + catalogPath, + entity.getType(), + entity.getSubType(), + entity.getName()); + + // should not be found + Assertions.assertEquals( + entityFound.getReturnStatus(), PolarisMetaStoreManager.ReturnStatus.ENTITY_NOT_FOUND); + + // make sure that the entity which was dropped is no longer referenced by a grant with any + // of the entity it was connected with before being dropped + for (PolarisBaseEntity connectedEntity : granteeEntities) { + PolarisMetaStoreManager.LoadGrantsResult grantResult = + polarisMetaStoreManager.loadGrantsToGrantee( + this.polarisCallContext, connectedEntity.getCatalogId(), connectedEntity.getId()); + if (grantResult.isSuccess()) { + long cnt = + grantResult.getGrantRecords().stream() + .filter(gr -> gr.getSecurableId() == entityToDrop.getId()) + .count(); + Assertions.assertEquals(0, cnt); + } else { + // special case when a catalog is dropped, the catalog_admin role is also dropped with it + Assertions.assertTrue( + grantResult.getReturnStatus() == PolarisMetaStoreManager.ReturnStatus.ENTITY_NOT_FOUND + && entityToDrop.getType() == PolarisEntityType.CATALOG + && connectedEntity.getType() == PolarisEntityType.CATALOG_ROLE + && connectedEntity + .getName() + .equals(PolarisEntityConstants.getNameOfCatalogAdminRole())); + } + } + for (PolarisBaseEntity connectedEntity : securableEntities) { + PolarisMetaStoreManager.LoadGrantsResult grantResult = + polarisMetaStoreManager.loadGrantsOnSecurable( + this.polarisCallContext, connectedEntity.getCatalogId(), connectedEntity.getId()); + long cnt = + grantResult.getGrantRecords().stream() + .filter(gr -> gr.getGranteeId() == entityToDrop.getId()) + .count(); + Assertions.assertEquals(0, cnt); + } + } + } + + /** Grant a privilege to a catalog role */ + void grantPrivilege( + PolarisBaseEntity role, + List catalogPath, + PolarisBaseEntity securable, + PolarisPrivilege priv) { + // grant the privilege + polarisMetaStoreManager.grantPrivilegeOnSecurableToRole( + this.polarisCallContext, role, catalogPath, securable, priv); + + // now validate the privilege + this.ensureGrantRecordExists(securable, role, priv); + } + + /** Revoke a privilege from a catalog role */ + void revokePrivilege( + PolarisBaseEntity role, + List catalogPath, + PolarisBaseEntity securable, + PolarisPrivilege priv) { + // grant the privilege + polarisMetaStoreManager.revokePrivilegeOnSecurableFromRole( + this.polarisCallContext, role, catalogPath, securable, priv); + + // now validate the privilege + this.ensureGrantRecordRemoved(securable, role, priv); + } + + /** Grant a privilege to a catalog role */ + void grantToGrantee( + PolarisEntityCore catalog, + PolarisBaseEntity granted, + PolarisBaseEntity grantee, + PolarisPrivilege priv) { + // grant the privilege + polarisMetaStoreManager.grantUsageOnRoleToGrantee( + this.polarisCallContext, catalog, granted, grantee); + + // now validate the privilege + this.ensureGrantRecordExists(granted, grantee, priv); + } + + /** Grant a privilege to a catalog role */ + void revokeToGrantee( + PolarisEntityCore catalog, + PolarisBaseEntity granted, + PolarisBaseEntity grantee, + PolarisPrivilege priv) { + // revoked the privilege + polarisMetaStoreManager.revokeUsageOnRoleFromGrantee( + this.polarisCallContext, catalog, granted, grantee); + + // now validate that the privilege is gone + this.ensureGrantRecordRemoved(granted, grantee, priv); + } + + /** Create a new catalog */ + PolarisBaseEntity createCatalog(String catalogName) { + // create new catalog + PolarisBaseEntity catalog = + new PolarisBaseEntity( + PolarisEntityConstants.getNullId(), + polarisMetaStoreManager.generateNewEntityId(this.polarisCallContext).getId(), + PolarisEntityType.CATALOG, + PolarisEntitySubType.NULL_SUBTYPE, + PolarisEntityConstants.getRootEntityId(), + "test"); + PolarisMetaStoreManager.CreateCatalogResult catalogCreated = + polarisMetaStoreManager.createCatalog(this.polarisCallContext, catalog, List.of()); + Assertions.assertNotNull(catalogCreated); + + // ensure well created + this.ensureExistsById( + null, + catalog.getId(), + true, + catalogName, + PolarisEntityType.CATALOG, + PolarisEntitySubType.NULL_SUBTYPE); + + // retry if we are asked to + if (this.doRetry) { + PolarisMetaStoreManager.CreateCatalogResult retryCatalogCreated = + polarisMetaStoreManager.createCatalog(this.polarisCallContext, catalog, List.of()); + Assertions.assertNotNull(retryCatalogCreated); + + // ensure well created + this.ensureExistsById( + null, + catalog.getId(), + true, + catalogName, + PolarisEntityType.CATALOG, + PolarisEntitySubType.NULL_SUBTYPE); + + // should be same id as the first time around + Assertions.assertEquals(catalog.getId(), retryCatalogCreated.getCatalog().getId()); + } + + return catalogCreated.getCatalog(); + } + + /** + * Create a test catalog. This is a new catalog which will have the following objects (N is for a + * namespace, T for a table, V for a view, R for a role, P for a principal): + * + *
+   * - C
+   * - (N1/N2/T1)
+   * - (N1/N2/T2)
+   * - (N1/N2/V1)
+   * - (N1/N3/T3)
+   * - (N1/N3/V2)
+   * - (N1/T4)
+   * - (N1/N4)
+   * - N5/N6/T5
+   * - N5/N6/T6
+   * - R1(TABLE_READ on N1/N2, VIEW_CREATE on C, TABLE_LIST on N1/N2, TABLE_DROP on N5/N6/T5)
+   * - R2(TABLE_WRITE_DATA on N5, VIEW_LIST on C)
+   * - PR1(R1, R2)
+   * - PR2(R2)
+   * - P1(PR1, PR2)
+   * - P2(PR1)
+   * 
+ */ + PolarisBaseEntity createTestCatalog(String catalogName) { + // create new catalog + PolarisBaseEntity catalog = + new PolarisBaseEntity( + PolarisEntityConstants.getNullId(), + polarisMetaStoreManager.generateNewEntityId(this.polarisCallContext).getId(), + PolarisEntityType.CATALOG, + PolarisEntitySubType.NULL_SUBTYPE, + PolarisEntityConstants.getRootEntityId(), + catalogName); + PolarisMetaStoreManager.CreateCatalogResult catalogCreated = + polarisMetaStoreManager.createCatalog(this.polarisCallContext, catalog, List.of()); + Assertions.assertNotNull(catalogCreated); + catalog = catalogCreated.getCatalog(); + + // now create all objects + PolarisBaseEntity N1 = this.createEntity(List.of(catalog), PolarisEntityType.NAMESPACE, "N1"); + PolarisBaseEntity N1_N2 = + this.createEntity(List.of(catalog, N1), PolarisEntityType.NAMESPACE, "N2"); + this.createEntity( + List.of(catalog, N1, N1_N2), + PolarisEntityType.TABLE_LIKE, + PolarisEntitySubType.TABLE, + "T1"); + this.createEntity( + List.of(catalog, N1, N1_N2), + PolarisEntityType.TABLE_LIKE, + PolarisEntitySubType.TABLE, + "T2"); + this.createEntity( + List.of(catalog, N1, N1_N2), PolarisEntityType.TABLE_LIKE, PolarisEntitySubType.VIEW, "V1"); + PolarisBaseEntity N1_N3 = + this.createEntity(List.of(catalog, N1), PolarisEntityType.NAMESPACE, "N3"); + this.createEntity( + List.of(catalog, N1, N1_N3), + PolarisEntityType.TABLE_LIKE, + PolarisEntitySubType.TABLE, + "T3"); + this.createEntity( + List.of(catalog, N1, N1_N3), PolarisEntityType.TABLE_LIKE, PolarisEntitySubType.VIEW, "V2"); + this.createEntity( + List.of(catalog, N1), PolarisEntityType.TABLE_LIKE, PolarisEntitySubType.TABLE, "T4"); + this.createEntity(List.of(catalog, N1), PolarisEntityType.NAMESPACE, "N4"); + PolarisBaseEntity N5 = this.createEntity(List.of(catalog), PolarisEntityType.NAMESPACE, "N5"); + PolarisBaseEntity N5_N6 = + this.createEntity(List.of(catalog, N5), PolarisEntityType.NAMESPACE, "N6"); + PolarisBaseEntity N5_N6_T5 = + this.createEntity( + List.of(catalog, N5, N5_N6), + PolarisEntityType.TABLE_LIKE, + PolarisEntitySubType.TABLE, + "T5"); + this.createEntity( + List.of(catalog, N5, N5_N6), + PolarisEntityType.TABLE_LIKE, + PolarisEntitySubType.TABLE, + "T6"); + + // the two catalog roles + PolarisBaseEntity R1 = + this.createEntity(List.of(catalog), PolarisEntityType.CATALOG_ROLE, "R1"); + PolarisBaseEntity R2 = + this.createEntity(List.of(catalog), PolarisEntityType.CATALOG_ROLE, "R2"); + + // perform the grants to R1 + grantPrivilege(R1, List.of(catalog, N1, N1_N2), N1_N2, PolarisPrivilege.TABLE_READ_DATA); + grantPrivilege(R1, List.of(catalog), catalog, PolarisPrivilege.VIEW_CREATE); + grantPrivilege(R1, List.of(catalog, N5), N5, PolarisPrivilege.TABLE_LIST); + grantPrivilege(R1, List.of(catalog, N1, N5_N6), N5_N6_T5, PolarisPrivilege.TABLE_DROP); + + // perform the grants to R2 + grantPrivilege(R2, List.of(catalog, N5), N5, PolarisPrivilege.TABLE_WRITE_DATA); + grantPrivilege(R2, List.of(catalog), catalog, PolarisPrivilege.VIEW_LIST); + + // now create two principal roles + PolarisBaseEntity PR1 = this.createEntity(null, PolarisEntityType.PRINCIPAL_ROLE, "PR1"); + PolarisBaseEntity PR2 = this.createEntity(null, PolarisEntityType.PRINCIPAL_ROLE, "PR2"); + + // assign R1 and R2 to PR1 + grantToGrantee(catalog, R1, PR1, PolarisPrivilege.CATALOG_ROLE_USAGE); + grantToGrantee(catalog, R2, PR1, PolarisPrivilege.CATALOG_ROLE_USAGE); + grantToGrantee(catalog, R2, PR2, PolarisPrivilege.CATALOG_ROLE_USAGE); + + // also create two new principals + PolarisBaseEntity P1 = this.createPrincipal("P1"); + PolarisBaseEntity P2 = this.createPrincipal("P2"); + + // assign PR1 and PR2 to this principal + grantToGrantee(null, PR1, P1, PolarisPrivilege.PRINCIPAL_ROLE_USAGE); + grantToGrantee(null, PR2, P1, PolarisPrivilege.PRINCIPAL_ROLE_USAGE); + grantToGrantee(null, PR2, P2, PolarisPrivilege.PRINCIPAL_ROLE_USAGE); + + return catalog; + } + + /** + * Find and entity by name, ensure it is there and has been properly initialized + * + * @return the identity we found + */ + PolarisBaseEntity ensureExistsByName( + List catalogPath, + PolarisEntityType entityType, + PolarisEntitySubType entitySubType, + String name) { + // find by name, ensure we found it + PolarisMetaStoreManager.EntityResult entityFound = + polarisMetaStoreManager.readEntityByName( + this.polarisCallContext, catalogPath, entityType, entitySubType, name); + Assertions.assertNotNull(entityFound); + Assertions.assertTrue(entityFound.isSuccess()); + + PolarisBaseEntity entity = entityFound.getEntity(); + Assertions.assertNotNull(entity); + Assertions.assertEquals(name, entity.getName()); + Assertions.assertEquals(entityType, entity.getType()); + if (entitySubType != PolarisEntitySubType.ANY_SUBTYPE) { + Assertions.assertEquals(entitySubType, entity.getSubType()); + } + Assertions.assertTrue(entity.getCreateTimestamp() >= this.testStartTime); + Assertions.assertEquals(0, entity.getDropTimestamp()); + Assertions.assertTrue(entity.getLastUpdateTimestamp() >= entity.getCreateTimestamp()); + Assertions.assertEquals(0, entity.getToPurgeTimestamp()); + Assertions.assertEquals(0, entity.getPurgeTimestamp()); + Assertions.assertEquals( + (catalogPath == null) ? PolarisEntityConstants.getNullId() : catalogPath.get(0).getId(), + entity.getCatalogId()); + Assertions.assertEquals( + (catalogPath == null) + ? PolarisEntityConstants.getRootEntityId() + : catalogPath.get(catalogPath.size() - 1).getId(), + entity.getParentId()); + Assertions.assertTrue(entity.getEntityVersion() >= 1 && entity.getGrantRecordsVersion() >= 1); + + return entity; + } + + /** + * Find and entity by name, ensure it is there and has been properly initialized + * + * @return the identity we found + */ + PolarisBaseEntity ensureExistsByName( + List catalogPath, PolarisEntityType entityType, String name) { + return this.ensureExistsByName( + catalogPath, entityType, PolarisEntitySubType.NULL_SUBTYPE, name); + } + + /** + * Update the specified entity. Validate that versions are properly maintained + * + * @param catalogPath path to the catalog where this entity is stored + * @param entity entity to update + * @param props updated properties + * @param internalProps updated internal properties + * @return updated entity + */ + PolarisBaseEntity updateEntity( + List catalogPath, + PolarisBaseEntity entity, + String props, + String internalProps) { + // ok, remember version and grants_version + int version = entity.getEntityVersion(); + int grantRecsVersion = entity.getGrantRecordsVersion(); + + // derive the catalogId for that entity + long catalogId = + (catalogPath == null) ? PolarisEntityConstants.getNullId() : catalogPath.get(0).getId(); + Assertions.assertEquals(entity.getCatalogId(), catalogId); + + // let's make some property updates + entity.setProperties(props); + entity.setInternalProperties(internalProps); + + // lookup that entity, ensure it exists + PolarisBaseEntity beforeUpdateEntity = + polarisMetaStoreManager + .loadEntity(this.polarisCallContext, entity.getCatalogId(), entity.getId()) + .getEntity(); + + // update that property + PolarisBaseEntity updatedEntity = + polarisMetaStoreManager + .updateEntityPropertiesIfNotChanged(this.polarisCallContext, catalogPath, entity) + .getEntity(); + + // if version mismatch, nothing should be updated + if (beforeUpdateEntity == null + || beforeUpdateEntity.getEntityVersion() != entity.getEntityVersion()) { + Assertions.assertNull(updatedEntity); + + // refresh catalog info + entity = + polarisMetaStoreManager + .loadEntity(this.polarisCallContext, entity.getCatalogId(), entity.getId()) + .getEntity(); + + // ensure nothing has changed + if (beforeUpdateEntity != null && entity != null) { + Assertions.assertEquals(beforeUpdateEntity.getEntityVersion(), entity.getEntityVersion()); + Assertions.assertEquals( + beforeUpdateEntity.getGrantRecordsVersion(), entity.getGrantRecordsVersion()); + Assertions.assertEquals(beforeUpdateEntity.getProperties(), entity.getProperties()); + Assertions.assertEquals( + beforeUpdateEntity.getInternalProperties(), entity.getInternalProperties()); + } + + return null; + } + + // entity should have been updated + Assertions.assertNotNull(updatedEntity); + + // read back this entity and ensure that the update was performed + PolarisBaseEntity afterUpdateEntity = + this.ensureExistsById( + catalogPath, + entity.getId(), + true, + entity.getName(), + entity.getType(), + entity.getSubType()); + + // verify that version has changed, but not grantRecsVersion + Assertions.assertEquals(version + 1, updatedEntity.getEntityVersion()); + Assertions.assertEquals(version, entity.getEntityVersion()); + Assertions.assertEquals(version + 1, afterUpdateEntity.getEntityVersion()); + + // grantRecsVersion should not have changed + Assertions.assertEquals(grantRecsVersion, updatedEntity.getGrantRecordsVersion()); + Assertions.assertEquals(grantRecsVersion, entity.getGrantRecordsVersion()); + Assertions.assertEquals(grantRecsVersion, afterUpdateEntity.getGrantRecordsVersion()); + + // update should have been performed + Assertions.assertEquals( + jsonNode(entity.getProperties()), jsonNode(updatedEntity.getProperties())); + Assertions.assertEquals( + jsonNode(entity.getProperties()), jsonNode(afterUpdateEntity.getProperties())); + Assertions.assertEquals( + jsonNode(entity.getInternalProperties()), jsonNode(updatedEntity.getInternalProperties())); + Assertions.assertEquals( + jsonNode(entity.getInternalProperties()), + jsonNode(afterUpdateEntity.getInternalProperties())); + + // lookup the tracking slice to verify this has been updated too + List versions = + polarisMetaStoreManager + .loadEntitiesChangeTracking( + this.polarisCallContext, List.of(new PolarisEntityId(catalogId, entity.getId()))) + .getChangeTrackingVersions(); + Assertions.assertEquals(1, versions.size()); + Assertions.assertEquals(updatedEntity.getEntityVersion(), versions.get(0).getEntityVersion()); + Assertions.assertEquals( + updatedEntity.getGrantRecordsVersion(), versions.get(0).getGrantRecordsVersion()); + + return updatedEntity; + } + + private JsonNode jsonNode(String json) { + if (json == null) { + return null; + } + try { + return objectMapper.readTree(json); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + /** Execute a list operation and validate the result */ + private void validateListReturn( + List path, + PolarisEntityType entityType, + PolarisEntitySubType entitySubType, + List> expectedResult) { + + // list the entities under the specified path + List result = + polarisMetaStoreManager + .listEntities(this.polarisCallContext, path, entityType, entitySubType) + .getEntities(); + Assertions.assertNotNull(result); + + // now validate the result + Assertions.assertEquals(expectedResult.size(), result.size()); + + // ensure all elements are found + for (Pair expected : expectedResult) { + boolean found = false; + for (PolarisEntityActiveRecord res : result) { + if (res.getName().equals(expected.getLeft()) + && expected.getRight().getCode() == res.getSubTypeCode()) { + found = true; + break; + } + } + // we should find it + Assertions.assertTrue(found); + } + } + + /** Execute a list operation and validate the result */ + private void validateListReturn( + List path, + PolarisEntityType entityType, + List> expectedResult) { + validateListReturn(path, entityType, PolarisEntitySubType.NULL_SUBTYPE, expectedResult); + } + + /** + * Validate a cached entry which has just been loaded from the store, assuming it is not null. + * + * @param cacheEntry the cached entity to validate + */ + private void validateCacheEntryLoad(PolarisMetaStoreManager.CachedEntryResult cacheEntry) { + + // cannot be null + Assertions.assertNotNull(cacheEntry); + PolarisEntity entity = PolarisEntity.of(cacheEntry.getEntity()); + Assertions.assertNotNull(entity); + List grantRecords = cacheEntry.getEntityGrantRecords(); + Assertions.assertNotNull(grantRecords); + + // same grant record version + Assertions.assertEquals(entity.getGrantRecordsVersion(), cacheEntry.getGrantRecordsVersion()); + + // reload the entity + PolarisEntity refEntity = + PolarisEntity.of( + this.polarisMetaStoreManager.loadEntity( + this.polarisCallContext, entity.getCatalogId(), entity.getId())); + Assertions.assertNotNull(refEntity); + + // same entity + Assertions.assertEquals(refEntity, entity); + // same version + Assertions.assertEquals(refEntity.getEntityVersion(), entity.getEntityVersion()); + + // reload the grants + List refGrantRecords = new ArrayList<>(); + if (refEntity.getType().isGrantee()) { + PolarisMetaStoreManager.LoadGrantsResult loadGrantResult = + this.polarisMetaStoreManager.loadGrantsToGrantee( + this.polarisCallContext, refEntity.getCatalogId(), refEntity.getId()); + this.validateLoadedGrants(loadGrantResult, true); + + // same version + Assertions.assertEquals( + cacheEntry.getGrantRecordsVersion(), loadGrantResult.getGrantsVersion()); + + refGrantRecords.addAll(loadGrantResult.getGrantRecords()); + } + + PolarisMetaStoreManager.LoadGrantsResult loadGrantResult = + this.polarisMetaStoreManager.loadGrantsOnSecurable( + this.polarisCallContext, refEntity.getCatalogId(), refEntity.getId()); + this.validateLoadedGrants(loadGrantResult, false); + + // same version + Assertions.assertEquals( + cacheEntry.getGrantRecordsVersion(), loadGrantResult.getGrantsVersion()); + + refGrantRecords.addAll(loadGrantResult.getGrantRecords()); + + // same grants + Assertions.assertEquals(new HashSet<>(refGrantRecords), new HashSet<>(grantRecords)); + } + + /** + * Validate a cached entry which has just been refreshed from the store, assuming it is not null. + * + * @param cacheEntry the cached entity to validate + */ + private void validateCacheEntryRefresh( + PolarisMetaStoreManager.CachedEntryResult cacheEntry, + long catalogId, + long entityId, + int entityVersion, + int entityGrantRecordsVersion) { + // cannot be null + Assertions.assertNotNull(cacheEntry); + PolarisBaseEntity entity = cacheEntry.getEntity(); + List grantRecords = cacheEntry.getEntityGrantRecords(); + + // reload the entity + PolarisBaseEntity refEntity = + this.polarisMetaStoreManager + .loadEntity(this.polarisCallContext, catalogId, entityId) + .getEntity(); + Assertions.assertNotNull(refEntity); + + // reload the grants + PolarisMetaStoreManager.LoadGrantsResult loadGrantResult = + refEntity.getType().isGrantee() + ? this.polarisMetaStoreManager.loadGrantsToGrantee( + this.polarisCallContext, catalogId, entityId) + : this.polarisMetaStoreManager.loadGrantsOnSecurable( + this.polarisCallContext, catalogId, entityId); + this.validateLoadedGrants(loadGrantResult, refEntity.getType().isGrantee()); + Assertions.assertEquals( + loadGrantResult.getGrantsVersion(), cacheEntry.getGrantRecordsVersion()); + + // if entity version has not changed, entity should not be loaded + if (refEntity.getEntityVersion() == entityVersion) { + // no need to reload in that case + Assertions.assertNull(entity); + } else { + // should have been reloaded + Assertions.assertNotNull(entity); + // should be same as refEntity + Assertions.assertEquals(PolarisEntity.of(refEntity), PolarisEntity.of(entity)); + // same version + Assertions.assertEquals(refEntity.getEntityVersion(), entity.getEntityVersion()); + } + + // if grant records version has not changed, grant records should not be loaded + if (refEntity.getGrantRecordsVersion() == entityGrantRecordsVersion) { + // no need to reload in that case + Assertions.assertNull(grantRecords); + } else { + List refGrantRecords = loadGrantResult.getGrantRecords(); + // should have been reloaded + Assertions.assertNotNull(grantRecords); + // should be same as refEntity + Assertions.assertEquals(new HashSet<>(refGrantRecords), new HashSet<>(grantRecords)); + // same version + Assertions.assertEquals( + loadGrantResult.getGrantsVersion(), cacheEntry.getGrantRecordsVersion()); + } + } + + /** + * Helper function to validate loading the cache by name. We will load the cache entry by name, + * check that the result is correct and return the entity or null if it cannot be found. + * + * @param entityCatalogId catalog id for the entity + * @param parentId parent id of the entity + * @param entityType type of the entity + * @param entityName name of the entity + * @param expectExists if true, we should find it + * @return return just the entity + */ + private PolarisBaseEntity loadCacheEntryByName( + long entityCatalogId, + long parentId, + @NotNull PolarisEntityType entityType, + @NotNull String entityName, + boolean expectExists) { + // load cached entry + PolarisMetaStoreManager.CachedEntryResult cacheEntry = + this.polarisMetaStoreManager.loadCachedEntryByName( + this.polarisCallContext, entityCatalogId, parentId, entityType, entityName); + + // if null, validate that indeed the entry does not exist + Assertions.assertEquals(expectExists, cacheEntry.isSuccess()); + + // if not null, validate it + if (cacheEntry.isSuccess()) { + this.validateCacheEntryLoad(cacheEntry); + return cacheEntry.getEntity(); + } else { + return null; + } + } + + /** + * Helper function to validate loading the cache by name. We will load the cache entry by name, + * check that the result exists and is correct and return the entity. + * + * @param entityCatalogId catalog id for the entity + * @param parentId parent id of the entity + * @param entityType type of the entity + * @param entityName name of the entity + * @return return just the entity + */ + private PolarisBaseEntity loadCacheEntryByName( + long entityCatalogId, + long parentId, + @NotNull PolarisEntityType entityType, + @NotNull String entityName) { + return this.loadCacheEntryByName(entityCatalogId, parentId, entityType, entityName, true); + } + + /** + * Helper function to validate loading the cache by id. We will load the cache entry by id, check + * that the result is correct and return the entity or null if it cannot be found. + * + * @param entityCatalogId catalog id for the entity + * @param entityId parent id of the entity + * @param expectExists if true, we should find it + * @return return just the entity + */ + private PolarisBaseEntity loadCacheEntryById( + long entityCatalogId, long entityId, boolean expectExists) { + // load cached entry + PolarisMetaStoreManager.CachedEntryResult cacheEntry = + this.polarisMetaStoreManager.loadCachedEntryById( + this.polarisCallContext, entityCatalogId, entityId); + + // if null, validate that indeed the entry does not exist + Assertions.assertEquals(expectExists, cacheEntry.isSuccess()); + + // if not null, validate it + if (cacheEntry.isSuccess()) { + this.validateCacheEntryLoad(cacheEntry); + return cacheEntry.getEntity(); + } else { + return null; + } + } + + /** + * Helper function to validate loading the cache by id. We will load the cache entry by id, check + * that it exists and validate the result. + * + * @param entityCatalogId catalog id for the entity + * @param entityId parent id of the entity + * @return return just the entity + */ + private PolarisBaseEntity loadCacheEntryById(long entityCatalogId, long entityId) { + return this.loadCacheEntryById(entityCatalogId, entityId, true); + } + + /** + * Helper function to validate the refresh of a cached entry. We will refresh the cache entry and + * check if the result exists based on "expectExists" and, if exists, validate it is correct + * + * @param entityVersion entity version in the cache + * @param entityGrantRecordsVersion entity grant records version in the cache + * @param entityType type of the entity to load + * @param entityCatalogId catalog id for the entity + * @param entityId parent id of the entity + * @param expectExists if true, we should find it + */ + private void refreshCacheEntry( + int entityVersion, + int entityGrantRecordsVersion, + PolarisEntityType entityType, + long entityCatalogId, + long entityId, + boolean expectExists) { + // load cached entry + PolarisMetaStoreManager.CachedEntryResult cacheEntry = + this.polarisMetaStoreManager.refreshCachedEntity( + this.polarisCallContext, + entityVersion, + entityGrantRecordsVersion, + entityType, + entityCatalogId, + entityId); + + // if null, validate that indeed the entry does not exist + Assertions.assertEquals(expectExists, cacheEntry.isSuccess()); + + // if not null, validate it + if (cacheEntry.isSuccess()) { + this.validateCacheEntryRefresh( + cacheEntry, entityCatalogId, entityId, entityVersion, entityGrantRecordsVersion); + } + } + + /** + * Helper function to validate the refresh of a cached entry. We will refresh the cache entry and + * check that the result exists and is correct + * + * @param entityVersion entity version in the cache + * @param entityGrantRecordsVersion entity grant records version in the cache + * @param entityType type of the entity to load + * @param entityCatalogId catalog id for the entity + * @param entityId parent id of the entity + */ + private void refreshCacheEntry( + int entityVersion, + int entityGrantRecordsVersion, + @NotNull PolarisEntityType entityType, + long entityCatalogId, + long entityId) { + // refresh cached entry + this.refreshCacheEntry( + entityVersion, entityGrantRecordsVersion, entityType, entityCatalogId, entityId, true); + } + + /** validate that the root catalog was properly constructed */ + void validateBootstrap() { + // load all principals + List principals = + polarisMetaStoreManager + .listEntities( + this.polarisCallContext, + null, + PolarisEntityType.PRINCIPAL, + PolarisEntitySubType.NULL_SUBTYPE) + .getEntities(); + + // ensure not null, one element only + Assertions.assertNotNull(principals); + Assertions.assertEquals(1, principals.size()); + + // get catalog list information + PolarisEntityActiveRecord principalListInfo = principals.get(0); + + // now make sure this principal was properly persisted + PolarisBaseEntity principal = + this.ensureExistsById( + null, + principalListInfo.getId(), + true, + PolarisEntityConstants.getRootPrincipalName(), + PolarisEntityType.PRINCIPAL, + PolarisEntitySubType.NULL_SUBTYPE); + + // load all principal roles + List principalRoles = + polarisMetaStoreManager + .listEntities( + this.polarisCallContext, + null, + PolarisEntityType.PRINCIPAL_ROLE, + PolarisEntitySubType.NULL_SUBTYPE) + .getEntities(); + + // ensure not null, one element only + Assertions.assertNotNull(principalRoles); + Assertions.assertEquals(1, principalRoles.size()); + + // get catalog list information + PolarisEntityActiveRecord roleListInfo = principalRoles.get(0); + + // now make sure this principal role was properly persisted + PolarisBaseEntity principalRole = + this.ensureExistsById( + null, + roleListInfo.getId(), + true, + PolarisEntityConstants.getNameOfPrincipalServiceAdminRole(), + PolarisEntityType.PRINCIPAL_ROLE, + PolarisEntitySubType.NULL_SUBTYPE); + + // also between the principal_role and the principal + this.ensureGrantRecordExists(principalRole, principal, PolarisPrivilege.PRINCIPAL_ROLE_USAGE); + } + + void testCreateTestCatalog() { + // create test catalog + this.createTestCatalog("test"); + + // validate that it has been properly created + PolarisBaseEntity catalog = this.ensureExistsByName(null, PolarisEntityType.CATALOG, "test"); + PolarisBaseEntity N1 = + this.ensureExistsByName(List.of(catalog), PolarisEntityType.NAMESPACE, "N1"); + PolarisBaseEntity N1_N2 = + this.ensureExistsByName(List.of(catalog, N1), PolarisEntityType.NAMESPACE, "N2"); + this.ensureExistsByName( + List.of(catalog, N1, N1_N2), + PolarisEntityType.TABLE_LIKE, + PolarisEntitySubType.TABLE, + "T1"); + this.ensureExistsByName( + List.of(catalog, N1, N1_N2), + PolarisEntityType.TABLE_LIKE, + PolarisEntitySubType.TABLE, + "T2"); + this.ensureExistsByName( + List.of(catalog, N1, N1_N2), + PolarisEntityType.TABLE_LIKE, + PolarisEntitySubType.ANY_SUBTYPE, + "T2"); + this.ensureExistsByName( + List.of(catalog, N1, N1_N2), PolarisEntityType.TABLE_LIKE, PolarisEntitySubType.VIEW, "V1"); + this.ensureExistsByName( + List.of(catalog, N1, N1_N2), + PolarisEntityType.TABLE_LIKE, + PolarisEntitySubType.ANY_SUBTYPE, + "V1"); + PolarisBaseEntity N1_N3 = + this.ensureExistsByName(List.of(catalog, N1), PolarisEntityType.NAMESPACE, "N3"); + this.ensureExistsByName( + List.of(catalog, N1, N1_N3), + PolarisEntityType.TABLE_LIKE, + PolarisEntitySubType.TABLE, + "T3"); + this.ensureExistsByName( + List.of(catalog, N1, N1_N3), + PolarisEntityType.TABLE_LIKE, + PolarisEntitySubType.ANY_SUBTYPE, + "V2"); + this.ensureExistsByName( + List.of(catalog, N1), PolarisEntityType.TABLE_LIKE, PolarisEntitySubType.TABLE, "T4"); + this.ensureExistsByName(List.of(catalog, N1), PolarisEntityType.NAMESPACE, "N4"); + PolarisBaseEntity N5 = + this.ensureExistsByName(List.of(catalog), PolarisEntityType.NAMESPACE, "N5"); + PolarisBaseEntity N5_N6 = + this.ensureExistsByName( + List.of(catalog, N5), + PolarisEntityType.NAMESPACE, + PolarisEntitySubType.ANY_SUBTYPE, + "N6"); + this.ensureExistsByName( + List.of(catalog, N5, N5_N6), + PolarisEntityType.TABLE_LIKE, + PolarisEntitySubType.TABLE, + "T5"); + PolarisBaseEntity N5_N6_T5 = + this.ensureExistsByName( + List.of(catalog, N5, N5_N6), + PolarisEntityType.TABLE_LIKE, + PolarisEntitySubType.ANY_SUBTYPE, + "T5"); + this.ensureExistsByName( + List.of(catalog, N5, N5_N6), + PolarisEntityType.TABLE_LIKE, + PolarisEntitySubType.TABLE, + "T6"); + PolarisBaseEntity R1 = + this.ensureExistsByName(List.of(catalog), PolarisEntityType.CATALOG_ROLE, "R1"); + PolarisBaseEntity R2 = + this.ensureExistsByName(List.of(catalog), PolarisEntityType.CATALOG_ROLE, "R2"); + this.ensureGrantRecordExists(N1_N2, R1, PolarisPrivilege.TABLE_READ_DATA); + this.ensureGrantRecordExists(catalog, R1, PolarisPrivilege.VIEW_CREATE); + this.ensureGrantRecordExists(N5, R1, PolarisPrivilege.TABLE_LIST); + this.ensureGrantRecordExists(N5_N6_T5, R1, PolarisPrivilege.TABLE_DROP); + this.ensureGrantRecordExists(N5, R2, PolarisPrivilege.TABLE_WRITE_DATA); + this.ensureGrantRecordExists(catalog, R2, PolarisPrivilege.VIEW_LIST); + PolarisBaseEntity PR1 = this.ensureExistsByName(null, PolarisEntityType.PRINCIPAL_ROLE, "PR1"); + PolarisBaseEntity PR2 = this.ensureExistsByName(null, PolarisEntityType.PRINCIPAL_ROLE, "PR2"); + this.ensureGrantRecordExists(R1, PR1, PolarisPrivilege.CATALOG_ROLE_USAGE); + this.ensureGrantRecordExists(R2, PR1, PolarisPrivilege.CATALOG_ROLE_USAGE); + this.ensureGrantRecordExists(R2, PR2, PolarisPrivilege.CATALOG_ROLE_USAGE); + PolarisBaseEntity P1 = this.ensureExistsByName(null, PolarisEntityType.PRINCIPAL, "P1"); + PolarisBaseEntity P2 = this.ensureExistsByName(null, PolarisEntityType.PRINCIPAL, "P2"); + this.ensureGrantRecordExists(PR1, P1, PolarisPrivilege.PRINCIPAL_ROLE_USAGE); + this.ensureGrantRecordExists(PR2, P1, PolarisPrivilege.PRINCIPAL_ROLE_USAGE); + this.ensureGrantRecordExists(PR2, P2, PolarisPrivilege.PRINCIPAL_ROLE_USAGE); + } + + void testBrowse() { + // create test catalog + PolarisBaseEntity catalog = this.createTestCatalog("test"); + Assertions.assertNotNull(catalog); + + // should see 2 top-level namespaces + this.validateListReturn( + List.of(catalog), + PolarisEntityType.NAMESPACE, + List.of( + ImmutablePair.of("N1", PolarisEntitySubType.NULL_SUBTYPE), + ImmutablePair.of("N5", PolarisEntitySubType.NULL_SUBTYPE))); + + // should see 3 top-level catalog roles including the admin one + this.validateListReturn( + List.of(catalog), + PolarisEntityType.CATALOG_ROLE, + List.of( + ImmutablePair.of( + PolarisEntityConstants.getNameOfCatalogAdminRole(), + PolarisEntitySubType.NULL_SUBTYPE), + ImmutablePair.of("R1", PolarisEntitySubType.NULL_SUBTYPE), + ImmutablePair.of("R2", PolarisEntitySubType.NULL_SUBTYPE))); + + // 2 principals + this.validateListReturn( + null, + PolarisEntityType.PRINCIPAL, + List.of( + ImmutablePair.of( + PolarisEntityConstants.getRootPrincipalName(), PolarisEntitySubType.NULL_SUBTYPE), + ImmutablePair.of("P1", PolarisEntitySubType.NULL_SUBTYPE), + ImmutablePair.of("P2", PolarisEntitySubType.NULL_SUBTYPE))); + + // 3 principal roles with the bootstrap service_admin + this.validateListReturn( + null, + PolarisEntityType.PRINCIPAL_ROLE, + List.of( + ImmutablePair.of("PR1", PolarisEntitySubType.NULL_SUBTYPE), + ImmutablePair.of("PR2", PolarisEntitySubType.NULL_SUBTYPE), + ImmutablePair.of( + PolarisEntityConstants.getNameOfPrincipalServiceAdminRole(), + PolarisEntitySubType.NULL_SUBTYPE))); + + // three namespaces under top-level namespace N1 + PolarisBaseEntity N1 = + this.ensureExistsByName(List.of(catalog), PolarisEntityType.NAMESPACE, "N1"); + this.validateListReturn( + List.of(catalog, N1), + PolarisEntityType.NAMESPACE, + PolarisEntitySubType.NULL_SUBTYPE, + List.of( + ImmutablePair.of("N2", PolarisEntitySubType.NULL_SUBTYPE), + ImmutablePair.of("N3", PolarisEntitySubType.NULL_SUBTYPE), + ImmutablePair.of("N4", PolarisEntitySubType.NULL_SUBTYPE))); + this.validateListReturn( + List.of(catalog, N1), + PolarisEntityType.TABLE_LIKE, + PolarisEntitySubType.ANY_SUBTYPE, + List.of(ImmutablePair.of("T4", PolarisEntitySubType.TABLE))); + PolarisBaseEntity N5 = + this.ensureExistsByName(List.of(catalog), PolarisEntityType.NAMESPACE, "N5"); + this.validateListReturn( + List.of(catalog, N5), + PolarisEntityType.NAMESPACE, + List.of(ImmutablePair.of("N6", PolarisEntitySubType.NULL_SUBTYPE))); + + // two tables and one view under top-level namespace N1_N1 + PolarisBaseEntity N1_N2 = + this.ensureExistsByName(List.of(catalog, N1), PolarisEntityType.NAMESPACE, "N2"); + // table or view object + this.validateListReturn( + List.of(catalog, N1, N1_N2), + PolarisEntityType.TABLE_LIKE, + PolarisEntitySubType.ANY_SUBTYPE, + List.of( + ImmutablePair.of("T1", PolarisEntitySubType.TABLE), + ImmutablePair.of("T2", PolarisEntitySubType.TABLE), + ImmutablePair.of("V1", PolarisEntitySubType.VIEW))); + // table object only + this.validateListReturn( + List.of(catalog, N1, N1_N2), + PolarisEntityType.TABLE_LIKE, + PolarisEntitySubType.TABLE, + List.of( + ImmutablePair.of("T1", PolarisEntitySubType.TABLE), + ImmutablePair.of("T2", PolarisEntitySubType.TABLE))); + // view object only + this.validateListReturn( + List.of(catalog, N1, N1_N2), + PolarisEntityType.TABLE_LIKE, + PolarisEntitySubType.VIEW, + List.of(ImmutablePair.of("V1", PolarisEntitySubType.VIEW))); + // list all principals + this.validateListReturn( + null, + PolarisEntityType.PRINCIPAL, + List.of( + ImmutablePair.of("root", PolarisEntitySubType.NULL_SUBTYPE), + ImmutablePair.of("P1", PolarisEntitySubType.NULL_SUBTYPE), + ImmutablePair.of("P2", PolarisEntitySubType.NULL_SUBTYPE))); + // list all principal roles + this.validateListReturn( + null, + PolarisEntityType.PRINCIPAL_ROLE, + List.of( + ImmutablePair.of( + PolarisEntityConstants.getNameOfPrincipalServiceAdminRole(), + PolarisEntitySubType.NULL_SUBTYPE), + ImmutablePair.of("PR1", PolarisEntitySubType.NULL_SUBTYPE), + ImmutablePair.of("PR2", PolarisEntitySubType.NULL_SUBTYPE))); + } + + /** Test that entity updates works well */ + void testUpdateEntities() { + // create test catalog + PolarisBaseEntity catalog = this.createTestCatalog("test"); + Assertions.assertNotNull(catalog); + + // find table N5/N6/T6 + PolarisBaseEntity N5 = + this.ensureExistsByName(List.of(catalog), PolarisEntityType.NAMESPACE, "N5"); + PolarisBaseEntity N5_N6 = + this.ensureExistsByName(List.of(catalog, N5), PolarisEntityType.NAMESPACE, "N6"); + PolarisBaseEntity T6v1 = + this.ensureExistsByName( + List.of(catalog, N5, N5_N6), + PolarisEntityType.TABLE_LIKE, + PolarisEntitySubType.TABLE, + "T6"); + Assertions.assertNotNull(T6v1); + + // update the entity + PolarisBaseEntity T6v2 = + this.updateEntity( + List.of(catalog, N5, N5_N6), + T6v1, + "{\"v2property\": \"some value\"}", + "{\"v2internal_property\": \"some other value\"}"); + Assertions.assertNotNull(T6v2); + + // update it again + PolarisBaseEntity T6v3 = + this.updateEntity( + List.of(catalog, N5, N5_N6), + T6v2, + "{\"v3property\": \"some value\"}", + "{\"v3internal_property\": \"some other value\"}"); + Assertions.assertNotNull(T6v3); + + // now simulate concurrency issue where another thread tries to update T2v2 again. This should + // not be updated + PolarisBaseEntity T6v3p = + this.updateEntity( + List.of(catalog, N5, N5_N6), + T6v2, + "{\"v3pproperty\": \"some value\"}", + "{\"v3pinternal_property\": \"some other value\"}"); + Assertions.assertNull(T6v3p); + + // update an entity which does not exist + PolarisBaseEntity T5v1 = + this.ensureExistsByName( + List.of(catalog, N5, N5_N6), + PolarisEntityType.TABLE_LIKE, + PolarisEntitySubType.TABLE, + "T5"); + T5v1.setId(100000L); + PolarisBaseEntity notExists = + this.updateEntity( + List.of(catalog, N5, N5_N6), + T5v1, + "{\"v3pproperty\": \"some value\"}", + "{\"v3pinternal_property\": \"some other value\"}"); + Assertions.assertNull(notExists); + } + + /** Test that dropping entities works well */ + void testDropEntities() { + // create test catalog + PolarisBaseEntity catalog = this.createTestCatalog("test"); + Assertions.assertNotNull(catalog); + + // find namespace N1/N2 + PolarisBaseEntity N1 = + this.ensureExistsByName(List.of(catalog), PolarisEntityType.NAMESPACE, "N1"); + PolarisBaseEntity N1_N2 = + this.ensureExistsByName(List.of(catalog, N1), PolarisEntityType.NAMESPACE, "N2"); + + // attempt to drop the N1/N2 namespace. Will fail because not empty + this.dropEntity(List.of(catalog, N1), N1_N2); + + // attempt to drop the N1/N4 namespace. Will succeed because empty + PolarisBaseEntity N1_N4 = + this.ensureExistsByName(List.of(catalog, N1), PolarisEntityType.NAMESPACE, "N4"); + this.dropEntity(List.of(catalog, N1), N1_N4); + + // find table N5/N6/T6 + PolarisBaseEntity N5 = + this.ensureExistsByName(List.of(catalog), PolarisEntityType.NAMESPACE, "N5"); + PolarisBaseEntity N5_N6 = + this.ensureExistsByName(List.of(catalog, N5), PolarisEntityType.NAMESPACE, "N6"); + PolarisBaseEntity T6 = + this.ensureExistsByName( + List.of(catalog, N5, N5_N6), + PolarisEntityType.TABLE_LIKE, + PolarisEntitySubType.TABLE, + "T6"); + Assertions.assertNotNull(T6); + + // drop table N5/N6/T6 + this.dropEntity(List.of(catalog, N5, N5_N6), T6); + + // drop the catalog role R2 + PolarisBaseEntity R2 = + this.ensureExistsByName(List.of(catalog), PolarisEntityType.CATALOG_ROLE, "R2"); + this.dropEntity(List.of(catalog), R2); + + // attempt to drop the entire catalog, should not work since not empty + this.dropEntity(null, catalog); + + // now drop everything + PolarisBaseEntity T1 = + this.ensureExistsByName( + List.of(catalog, N1, N1_N2), + PolarisEntityType.TABLE_LIKE, + PolarisEntitySubType.TABLE, + "T1"); + this.dropEntity(List.of(catalog, N1, N1_N2), T1); + PolarisBaseEntity T2 = + this.ensureExistsByName( + List.of(catalog, N1, N1_N2), + PolarisEntityType.TABLE_LIKE, + PolarisEntitySubType.TABLE, + "T2"); + this.dropEntity(List.of(catalog, N1, N1_N2), T2); + PolarisBaseEntity V1 = + this.ensureExistsByName( + List.of(catalog, N1, N1_N2), + PolarisEntityType.TABLE_LIKE, + PolarisEntitySubType.VIEW, + "V1"); + this.dropEntity(List.of(catalog, N1, N1_N2), V1); + this.dropEntity(List.of(catalog, N1), N1_N2); + + PolarisBaseEntity N1_N3 = + this.ensureExistsByName(List.of(catalog, N1), PolarisEntityType.NAMESPACE, "N3"); + PolarisBaseEntity T3 = + this.ensureExistsByName( + List.of(catalog, N1, N1_N3), + PolarisEntityType.TABLE_LIKE, + PolarisEntitySubType.TABLE, + "T3"); + this.dropEntity(List.of(catalog, N1, N1_N3), T3); + PolarisBaseEntity V2 = + this.ensureExistsByName( + List.of(catalog, N1, N1_N3), + PolarisEntityType.TABLE_LIKE, + PolarisEntitySubType.VIEW, + "V2"); + this.dropEntity(List.of(catalog, N1, N1_N3), V2); + this.dropEntity(List.of(catalog, N1), N1_N3); + + PolarisBaseEntity T4 = + this.ensureExistsByName( + List.of(catalog, N1), PolarisEntityType.TABLE_LIKE, PolarisEntitySubType.TABLE, "T4"); + this.dropEntity(List.of(catalog, N1), T4); + this.dropEntity(List.of(catalog), N1); + + PolarisBaseEntity T5 = + this.ensureExistsByName( + List.of(catalog, N5, N5_N6), + PolarisEntityType.TABLE_LIKE, + PolarisEntitySubType.TABLE, + "T5"); + this.dropEntity(List.of(catalog, N5, N5_N6), T5); + this.dropEntity(List.of(catalog, N5), N5_N6); + this.dropEntity(List.of(catalog), N5); + + // attempt to drop the catalog again, should fail because of role R1 + this.dropEntity(null, catalog); + + // catalog exists + PolarisMetaStoreManager.EntityResult catalogFound = + polarisMetaStoreManager.readEntityByName( + this.polarisCallContext, + null, + PolarisEntityType.CATALOG, + PolarisEntitySubType.NULL_SUBTYPE, + "test"); + // success and found + Assertions.assertTrue(catalogFound.isSuccess()); + Assertions.assertNotNull(catalogFound.getEntity()); + + // drop the last role + PolarisBaseEntity R1 = + this.ensureExistsByName(List.of(catalog), PolarisEntityType.CATALOG_ROLE, "R1"); + this.dropEntity(List.of(catalog), R1); + + // the catalog admin role cannot be dropped + PolarisBaseEntity CATALOG_ADMIN = + this.ensureExistsByName( + List.of(catalog), + PolarisEntityType.CATALOG_ROLE, + PolarisEntityConstants.getNameOfCatalogAdminRole()); + this.dropEntity(List.of(catalog), CATALOG_ADMIN); + // should be found since it is undroppable + this.ensureExistsByName( + List.of(catalog), + PolarisEntityType.CATALOG_ROLE, + PolarisEntityConstants.getNameOfCatalogAdminRole()); + + // drop the catalog, should work now. The CATALOG_ADMIN role will be dropped too + this.dropEntity(null, catalog); + + // catalog exists? + catalogFound = + polarisMetaStoreManager.readEntityByName( + this.polarisCallContext, + null, + PolarisEntityType.CATALOG, + PolarisEntitySubType.NULL_SUBTYPE, + "test"); + // success and not found + Assertions.assertEquals( + catalogFound.getReturnStatus(), PolarisMetaStoreManager.ReturnStatus.ENTITY_NOT_FOUND); + + // drop the principal role PR1 + PolarisBaseEntity PR1 = this.ensureExistsByName(null, PolarisEntityType.PRINCIPAL_ROLE, "PR1"); + this.dropEntity(null, PR1); + + // drop the principal role P1 + PolarisBaseEntity P1 = this.ensureExistsByName(null, PolarisEntityType.PRINCIPAL, "P1"); + this.dropEntity(null, P1); + } + + /** Test granting/revoking privileges */ + public void testPrivileges() { + // create test catalog + PolarisBaseEntity catalog = this.createTestCatalog("test"); + Assertions.assertNotNull(catalog); + + // get catalog role R1 + PolarisBaseEntity R1 = + this.ensureExistsByName(List.of(catalog), PolarisEntityType.CATALOG_ROLE, "R1"); + + // get principal role PR1 + PolarisBaseEntity PR1 = this.ensureExistsByName(null, PolarisEntityType.PRINCIPAL_ROLE, "PR1"); + + // get principal P1 + PolarisBaseEntity P1 = this.ensureExistsByName(null, PolarisEntityType.PRINCIPAL, "P1"); + + // test revoking usage on catalog/principal roles + this.revokeToGrantee(catalog, R1, PR1, PolarisPrivilege.CATALOG_ROLE_USAGE); + this.revokeToGrantee(null, PR1, P1, PolarisPrivilege.PRINCIPAL_ROLE_USAGE); + + // remove some privileges + PolarisBaseEntity N1 = + this.ensureExistsByName(List.of(catalog), PolarisEntityType.NAMESPACE, "N1"); + PolarisBaseEntity N1_N2 = + this.ensureExistsByName(List.of(catalog, N1), PolarisEntityType.NAMESPACE, "N2"); + PolarisBaseEntity N5 = + this.ensureExistsByName(List.of(catalog), PolarisEntityType.NAMESPACE, "N5"); + PolarisBaseEntity N5_N6 = + this.ensureExistsByName( + List.of(catalog, N5), + PolarisEntityType.NAMESPACE, + PolarisEntitySubType.ANY_SUBTYPE, + "N6"); + PolarisBaseEntity N5_N6_T5 = + this.ensureExistsByName( + List.of(catalog, N5, N5_N6), + PolarisEntityType.TABLE_LIKE, + PolarisEntitySubType.ANY_SUBTYPE, + "T5"); + + // revoke grants + this.revokePrivilege(R1, List.of(catalog, N1), N1_N2, PolarisPrivilege.TABLE_READ_DATA); + + // revoke priv from the catalog itself + this.revokePrivilege(R1, List.of(catalog), catalog, PolarisPrivilege.VIEW_CREATE); + + // revoke privs from securables inside the catalog itself + this.revokePrivilege(R1, List.of(catalog), N5, PolarisPrivilege.TABLE_LIST); + this.revokePrivilege(R1, List.of(catalog, N5, N5_N6), N5_N6_T5, PolarisPrivilege.TABLE_DROP); + + // test with some entity ids which are prefixes of other entity ids + PolarisBaseEntity PR900 = + this.createEntity( + null, + PolarisEntityType.PRINCIPAL_ROLE, + PolarisEntitySubType.NULL_SUBTYPE, + "PR900", + 900L); + PolarisBaseEntity PR9000 = + this.createEntity( + null, + PolarisEntityType.PRINCIPAL_ROLE, + PolarisEntitySubType.NULL_SUBTYPE, + "PR9000", + 9000L); + + // assign catalog role to PR9000 + grantToGrantee(catalog, R1, PR9000, PolarisPrivilege.CATALOG_ROLE_USAGE); + + PolarisMetaStoreManager.LoadGrantsResult loadGrantsResult = + polarisMetaStoreManager.loadGrantsToGrantee(this.polarisCallContext, 0L, PR9000.getId()); + this.validateLoadedGrants(loadGrantsResult, true); + Assertions.assertEquals(1, loadGrantsResult.getGrantRecords().size()); + Assertions.assertEquals( + R1.getCatalogId(), loadGrantsResult.getGrantRecords().get(0).getSecurableCatalogId()); + Assertions.assertEquals(R1.getId(), loadGrantsResult.getGrantRecords().get(0).getSecurableId()); + + loadGrantsResult = + polarisMetaStoreManager.loadGrantsToGrantee(this.polarisCallContext, 0L, PR900.getId()); + Assertions.assertNotNull(loadGrantsResult); + Assertions.assertEquals(0, loadGrantsResult.getGrantRecords().size()); + } + + /** + * Rename an entity and validate it worked + * + * @param catPath catalog path + * @param entity entity to rename + * @param newCatPath new catalog path + * @param newName new name + */ + void renameEntity( + List catPath, + PolarisBaseEntity entity, + List newCatPath, + String newName) { + + // save old name + String oldName = entity.getName(); + + // the renamed entity + PolarisEntity renamedEntityInput = new PolarisEntity(entity); + renamedEntityInput.setName(newName); + String updatedInternalPropertiesString = "updatedDataForInternalProperties1234"; + String updatedPropertiesString = "updatedDataForProperties9876"; + + // this is to test that properties are also updated during the rename operation + renamedEntityInput.setInternalProperties(updatedInternalPropertiesString); + renamedEntityInput.setProperties(updatedPropertiesString); + + // check to see if we would have a name conflict + PolarisMetaStoreManager.EntityResult newNameLookup = + polarisMetaStoreManager.readEntityByName( + polarisCallContext, + newCatPath == null ? catPath : newCatPath, + entity.getType(), + PolarisEntitySubType.ANY_SUBTYPE, + newName); + + // rename it + PolarisBaseEntity renamedEntity = + polarisMetaStoreManager + .renameEntity(polarisCallContext, catPath, entity, newCatPath, renamedEntityInput) + .getEntity(); + + // ensure success + if (newNameLookup.getReturnStatus() == PolarisMetaStoreManager.ReturnStatus.ENTITY_NOT_FOUND) { + Assertions.assertNotNull(renamedEntity); + + // ensure it exists + PolarisBaseEntity renamedEntityOut = + this.ensureExistsByName( + newCatPath == null ? catPath : newCatPath, + entity.getType(), + entity.getSubType(), + newName); + + // what is returned should be same has what has been loaded + Assertions.assertEquals(renamedEntityOut, renamedEntity); + + // ensure properties have been updated + Assertions.assertEquals( + updatedInternalPropertiesString, renamedEntityOut.getInternalProperties()); + Assertions.assertEquals(updatedPropertiesString, renamedEntityOut.getProperties()); + + // ensure the old one is gone + PolarisMetaStoreManager.EntityResult res = + polarisMetaStoreManager.readEntityByName( + polarisCallContext, catPath, entity.getType(), entity.getSubType(), oldName); + + // not found + Assertions.assertEquals( + res.getReturnStatus(), PolarisMetaStoreManager.ReturnStatus.ENTITY_NOT_FOUND); + } else { + // cannot rename since the entity exists + Assertions.assertNull(renamedEntity); + } + } + + /** Play with renaming entities */ + public void testRename() { + // create test catalog + PolarisBaseEntity catalog = this.createTestCatalog("test"); + Assertions.assertNotNull(catalog); + + // get catalog role R1 and rename it to R3 + PolarisBaseEntity R1 = + this.ensureExistsByName(List.of(catalog), PolarisEntityType.CATALOG_ROLE, "R1"); + + // rename it to something that exists, should fail + this.renameEntity(List.of(catalog), R1, List.of(catalog), "R2"); + + // rename it to something that exists using null newCatalogPath as shorthand, should fail + this.renameEntity(List.of(catalog), R1, null, "R2"); + + // this one should succeed + this.renameEntity(List.of(catalog), R1, List.of(catalog), "R3"); + + // get principal role PR1 and rename it to PR3 + PolarisBaseEntity PR1 = this.ensureExistsByName(null, PolarisEntityType.PRINCIPAL_ROLE, "PR1"); + // exists => fails + this.renameEntity(null, PR1, null, "PR2"); + // does not exists => succeeds + this.renameEntity(null, PR1, null, "PR3"); + + // get principal P1 and rename it to P3 + PolarisBaseEntity P1 = this.ensureExistsByName(null, PolarisEntityType.PRINCIPAL, "P1"); + // exists => fails + this.renameEntity(null, P1, null, "P2"); + // does not exists => succeeds + this.renameEntity(null, P1, null, "P3"); + + // N2 namespace + PolarisBaseEntity N5 = + this.ensureExistsByName(List.of(catalog), PolarisEntityType.NAMESPACE, "N5"); + + // rename N1/N2/T1 to N5/T7 + PolarisBaseEntity N1 = + this.ensureExistsByName(List.of(catalog), PolarisEntityType.NAMESPACE, "N1"); + PolarisBaseEntity N1_N2 = + this.ensureExistsByName(List.of(catalog, N1), PolarisEntityType.NAMESPACE, "N2"); + PolarisBaseEntity N1_N3 = + this.ensureExistsByName(List.of(catalog, N1), PolarisEntityType.NAMESPACE, "N3"); + PolarisBaseEntity N1_N2_T1 = + this.ensureExistsByName( + List.of(catalog, N1, N1_N2), + PolarisEntityType.TABLE_LIKE, + PolarisEntitySubType.ANY_SUBTYPE, + "T1"); + // view with the same name exists, should fail + this.renameEntity(List.of(catalog, N1, N1_N2), N1_N2_T1, List.of(catalog, N1, N1_N2), "V1"); + // table with the same name exists, should fail + this.renameEntity(List.of(catalog, N1, N1_N2), N1_N2_T1, List.of(catalog, N1, N1_N2), "T2"); + // view with the same name exists, should fail + this.renameEntity(List.of(catalog, N1, N1_N2), N1_N2_T1, List.of(catalog, N1, N1_N3), "V2"); + // table with the same name exists, should fail + this.renameEntity(List.of(catalog, N1, N1_N2), N1_N2_T1, List.of(catalog, N1, N1_N3), "T3"); + + // this should work, T7 does not exist + this.renameEntity(List.of(catalog, N1, N1_N2), N1_N2_T1, List.of(catalog, N5), "T7"); + } + + /** Test the set of functions for the entity cache */ + public void testEntityCache() { + // create test catalog + PolarisBaseEntity catalog = this.createTestCatalog("test"); + Assertions.assertNotNull(catalog); + + // load catalog by name + PolarisBaseEntity TEST = + this.loadCacheEntryByName( + PolarisEntityConstants.getNullId(), + PolarisEntityConstants.getNullId(), + PolarisEntityType.CATALOG, + "test"); + + // and again by id + TEST = this.loadCacheEntryById(TEST.getCatalogId(), TEST.getId()); + + // get namespace N1 + PolarisBaseEntity N1 = + this.loadCacheEntryByName(TEST.getId(), TEST.getId(), PolarisEntityType.NAMESPACE, "N1"); + + // refresh it, nothing changed + this.refreshCacheEntry( + N1.getEntityVersion(), + N1.getGrantRecordsVersion(), + N1.getType(), + N1.getCatalogId(), + N1.getId()); + + // now update this N1 entity + this.updateEntity(List.of(TEST), N1, "{\"v1property\": \"property value\"}", null); + + // get namespace N1 + PolarisBaseEntity N1p = + this.loadCacheEntryByName(TEST.getId(), TEST.getId(), PolarisEntityType.NAMESPACE, "N1"); + + // entity version should have changed + Assertions.assertEquals(N1.getEntityVersion() + 1, N1p.getEntityVersion()); + + // but not the grant records version + Assertions.assertEquals(N1.getGrantRecordsVersion(), N1p.getGrantRecordsVersion()); + + // refresh it, nothing changed + this.refreshCacheEntry( + N1.getEntityVersion(), + N1.getGrantRecordsVersion(), + N1.getType(), + N1.getCatalogId(), + N1.getId()); + + // load role R1 + PolarisBaseEntity R1 = + this.loadCacheEntryByName(TEST.getId(), TEST.getId(), PolarisEntityType.CATALOG_ROLE, "R1"); + R1 = this.loadCacheEntryById(R1.getCatalogId(), R1.getId()); + + // add a grant record to N1 + this.grantPrivilege(R1, List.of(TEST), N1, PolarisPrivilege.NAMESPACE_FULL_METADATA); + + // get namespace N1 again + PolarisBaseEntity N1pp = + this.loadCacheEntryByName(TEST.getId(), TEST.getId(), PolarisEntityType.NAMESPACE, "N1"); + + // entity version should not have changed compared to N1p + Assertions.assertEquals(N1p.getEntityVersion(), N1pp.getEntityVersion()); + + // but the grant records version should have + Assertions.assertEquals(N1p.getGrantRecordsVersion() + 1, N1pp.getGrantRecordsVersion()); + + // refresh it, grants should be updated + this.refreshCacheEntry( + N1.getEntityVersion(), + N1.getGrantRecordsVersion(), + N1.getType(), + N1.getCatalogId(), + N1.getId()); + + // now validate that load something which does not exist, will also work + this.loadCacheEntryByName( + N1.getCatalogId(), N1.getId(), PolarisEntityType.TABLE_LIKE, "do_not_exists", false); + this.loadCacheEntryById(N1.getCatalogId() + 1000, N1.getId(), false); + + // refresh a purged entity + this.refreshCacheEntry( + 1, 1, PolarisEntityType.TABLE_LIKE, N1.getCatalogId() + 1000, N1.getId(), false); + } +} diff --git a/polaris-server.yml b/polaris-server.yml new file mode 100644 index 0000000000..0311b423c0 --- /dev/null +++ b/polaris-server.yml @@ -0,0 +1,150 @@ +server: + # Maximum number of threads. + maxThreads: 200 + + # Minimum number of thread to keep alive. + minThreads: 10 + applicationConnectors: + # HTTP-specific options. + - type: http + + # The port on which the HTTP server listens for service requests. + port: 8181 + + adminConnectors: + - type: http + port: 8182 + + # The hostname of the interface to which the HTTP server socket wil be found. If omitted, the + # socket will listen on all interfaces. + #bindHost: localhost + + # ssl: + # keyStore: ./example.keystore + # keyStorePassword: example + # + # keyStoreType: JKS # (optional, JKS is default) + + # HTTP request log settings + requestLog: + appenders: + # Settings for logging to stdout. + - type: console + + # Settings for logging to a file. + - type: file + + # The file to which statements will be logged. + currentLogFilename: ./logs/request.log + + # When the log file rolls over, the file will be archived to requests-2012-03-15.log.gz, + # requests.log will be truncated, and new statements written to it. + archivedLogFilenamePattern: ./logs/requests-%d.log.gz + + # The maximum number of log files to archive. + archivedFileCount: 14 + + # Enable archiving if the request log entries go to the their own file + archive: true + +# Either 'jdbc' or 'polaris'; specifies the underlying delegate catalog +baseCatalogType: "polaris" + +featureConfiguration: + ENFORCE_PRINCIPAL_CREDENTIAL_ROTATION_REQUIRED_CHECKING: false + DISABLE_TOKEN_GENERATION_FOR_USER_PRINCIPALS: true + SUPPORTED_CATALOG_STORAGE_TYPES: + - S3 + - GCS + - AZURE + - FILE + + +# Whether we want to enable Snowflake OAuth locally. Setting this to true requires +# that you go through the setup outlined in the `README.md` file, specifically the +# `OAuth + Snowflake: Local Testing And Then Some` section +callContextResolver: + type: default + +realmContextResolver: + type: default + +defaultRealms: + - default-realm + +metaStoreManager: + type: in-memory + +# TODO - avoid duplicating token broker config +oauth2: + type: test +# type: default # - uncomment to support Auth0 JWT tokens +# tokenBroker: +# type: symmetric-key +# secret: polaris + +authenticator: + class: io.polaris.service.auth.TestInlineBearerTokenPolarisAuthenticator +# class: io.polaris.service.auth.DefaultPolarisAuthenticator # - uncomment to support Auth0 JWT tokens +# tokenBroker: +# type: symmetric-key +# secret: polaris + +cors: + allowed-origins: + - http://localhost:8080 + allowed-timing-origins: + - http://localhost:8080 + allowed-methods: + - PATCH + - POST + - DELETE + - GET + - PUT + allowed-headers: + - "*" + exposed-headers: + - "*" + preflight-max-age: 600 + allowed-credentials: true + +# Logging settings. + +logging: + + # The default level of all loggers. Can be OFF, ERROR, WARN, INFO, DEBUG, TRACE, or ALL. + level: INFO + + # Logger-specific levels. + loggers: + org.apache.iceberg.rest: DEBUG + io.polaris: DEBUG + + appenders: + + - type: console + # If true, write log statements to stdout. + # enabled: true + # Do not display log statements below this threshold to stdout. + threshold: ALL + # Custom Logback PatternLayout with threadname. + logFormat: "%-5p [%d{ISO8601} - %-6r] [%t] [%X{aid}%X{sid}%X{tid}%X{wid}%X{oid}%X{srv}%X{job}%X{rid}] %c{30}: %m %kvp%n%ex" + + # Settings for logging to a file. + - type: file + # If true, write log statements to a file. + # enabled: true + # Do not write log statements below this threshold to the file. + threshold: ALL + layout: + type: polaris + flattenKeyValues: false + includeKeyValues: true + + # The file to which statements will be logged. + currentLogFilename: ./logs/polaris.log + # When the log file rolls over, the file will be archived to snowflake-2012-03-15.log.gz, + # snowflake.log will be truncated, and new statements written to it. + archivedLogFilenamePattern: ./logs/polaris-%d.log.gz + # The maximum number of log files to archive. + archivedFileCount: 14 diff --git a/polaris-service/build.gradle b/polaris-service/build.gradle new file mode 100644 index 0000000000..689c80c772 --- /dev/null +++ b/polaris-service/build.gradle @@ -0,0 +1,191 @@ +plugins { + id 'com.github.johnrengelman.shadow' version '8.1.1' + id 'org.openapi.generator' version '7.6.0' +} + +dependencies { + implementation project(':polaris-core') + + implementation "org.apache.iceberg:iceberg-api:${icebergVersion}" + implementation "org.apache.iceberg:iceberg-core:${icebergVersion}" + implementation "org.apache.iceberg:iceberg-aws:${icebergVersion}" + + implementation "io.dropwizard:dropwizard-core:${dropwizardVersion}" + implementation "io.dropwizard:dropwizard-auth:${dropwizardVersion}" + implementation "io.dropwizard:dropwizard-json-logging:${dropwizardVersion}" + + implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.16.2' + + implementation "io.opentelemetry:opentelemetry-api:1.38.0" + implementation "io.opentelemetry:opentelemetry-sdk-trace:1.38.0" + implementation "io.opentelemetry:opentelemetry-exporter-logging:1.38.0"; + implementation "io.opentelemetry.semconv:opentelemetry-semconv:1.25.0-alpha"; + + implementation 'com.github.ben-manes.caffeine:caffeine:3.1.8' + + implementation 'io.prometheus:prometheus-metrics-exporter-servlet-jakarta:1.3.0' + implementation 'io.micrometer:micrometer-core:1.13.2' + implementation 'io.micrometer:micrometer-registry-prometheus:1.13.2' + + implementation "io.swagger:swagger-annotations:1.6.14" + implementation "io.swagger:swagger-jaxrs:1.6.14" + implementation "javax.annotation:javax.annotation-api:1.3.2" + + implementation "org.apache.hadoop:hadoop-client-api:${hadoopVersion}" + + implementation 'org.xerial:sqlite-jdbc:3.45.1.0' + implementation 'com.auth0:java-jwt:4.2.1' + + implementation "ch.qos.logback:logback-core:1.4.14" + implementation group: 'org.bouncycastle', name: 'bcprov-jdk18on', version: '1.78' + + implementation "com.google.cloud:google-cloud-storage:2.39.0" + implementation "software.amazon.awssdk:sts:2.25.61" + implementation "software.amazon.awssdk:sts:2.25.61" + implementation "software.amazon.awssdk:iam-policy-builder:2.25.61" + implementation "software.amazon.awssdk:s3:2.25.61" + + + testImplementation "org.apache.iceberg:iceberg-api:${icebergVersion}:tests" + testImplementation "org.apache.iceberg:iceberg-core:${icebergVersion}:tests" + testImplementation "io.dropwizard:dropwizard-testing:${dropwizardVersion}" + testImplementation "org.testcontainers:testcontainers:1.19.8" + testImplementation "com.adobe.testing:s3mock-testcontainers:3.9.1" + + testImplementation "org.apache.iceberg:iceberg-spark-3.5_2.12:1.5.0" + testImplementation "org.apache.iceberg:iceberg-spark-extensions-3.5_2.12:1.5.0" + testImplementation("org.apache.spark:spark-sql_2.12:3.5.1") { + // exclude log4j dependencies + exclude group: 'org.apache.logging.log4j', module: 'log4j-slf4j2-impl' + exclude group: 'org.apache.logging.log4j', module: 'log4j-api' + exclude group: 'org.apache.logging.log4j', module: 'log4j-1.2-api' + } + + testImplementation "software.amazon.awssdk:glue:2.25.61" + testImplementation "software.amazon.awssdk:kms:2.25.61" + testImplementation "software.amazon.awssdk:dynamodb:2.25.61" +} + +openApiGenerate { + inputSpec = "$rootDir/spec/rest-catalog-open-api.yaml" + generatorName = "jaxrs-resteasy" + outputDir = "$buildDir/generated" + apiPackage = "io.polaris.service.catalog.api" + ignoreFileOverride = "$rootDir/.openapi-generator-ignore" + removeOperationIdPrefix = true + templateDir = "$rootDir/server-templates" + globalProperties = [ + apis : "", + models : "false", + apiDocs : "false", + modelTests: "false", + ] + configOptions = [ + resourceName : "catalog", + useTags : "true", + useBeanValidation: "false", + sourceFolder : "src/main/java", + useJakartaEe : "true" + ] + openapiNormalizer = ["REFACTOR_ALLOF_WITH_PROPERTIES_ONLY": "true"] + additionalProperties = [apiNamePrefix: "IcebergRest", apiNameSuffix: "", metricsPrefix: "polaris"] + serverVariables = [basePath: "api/catalog"] + importMappings = [ + CatalogConfig : "org.apache.iceberg.rest.responses.ConfigResponse", + CommitTableResponse : "org.apache.iceberg.rest.responses.LoadTableResponse", + CreateNamespaceRequest : "org.apache.iceberg.rest.requests.CreateNamespaceRequest", + CreateNamespaceResponse : "org.apache.iceberg.rest.responses.CreateNamespaceResponse", + CreateTableRequest : "org.apache.iceberg.rest.requests.CreateTableRequest", + ErrorModel : "org.apache.iceberg.rest.responses.ErrorResponse", + GetNamespaceResponse : "org.apache.iceberg.rest.responses.GetNamespaceResponse", + ListNamespacesResponse : "org.apache.iceberg.rest.responses.ListNamespacesResponse", + ListTablesResponse : "org.apache.iceberg.rest.responses.ListTablesResponse", + LoadTableResult : "org.apache.iceberg.rest.responses.LoadTableResponse", + LoadViewResult : "org.apache.iceberg.rest.responses.LoadTableResponse", + OAuthTokenResponse : "org.apache.iceberg.rest.responses.OAuthTokenResponse", + OAuthErrorResponse : "org.apache.iceberg.rest.responses.OAuthErrorResponse", + RenameTableRequest : "org.apache.iceberg.rest.requests.RenameTableRequest", + ReportMetricsRequest : "org.apache.iceberg.rest.requests.ReportMetricsRequest", + UpdateNamespacePropertiesRequest : "org.apache.iceberg.rest.requests.UpdateNamespacePropertiesRequest", + UpdateNamespacePropertiesResponse: "org.apache.iceberg.rest.responses.UpdateNamespacePropertiesResponse", + CommitTransactionRequest : "org.apache.iceberg.rest.requests.CommitTransactionRequest", + CreateViewRequest : "org.apache.iceberg.rest.requests.CreateViewRequest", + RegisterTableRequest : "org.apache.iceberg.rest.requests.RegisterTableRequest", + IcebergErrorResponse : "org.apache.iceberg.rest.responses.ErrorResponse", + OAuthError : "org.apache.iceberg.rest.responses.ErrorResponse", + + // Custom types defined below + CommitViewRequest : "io.polaris.service.types.CommitViewRequest", + TokenType : "io.polaris.service.types.TokenType", + CommitTableRequest : "io.polaris.service.types.CommitTableRequest", + + NotificationRequest : "io.polaris.service.types.NotificationRequest", + TableUpdateNotification : "io.polaris.service.types.TableUpdateNotification", + NotificationType : "io.polaris.service.types.NotificationType" + ] +} + +task generatePolarisService(type: org.openapitools.generator.gradle.plugin.tasks.GenerateTask) { + inputSpec = "$rootDir/spec/polaris-management-service.yml" + generatorName = "jaxrs-resteasy" + outputDir = "$buildDir/generated" + apiPackage = "io.polaris.service.admin.api" + modelPackage = "io.polaris.core.admin.model" + ignoreFileOverride = "$rootDir/.openapi-generator-ignore" + removeOperationIdPrefix = true + templateDir = "$rootDir/server-templates" + globalProperties = [ + apis : "", + models : "false", + apiDocs : "false", + modelTests: "false" + ] + configOptions = [ + useBeanValidation : "true", + sourceFolder : "src/main/java", + useJakartaEe : "true", + generateBuilders : "true", + generateConstructorWithAllArgs: "true", + ] + additionalProperties = [apiNamePrefix: "Polaris", apiNameSuffix: "Api", metricsPrefix: "polaris"] + serverVariables = [basePath: "api/v1"] +} + +compileJava.dependsOn tasks.openApiGenerate, tasks.generatePolarisService +sourceSets.main.java.srcDirs += ["$buildDir/generated/src/main/java"] + +test { + if (System.getenv('AWS_REGION') == null) { + environment 'AWS_REGION', 'us-west-2' + } + jvmArgs '--add-exports', 'java.base/sun.nio.ch=ALL-UNNAMED' + useJUnitPlatform() + maxParallelForks = 4 +} + +task runApp(type: JavaExec) { + if (System.getenv('AWS_REGION') == null) { + environment 'AWS_REGION', 'us-west-2' + } + classpath = sourceSets.main.runtimeClasspath + mainClass = 'io.polaris.service.PolarisApplication' + args 'server', "$rootDir/polaris-server.yml" +} + +application { + mainClass = 'io.polaris.service.PolarisApplication' +} + +jar { + manifest { + attributes 'Main-Class': 'io.polaris.service.PolarisApplication' + } +} + +shadowJar { + mainClassName = 'io.polaris.service.PolarisApplication' + mergeServiceFiles() + zip64 true +} + +build.dependsOn(shadowJar) diff --git a/polaris-service/src/main/java/io/polaris/service/BootstrapRealmsCommand.java b/polaris-service/src/main/java/io/polaris/service/BootstrapRealmsCommand.java new file mode 100644 index 0000000000..e815a08b71 --- /dev/null +++ b/polaris-service/src/main/java/io/polaris/service/BootstrapRealmsCommand.java @@ -0,0 +1,45 @@ +package io.polaris.service; + +import io.dropwizard.core.cli.ConfiguredCommand; +import io.dropwizard.core.setup.Bootstrap; +import io.polaris.core.PolarisConfigurationStore; +import io.polaris.core.persistence.MetaStoreManagerFactory; +import io.polaris.service.config.ConfigurationStoreAware; +import io.polaris.service.config.PolarisApplicationConfig; +import io.polaris.service.config.RealmEntityManagerFactory; +import io.polaris.service.context.CallContextResolver; +import net.sourceforge.argparse4j.inf.Namespace; + +/** + * Command for bootstrapping root level service principals for each realm. This command will invoke + * a default implementation which generates random user id and secret. These credentials will be + * printed out to the log and standard output (stdout). + */ +public class BootstrapRealmsCommand extends ConfiguredCommand { + public BootstrapRealmsCommand() { + super("bootstrap", "bootstraps principal credentials for all realms and prints them to log"); + } + + @Override + protected void run( + Bootstrap bootstrap, + Namespace namespace, + PolarisApplicationConfig configuration) + throws Exception { + MetaStoreManagerFactory metaStoreManagerFactory = configuration.getMetaStoreManagerFactory(); + + PolarisConfigurationStore configurationStore = configuration.getConfigurationStore(); + if (metaStoreManagerFactory instanceof ConfigurationStoreAware) { + ((ConfigurationStoreAware) metaStoreManagerFactory).setConfigurationStore(configurationStore); + } + RealmEntityManagerFactory entityManagerFactory = + new RealmEntityManagerFactory(metaStoreManagerFactory); + CallContextResolver callContextResolver = configuration.getCallContextResolver(); + callContextResolver.setEntityManagerFactory(entityManagerFactory); + if (callContextResolver instanceof ConfigurationStoreAware csa) { + csa.setConfigurationStore(configurationStore); + } + + metaStoreManagerFactory.bootstrapRealms(configuration.getDefaultRealms()); + } +} diff --git a/polaris-service/src/main/java/io/polaris/service/IcebergExceptionMapper.java b/polaris-service/src/main/java/io/polaris/service/IcebergExceptionMapper.java new file mode 100644 index 0000000000..64bcdcb93d --- /dev/null +++ b/polaris-service/src/main/java/io/polaris/service/IcebergExceptionMapper.java @@ -0,0 +1,85 @@ +package io.polaris.service; + +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.ExceptionMapper; +import org.apache.iceberg.exceptions.AlreadyExistsException; +import org.apache.iceberg.exceptions.CherrypickAncestorCommitException; +import org.apache.iceberg.exceptions.CleanableFailure; +import org.apache.iceberg.exceptions.CommitFailedException; +import org.apache.iceberg.exceptions.CommitStateUnknownException; +import org.apache.iceberg.exceptions.DuplicateWAPCommitException; +import org.apache.iceberg.exceptions.ForbiddenException; +import org.apache.iceberg.exceptions.NamespaceNotEmptyException; +import org.apache.iceberg.exceptions.NoSuchIcebergTableException; +import org.apache.iceberg.exceptions.NoSuchNamespaceException; +import org.apache.iceberg.exceptions.NoSuchTableException; +import org.apache.iceberg.exceptions.NoSuchViewException; +import org.apache.iceberg.exceptions.NotAuthorizedException; +import org.apache.iceberg.exceptions.NotFoundException; +import org.apache.iceberg.exceptions.RESTException; +import org.apache.iceberg.exceptions.RuntimeIOException; +import org.apache.iceberg.exceptions.ServiceFailureException; +import org.apache.iceberg.exceptions.ServiceUnavailableException; +import org.apache.iceberg.exceptions.UnprocessableEntityException; +import org.apache.iceberg.exceptions.ValidationException; +import org.apache.iceberg.rest.responses.ErrorResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class IcebergExceptionMapper implements ExceptionMapper { + private static final Logger LOG = LoggerFactory.getLogger(IcebergExceptionMapper.class); + + public IcebergExceptionMapper() {} + + @Override + public Response toResponse(RuntimeException runtimeException) { + LOG.info("Handling runtimeException {}", runtimeException.getMessage()); + int responseCode = + switch (runtimeException) { + case NoSuchNamespaceException e -> Response.Status.NOT_FOUND.getStatusCode(); + case NoSuchIcebergTableException e -> Response.Status.NOT_FOUND.getStatusCode(); + case NoSuchTableException e -> Response.Status.NOT_FOUND.getStatusCode(); + case NoSuchViewException e -> Response.Status.NOT_FOUND.getStatusCode(); + case NotFoundException e -> Response.Status.NOT_FOUND.getStatusCode(); + case AlreadyExistsException e -> Response.Status.CONFLICT.getStatusCode(); + case CommitFailedException e -> Response.Status.CONFLICT.getStatusCode(); + case UnprocessableEntityException e -> 422; + case CherrypickAncestorCommitException e -> Response.Status.BAD_REQUEST.getStatusCode(); + case CommitStateUnknownException e -> Response.Status.BAD_REQUEST.getStatusCode(); + case DuplicateWAPCommitException e -> Response.Status.BAD_REQUEST.getStatusCode(); + case ForbiddenException e -> Response.Status.FORBIDDEN.getStatusCode(); + case jakarta.ws.rs.ForbiddenException e -> Response.Status.FORBIDDEN.getStatusCode(); + case NotAuthorizedException e -> Response.Status.UNAUTHORIZED.getStatusCode(); + case NamespaceNotEmptyException e -> Response.Status.BAD_REQUEST.getStatusCode(); + case ValidationException e -> Response.Status.BAD_REQUEST.getStatusCode(); + case ServiceUnavailableException e -> Response.Status.SERVICE_UNAVAILABLE.getStatusCode(); + case RuntimeIOException e -> Response.Status.SERVICE_UNAVAILABLE.getStatusCode(); + case ServiceFailureException e -> Response.Status.SERVICE_UNAVAILABLE.getStatusCode(); + case CleanableFailure e -> Response.Status.BAD_REQUEST.getStatusCode(); + case RESTException e -> Response.Status.SERVICE_UNAVAILABLE.getStatusCode(); + case IllegalArgumentException e -> Response.Status.BAD_REQUEST.getStatusCode(); + case UnsupportedOperationException e -> Response.Status.NOT_ACCEPTABLE.getStatusCode(); + case WebApplicationException e -> e.getResponse().getStatus(); + default -> Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(); + }; + if (responseCode == Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()) { + LOG.error("Unhandled exception returning INTERNAL_SERVER_ERROR", runtimeException); + } + + ErrorResponse icebergErrorResponse = + ErrorResponse.builder() + .responseCode(responseCode) + .withType(runtimeException.getClass().getSimpleName()) + .withMessage(runtimeException.getMessage()) + .build(); + Response errorResp = + Response.status(responseCode) + .entity(icebergErrorResponse) + .type(MediaType.APPLICATION_JSON_TYPE) + .build(); + LOG.debug("Mapped exception to errorResp: {}", errorResp); + return errorResp; + } +} diff --git a/polaris-service/src/main/java/io/polaris/service/IcebergJerseyViolationExceptionMapper.java b/polaris-service/src/main/java/io/polaris/service/IcebergJerseyViolationExceptionMapper.java new file mode 100644 index 0000000000..166be1b278 --- /dev/null +++ b/polaris-service/src/main/java/io/polaris/service/IcebergJerseyViolationExceptionMapper.java @@ -0,0 +1,31 @@ +package io.polaris.service; + +import io.dropwizard.jersey.validation.JerseyViolationException; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.ExceptionMapper; +import jakarta.ws.rs.ext.Provider; +import org.apache.iceberg.rest.responses.ErrorResponse; + +/** + * Override of the default JerseyViolationExceptionMapper to provide an Iceberg ErrorResponse with + * the exception details. + */ +@Provider +public class IcebergJerseyViolationExceptionMapper + implements ExceptionMapper { + @Override + public Response toResponse(JerseyViolationException exception) { + final String message = "Invalid value: " + exception.getMessage(); + ErrorResponse icebergErrorResponse = + ErrorResponse.builder() + .responseCode(Response.Status.BAD_REQUEST.getStatusCode()) + .withType(exception.getClass().getSimpleName()) + .withMessage(message) + .build(); + return Response.status(Response.Status.BAD_REQUEST) + .type(MediaType.APPLICATION_JSON_TYPE) + .entity(icebergErrorResponse) + .build(); + } +} diff --git a/polaris-service/src/main/java/io/polaris/service/IcebergJsonProcessingExceptionMapper.java b/polaris-service/src/main/java/io/polaris/service/IcebergJsonProcessingExceptionMapper.java new file mode 100644 index 0000000000..b6cc1739ad --- /dev/null +++ b/polaris-service/src/main/java/io/polaris/service/IcebergJsonProcessingExceptionMapper.java @@ -0,0 +1,55 @@ +package io.polaris.service; + +import com.fasterxml.jackson.core.JsonGenerationException; +import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.exc.InvalidDefinitionException; +import com.fasterxml.jackson.databind.exc.ValueInstantiationException; +import io.dropwizard.jersey.errors.LoggingExceptionMapper; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.Provider; +import org.apache.iceberg.rest.responses.ErrorResponse; + +/** + * Override of the default JsonProcessingExceptionMapper to provide an Iceberg ErrorResponse with + * the exception details. This code mostly comes from Dropwizard's {@link + * io.dropwizard.jersey.jackson.JsonProcessingExceptionMapper} + */ +@Provider +public final class IcebergJsonProcessingExceptionMapper + extends LoggingExceptionMapper { + @Override + public Response toResponse(JsonProcessingException exception) { + /* + * If the error is in the JSON generation or an invalid definition, it's a server error. + */ + if (exception instanceof JsonGenerationException + || exception instanceof InvalidDefinitionException) { + return super.toResponse(exception); // LoggingExceptionMapper will log exception + } + + /* + * Otherwise, it's those pesky users. + */ + logger.info("Unable to process JSON: {}", exception.getMessage()); + + String messagePrefix = + switch (exception) { + case JsonParseException e -> "Invalid JSON: "; + case ValueInstantiationException ve -> "Invalid value: "; + default -> ""; + }; + final String message = messagePrefix + exception.getOriginalMessage(); + ErrorResponse icebergErrorResponse = + ErrorResponse.builder() + .responseCode(Response.Status.BAD_REQUEST.getStatusCode()) + .withType(exception.getClass().getSimpleName()) + .withMessage(message) + .build(); + return Response.status(Response.Status.BAD_REQUEST) + .type(MediaType.APPLICATION_JSON_TYPE) + .entity(icebergErrorResponse) + .build(); + } +} diff --git a/polaris-service/src/main/java/io/polaris/service/PolarisApplication.java b/polaris-service/src/main/java/io/polaris/service/PolarisApplication.java new file mode 100644 index 0000000000..adb1c8cb74 --- /dev/null +++ b/polaris-service/src/main/java/io/polaris/service/PolarisApplication.java @@ -0,0 +1,322 @@ +package io.polaris.service; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import io.dropwizard.auth.AuthDynamicFeature; +import io.dropwizard.auth.AuthFilter; +import io.dropwizard.auth.oauth.OAuthCredentialAuthFilter; +import io.dropwizard.configuration.EnvironmentVariableSubstitutor; +import io.dropwizard.configuration.SubstitutingSourceProvider; +import io.dropwizard.core.Application; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; +import io.micrometer.prometheusmetrics.PrometheusConfig; +import io.micrometer.prometheusmetrics.PrometheusMeterRegistry; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.baggage.propagation.W3CBaggagePropagator; +import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; +import io.opentelemetry.context.propagation.ContextPropagators; +import io.opentelemetry.context.propagation.TextMapPropagator; +import io.opentelemetry.exporter.logging.LoggingSpanExporter; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; +import io.opentelemetry.semconv.ServiceAttributes; +import io.polaris.core.PolarisConfigurationStore; +import io.polaris.core.auth.AuthenticatedPolarisPrincipal; +import io.polaris.core.auth.PolarisAuthorizer; +import io.polaris.core.context.CallContext; +import io.polaris.core.context.RealmContext; +import io.polaris.core.persistence.MetaStoreManagerFactory; +import io.polaris.service.admin.PolarisServiceImpl; +import io.polaris.service.admin.api.PolarisCatalogsApi; +import io.polaris.service.admin.api.PolarisPrincipalRolesApi; +import io.polaris.service.admin.api.PolarisPrincipalsApi; +import io.polaris.service.auth.DiscoverableAuthenticator; +import io.polaris.service.catalog.IcebergCatalogAdapter; +import io.polaris.service.catalog.api.IcebergRestCatalogApi; +import io.polaris.service.catalog.api.IcebergRestConfigurationApi; +import io.polaris.service.catalog.api.IcebergRestOAuth2Api; +import io.polaris.service.config.ConfigurationStoreAware; +import io.polaris.service.config.HasEntityManagerFactory; +import io.polaris.service.config.OAuth2ApiService; +import io.polaris.service.config.PolarisApplicationConfig; +import io.polaris.service.config.RealmEntityManagerFactory; +import io.polaris.service.config.Serializers; +import io.polaris.service.config.TaskHandlerConfiguration; +import io.polaris.service.context.CallContextCatalogFactory; +import io.polaris.service.context.CallContextResolver; +import io.polaris.service.context.PolarisCallContextCatalogFactory; +import io.polaris.service.context.RealmContextResolver; +import io.polaris.service.context.SqlliteCallContextCatalogFactory; +import io.polaris.service.persistence.InMemoryPolarisMetaStoreManagerFactory; +import io.polaris.service.storage.PolarisStorageIntegrationProviderImpl; +import io.polaris.service.task.ManifestFileCleanupTaskHandler; +import io.polaris.service.task.TableCleanupTaskHandler; +import io.polaris.service.task.TaskExecutorImpl; +import io.polaris.service.task.TaskFileIOSupplier; +import io.polaris.service.tracing.OpenTelemetryAware; +import io.polaris.service.tracing.TracingFilter; +import io.prometheus.metrics.exporter.servlet.jakarta.PrometheusMetricsServlet; +import jakarta.servlet.DispatcherType; +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterRegistration; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import java.io.Closeable; +import java.io.IOException; +import java.util.Collections; +import java.util.EnumSet; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.Executors; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.apache.iceberg.rest.RESTSerializers; +import org.eclipse.jetty.servlets.CrossOriginFilter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.MDC; + +public class PolarisApplication extends Application { + private static final Logger LOGGER = LoggerFactory.getLogger(PolarisApplication.class); + + public static void main(final String[] args) throws Exception { + new PolarisApplication().run(args); + } + + @Override + public void initialize(Bootstrap bootstrap) { + // Enable variable substitution with environment variables + EnvironmentVariableSubstitutor substitutor = new EnvironmentVariableSubstitutor(false); + SubstitutingSourceProvider provider = + new SubstitutingSourceProvider(bootstrap.getConfigurationSourceProvider(), substitutor); + bootstrap.setConfigurationSourceProvider(provider); + + bootstrap.addCommand(new BootstrapRealmsCommand()); + } + + @Override + public void run(PolarisApplicationConfig configuration, Environment environment) { + // PolarisEntityManager will be used for Management APIs and optionally the core Catalog APIs + // depending on the value of the baseCatalogType config. + MetaStoreManagerFactory metaStoreManagerFactory = configuration.getMetaStoreManagerFactory(); + metaStoreManagerFactory.setStorageIntegrationProvider( + new PolarisStorageIntegrationProviderImpl()); + + PrometheusMeterRegistry meterRegistry = new PrometheusMeterRegistry(PrometheusConfig.DEFAULT); + metaStoreManagerFactory.setMetricRegistry(meterRegistry); + + OpenTelemetry openTelemetry = setupTracing(); + if (metaStoreManagerFactory instanceof OpenTelemetryAware otAware) { + otAware.setOpenTelemetry(openTelemetry); + } + PolarisConfigurationStore configurationStore = configuration.getConfigurationStore(); + if (metaStoreManagerFactory instanceof ConfigurationStoreAware) { + ((ConfigurationStoreAware) metaStoreManagerFactory).setConfigurationStore(configurationStore); + } + RealmEntityManagerFactory entityManagerFactory = + new RealmEntityManagerFactory(metaStoreManagerFactory); + CallContextResolver callContextResolver = configuration.getCallContextResolver(); + callContextResolver.setEntityManagerFactory(entityManagerFactory); + if (callContextResolver instanceof ConfigurationStoreAware csa) { + csa.setConfigurationStore(configurationStore); + } + + RealmContextResolver realmContextResolver = configuration.getRealmContextResolver(); + realmContextResolver.setEntityManagerFactory(entityManagerFactory); + environment + .servlets() + .addFilter( + "realmContext", new ContextResolverFilter(realmContextResolver, callContextResolver)) + .addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), true, "/*"); + + TaskHandlerConfiguration taskConfig = configuration.getTaskHandler(); + TaskExecutorImpl taskExecutor = + new TaskExecutorImpl(taskConfig.executorService(), metaStoreManagerFactory); + TaskFileIOSupplier fileIOSupplier = new TaskFileIOSupplier(metaStoreManagerFactory); + taskExecutor.addTaskHandler( + new TableCleanupTaskHandler(taskExecutor, metaStoreManagerFactory, fileIOSupplier)); + taskExecutor.addTaskHandler( + new ManifestFileCleanupTaskHandler( + fileIOSupplier, Executors.newVirtualThreadPerTaskExecutor())); + + CallContextCatalogFactory catalogFactory; + if ("polaris".equals(configuration.getBaseCatalogType())) { + LOGGER.info( + "Initializing PolarisCallContextCatalogFactory for baseCatalogType {}, metaStoreManagerType {}", + configuration.getBaseCatalogType(), + metaStoreManagerFactory); + catalogFactory = new PolarisCallContextCatalogFactory(entityManagerFactory, taskExecutor); + } else if ("jdbc".equals(configuration.getBaseCatalogType())) { + LOGGER.info( + "Initializing SqlliteCallContextCatalogFactory for baseCatalogType {}", + configuration.getBaseCatalogType()); + catalogFactory = new SqlliteCallContextCatalogFactory(configuration.getSqlLiteCatalogDirs()); + } else { + LOGGER.error("Unrecognized baseCatalogType: {}", configuration.getBaseCatalogType()); + throw new RuntimeException("Invalid baseCatalogType: " + configuration.getBaseCatalogType()); + } + + PolarisAuthorizer authorizer = new PolarisAuthorizer(configurationStore); + IcebergCatalogAdapter catalogAdapter = + new IcebergCatalogAdapter(catalogFactory, entityManagerFactory, authorizer); + environment.jersey().register(new IcebergRestCatalogApi(catalogAdapter)); + environment.jersey().register(new IcebergRestConfigurationApi(catalogAdapter)); + + FilterRegistration.Dynamic corsRegistration = + environment.servlets().addFilter("CORS", CrossOriginFilter.class); + corsRegistration.setInitParameter( + CrossOriginFilter.ALLOWED_ORIGINS_PARAM, + String.join(",", configuration.getCorsConfiguration().getAllowedOrigins())); + corsRegistration.setInitParameter( + CrossOriginFilter.ALLOWED_TIMING_ORIGINS_PARAM, + String.join(",", configuration.getCorsConfiguration().getAllowedTimingOrigins())); + corsRegistration.setInitParameter( + CrossOriginFilter.ALLOWED_METHODS_PARAM, + String.join(",", configuration.getCorsConfiguration().getAllowedMethods())); + corsRegistration.setInitParameter( + CrossOriginFilter.ALLOWED_HEADERS_PARAM, + String.join(",", configuration.getCorsConfiguration().getAllowedHeaders())); + corsRegistration.setInitParameter( + CrossOriginFilter.ALLOW_CREDENTIALS_PARAM, + String.join(",", configuration.getCorsConfiguration().getAllowCredentials())); + corsRegistration.setInitParameter( + CrossOriginFilter.PREFLIGHT_MAX_AGE_PARAM, + Objects.toString(configuration.getCorsConfiguration().getPreflightMaxAge())); + corsRegistration.setInitParameter( + CrossOriginFilter.ALLOW_CREDENTIALS_PARAM, + configuration.getCorsConfiguration().getAllowCredentials()); + corsRegistration.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), true, "/*"); + environment + .servlets() + .addFilter("tracing", new TracingFilter(openTelemetry)) + .addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), true, "/*"); + DiscoverableAuthenticator authenticator = + configuration.getPolarisAuthenticator(); + authenticator.setEntityManagerFactory(entityManagerFactory); + AuthFilter oauthCredentialAuthFilter = + new OAuthCredentialAuthFilter.Builder() + .setAuthenticator(authenticator) + .setPrefix("Bearer") + .buildAuthFilter(); + environment.jersey().register(new AuthDynamicFeature(oauthCredentialAuthFilter)); + environment.healthChecks().register("polaris", new PolarisHealthCheck()); + OAuth2ApiService oauth2Service = configuration.getOauth2Service(); + if (oauth2Service instanceof HasEntityManagerFactory emfAware) { + emfAware.setEntityManagerFactory(entityManagerFactory); + } + environment.jersey().register(new IcebergRestOAuth2Api(oauth2Service)); + environment.jersey().register(new IcebergExceptionMapper()); + PolarisServiceImpl polarisService = new PolarisServiceImpl(entityManagerFactory, authorizer); + environment.jersey().register(new PolarisCatalogsApi(polarisService)); + environment.jersey().register(new PolarisPrincipalsApi(polarisService)); + environment.jersey().register(new PolarisPrincipalRolesApi(polarisService)); + ObjectMapper objectMapper = environment.getObjectMapper(); + objectMapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY); + objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + objectMapper.setPropertyNamingStrategy(new PropertyNamingStrategies.KebabCaseStrategy()); + RESTSerializers.registerAll(objectMapper); + Serializers.registerSerializers(objectMapper); + environment.jersey().register(new IcebergJsonProcessingExceptionMapper()); + environment.jersey().register(new IcebergJerseyViolationExceptionMapper()); + environment.jersey().register(new TimedApplicationEventListener(meterRegistry)); + + environment + .admin() + .addServlet("metrics", new PrometheusMetricsServlet(meterRegistry.getPrometheusRegistry())) + .addMapping("/metrics"); + + // For in-memory metastore we need to bootstrap Service and Service principal at startup (for + // default realm) + // We can not utilize dropwizard Bootstrap command as command and server will be running two + // different processes + // and in-memory state will be lost b/w invocation of bootstrap command and running a server + if (metaStoreManagerFactory instanceof InMemoryPolarisMetaStoreManagerFactory) { + metaStoreManagerFactory.getOrCreateMetaStoreManager(configuration::getDefaultRealm); + } + } + + private static OpenTelemetry setupTracing() { + Resource resource = + Resource.getDefault().toBuilder() + .put(ServiceAttributes.SERVICE_NAME, "polaris") + .put(ServiceAttributes.SERVICE_VERSION, "0.1.0") + .build(); + SdkTracerProvider sdkTracerProvider = + SdkTracerProvider.builder() + .addSpanProcessor(SimpleSpanProcessor.create(LoggingSpanExporter.create())) + .setResource(resource) + .build(); + return OpenTelemetrySdk.builder() + .setTracerProvider(sdkTracerProvider) + .setPropagators( + ContextPropagators.create( + TextMapPropagator.composite( + W3CTraceContextPropagator.getInstance(), W3CBaggagePropagator.getInstance()))) + .build(); + } + + /** Resolves and sets ThreadLocal CallContext/RealmContext based on the request contents. */ + private static class ContextResolverFilter implements Filter { + private final RealmContextResolver realmContextResolver; + private final CallContextResolver callContextResolver; + + public ContextResolverFilter( + RealmContextResolver realmContextResolver, CallContextResolver callContextResolver) { + this.realmContextResolver = realmContextResolver; + this.callContextResolver = callContextResolver; + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + HttpServletRequest httpRequest = (HttpServletRequest) request; + Stream headerNames = Collections.list(httpRequest.getHeaderNames()).stream(); + Map headers = + headerNames.collect(Collectors.toMap(Function.identity(), httpRequest::getHeader)); + RealmContext currentRealmContext = + realmContextResolver.resolveRealmContext( + httpRequest.getRequestURL().toString(), + httpRequest.getMethod(), + httpRequest.getRequestURI().substring(1), + request.getParameterMap().entrySet().stream() + .collect( + Collectors.toMap(Map.Entry::getKey, (e) -> ((String[]) e.getValue())[0])), + headers); + CallContext currentCallContext = + callContextResolver.resolveCallContext( + currentRealmContext, + httpRequest.getMethod(), + httpRequest.getRequestURI().substring(1), + request.getParameterMap().entrySet().stream() + .collect( + Collectors.toMap(Map.Entry::getKey, (e) -> ((String[]) e.getValue())[0])), + headers); + CallContext.setCurrentContext(currentCallContext); + try (MDC.MDCCloseable context = + MDC.putCloseable("realm", currentRealmContext.getRealmIdentifier()); + MDC.MDCCloseable requestId = + MDC.putCloseable("request_id", httpRequest.getHeader("request_id"))) { + chain.doFilter(request, response); + } finally { + Object contextCatalog = + currentCallContext + .contextVariables() + .get(CallContext.REQUEST_PATH_CATALOG_INSTANCE_KEY); + if (contextCatalog != null && contextCatalog instanceof Closeable) { + ((Closeable) contextCatalog).close(); + } + currentCallContext.close(); + } + } + } +} diff --git a/polaris-service/src/main/java/io/polaris/service/PolarisHealthCheck.java b/polaris-service/src/main/java/io/polaris/service/PolarisHealthCheck.java new file mode 100644 index 0000000000..ecca6887a1 --- /dev/null +++ b/polaris-service/src/main/java/io/polaris/service/PolarisHealthCheck.java @@ -0,0 +1,11 @@ +package io.polaris.service; + +import com.codahale.metrics.health.HealthCheck; + +/** Default {@link HealthCheck} implementation. */ +public class PolarisHealthCheck extends HealthCheck { + @Override + protected Result check() throws Exception { + return Result.healthy(); + } +} diff --git a/polaris-service/src/main/java/io/polaris/service/TimedApplicationEventListener.java b/polaris-service/src/main/java/io/polaris/service/TimedApplicationEventListener.java new file mode 100644 index 0000000000..59d9fa54d0 --- /dev/null +++ b/polaris-service/src/main/java/io/polaris/service/TimedApplicationEventListener.java @@ -0,0 +1,72 @@ +package io.polaris.service; + +import com.google.common.base.Stopwatch; +import io.micrometer.core.instrument.MeterRegistry; +import io.polaris.core.context.CallContext; +import io.polaris.core.monitor.PolarisMetricRegistry; +import io.polaris.service.resource.TimedApi; +import java.lang.reflect.Method; +import java.util.concurrent.TimeUnit; +import javax.ws.rs.ext.Provider; +import org.glassfish.jersey.server.monitoring.ApplicationEvent; +import org.glassfish.jersey.server.monitoring.ApplicationEventListener; +import org.glassfish.jersey.server.monitoring.RequestEvent; +import org.glassfish.jersey.server.monitoring.RequestEventListener; + +/** + * An ApplicationEventListener that supports timing of resource method execution and error counting. + * It uses Micrometer for metrics collection and provides detailed metrics tagged with realm + * identifiers and distinguishes between successful executions and errors. + */ +@Provider +public class TimedApplicationEventListener implements ApplicationEventListener { + + // The PolarisMetricRegistry instance used for recording metrics and error counters. + private final PolarisMetricRegistry polarisMetricRegistry; + + public TimedApplicationEventListener(MeterRegistry meterRegistry) { + this.polarisMetricRegistry = new PolarisMetricRegistry(meterRegistry); + } + + @Override + public void onEvent(ApplicationEvent event) {} + + @Override + public RequestEventListener onRequest(RequestEvent event) { + return new TimedRequestEventListener(); + } + + /** + * A RequestEventListener implementation that handles timing of resource method execution and + * increments error counters on failures. The lifetime of the listener is tied to a single HTTP + * request. + */ + private class TimedRequestEventListener implements RequestEventListener { + private String metric; + private Stopwatch sw; + + /** Handles various types of RequestEvents to start timing, stop timing, and record metrics. */ + @Override + public void onEvent(RequestEvent event) { + if (event.getType() == RequestEvent.Type.RESOURCE_METHOD_START) { + Method method = + event.getUriInfo().getMatchedResourceMethod().getInvocable().getHandlingMethod(); + if (method.isAnnotationPresent(TimedApi.class)) { + TimedApi timedApi = method.getAnnotation(TimedApi.class); + metric = timedApi.value(); + sw = Stopwatch.createStarted(); + } + + } else if (event.getType() == RequestEvent.Type.FINISHED && metric != null) { + String realmId = CallContext.getCurrentContext().getRealmContext().getRealmIdentifier(); + if (event.isSuccess()) { + sw.stop(); + polarisMetricRegistry.recordTimer(metric, sw.elapsed(TimeUnit.MILLISECONDS), realmId); + } else { + int statusCode = event.getContainerResponse().getStatus(); + polarisMetricRegistry.incrementErrorCounter(metric, statusCode, realmId); + } + } + } + } +} diff --git a/polaris-service/src/main/java/io/polaris/service/admin/PolarisAdminService.java b/polaris-service/src/main/java/io/polaris/service/admin/PolarisAdminService.java new file mode 100644 index 0000000000..a2d78ce1c6 --- /dev/null +++ b/polaris-service/src/main/java/io/polaris/service/admin/PolarisAdminService.java @@ -0,0 +1,1686 @@ +package io.polaris.service.admin; + +import io.polaris.core.PolarisCallContext; +import io.polaris.core.admin.model.CatalogGrant; +import io.polaris.core.admin.model.CatalogPrivilege; +import io.polaris.core.admin.model.GrantResource; +import io.polaris.core.admin.model.NamespaceGrant; +import io.polaris.core.admin.model.NamespacePrivilege; +import io.polaris.core.admin.model.PrincipalWithCredentials; +import io.polaris.core.admin.model.PrincipalWithCredentialsCredentials; +import io.polaris.core.admin.model.TableGrant; +import io.polaris.core.admin.model.TablePrivilege; +import io.polaris.core.admin.model.UpdateCatalogRequest; +import io.polaris.core.admin.model.UpdateCatalogRoleRequest; +import io.polaris.core.admin.model.UpdatePrincipalRequest; +import io.polaris.core.admin.model.UpdatePrincipalRoleRequest; +import io.polaris.core.admin.model.ViewGrant; +import io.polaris.core.admin.model.ViewPrivilege; +import io.polaris.core.auth.AuthenticatedPolarisPrincipal; +import io.polaris.core.auth.PolarisAuthorizableOperation; +import io.polaris.core.auth.PolarisAuthorizer; +import io.polaris.core.catalog.PolarisCatalogHelpers; +import io.polaris.core.context.CallContext; +import io.polaris.core.entity.CatalogEntity; +import io.polaris.core.entity.CatalogRoleEntity; +import io.polaris.core.entity.NamespaceEntity; +import io.polaris.core.entity.PolarisBaseEntity; +import io.polaris.core.entity.PolarisEntity; +import io.polaris.core.entity.PolarisEntitySubType; +import io.polaris.core.entity.PolarisEntityType; +import io.polaris.core.entity.PolarisGrantRecord; +import io.polaris.core.entity.PolarisPrincipalSecrets; +import io.polaris.core.entity.PolarisPrivilege; +import io.polaris.core.entity.PrincipalEntity; +import io.polaris.core.entity.PrincipalRoleEntity; +import io.polaris.core.entity.TableLikeEntity; +import io.polaris.core.persistence.PolarisEntityManager; +import io.polaris.core.persistence.PolarisMetaStoreManager; +import io.polaris.core.persistence.PolarisResolvedPathWrapper; +import io.polaris.core.persistence.resolver.PolarisResolutionManifest; +import io.polaris.core.persistence.resolver.ResolverPath; +import io.polaris.core.persistence.resolver.ResolverStatus; +import io.polaris.core.storage.PolarisStorageConfigurationInfo; +import io.polaris.core.storage.aws.AwsStorageConfigurationInfo; +import io.polaris.core.storage.azure.AzureStorageConfigurationInfo; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; +import org.apache.iceberg.catalog.Namespace; +import org.apache.iceberg.catalog.TableIdentifier; +import org.apache.iceberg.exceptions.AlreadyExistsException; +import org.apache.iceberg.exceptions.BadRequestException; +import org.apache.iceberg.exceptions.CommitFailedException; +import org.apache.iceberg.exceptions.NoSuchNamespaceException; +import org.apache.iceberg.exceptions.NoSuchTableException; +import org.apache.iceberg.exceptions.NoSuchViewException; +import org.apache.iceberg.exceptions.NotFoundException; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Just as an Iceberg Catalog represents the logical model of Iceberg business logic to manage + * Namespaces, Tables and Views, abstracted away from Iceberg REST objects, this class represents + * the logical model for managing realm-level Catalogs, Principals, Roles, and Grants. + * + *

Different API implementors could expose different REST, gRPC, etc., interfaces that delegate + * to this logical model without being tightly coupled to a single frontend protocol, and can + * provide different implementations of PolarisEntityManager to abstract away the implementation of + * the persistence layer. + */ +public class PolarisAdminService { + private static final Logger LOG = LoggerFactory.getLogger(PolarisAdminService.class); + public static final String CLEANUP_ON_CATALOG_DROP = "CLEANUP_ON_CATALOG_DROP"; + + private final CallContext callContext; + private PolarisEntityManager entityManager; + private final AuthenticatedPolarisPrincipal authenticatedPrincipal; + private final PolarisAuthorizer authorizer; + + // Initialized in the authorize methods. + private PolarisResolutionManifest resolutionManifest = null; + + public PolarisAdminService( + CallContext callContext, + PolarisEntityManager entityManager, + AuthenticatedPolarisPrincipal authenticatedPrincipal, + PolarisAuthorizer authorizer) { + this.callContext = callContext; + this.entityManager = entityManager; + this.authenticatedPrincipal = authenticatedPrincipal; + this.authorizer = authorizer; + } + + private PolarisCallContext getCurrentPolarisContext() { + return callContext.getPolarisCallContext(); + } + + private Optional findCatalogByName(String name) { + return Optional.ofNullable(resolutionManifest.getResolvedReferenceCatalogEntity()) + .map(path -> CatalogEntity.of(path.getRawLeafEntity())); + } + + private Optional findPrincipalByName(String name) { + return Optional.ofNullable( + resolutionManifest.getResolvedTopLevelEntity(name, PolarisEntityType.PRINCIPAL)) + .map(path -> PrincipalEntity.of(path.getRawLeafEntity())); + } + + private Optional findPrincipalRoleByName(String name) { + return Optional.ofNullable( + resolutionManifest.getResolvedTopLevelEntity(name, PolarisEntityType.PRINCIPAL_ROLE)) + .map(path -> PrincipalRoleEntity.of(path.getRawLeafEntity())); + } + + private Optional findCatalogRoleByName(String catalogName, String name) { + return Optional.ofNullable(resolutionManifest.getResolvedPath(name)) + .map(path -> CatalogRoleEntity.of(path.getRawLeafEntity())); + } + + private void authorizeBasicRootOperationOrThrow(PolarisAuthorizableOperation op) { + resolutionManifest = + entityManager.prepareResolutionManifest( + callContext, authenticatedPrincipal, null /* referenceCatalogName */); + resolutionManifest.resolveAll(); + PolarisResolvedPathWrapper rootContainerWrapper = + resolutionManifest.getResolvedRootContainerEntityAsPath(); + authorizer.authorizeOrThrow( + authenticatedPrincipal, + resolutionManifest.getAllActivatedPrincipalRoleIds(), + op, + rootContainerWrapper, + null /* secondary */); + } + + private void authorizeBasicTopLevelEntityOperationOrThrow( + PolarisAuthorizableOperation op, String topLevelEntityName, PolarisEntityType entityType) { + String referenceCatalogName = + entityType == PolarisEntityType.CATALOG ? topLevelEntityName : null; + authorizeBasicTopLevelEntityOperationOrThrow( + op, topLevelEntityName, entityType, referenceCatalogName); + } + + private void authorizeBasicTopLevelEntityOperationOrThrow( + PolarisAuthorizableOperation op, + String topLevelEntityName, + PolarisEntityType entityType, + @Nullable String referenceCatalogName) { + resolutionManifest = + entityManager.prepareResolutionManifest( + callContext, authenticatedPrincipal, referenceCatalogName); + resolutionManifest.addTopLevelName(topLevelEntityName, entityType, false /* isOptional */); + ResolverStatus status = resolutionManifest.resolveAll(); + if (status.getStatus() == ResolverStatus.StatusEnum.ENTITY_COULD_NOT_BE_RESOLVED) { + throw new NotFoundException( + "TopLevelEntity of type %s does not exist: %s", entityType, topLevelEntityName); + } + PolarisResolvedPathWrapper topLevelEntityWrapper = + resolutionManifest.getResolvedTopLevelEntity(topLevelEntityName, entityType); + + // TODO: If we do add more "self" privilege operations for PRINCIPAL targets this should + // be extracted into an EnumSet and/or pushed down into PolarisAuthorizer. + if (topLevelEntityWrapper.getResolvedLeafEntity().getEntity().getId() + == authenticatedPrincipal.getPrincipalEntity().getId() + && (op.equals(PolarisAuthorizableOperation.ROTATE_CREDENTIALS) + || op.equals(PolarisAuthorizableOperation.RESET_CREDENTIALS))) { + LOG.atDebug() + .addKeyValue("principalName", topLevelEntityName) + .log("Allowing rotate own credentials"); + return; + } + authorizer.authorizeOrThrow( + authenticatedPrincipal, + resolutionManifest.getAllActivatedCatalogRoleAndPrincipalRoleIds(), + op, + topLevelEntityWrapper, + null /* secondary */); + } + + private void authorizeBasicCatalogRoleOperationOrThrow( + PolarisAuthorizableOperation op, String catalogName, String catalogRoleName) { + resolutionManifest = + entityManager.prepareResolutionManifest(callContext, authenticatedPrincipal, catalogName); + resolutionManifest.addPath( + new ResolverPath(List.of(catalogRoleName), PolarisEntityType.CATALOG_ROLE), + catalogRoleName); + resolutionManifest.resolveAll(); + PolarisResolvedPathWrapper target = resolutionManifest.getResolvedPath(catalogRoleName, true); + if (target == null) { + throw new NotFoundException("CatalogRole does not exist: %s", catalogRoleName); + } + authorizer.authorizeOrThrow( + authenticatedPrincipal, + resolutionManifest.getAllActivatedCatalogRoleAndPrincipalRoleIds(), + op, + target, + null /* secondary */); + } + + private void authorizeGrantOnRootContainerToPrincipalRoleOperationOrThrow( + PolarisAuthorizableOperation op, String principalRoleName) { + resolutionManifest = + entityManager.prepareResolutionManifest(callContext, authenticatedPrincipal, null); + resolutionManifest.addTopLevelName( + principalRoleName, PolarisEntityType.PRINCIPAL_ROLE, false /* isOptional */); + ResolverStatus status = resolutionManifest.resolveAll(); + + if (status.getStatus() == ResolverStatus.StatusEnum.ENTITY_COULD_NOT_BE_RESOLVED) { + throw new NotFoundException( + "Entity %s not found when trying to grant on root to %s", + status.getFailedToResolvedEntityName(), principalRoleName); + } + + // TODO: Merge this method into authorizeGrantOnTopLevelEntityToPrincipalRoleOperationOrThrow + // once we remove any special handling logic for the rootContainer. + PolarisResolvedPathWrapper rootContainerWrapper = + resolutionManifest.getResolvedRootContainerEntityAsPath(); + PolarisResolvedPathWrapper principalRoleWrapper = + resolutionManifest.getResolvedTopLevelEntity( + principalRoleName, PolarisEntityType.PRINCIPAL_ROLE); + + authorizer.authorizeOrThrow( + authenticatedPrincipal, + resolutionManifest.getAllActivatedCatalogRoleAndPrincipalRoleIds(), + op, + rootContainerWrapper, + principalRoleWrapper); + } + + private void authorizeGrantOnTopLevelEntityToPrincipalRoleOperationOrThrow( + PolarisAuthorizableOperation op, + String topLevelEntityName, + PolarisEntityType topLevelEntityType, + String principalRoleName) { + resolutionManifest = + entityManager.prepareResolutionManifest(callContext, authenticatedPrincipal, null); + resolutionManifest.addTopLevelName( + topLevelEntityName, topLevelEntityType, false /* isOptional */); + resolutionManifest.addTopLevelName( + principalRoleName, PolarisEntityType.PRINCIPAL_ROLE, false /* isOptional */); + ResolverStatus status = resolutionManifest.resolveAll(); + + if (status.getStatus() == ResolverStatus.StatusEnum.ENTITY_COULD_NOT_BE_RESOLVED) { + throw new NotFoundException( + "Entity %s not found when trying to assign %s of type %s to %s", + status.getFailedToResolvedEntityName(), + topLevelEntityName, + topLevelEntityType, + principalRoleName); + } + + PolarisResolvedPathWrapper topLevelEntityWrapper = + resolutionManifest.getResolvedTopLevelEntity(topLevelEntityName, topLevelEntityType); + PolarisResolvedPathWrapper principalRoleWrapper = + resolutionManifest.getResolvedTopLevelEntity( + principalRoleName, PolarisEntityType.PRINCIPAL_ROLE); + + authorizer.authorizeOrThrow( + authenticatedPrincipal, + resolutionManifest.getAllActivatedCatalogRoleAndPrincipalRoleIds(), + op, + topLevelEntityWrapper, + principalRoleWrapper); + } + + private void authorizeGrantOnPrincipalRoleToPrincipalOperationOrThrow( + PolarisAuthorizableOperation op, String principalRoleName, String principalName) { + resolutionManifest = + entityManager.prepareResolutionManifest(callContext, authenticatedPrincipal, null); + resolutionManifest.addTopLevelName( + principalRoleName, PolarisEntityType.PRINCIPAL_ROLE, false /* isOptional */); + resolutionManifest.addTopLevelName( + principalName, PolarisEntityType.PRINCIPAL, false /* isOptional */); + ResolverStatus status = resolutionManifest.resolveAll(); + + if (status.getStatus() == ResolverStatus.StatusEnum.ENTITY_COULD_NOT_BE_RESOLVED) { + throw new NotFoundException( + "Entity %s not found when trying to assign %s to %s", + status.getFailedToResolvedEntityName(), principalRoleName, principalName); + } + + PolarisResolvedPathWrapper principalRoleWrapper = + resolutionManifest.getResolvedTopLevelEntity( + principalRoleName, PolarisEntityType.PRINCIPAL_ROLE); + PolarisResolvedPathWrapper principalWrapper = + resolutionManifest.getResolvedTopLevelEntity(principalName, PolarisEntityType.PRINCIPAL); + + authorizer.authorizeOrThrow( + authenticatedPrincipal, + resolutionManifest.getAllActivatedCatalogRoleAndPrincipalRoleIds(), + op, + principalRoleWrapper, + principalWrapper); + } + + private void authorizeGrantOnCatalogRoleToPrincipalRoleOperationOrThrow( + PolarisAuthorizableOperation op, + String catalogName, + String catalogRoleName, + String principalRoleName) { + resolutionManifest = + entityManager.prepareResolutionManifest(callContext, authenticatedPrincipal, catalogName); + resolutionManifest.addPath( + new ResolverPath(List.of(catalogRoleName), PolarisEntityType.CATALOG_ROLE), + catalogRoleName); + resolutionManifest.addTopLevelName( + principalRoleName, PolarisEntityType.PRINCIPAL_ROLE, false /* isOptional */); + ResolverStatus status = resolutionManifest.resolveAll(); + + if (status.getStatus() == ResolverStatus.StatusEnum.ENTITY_COULD_NOT_BE_RESOLVED) { + throw new NotFoundException( + "Entity %s not found when trying to assign %s.%s to %s", + status.getFailedToResolvedEntityName(), catalogName, catalogRoleName, principalRoleName); + } else if (status.getStatus() == ResolverStatus.StatusEnum.PATH_COULD_NOT_BE_FULLY_RESOLVED) { + throw new NotFoundException( + "Entity %s not found when trying to assign %s.%s to %s", + status.getFailedToResolvePath(), catalogName, catalogRoleName, principalRoleName); + } + + PolarisResolvedPathWrapper principalRoleWrapper = + resolutionManifest.getResolvedTopLevelEntity( + principalRoleName, PolarisEntityType.PRINCIPAL_ROLE); + PolarisResolvedPathWrapper catalogRoleWrapper = + resolutionManifest.getResolvedPath(catalogRoleName, true); + + authorizer.authorizeOrThrow( + authenticatedPrincipal, + resolutionManifest.getAllActivatedCatalogRoleAndPrincipalRoleIds(), + op, + catalogRoleWrapper, + principalRoleWrapper); + } + + private void authorizeGrantOnCatalogOperationOrThrow( + PolarisAuthorizableOperation op, String catalogName, String catalogRoleName) { + resolutionManifest = + entityManager.prepareResolutionManifest(callContext, authenticatedPrincipal, catalogName); + resolutionManifest.addTopLevelName( + catalogName, PolarisEntityType.CATALOG, false /* isOptional */); + resolutionManifest.addPath( + new ResolverPath(List.of(catalogRoleName), PolarisEntityType.CATALOG_ROLE), + catalogRoleName); + ResolverStatus status = resolutionManifest.resolveAll(); + + if (status.getStatus() == ResolverStatus.StatusEnum.ENTITY_COULD_NOT_BE_RESOLVED) { + throw new NotFoundException("Catalog not found: %s", catalogName); + } else if (status.getStatus() == ResolverStatus.StatusEnum.PATH_COULD_NOT_BE_FULLY_RESOLVED) { + throw new NotFoundException("CatalogRole not found: %s.%s", catalogName, catalogRoleName); + } + + PolarisResolvedPathWrapper catalogWrapper = + resolutionManifest.getResolvedTopLevelEntity(catalogName, PolarisEntityType.CATALOG); + PolarisResolvedPathWrapper catalogRoleWrapper = + resolutionManifest.getResolvedPath(catalogRoleName, true); + authorizer.authorizeOrThrow( + authenticatedPrincipal, + resolutionManifest.getAllActivatedCatalogRoleAndPrincipalRoleIds(), + op, + catalogWrapper, + catalogRoleWrapper); + } + + private void authorizeGrantOnNamespaceOperationOrThrow( + PolarisAuthorizableOperation op, + String catalogName, + Namespace namespace, + String catalogRoleName) { + resolutionManifest = + entityManager.prepareResolutionManifest(callContext, authenticatedPrincipal, catalogName); + resolutionManifest.addPath( + new ResolverPath(Arrays.asList(namespace.levels()), PolarisEntityType.NAMESPACE), + namespace); + resolutionManifest.addPath( + new ResolverPath(List.of(catalogRoleName), PolarisEntityType.CATALOG_ROLE), + catalogRoleName); + ResolverStatus status = resolutionManifest.resolveAll(); + + if (status.getStatus() == ResolverStatus.StatusEnum.ENTITY_COULD_NOT_BE_RESOLVED) { + throw new NotFoundException("Catalog not found: %s", catalogName); + } else if (status.getStatus() == ResolverStatus.StatusEnum.PATH_COULD_NOT_BE_FULLY_RESOLVED) { + if (status.getFailedToResolvePath().getLastEntityType() == PolarisEntityType.NAMESPACE) { + throw new NoSuchNamespaceException( + "Namespace does not exist: %s", status.getFailedToResolvePath().getEntityNames()); + } else { + throw new NotFoundException("CatalogRole not found: %s.%s", catalogName, catalogRoleName); + } + } + + PolarisResolvedPathWrapper namespaceWrapper = + resolutionManifest.getResolvedPath(namespace, true); + PolarisResolvedPathWrapper catalogRoleWrapper = + resolutionManifest.getResolvedPath(catalogRoleName, true); + + authorizer.authorizeOrThrow( + authenticatedPrincipal, + resolutionManifest.getAllActivatedCatalogRoleAndPrincipalRoleIds(), + op, + namespaceWrapper, + catalogRoleWrapper); + } + + private void authorizeGrantOnTableLikeOperationOrThrow( + PolarisAuthorizableOperation op, + String catalogName, + PolarisEntitySubType subType, + TableIdentifier identifier, + String catalogRoleName) { + resolutionManifest = + entityManager.prepareResolutionManifest(callContext, authenticatedPrincipal, catalogName); + resolutionManifest.addPath( + new ResolverPath( + PolarisCatalogHelpers.tableIdentifierToList(identifier), PolarisEntityType.TABLE_LIKE), + identifier); + resolutionManifest.addPath( + new ResolverPath(List.of(catalogRoleName), PolarisEntityType.CATALOG_ROLE), + catalogRoleName); + ResolverStatus status = resolutionManifest.resolveAll(); + + if (status.getStatus() == ResolverStatus.StatusEnum.ENTITY_COULD_NOT_BE_RESOLVED) { + throw new NotFoundException("Catalog not found: %s", catalogName); + } else if (status.getStatus() == ResolverStatus.StatusEnum.PATH_COULD_NOT_BE_FULLY_RESOLVED) { + if (status.getFailedToResolvePath().getLastEntityType() == PolarisEntityType.TABLE_LIKE) { + if (subType == PolarisEntitySubType.TABLE) { + throw new NoSuchTableException("Table does not exist: %s", identifier); + } else { + throw new NoSuchViewException("View does not exist: %s", identifier); + } + } else { + throw new NotFoundException("CatalogRole not found: %s.%s", catalogName, catalogRoleName); + } + } + + PolarisResolvedPathWrapper tableLikeWrapper = + resolutionManifest.getResolvedPath(identifier, subType, true); + PolarisResolvedPathWrapper catalogRoleWrapper = + resolutionManifest.getResolvedPath(catalogRoleName, true); + + authorizer.authorizeOrThrow( + authenticatedPrincipal, + resolutionManifest.getAllActivatedCatalogRoleAndPrincipalRoleIds(), + op, + tableLikeWrapper, + catalogRoleWrapper); + } + + public PolarisEntity createCatalog(PolarisEntity entity) { + PolarisAuthorizableOperation op = PolarisAuthorizableOperation.CREATE_CATALOG; + authorizeBasicRootOperationOrThrow(op); + + long id = + entity.getId() <= 0 + ? entityManager + .getMetaStoreManager() + .generateNewEntityId(getCurrentPolarisContext()) + .getId() + : entity.getId(); + PolarisEntity polarisEntity = + new PolarisEntity.Builder(entity) + .setId(id) + .setCreateTimestamp(System.currentTimeMillis()) + .build(); + PolarisMetaStoreManager.CreateCatalogResult catalogResult = + entityManager + .getMetaStoreManager() + .createCatalog(getCurrentPolarisContext(), polarisEntity, List.of()); + if (catalogResult.alreadyExists()) { + throw new AlreadyExistsException( + "Cannot create Catalog %s. Catalog already exists or resolution failed", + entity.getName()); + } + return PolarisEntity.of(catalogResult.getCatalog()); + } + + public void deleteCatalog(String name) { + PolarisAuthorizableOperation op = PolarisAuthorizableOperation.DELETE_CATALOG; + authorizeBasicTopLevelEntityOperationOrThrow(op, name, PolarisEntityType.CATALOG); + + PolarisEntity entity = + findCatalogByName(name) + .orElseThrow(() -> new NotFoundException("Catalog %s not found", name)); + // TODO: Handle return value in case of concurrent modification + PolarisCallContext polarisCallContext = callContext.getPolarisCallContext(); + boolean cleanup = + polarisCallContext + .getConfigurationStore() + .getConfiguration(polarisCallContext, CLEANUP_ON_CATALOG_DROP, false); + PolarisMetaStoreManager.DropEntityResult dropEntityResult = + entityManager + .getMetaStoreManager() + .dropEntityIfExists(getCurrentPolarisContext(), null, entity, Map.of(), cleanup); + + // at least some handling of error + if (!dropEntityResult.isSuccess()) { + if (dropEntityResult.failedBecauseNotEmpty()) { + throw new BadRequestException( + String.format("Catalog '%s' cannot be dropped, it is not empty", entity.getName())); + } else { + throw new BadRequestException( + String.format( + "Catalog '%s' cannot be dropped, concurrent modification detected. Please try " + + "again", + entity.getName())); + } + } + } + + public @NotNull CatalogEntity getCatalog(String name) { + PolarisAuthorizableOperation op = PolarisAuthorizableOperation.GET_CATALOG; + authorizeBasicTopLevelEntityOperationOrThrow(op, name, PolarisEntityType.CATALOG); + + return findCatalogByName(name) + .orElseThrow(() -> new NotFoundException("Catalog %s not found", name)); + } + + /** + * Helper to validate business logic of what is allowed to be updated or throw a + * BadRequestException. + */ + private void validateUpdateCatalogDiffOrThrow( + CatalogEntity currentEntity, CatalogEntity newEntity) { + // TODO: Expand the set of validations if there are other fields for other cloud providers + // that we can't successfully apply changes to. + PolarisStorageConfigurationInfo currentStorageConfig = + currentEntity.getStorageConfigurationInfo(); + PolarisStorageConfigurationInfo newStorageConfig = newEntity.getStorageConfigurationInfo(); + + if (currentStorageConfig == null && newStorageConfig == null) { + return; + } + + if (!currentStorageConfig.getClass().equals(newStorageConfig.getClass())) { + throw new BadRequestException( + "Cannot modify storage type of storage config from %s to %s", + currentStorageConfig, newStorageConfig); + } + + if (currentStorageConfig instanceof AwsStorageConfigurationInfo + && newStorageConfig instanceof AwsStorageConfigurationInfo) { + AwsStorageConfigurationInfo currentAwsConfig = + (AwsStorageConfigurationInfo) currentStorageConfig; + AwsStorageConfigurationInfo newAwsConfig = (AwsStorageConfigurationInfo) newStorageConfig; + + if ((currentAwsConfig.getRoleARN() != null + && !currentAwsConfig.getRoleARN().equals(newAwsConfig.getRoleARN())) + || (newAwsConfig.getRoleARN() != null + && !newAwsConfig.getRoleARN().equals(currentAwsConfig.getRoleARN()))) { + throw new BadRequestException( + "Cannot modify Role ARN in storage config from %s to %s", + currentStorageConfig, newStorageConfig); + } + + if ((currentAwsConfig.getExternalId() != null + && !currentAwsConfig.getExternalId().equals(newAwsConfig.getExternalId())) + || (newAwsConfig.getExternalId() != null + && !newAwsConfig.getExternalId().equals(currentAwsConfig.getExternalId()))) { + throw new BadRequestException( + "Cannot modify ExternalId in storage config from %s to %s", + currentStorageConfig, newStorageConfig); + } + } else if (currentStorageConfig instanceof AzureStorageConfigurationInfo + && newStorageConfig instanceof AzureStorageConfigurationInfo) { + AzureStorageConfigurationInfo currentAzureConfig = + (AzureStorageConfigurationInfo) currentStorageConfig; + AzureStorageConfigurationInfo newAzureConfig = + (AzureStorageConfigurationInfo) newStorageConfig; + + if ((currentAzureConfig.getTenantId() != null + && !currentAzureConfig.getTenantId().equals(newAzureConfig.getTenantId())) + || (newAzureConfig.getTenantId() != null + && !newAzureConfig.getTenantId().equals(currentAzureConfig.getTenantId()))) { + throw new BadRequestException( + "Cannot modify TenantId in storage config from %s to %s", + currentStorageConfig, newStorageConfig); + } + } + } + + public @NotNull CatalogEntity updateCatalog(String name, UpdateCatalogRequest updateRequest) { + PolarisAuthorizableOperation op = PolarisAuthorizableOperation.UPDATE_CATALOG; + authorizeBasicTopLevelEntityOperationOrThrow(op, name, PolarisEntityType.CATALOG); + + CatalogEntity currentCatalogEntity = + findCatalogByName(name) + .orElseThrow(() -> new NotFoundException("Catalog %s not found", name)); + + if (currentCatalogEntity.getEntityVersion() != updateRequest.getCurrentEntityVersion()) { + throw new CommitFailedException( + "Failed to update Catalog; currentEntityVersion '%s', expected '%s'", + currentCatalogEntity.getEntityVersion(), updateRequest.getCurrentEntityVersion()); + } + + CatalogEntity.Builder updateBuilder = new CatalogEntity.Builder(currentCatalogEntity); + String defaultBaseLocation = currentCatalogEntity.getDefaultBaseLocation(); + if (updateRequest.getProperties() != null) { + updateBuilder.setProperties(updateRequest.getProperties()); + defaultBaseLocation = + updateRequest.getProperties().get(CatalogEntity.DEFAULT_BASE_LOCATION_KEY); + } + if (updateRequest.getStorageConfigInfo() != null) { + updateBuilder.setStorageConfigurationInfo( + updateRequest.getStorageConfigInfo(), defaultBaseLocation); + } + CatalogEntity updatedEntity = updateBuilder.build(); + + validateUpdateCatalogDiffOrThrow(currentCatalogEntity, updatedEntity); + + CatalogEntity returnedEntity = + Optional.ofNullable( + CatalogEntity.of( + PolarisEntity.of( + entityManager + .getMetaStoreManager() + .updateEntityPropertiesIfNotChanged( + getCurrentPolarisContext(), null, updatedEntity)))) + .orElseThrow( + () -> + new CommitFailedException( + "Concurrent modification on Catalog '%s'; retry later")); + return returnedEntity; + } + + public List listCatalogs() { + PolarisAuthorizableOperation op = PolarisAuthorizableOperation.LIST_CATALOGS; + authorizeBasicRootOperationOrThrow(op); + + return entityManager + .getMetaStoreManager() + .listEntities( + getCurrentPolarisContext(), + null, + PolarisEntityType.CATALOG, + PolarisEntitySubType.ANY_SUBTYPE) + .getEntities() + .stream() + .map( + nameAndId -> + PolarisEntity.of( + entityManager + .getMetaStoreManager() + .loadEntity(getCurrentPolarisContext(), 0, nameAndId.getId()))) + .toList(); + } + + public PrincipalWithCredentials createPrincipal(PolarisEntity entity) { + PolarisAuthorizableOperation op = PolarisAuthorizableOperation.CREATE_PRINCIPAL; + authorizeBasicRootOperationOrThrow(op); + + long id = + entity.getId() <= 0 + ? entityManager + .getMetaStoreManager() + .generateNewEntityId(getCurrentPolarisContext()) + .getId() + : entity.getId(); + PolarisMetaStoreManager.CreatePrincipalResult principalResult = + entityManager + .getMetaStoreManager() + .createPrincipal( + getCurrentPolarisContext(), + new PolarisEntity.Builder(entity) + .setId(id) + .setCreateTimestamp(System.currentTimeMillis()) + .build()); + if (principalResult.alreadyExists()) { + throw new AlreadyExistsException( + "Cannot create Principal %s. Principal already exists or resolution failed", + entity.getName()); + } + return new PrincipalWithCredentials( + new PrincipalEntity(principalResult.getPrincipal()).asPrincipal(), + new PrincipalWithCredentialsCredentials( + principalResult.getPrincipalSecrets().getPrincipalClientId(), + principalResult.getPrincipalSecrets().getMainSecret())); + } + + public void deletePrincipal(String name) { + PolarisAuthorizableOperation op = PolarisAuthorizableOperation.DELETE_PRINCIPAL; + authorizeBasicTopLevelEntityOperationOrThrow(op, name, PolarisEntityType.PRINCIPAL); + + PolarisEntity entity = + findPrincipalByName(name) + .orElseThrow(() -> new NotFoundException("Principal %s not found", name)); + // TODO: Handle return value in case of concurrent modification + PolarisMetaStoreManager.DropEntityResult dropEntityResult = + entityManager + .getMetaStoreManager() + .dropEntityIfExists(getCurrentPolarisContext(), null, entity, Map.of(), false); + + // at least some handling of error + if (!dropEntityResult.isSuccess()) { + if (dropEntityResult.isEntityUnDroppable()) { + throw new BadRequestException("Root principal cannot be dropped"); + } else { + throw new BadRequestException( + "Root principal cannot be dropped, concurrent modification " + + "detected. Please try again"); + } + } + } + + public @NotNull PrincipalEntity getPrincipal(String name) { + PolarisAuthorizableOperation op = PolarisAuthorizableOperation.GET_PRINCIPAL; + authorizeBasicTopLevelEntityOperationOrThrow(op, name, PolarisEntityType.PRINCIPAL); + + return findPrincipalByName(name) + .orElseThrow(() -> new NotFoundException("Principal %s not found", name)); + } + + public @NotNull PrincipalEntity updatePrincipal( + String name, UpdatePrincipalRequest updateRequest) { + PolarisAuthorizableOperation op = PolarisAuthorizableOperation.UPDATE_PRINCIPAL; + authorizeBasicTopLevelEntityOperationOrThrow(op, name, PolarisEntityType.PRINCIPAL); + + PrincipalEntity currentPrincipalEntity = + findPrincipalByName(name) + .orElseThrow(() -> new NotFoundException("Principal %s not found", name)); + + if (currentPrincipalEntity.getEntityVersion() != updateRequest.getCurrentEntityVersion()) { + throw new CommitFailedException( + "Failed to update Principal; currentEntityVersion '%s', expected '%s'", + currentPrincipalEntity.getEntityVersion(), updateRequest.getCurrentEntityVersion()); + } + + PrincipalEntity.Builder updateBuilder = new PrincipalEntity.Builder(currentPrincipalEntity); + if (updateRequest.getProperties() != null) { + updateBuilder.setProperties(updateRequest.getProperties()); + } + PrincipalEntity updatedEntity = updateBuilder.build(); + PrincipalEntity returnedEntity = + Optional.ofNullable( + PrincipalEntity.of( + PolarisEntity.of( + entityManager + .getMetaStoreManager() + .updateEntityPropertiesIfNotChanged( + getCurrentPolarisContext(), null, updatedEntity)))) + .orElseThrow( + () -> + new CommitFailedException( + "Concurrent modification on Principal '%s'; retry later")); + return returnedEntity; + } + + private @NotNull PrincipalWithCredentials rotateOrResetCredentialsHelper( + String principalName, boolean shouldReset) { + PrincipalEntity currentPrincipalEntity = + findPrincipalByName(principalName) + .orElseThrow(() -> new NotFoundException("Principal %s not found", principalName)); + + PolarisPrincipalSecrets currentSecrets = + entityManager + .getMetaStoreManager() + .loadPrincipalSecrets(getCurrentPolarisContext(), currentPrincipalEntity.getClientId()) + .getPrincipalSecrets(); + if (currentSecrets == null) { + throw new IllegalArgumentException( + String.format("Failed to load current secrets for principal '%s'", principalName)); + } + PolarisPrincipalSecrets newSecrets = + entityManager + .getMetaStoreManager() + .rotatePrincipalSecrets( + getCurrentPolarisContext(), + currentPrincipalEntity.getClientId(), + currentPrincipalEntity.getId(), + currentSecrets.getMainSecret(), + shouldReset) + .getPrincipalSecrets(); + if (newSecrets == null) { + throw new IllegalStateException( + String.format( + "Failed to %s secrets for principal '%s'", + shouldReset ? "reset" : "rotate", principalName)); + } + PolarisEntity newPrincipal = + PolarisEntity.of( + entityManager + .getMetaStoreManager() + .loadEntity(getCurrentPolarisContext(), 0L, currentPrincipalEntity.getId())); + return new PrincipalWithCredentials( + PrincipalEntity.of(newPrincipal).asPrincipal(), + new PrincipalWithCredentialsCredentials( + newSecrets.getPrincipalClientId(), newSecrets.getMainSecret())); + } + + public @NotNull PrincipalWithCredentials rotateCredentials(String principalName) { + PolarisAuthorizableOperation op = PolarisAuthorizableOperation.ROTATE_CREDENTIALS; + authorizeBasicTopLevelEntityOperationOrThrow(op, principalName, PolarisEntityType.PRINCIPAL); + + return rotateOrResetCredentialsHelper(principalName, false); + } + + public @NotNull PrincipalWithCredentials resetCredentials(String principalName) { + PolarisAuthorizableOperation op = PolarisAuthorizableOperation.RESET_CREDENTIALS; + authorizeBasicTopLevelEntityOperationOrThrow(op, principalName, PolarisEntityType.PRINCIPAL); + + return rotateOrResetCredentialsHelper(principalName, true); + } + + public List listPrincipals() { + PolarisAuthorizableOperation op = PolarisAuthorizableOperation.LIST_PRINCIPALS; + authorizeBasicRootOperationOrThrow(op); + + return entityManager + .getMetaStoreManager() + .listEntities( + getCurrentPolarisContext(), + null, + PolarisEntityType.PRINCIPAL, + PolarisEntitySubType.NULL_SUBTYPE) + .getEntities() + .stream() + .map( + nameAndId -> + PolarisEntity.of( + entityManager + .getMetaStoreManager() + .loadEntity(getCurrentPolarisContext(), 0, nameAndId.getId()))) + .toList(); + } + + public PolarisEntity createPrincipalRole(PolarisEntity entity) { + PolarisAuthorizableOperation op = PolarisAuthorizableOperation.CREATE_PRINCIPAL_ROLE; + authorizeBasicRootOperationOrThrow(op); + + long id = + entity.getId() <= 0 + ? entityManager + .getMetaStoreManager() + .generateNewEntityId(getCurrentPolarisContext()) + .getId() + : entity.getId(); + PolarisEntity returnedEntity = + PolarisEntity.of( + entityManager + .getMetaStoreManager() + .createEntityIfNotExists( + getCurrentPolarisContext(), + null, + new PolarisEntity.Builder(entity) + .setId(id) + .setCreateTimestamp(System.currentTimeMillis()) + .build())); + if (returnedEntity == null) { + throw new AlreadyExistsException( + "Cannot create PrincipalRole %s. PrincipalRole already exists or resolution failed", + entity.getName()); + } + return returnedEntity; + } + + public void deletePrincipalRole(String name) { + PolarisAuthorizableOperation op = PolarisAuthorizableOperation.DELETE_PRINCIPAL_ROLE; + authorizeBasicTopLevelEntityOperationOrThrow(op, name, PolarisEntityType.PRINCIPAL_ROLE); + + PolarisEntity entity = + findPrincipalRoleByName(name) + .orElseThrow(() -> new NotFoundException("PrincipalRole %s not found", name)); + // TODO: Handle return value in case of concurrent modification + PolarisMetaStoreManager.DropEntityResult dropEntityResult = + entityManager + .getMetaStoreManager() + .dropEntityIfExists( + getCurrentPolarisContext(), null, entity, Map.of(), true); // cleanup grants + + // at least some handling of error + if (!dropEntityResult.isSuccess()) { + if (dropEntityResult.isEntityUnDroppable()) { + throw new BadRequestException("Polaris service admin principal role cannot be dropped"); + } else { + throw new BadRequestException( + "Polaris service admin principal role cannot be dropped, " + + "concurrent modification detected. Please try again"); + } + } + } + + public @NotNull PrincipalRoleEntity getPrincipalRole(String name) { + PolarisAuthorizableOperation op = PolarisAuthorizableOperation.GET_PRINCIPAL_ROLE; + authorizeBasicTopLevelEntityOperationOrThrow(op, name, PolarisEntityType.PRINCIPAL_ROLE); + + return findPrincipalRoleByName(name) + .orElseThrow(() -> new NotFoundException("PrincipalRole %s not found", name)); + } + + public @NotNull PrincipalRoleEntity updatePrincipalRole( + String name, UpdatePrincipalRoleRequest updateRequest) { + PolarisAuthorizableOperation op = PolarisAuthorizableOperation.UPDATE_PRINCIPAL_ROLE; + authorizeBasicTopLevelEntityOperationOrThrow(op, name, PolarisEntityType.PRINCIPAL_ROLE); + + PrincipalRoleEntity currentPrincipalRoleEntity = + findPrincipalRoleByName(name) + .orElseThrow(() -> new NotFoundException("PrincipalRole %s not found", name)); + + if (currentPrincipalRoleEntity.getEntityVersion() != updateRequest.getCurrentEntityVersion()) { + throw new CommitFailedException( + "Failed to update PrincipalRole; currentEntityVersion '%s', expected '%s'", + currentPrincipalRoleEntity.getEntityVersion(), updateRequest.getCurrentEntityVersion()); + } + + PrincipalRoleEntity.Builder updateBuilder = + new PrincipalRoleEntity.Builder(currentPrincipalRoleEntity); + if (updateRequest.getProperties() != null) { + updateBuilder.setProperties(updateRequest.getProperties()); + } + PrincipalRoleEntity updatedEntity = updateBuilder.build(); + PrincipalRoleEntity returnedEntity = + Optional.ofNullable( + PrincipalRoleEntity.of( + PolarisEntity.of( + entityManager + .getMetaStoreManager() + .updateEntityPropertiesIfNotChanged( + getCurrentPolarisContext(), null, updatedEntity)))) + .orElseThrow( + () -> + new CommitFailedException( + "Concurrent modification on PrincipalRole '%s'; retry later")); + return returnedEntity; + } + + public List listPrincipalRoles() { + PolarisAuthorizableOperation op = PolarisAuthorizableOperation.LIST_PRINCIPAL_ROLES; + authorizeBasicRootOperationOrThrow(op); + + return entityManager + .getMetaStoreManager() + .listEntities( + getCurrentPolarisContext(), + null, + PolarisEntityType.PRINCIPAL_ROLE, + PolarisEntitySubType.NULL_SUBTYPE) + .getEntities() + .stream() + .map( + nameAndId -> + PolarisEntity.of( + entityManager + .getMetaStoreManager() + .loadEntity(getCurrentPolarisContext(), 0, nameAndId.getId()))) + .toList(); + } + + public PolarisEntity createCatalogRole(String catalogName, PolarisEntity entity) { + PolarisAuthorizableOperation op = PolarisAuthorizableOperation.CREATE_CATALOG_ROLE; + authorizeBasicTopLevelEntityOperationOrThrow(op, catalogName, PolarisEntityType.CATALOG); + + PolarisEntity catalogEntity = + findCatalogByName(catalogName) + .orElseThrow(() -> new NotFoundException("Parent catalog %s not found", catalogName)); + + long id = + entity.getId() <= 0 + ? entityManager + .getMetaStoreManager() + .generateNewEntityId(getCurrentPolarisContext()) + .getId() + : entity.getId(); + PolarisEntity returnedEntity = + PolarisEntity.of( + entityManager + .getMetaStoreManager() + .createEntityIfNotExists( + getCurrentPolarisContext(), + PolarisEntity.toCoreList(List.of(catalogEntity)), + new PolarisEntity.Builder(entity) + .setId(id) + .setCatalogId(catalogEntity.getId()) + .setParentId(catalogEntity.getId()) + .setCreateTimestamp(System.currentTimeMillis()) + .build())); + if (returnedEntity == null) { + throw new AlreadyExistsException( + "Cannot create CatalogRole %s in %s. CatalogRole already exists or resolution failed", + entity.getName(), catalogName); + } + return returnedEntity; + } + + public void deleteCatalogRole(String catalogName, String name) { + PolarisAuthorizableOperation op = PolarisAuthorizableOperation.DELETE_CATALOG_ROLE; + authorizeBasicCatalogRoleOperationOrThrow(op, catalogName, name); + + PolarisResolvedPathWrapper resolvedCatalogRoleEntity = resolutionManifest.getResolvedPath(name); + if (resolvedCatalogRoleEntity == null) { + throw new NotFoundException("CatalogRole %s not found in catalog %s", name, catalogName); + } + // TODO: Handle return value in case of concurrent modification + PolarisMetaStoreManager.DropEntityResult dropEntityResult = + entityManager + .getMetaStoreManager() + .dropEntityIfExists( + getCurrentPolarisContext(), + PolarisEntity.toCoreList(resolvedCatalogRoleEntity.getRawParentPath()), + resolvedCatalogRoleEntity.getRawLeafEntity(), + Map.of(), + true); // cleanup grants + + // at least some handling of error + if (!dropEntityResult.isSuccess()) { + if (dropEntityResult.isEntityUnDroppable()) { + throw new BadRequestException("Catalog admin role cannot be dropped"); + } else { + throw new BadRequestException( + "Catalog admin role cannot be dropped, concurrent " + + "modification detected. Please try again"); + } + } + } + + public @NotNull CatalogRoleEntity getCatalogRole(String catalogName, String name) { + PolarisAuthorizableOperation op = PolarisAuthorizableOperation.GET_CATALOG_ROLE; + authorizeBasicCatalogRoleOperationOrThrow(op, catalogName, name); + + return findCatalogRoleByName(catalogName, name) + .orElseThrow(() -> new NotFoundException("CatalogRole %s not found", name)); + } + + public @NotNull CatalogRoleEntity updateCatalogRole( + String catalogName, String name, UpdateCatalogRoleRequest updateRequest) { + PolarisAuthorizableOperation op = PolarisAuthorizableOperation.UPDATE_CATALOG_ROLE; + authorizeBasicCatalogRoleOperationOrThrow(op, catalogName, name); + + CatalogEntity catalogEntity = + findCatalogByName(catalogName) + .orElseThrow(() -> new NotFoundException("Catalog %s not found", catalogName)); + CatalogRoleEntity currentCatalogRoleEntity = + findCatalogRoleByName(catalogName, name) + .orElseThrow(() -> new NotFoundException("CatalogRole %s not found", name)); + + if (currentCatalogRoleEntity.getEntityVersion() != updateRequest.getCurrentEntityVersion()) { + throw new CommitFailedException( + "Failed to update CatalogRole; currentEntityVersion '%s', expected '%s'", + currentCatalogRoleEntity.getEntityVersion(), updateRequest.getCurrentEntityVersion()); + } + + CatalogRoleEntity.Builder updateBuilder = + new CatalogRoleEntity.Builder(currentCatalogRoleEntity); + if (updateRequest.getProperties() != null) { + updateBuilder.setProperties(updateRequest.getProperties()); + } + CatalogRoleEntity updatedEntity = updateBuilder.build(); + CatalogRoleEntity returnedEntity = + Optional.ofNullable( + CatalogRoleEntity.of( + PolarisEntity.of( + entityManager + .getMetaStoreManager() + .updateEntityPropertiesIfNotChanged( + getCurrentPolarisContext(), + PolarisEntity.toCoreList(List.of(catalogEntity)), + updatedEntity)))) + .orElseThrow( + () -> + new CommitFailedException( + "Concurrent modification on CatalogRole '%s'; retry later")); + return returnedEntity; + } + + public List listCatalogRoles(String catalogName) { + PolarisAuthorizableOperation op = PolarisAuthorizableOperation.LIST_CATALOG_ROLES; + authorizeBasicTopLevelEntityOperationOrThrow(op, catalogName, PolarisEntityType.CATALOG); + + PolarisEntity catalogEntity = + findCatalogByName(catalogName) + .orElseThrow(() -> new NotFoundException("Parent catalog %s not found", catalogName)); + return entityManager + .getMetaStoreManager() + .listEntities( + getCurrentPolarisContext(), + PolarisEntity.toCoreList(List.of(catalogEntity)), + PolarisEntityType.CATALOG_ROLE, + PolarisEntitySubType.NULL_SUBTYPE) + .getEntities() + .stream() + .map( + nameAndId -> + PolarisEntity.of( + entityManager + .getMetaStoreManager() + .loadEntity( + getCurrentPolarisContext(), catalogEntity.getId(), nameAndId.getId()))) + .toList(); + } + + public boolean assignPrincipalRole(String principalName, String principalRoleName) { + PolarisAuthorizableOperation op = PolarisAuthorizableOperation.ASSIGN_PRINCIPAL_ROLE; + authorizeGrantOnPrincipalRoleToPrincipalOperationOrThrow(op, principalRoleName, principalName); + + PolarisEntity principalEntity = + findPrincipalByName(principalName) + .orElseThrow(() -> new NotFoundException("Principal %s not found", principalName)); + PolarisEntity principalRoleEntity = + findPrincipalRoleByName(principalRoleName) + .orElseThrow( + () -> new NotFoundException("PrincipalRole %s not found", principalRoleName)); + + return entityManager + .getMetaStoreManager() + .grantUsageOnRoleToGrantee( + getCurrentPolarisContext(), null, principalRoleEntity, principalEntity) + .isSuccess(); + } + + public boolean revokePrincipalRole(String principalName, String principalRoleName) { + PolarisAuthorizableOperation op = PolarisAuthorizableOperation.REVOKE_PRINCIPAL_ROLE; + authorizeGrantOnPrincipalRoleToPrincipalOperationOrThrow(op, principalRoleName, principalName); + + PolarisEntity principalEntity = + findPrincipalByName(principalName) + .orElseThrow(() -> new NotFoundException("Principal %s not found", principalName)); + PolarisEntity principalRoleEntity = + findPrincipalRoleByName(principalRoleName) + .orElseThrow( + () -> new NotFoundException("PrincipalRole %s not found", principalRoleName)); + return entityManager + .getMetaStoreManager() + .revokeUsageOnRoleFromGrantee( + getCurrentPolarisContext(), null, principalRoleEntity, principalEntity) + .isSuccess(); + } + + public List listPrincipalRolesAssigned(String principalName) { + PolarisAuthorizableOperation op = PolarisAuthorizableOperation.LIST_PRINCIPAL_ROLES_ASSIGNED; + + authorizeBasicTopLevelEntityOperationOrThrow(op, principalName, PolarisEntityType.PRINCIPAL); + + PolarisEntity principalEntity = + findPrincipalByName(principalName) + .orElseThrow(() -> new NotFoundException("Principal %s not found", principalName)); + PolarisMetaStoreManager.LoadGrantsResult grantList = + entityManager + .getMetaStoreManager() + .loadGrantsToGrantee( + getCurrentPolarisContext(), + principalEntity.getCatalogId(), + principalEntity.getId()); + return buildEntitiesFromGrantResults(grantList, false, null); + } + + public boolean assignCatalogRoleToPrincipalRole( + String principalRoleName, String catalogName, String catalogRoleName) { + PolarisAuthorizableOperation op = + PolarisAuthorizableOperation.ASSIGN_CATALOG_ROLE_TO_PRINCIPAL_ROLE; + authorizeGrantOnCatalogRoleToPrincipalRoleOperationOrThrow( + op, catalogName, catalogRoleName, principalRoleName); + + PolarisEntity principalRoleEntity = + findPrincipalRoleByName(principalRoleName) + .orElseThrow( + () -> new NotFoundException("PrincipalRole %s not found", principalRoleName)); + PolarisEntity catalogEntity = + findCatalogByName(catalogName) + .orElseThrow(() -> new NotFoundException("Parent catalog %s not found", catalogName)); + PolarisEntity catalogRoleEntity = + findCatalogRoleByName(catalogName, catalogRoleName) + .orElseThrow(() -> new NotFoundException("CatalogRole %s not found", catalogRoleName)); + + return entityManager + .getMetaStoreManager() + .grantUsageOnRoleToGrantee( + getCurrentPolarisContext(), catalogEntity, catalogRoleEntity, principalRoleEntity) + .isSuccess(); + } + + public boolean revokeCatalogRoleFromPrincipalRole( + String principalRoleName, String catalogName, String catalogRoleName) { + PolarisAuthorizableOperation op = + PolarisAuthorizableOperation.REVOKE_CATALOG_ROLE_FROM_PRINCIPAL_ROLE; + authorizeGrantOnCatalogRoleToPrincipalRoleOperationOrThrow( + op, catalogName, catalogRoleName, principalRoleName); + + PolarisEntity principalRoleEntity = + findPrincipalRoleByName(principalRoleName) + .orElseThrow( + () -> new NotFoundException("PrincipalRole %s not found", principalRoleName)); + PolarisEntity catalogEntity = + findCatalogByName(catalogName) + .orElseThrow(() -> new NotFoundException("Parent catalog %s not found", catalogName)); + PolarisEntity catalogRoleEntity = + findCatalogRoleByName(catalogName, catalogRoleName) + .orElseThrow(() -> new NotFoundException("CatalogRole %s not found", catalogRoleName)); + return entityManager + .getMetaStoreManager() + .revokeUsageOnRoleFromGrantee( + getCurrentPolarisContext(), catalogEntity, catalogRoleEntity, principalRoleEntity) + .isSuccess(); + } + + public List listAssigneePrincipalsForPrincipalRole(String principalRoleName) { + PolarisAuthorizableOperation op = + PolarisAuthorizableOperation.LIST_ASSIGNEE_PRINCIPALS_FOR_PRINCIPAL_ROLE; + + authorizeBasicTopLevelEntityOperationOrThrow( + op, principalRoleName, PolarisEntityType.PRINCIPAL_ROLE); + + PolarisEntity principalRoleEntity = + findPrincipalRoleByName(principalRoleName) + .orElseThrow( + () -> new NotFoundException("PrincipalRole %s not found", principalRoleName)); + PolarisMetaStoreManager.LoadGrantsResult grantList = + entityManager + .getMetaStoreManager() + .loadGrantsOnSecurable( + getCurrentPolarisContext(), + principalRoleEntity.getCatalogId(), + principalRoleEntity.getId()); + return buildEntitiesFromGrantResults(grantList, true, null); + } + + /** + * Build the list of entities matching the set of grant records returned by a grant lookup + * request. + * + * @param grantList result of a load grants on a securable or to a grantee + * @param grantees if true, return the list of grantee entities, else the list of securable + * entities + * @param grantFilter filter on the grant records, use null for all + * @return list of grantees or securables matching the filter + */ + private List buildEntitiesFromGrantResults( + @NotNull PolarisMetaStoreManager.LoadGrantsResult grantList, + boolean grantees, + @Nullable Function grantFilter) { + Map granteeMap = grantList.getEntitiesAsMap(); + List toReturn = new ArrayList<>(grantList.getGrantRecords().size()); + for (PolarisGrantRecord grantRecord : grantList.getGrantRecords()) { + if (grantFilter == null || grantFilter.apply(grantRecord)) { + long catalogId = + grantees ? grantRecord.getGranteeCatalogId() : grantRecord.getSecurableCatalogId(); + long entityId = grantees ? grantRecord.getGranteeId() : grantRecord.getSecurableId(); + // get the entity associated with the grantee + PolarisBaseEntity entity = this.getOrLoadEntity(granteeMap, catalogId, entityId); + if (entity != null) { + toReturn.add(PolarisEntity.of(entity)); + } + } + } + return toReturn; + } + + public List listCatalogRolesForPrincipalRole( + String principalRoleName, String catalogName) { + PolarisAuthorizableOperation op = + PolarisAuthorizableOperation.LIST_CATALOG_ROLES_FOR_PRINCIPAL_ROLE; + authorizeBasicTopLevelEntityOperationOrThrow( + op, principalRoleName, PolarisEntityType.PRINCIPAL_ROLE, catalogName); + + PolarisEntity catalogEntity = + findCatalogByName(catalogName) + .orElseThrow(() -> new NotFoundException("Parent catalog %s not found", catalogName)); + PolarisEntity principalRoleEntity = + findPrincipalRoleByName(principalRoleName) + .orElseThrow( + () -> new NotFoundException("PrincipalRole %s not found", principalRoleName)); + PolarisMetaStoreManager.LoadGrantsResult grantList = + entityManager + .getMetaStoreManager() + .loadGrantsToGrantee( + getCurrentPolarisContext(), + principalRoleEntity.getCatalogId(), + principalRoleEntity.getId()); + return buildEntitiesFromGrantResults( + grantList, false, grantRec -> grantRec.getSecurableCatalogId() == catalogEntity.getId()); + } + + /** Adds a grant on the root container of this realm to {@code principalRoleName}. */ + public boolean grantPrivilegeOnRootContainerToPrincipalRole( + String principalRoleName, PolarisPrivilege privilege) { + PolarisAuthorizableOperation op = PolarisAuthorizableOperation.ADD_ROOT_GRANT_TO_PRINCIPAL_ROLE; + authorizeGrantOnRootContainerToPrincipalRoleOperationOrThrow(op, principalRoleName); + + PolarisEntity rootContainerEntity = + resolutionManifest.getResolvedRootContainerEntityAsPath().getRawLeafEntity(); + PolarisEntity principalRoleEntity = + findPrincipalRoleByName(principalRoleName) + .orElseThrow( + () -> new NotFoundException("PrincipalRole %s not found", principalRoleName)); + + return entityManager + .getMetaStoreManager() + .grantPrivilegeOnSecurableToRole( + getCurrentPolarisContext(), principalRoleEntity, null, rootContainerEntity, privilege) + .isSuccess(); + } + + /** Revokes a grant on the root container of this realm from {@code principalRoleName}. */ + public boolean revokePrivilegeOnRootContainerFromPrincipalRole( + String principalRoleName, PolarisPrivilege privilege) { + PolarisAuthorizableOperation op = + PolarisAuthorizableOperation.REVOKE_ROOT_GRANT_FROM_PRINCIPAL_ROLE; + authorizeGrantOnRootContainerToPrincipalRoleOperationOrThrow(op, principalRoleName); + + PolarisEntity rootContainerEntity = + resolutionManifest.getResolvedRootContainerEntityAsPath().getRawLeafEntity(); + PolarisEntity principalRoleEntity = + findPrincipalRoleByName(principalRoleName) + .orElseThrow( + () -> new NotFoundException("PrincipalRole %s not found", principalRoleName)); + + return entityManager + .getMetaStoreManager() + .revokePrivilegeOnSecurableFromRole( + getCurrentPolarisContext(), principalRoleEntity, null, rootContainerEntity, privilege) + .isSuccess(); + } + + /** + * Adds a catalog-level grant on {@code catalogName} to {@code catalogRoleName} which resides + * within the same catalog on which it is being granted the privilege. + */ + public boolean grantPrivilegeOnCatalogToRole( + String catalogName, String catalogRoleName, PolarisPrivilege privilege) { + PolarisAuthorizableOperation op = + PolarisAuthorizableOperation.ADD_CATALOG_GRANT_TO_CATALOG_ROLE; + + authorizeGrantOnCatalogOperationOrThrow(op, catalogName, catalogRoleName); + + PolarisEntity catalogEntity = + findCatalogByName(catalogName) + .orElseThrow(() -> new NotFoundException("Parent catalog %s not found", catalogName)); + PolarisEntity catalogRoleEntity = + findCatalogRoleByName(catalogName, catalogRoleName) + .orElseThrow(() -> new NotFoundException("CatalogRole %s not found", catalogRoleName)); + + return entityManager + .getMetaStoreManager() + .grantPrivilegeOnSecurableToRole( + getCurrentPolarisContext(), + catalogRoleEntity, + PolarisEntity.toCoreList(List.of(catalogEntity)), + catalogEntity, + privilege) + .isSuccess(); + } + + /** Removes a catalog-level grant on {@code catalogName} from {@code catalogRoleName}. */ + public boolean revokePrivilegeOnCatalogFromRole( + String catalogName, String catalogRoleName, PolarisPrivilege privilege) { + PolarisAuthorizableOperation op = + PolarisAuthorizableOperation.REVOKE_CATALOG_GRANT_FROM_CATALOG_ROLE; + authorizeGrantOnCatalogOperationOrThrow(op, catalogName, catalogRoleName); + + PolarisEntity catalogEntity = + findCatalogByName(catalogName) + .orElseThrow(() -> new NotFoundException("Parent catalog %s not found", catalogName)); + PolarisEntity catalogRoleEntity = + findCatalogRoleByName(catalogName, catalogRoleName) + .orElseThrow(() -> new NotFoundException("CatalogRole %s not found", catalogRoleName)); + + return entityManager + .getMetaStoreManager() + .revokePrivilegeOnSecurableFromRole( + getCurrentPolarisContext(), + catalogRoleEntity, + PolarisEntity.toCoreList(List.of(catalogEntity)), + catalogEntity, + privilege) + .isSuccess(); + } + + /** Adds a namespace-level grant on {@code namespace} to {@code catalogRoleName}. */ + public boolean grantPrivilegeOnNamespaceToRole( + String catalogName, String catalogRoleName, Namespace namespace, PolarisPrivilege privilege) { + PolarisAuthorizableOperation op = + PolarisAuthorizableOperation.ADD_NAMESPACE_GRANT_TO_CATALOG_ROLE; + authorizeGrantOnNamespaceOperationOrThrow(op, catalogName, namespace, catalogRoleName); + + PolarisEntity catalogRoleEntity = + findCatalogRoleByName(catalogName, catalogRoleName) + .orElseThrow(() -> new NotFoundException("CatalogRole %s not found", catalogRoleName)); + + PolarisResolvedPathWrapper resolvedPathWrapper = resolutionManifest.getResolvedPath(namespace); + if (resolvedPathWrapper == null) { + throw new NotFoundException("Namespace %s not found", namespace); + } + List catalogPath = resolvedPathWrapper.getRawParentPath(); + PolarisEntity namespaceEntity = resolvedPathWrapper.getRawLeafEntity(); + + return entityManager + .getMetaStoreManager() + .grantPrivilegeOnSecurableToRole( + getCurrentPolarisContext(), + catalogRoleEntity, + PolarisEntity.toCoreList(catalogPath), + namespaceEntity, + privilege) + .isSuccess(); + } + + /** Removes a namespace-level grant on {@code namespace} from {@code catalogRoleName}. */ + public boolean revokePrivilegeOnNamespaceFromRole( + String catalogName, String catalogRoleName, Namespace namespace, PolarisPrivilege privilege) { + PolarisAuthorizableOperation op = + PolarisAuthorizableOperation.REVOKE_NAMESPACE_GRANT_FROM_CATALOG_ROLE; + authorizeGrantOnNamespaceOperationOrThrow(op, catalogName, namespace, catalogRoleName); + + PolarisEntity catalogRoleEntity = + findCatalogRoleByName(catalogName, catalogRoleName) + .orElseThrow(() -> new NotFoundException("CatalogRole %s not found", catalogRoleName)); + + PolarisResolvedPathWrapper resolvedPathWrapper = resolutionManifest.getResolvedPath(namespace); + if (resolvedPathWrapper == null) { + throw new NotFoundException("Namespace %s not found", namespace); + } + List catalogPath = resolvedPathWrapper.getRawParentPath(); + PolarisEntity namespaceEntity = resolvedPathWrapper.getRawLeafEntity(); + + return entityManager + .getMetaStoreManager() + .revokePrivilegeOnSecurableFromRole( + getCurrentPolarisContext(), + catalogRoleEntity, + PolarisEntity.toCoreList(catalogPath), + namespaceEntity, + privilege) + .isSuccess(); + } + + public boolean grantPrivilegeOnTableToRole( + String catalogName, + String catalogRoleName, + TableIdentifier identifier, + PolarisPrivilege privilege) { + PolarisAuthorizableOperation op = PolarisAuthorizableOperation.ADD_TABLE_GRANT_TO_CATALOG_ROLE; + + authorizeGrantOnTableLikeOperationOrThrow( + op, catalogName, PolarisEntitySubType.TABLE, identifier, catalogRoleName); + + return grantPrivilegeOnTableLikeToRole( + catalogName, catalogRoleName, identifier, PolarisEntitySubType.TABLE, privilege); + } + + public boolean revokePrivilegeOnTableFromRole( + String catalogName, + String catalogRoleName, + TableIdentifier identifier, + PolarisPrivilege privilege) { + PolarisAuthorizableOperation op = + PolarisAuthorizableOperation.REVOKE_TABLE_GRANT_FROM_CATALOG_ROLE; + + authorizeGrantOnTableLikeOperationOrThrow( + op, catalogName, PolarisEntitySubType.TABLE, identifier, catalogRoleName); + + return revokePrivilegeOnTableLikeFromRole( + catalogName, catalogRoleName, identifier, PolarisEntitySubType.TABLE, privilege); + } + + public boolean grantPrivilegeOnViewToRole( + String catalogName, + String catalogRoleName, + TableIdentifier identifier, + PolarisPrivilege privilege) { + PolarisAuthorizableOperation op = PolarisAuthorizableOperation.ADD_VIEW_GRANT_TO_CATALOG_ROLE; + + authorizeGrantOnTableLikeOperationOrThrow( + op, catalogName, PolarisEntitySubType.VIEW, identifier, catalogRoleName); + + return grantPrivilegeOnTableLikeToRole( + catalogName, catalogRoleName, identifier, PolarisEntitySubType.VIEW, privilege); + } + + public boolean revokePrivilegeOnViewFromRole( + String catalogName, + String catalogRoleName, + TableIdentifier identifier, + PolarisPrivilege privilege) { + PolarisAuthorizableOperation op = + PolarisAuthorizableOperation.REVOKE_VIEW_GRANT_FROM_CATALOG_ROLE; + + authorizeGrantOnTableLikeOperationOrThrow( + op, catalogName, PolarisEntitySubType.VIEW, identifier, catalogRoleName); + + return revokePrivilegeOnTableLikeFromRole( + catalogName, catalogRoleName, identifier, PolarisEntitySubType.VIEW, privilege); + } + + public List listAssigneePrincipalRolesForCatalogRole( + String catalogName, String catalogRoleName) { + PolarisAuthorizableOperation op = + PolarisAuthorizableOperation.LIST_ASSIGNEE_PRINCIPAL_ROLES_FOR_CATALOG_ROLE; + authorizeBasicCatalogRoleOperationOrThrow(op, catalogName, catalogRoleName); + + PolarisEntity catalogEntity = + findCatalogByName(catalogName) + .orElseThrow(() -> new NotFoundException("Parent catalog %s not found", catalogName)); + PolarisEntity catalogRoleEntity = + findCatalogRoleByName(catalogName, catalogRoleName) + .orElseThrow(() -> new NotFoundException("CatalogRole %s not found", catalogRoleName)); + PolarisMetaStoreManager.LoadGrantsResult grantList = + entityManager + .getMetaStoreManager() + .loadGrantsOnSecurable( + getCurrentPolarisContext(), + catalogRoleEntity.getCatalogId(), + catalogRoleEntity.getId()); + return buildEntitiesFromGrantResults(grantList, true, null); + } + + /** + * Lists all grants on Catalog-level resources (Catalog/Namespace/Table/View) granted to the + * specified catalogRole. + */ + public List listGrantsForCatalogRole(String catalogName, String catalogRoleName) { + PolarisAuthorizableOperation op = PolarisAuthorizableOperation.LIST_GRANTS_FOR_CATALOG_ROLE; + authorizeBasicCatalogRoleOperationOrThrow(op, catalogName, catalogRoleName); + + PolarisEntity catalogRoleEntity = + findCatalogRoleByName(catalogName, catalogRoleName) + .orElseThrow(() -> new NotFoundException("CatalogRole %s not found", catalogRoleName)); + PolarisMetaStoreManager.LoadGrantsResult grantList = + entityManager + .getMetaStoreManager() + .loadGrantsToGrantee( + getCurrentPolarisContext(), + catalogRoleEntity.getCatalogId(), + catalogRoleEntity.getId()); + List catalogGrants = new ArrayList<>(); + List namespaceGrants = new ArrayList<>(); + List tableGrants = new ArrayList<>(); + List viewGrants = new ArrayList<>(); + Map entityMap = grantList.getEntitiesAsMap(); + for (PolarisGrantRecord record : grantList.getGrantRecords()) { + PolarisPrivilege privilege = PolarisPrivilege.fromCode(record.getPrivilegeCode()); + PolarisBaseEntity baseEntity = + this.getOrLoadEntity(entityMap, record.getSecurableCatalogId(), record.getSecurableId()); + if (baseEntity != null) { + switch (baseEntity.getType()) { + case CATALOG: + { + CatalogGrant grant = + new CatalogGrant( + CatalogPrivilege.valueOf(privilege.toString()), + GrantResource.TypeEnum.CATALOG); + catalogGrants.add(grant); + break; + } + case NAMESPACE: + { + NamespaceGrant grant = + new NamespaceGrant( + List.of(NamespaceEntity.of(baseEntity).asNamespace().levels()), + NamespacePrivilege.valueOf(privilege.toString()), + GrantResource.TypeEnum.NAMESPACE); + namespaceGrants.add(grant); + break; + } + case TABLE_LIKE: + { + if (baseEntity.getSubType() == PolarisEntitySubType.TABLE) { + TableIdentifier identifier = TableLikeEntity.of(baseEntity).getTableIdentifier(); + TableGrant grant = + new TableGrant( + List.of(identifier.namespace().levels()), + identifier.name(), + TablePrivilege.valueOf(privilege.toString()), + GrantResource.TypeEnum.TABLE); + tableGrants.add(grant); + } else { + TableIdentifier identifier = TableLikeEntity.of(baseEntity).getTableIdentifier(); + ViewGrant grant = + new ViewGrant( + List.of(identifier.namespace().levels()), + identifier.name(), + ViewPrivilege.valueOf(privilege.toString()), + GrantResource.TypeEnum.VIEW); + viewGrants.add(grant); + } + break; + } + default: + throw new IllegalArgumentException( + String.format( + "Unexpected entity type '%s' listing grants for catalogRole '%s' in catalog '%s'", + baseEntity.getType(), catalogRoleName, catalogName)); + } + } + } + // Assemble these at the end so that they're grouped by type. + List allGrants = new ArrayList<>(); + allGrants.addAll(catalogGrants); + allGrants.addAll(namespaceGrants); + allGrants.addAll(tableGrants); + allGrants.addAll(viewGrants); + return allGrants; + } + + /** + * Get the specified entity from the input map or load it from backend if the input map is null. + * Normally the input map is not expected to be null, except for backward compatibility issue. + * + * @param entitiesMap map of entities + * @param catalogId the id of the catalog of the entity we are looking for + * @param id id of the entity we are looking for + * @return null if the entity does not exist + */ + private @Nullable PolarisBaseEntity getOrLoadEntity( + @Nullable Map entitiesMap, long catalogId, long id) { + return (entitiesMap == null) + ? entityManager + .getMetaStoreManager() + .loadEntity(getCurrentPolarisContext(), catalogId, id) + .getEntity() + : entitiesMap.get(id); + } + + /** Adds a table-level or view-level grant on {@code identifier} to {@code catalogRoleName}. */ + private boolean grantPrivilegeOnTableLikeToRole( + String catalogName, + String catalogRoleName, + TableIdentifier identifier, + PolarisEntitySubType subType, + PolarisPrivilege privilege) { + PolarisEntity catalogEntity = + findCatalogByName(catalogName) + .orElseThrow(() -> new NotFoundException("Parent catalog %s not found", catalogName)); + PolarisEntity catalogRoleEntity = + findCatalogRoleByName(catalogName, catalogRoleName) + .orElseThrow(() -> new NotFoundException("CatalogRole %s not found", catalogRoleName)); + + PolarisResolvedPathWrapper resolvedPathWrapper = + resolutionManifest.getResolvedPath(identifier, subType); + if (resolvedPathWrapper == null) { + if (subType == PolarisEntitySubType.VIEW) { + throw new NotFoundException("View %s not found", identifier); + } else { + throw new NotFoundException("Table %s not found", identifier); + } + } + List catalogPath = resolvedPathWrapper.getRawParentPath(); + PolarisEntity tableLikeEntity = resolvedPathWrapper.getRawLeafEntity(); + + return entityManager + .getMetaStoreManager() + .grantPrivilegeOnSecurableToRole( + getCurrentPolarisContext(), + catalogRoleEntity, + PolarisEntity.toCoreList(catalogPath), + tableLikeEntity, + privilege) + .isSuccess(); + } + + /** + * Removes a table-level or view-level grant on {@code identifier} from {@code catalogRoleName}. + */ + private boolean revokePrivilegeOnTableLikeFromRole( + String catalogName, + String catalogRoleName, + TableIdentifier identifier, + PolarisEntitySubType subType, + PolarisPrivilege privilege) { + PolarisEntity catalogEntity = + findCatalogByName(catalogName) + .orElseThrow(() -> new NotFoundException("Parent catalog %s not found", catalogName)); + PolarisEntity catalogRoleEntity = + findCatalogRoleByName(catalogName, catalogRoleName) + .orElseThrow(() -> new NotFoundException("CatalogRole %s not found", catalogRoleName)); + + PolarisResolvedPathWrapper resolvedPathWrapper = + resolutionManifest.getResolvedPath(identifier, subType); + if (resolvedPathWrapper == null) { + if (subType == PolarisEntitySubType.VIEW) { + throw new NotFoundException("View %s not found", identifier); + } else { + throw new NotFoundException("Table %s not found", identifier); + } + } + List catalogPath = resolvedPathWrapper.getRawParentPath(); + PolarisEntity tableLikeEntity = resolvedPathWrapper.getRawLeafEntity(); + + return entityManager + .getMetaStoreManager() + .revokePrivilegeOnSecurableFromRole( + getCurrentPolarisContext(), + catalogRoleEntity, + PolarisEntity.toCoreList(catalogPath), + tableLikeEntity, + privilege) + .isSuccess(); + } +} diff --git a/polaris-service/src/main/java/io/polaris/service/admin/PolarisServiceImpl.java b/polaris-service/src/main/java/io/polaris/service/admin/PolarisServiceImpl.java new file mode 100644 index 0000000000..344b4fa145 --- /dev/null +++ b/polaris-service/src/main/java/io/polaris/service/admin/PolarisServiceImpl.java @@ -0,0 +1,605 @@ +package io.polaris.service.admin; + +import io.polaris.core.PolarisCallContext; +import io.polaris.core.admin.model.AddGrantRequest; +import io.polaris.core.admin.model.Catalog; +import io.polaris.core.admin.model.CatalogGrant; +import io.polaris.core.admin.model.CatalogRole; +import io.polaris.core.admin.model.CatalogRoles; +import io.polaris.core.admin.model.Catalogs; +import io.polaris.core.admin.model.CreateCatalogRequest; +import io.polaris.core.admin.model.CreateCatalogRoleRequest; +import io.polaris.core.admin.model.CreatePrincipalRequest; +import io.polaris.core.admin.model.CreatePrincipalRoleRequest; +import io.polaris.core.admin.model.GrantCatalogRoleRequest; +import io.polaris.core.admin.model.GrantPrincipalRoleRequest; +import io.polaris.core.admin.model.GrantResource; +import io.polaris.core.admin.model.GrantResources; +import io.polaris.core.admin.model.NamespaceGrant; +import io.polaris.core.admin.model.Principal; +import io.polaris.core.admin.model.PrincipalRole; +import io.polaris.core.admin.model.PrincipalRoles; +import io.polaris.core.admin.model.PrincipalWithCredentials; +import io.polaris.core.admin.model.Principals; +import io.polaris.core.admin.model.RevokeGrantRequest; +import io.polaris.core.admin.model.StorageConfigInfo; +import io.polaris.core.admin.model.TableGrant; +import io.polaris.core.admin.model.UpdateCatalogRequest; +import io.polaris.core.admin.model.UpdateCatalogRoleRequest; +import io.polaris.core.admin.model.UpdatePrincipalRequest; +import io.polaris.core.admin.model.UpdatePrincipalRoleRequest; +import io.polaris.core.admin.model.ViewGrant; +import io.polaris.core.auth.AuthenticatedPolarisPrincipal; +import io.polaris.core.auth.PolarisAuthorizer; +import io.polaris.core.context.CallContext; +import io.polaris.core.entity.CatalogEntity; +import io.polaris.core.entity.CatalogRoleEntity; +import io.polaris.core.entity.PolarisPrivilege; +import io.polaris.core.entity.PrincipalEntity; +import io.polaris.core.entity.PrincipalRoleEntity; +import io.polaris.core.persistence.PolarisEntityManager; +import io.polaris.service.admin.api.PolarisCatalogsApiService; +import io.polaris.service.admin.api.PolarisPrincipalRolesApiService; +import io.polaris.service.admin.api.PolarisPrincipalsApiService; +import io.polaris.service.config.RealmEntityManagerFactory; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.SecurityContext; +import java.util.List; +import org.apache.iceberg.catalog.Namespace; +import org.apache.iceberg.catalog.TableIdentifier; +import org.apache.iceberg.exceptions.NotAuthorizedException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** Concrete implementation of the Polaris API services */ +public class PolarisServiceImpl + implements PolarisCatalogsApiService, + PolarisPrincipalsApiService, + PolarisPrincipalRolesApiService { + private static final Logger LOG = LoggerFactory.getLogger(PolarisServiceImpl.class); + private final RealmEntityManagerFactory entityManagerFactory; + private final PolarisAuthorizer polarisAuthorizer; + + public PolarisServiceImpl( + RealmEntityManagerFactory entityManagerFactory, PolarisAuthorizer polarisAuthorizer) { + this.entityManagerFactory = entityManagerFactory; + this.polarisAuthorizer = polarisAuthorizer; + } + + private PolarisAdminService newAdminService(SecurityContext securityContext) { + CallContext callContext = CallContext.getCurrentContext(); + AuthenticatedPolarisPrincipal authenticatedPrincipal = + (AuthenticatedPolarisPrincipal) securityContext.getUserPrincipal(); + if (authenticatedPrincipal == null) { + throw new NotAuthorizedException("Failed to find authenticatedPrincipal in SecurityContext"); + } + + PolarisEntityManager entityManager = + entityManagerFactory.getOrCreateEntityManager(callContext.getRealmContext()); + return new PolarisAdminService( + callContext, entityManager, authenticatedPrincipal, polarisAuthorizer); + } + + /** From PolarisCatalogsApiService */ + @Override + public Response createCatalog(CreateCatalogRequest request, SecurityContext securityContext) { + PolarisAdminService adminService = newAdminService(securityContext); + Catalog catalog = request.getCatalog(); + validateStorageConfig(catalog.getStorageConfigInfo()); + Catalog newCatalog = + new CatalogEntity(adminService.createCatalog(CatalogEntity.fromCatalog(catalog))) + .asCatalog(); + LOG.info("Created new catalog {}", newCatalog); + return Response.status(Response.Status.CREATED).build(); + } + + private void validateStorageConfig(StorageConfigInfo storageConfigInfo) { + CallContext callContext = CallContext.getCurrentContext(); + PolarisCallContext polarisCallContext = callContext.getPolarisCallContext(); + List allowedStorageTypes = + polarisCallContext + .getConfigurationStore() + .getConfiguration( + polarisCallContext, + "SUPPORTED_CATALOG_STORAGE_TYPES", + List.of( + StorageConfigInfo.StorageTypeEnum.S3.name(), + StorageConfigInfo.StorageTypeEnum.AZURE.name(), + StorageConfigInfo.StorageTypeEnum.GCS.name(), + StorageConfigInfo.StorageTypeEnum.FILE.name())); + if (!allowedStorageTypes.contains(storageConfigInfo.getStorageType().name())) { + LOG.atWarn() + .addKeyValue("storageConfig", storageConfigInfo) + .log("Disallowed storage type in catalog"); + throw new IllegalArgumentException( + "Unsupported storage type: " + storageConfigInfo.getStorageType()); + } + } + + /** From PolarisCatalogsApiService */ + @Override + public Response deleteCatalog(String catalogName, SecurityContext securityContext) { + PolarisAdminService adminService = newAdminService(securityContext); + adminService.deleteCatalog(catalogName); + return Response.status(Response.Status.NO_CONTENT).build(); + } + + /** From PolarisCatalogsApiService */ + @Override + public Response getCatalog(String catalogName, SecurityContext securityContext) { + PolarisAdminService adminService = newAdminService(securityContext); + return Response.ok(adminService.getCatalog(catalogName).asCatalog()).build(); + } + + /** From PolarisCatalogsApiService */ + @Override + public Response updateCatalog( + String catalogName, UpdateCatalogRequest updateRequest, SecurityContext securityContext) { + PolarisAdminService adminService = newAdminService(securityContext); + if (updateRequest.getStorageConfigInfo() != null) { + validateStorageConfig(updateRequest.getStorageConfigInfo()); + } + return Response.ok(adminService.updateCatalog(catalogName, updateRequest).asCatalog()).build(); + } + + /** From PolarisCatalogsApiService */ + @Override + public Response listCatalogs(SecurityContext securityContext) { + PolarisAdminService adminService = newAdminService(securityContext); + List catalogList = + adminService.listCatalogs().stream() + .map(CatalogEntity::new) + .map(CatalogEntity::asCatalog) + .toList(); + Catalogs catalogs = new Catalogs(catalogList); + LOG.debug("listCatalogs returning: {}", catalogs); + return Response.ok(catalogs).build(); + } + + /** From PolarisPrincipalsApiService */ + @Override + public Response createPrincipal(CreatePrincipalRequest request, SecurityContext securityContext) { + PolarisAdminService adminService = newAdminService(securityContext); + PrincipalEntity principal = PrincipalEntity.fromPrincipal(request.getPrincipal()); + if (Boolean.TRUE.equals(request.getCredentialRotationRequired())) { + principal = + new PrincipalEntity.Builder(principal).setCredentialRotationRequiredState().build(); + } + PrincipalWithCredentials createdPrincipal = adminService.createPrincipal(principal); + LOG.info("Created new principal {}", createdPrincipal); + return Response.status(Response.Status.CREATED).entity(createdPrincipal).build(); + } + + /** From PolarisPrincipalsApiService */ + @Override + public Response deletePrincipal(String principalName, SecurityContext securityContext) { + PolarisAdminService adminService = newAdminService(securityContext); + adminService.deletePrincipal(principalName); + return Response.status(Response.Status.NO_CONTENT).build(); + } + + /** From PolarisPrincipalsApiService */ + @Override + public Response getPrincipal(String principalName, SecurityContext securityContext) { + PolarisAdminService adminService = newAdminService(securityContext); + return Response.ok(adminService.getPrincipal(principalName).asPrincipal()).build(); + } + + /** From PolarisPrincipalsApiService */ + @Override + public Response updatePrincipal( + String principalName, UpdatePrincipalRequest updateRequest, SecurityContext securityContext) { + PolarisAdminService adminService = newAdminService(securityContext); + return Response.ok(adminService.updatePrincipal(principalName, updateRequest).asPrincipal()) + .build(); + } + + /** From PolarisPrincipalsApiService */ + @Override + public Response rotateCredentials(String principalName, SecurityContext securityContext) { + PolarisAdminService adminService = newAdminService(securityContext); + return Response.ok(adminService.rotateCredentials(principalName)).build(); + } + + /** From PolarisPrincipalsApiService */ + @Override + public Response listPrincipals(SecurityContext securityContext) { + PolarisAdminService adminService = newAdminService(securityContext); + List principalList = + adminService.listPrincipals().stream() + .map(PrincipalEntity::new) + .map(PrincipalEntity::asPrincipal) + .toList(); + Principals principals = new Principals(principalList); + LOG.debug("listPrincipals returning: {}", principals); + return Response.ok(principals).build(); + } + + /** From PolarisPrincipalRolesApiService */ + @Override + public Response createPrincipalRole( + CreatePrincipalRoleRequest request, SecurityContext securityContext) { + PolarisAdminService adminService = newAdminService(securityContext); + PrincipalRole newPrincipalRole = + new PrincipalRoleEntity( + adminService.createPrincipalRole( + PrincipalRoleEntity.fromPrincipalRole(request.getPrincipalRole()))) + .asPrincipalRole(); + LOG.info("Created new principalRole {}", newPrincipalRole); + return Response.status(Response.Status.CREATED).build(); + } + + /** From PolarisPrincipalRolesApiService */ + @Override + public Response deletePrincipalRole(String principalRoleName, SecurityContext securityContext) { + PolarisAdminService adminService = newAdminService(securityContext); + adminService.deletePrincipalRole(principalRoleName); + return Response.status(Response.Status.NO_CONTENT).build(); + } + + /** From PolarisPrincipalRolesApiService */ + @Override + public Response getPrincipalRole(String principalRoleName, SecurityContext securityContext) { + PolarisAdminService adminService = newAdminService(securityContext); + return Response.ok(adminService.getPrincipalRole(principalRoleName).asPrincipalRole()).build(); + } + + /** From PolarisPrincipalRolesApiService */ + @Override + public Response updatePrincipalRole( + String principalRoleName, + UpdatePrincipalRoleRequest updateRequest, + SecurityContext securityContext) { + PolarisAdminService adminService = newAdminService(securityContext); + return Response.ok( + adminService.updatePrincipalRole(principalRoleName, updateRequest).asPrincipalRole()) + .build(); + } + + /** From PolarisPrincipalRolesApiService */ + @Override + public Response listPrincipalRoles(SecurityContext securityContext) { + PolarisAdminService adminService = newAdminService(securityContext); + List principalRoleList = + adminService.listPrincipalRoles().stream() + .map(PrincipalRoleEntity::new) + .map(PrincipalRoleEntity::asPrincipalRole) + .toList(); + PrincipalRoles principalRoles = new PrincipalRoles(principalRoleList); + LOG.debug("listPrincipalRoles returning: {}", principalRoles); + return Response.ok(principalRoles).build(); + } + + /** From PolarisCatalogsApiService */ + @Override + public Response createCatalogRole( + String catalogName, CreateCatalogRoleRequest request, SecurityContext securityContext) { + PolarisAdminService adminService = newAdminService(securityContext); + CatalogRole newCatalogRole = + new CatalogRoleEntity( + adminService.createCatalogRole( + catalogName, CatalogRoleEntity.fromCatalogRole(request.getCatalogRole()))) + .asCatalogRole(); + LOG.info("Created new catalogRole {}", newCatalogRole); + return Response.status(Response.Status.CREATED).build(); + } + + /** From PolarisCatalogsApiService */ + @Override + public Response deleteCatalogRole( + String catalogName, String catalogRoleName, SecurityContext securityContext) { + PolarisAdminService adminService = newAdminService(securityContext); + adminService.deleteCatalogRole(catalogName, catalogRoleName); + return Response.status(Response.Status.NO_CONTENT).build(); + } + + /** From PolarisCatalogsApiService */ + @Override + public Response getCatalogRole( + String catalogName, String catalogRoleName, SecurityContext securityContext) { + PolarisAdminService adminService = newAdminService(securityContext); + return Response.ok(adminService.getCatalogRole(catalogName, catalogRoleName).asCatalogRole()) + .build(); + } + + /** From PolarisCatalogsApiService */ + @Override + public Response updateCatalogRole( + String catalogName, + String catalogRoleName, + UpdateCatalogRoleRequest updateRequest, + SecurityContext securityContext) { + PolarisAdminService adminService = newAdminService(securityContext); + return Response.ok( + adminService + .updateCatalogRole(catalogName, catalogRoleName, updateRequest) + .asCatalogRole()) + .build(); + } + + /** From PolarisCatalogsApiService */ + @Override + public Response listCatalogRoles(String catalogName, SecurityContext securityContext) { + PolarisAdminService adminService = newAdminService(securityContext); + List catalogRoleList = + adminService.listCatalogRoles(catalogName).stream() + .map(CatalogRoleEntity::new) + .map(CatalogRoleEntity::asCatalogRole) + .toList(); + CatalogRoles catalogRoles = new CatalogRoles(catalogRoleList); + LOG.debug("listCatalogRoles returning: {}", catalogRoles); + return Response.ok(catalogRoles).build(); + } + + /** From PolarisPrincipalsApiService */ + @Override + public Response assignPrincipalRole( + String principalName, GrantPrincipalRoleRequest request, SecurityContext securityContext) { + LOG.info( + "Assigning principalRole {} to principal {}", + request.getPrincipalRole().getName(), + principalName); + PolarisAdminService adminService = newAdminService(securityContext); + adminService.assignPrincipalRole(principalName, request.getPrincipalRole().getName()); + return Response.status(Response.Status.CREATED).build(); + } + + /** From PolarisPrincipalsApiService */ + @Override + public Response revokePrincipalRole( + String principalName, String principalRoleName, SecurityContext securityContext) { + LOG.info("Revoking principalRole {} from principal {}", principalRoleName, principalName); + PolarisAdminService adminService = newAdminService(securityContext); + adminService.revokePrincipalRole(principalName, principalRoleName); + return Response.status(Response.Status.NO_CONTENT).build(); + } + + /** From PolarisPrincipalsApiService */ + @Override + public Response listPrincipalRolesAssigned( + String principalName, SecurityContext securityContext) { + PolarisAdminService adminService = newAdminService(securityContext); + List principalRoleList = + adminService.listPrincipalRolesAssigned(principalName).stream() + .map(PrincipalRoleEntity::new) + .map(PrincipalRoleEntity::asPrincipalRole) + .toList(); + PrincipalRoles principalRoles = new PrincipalRoles(principalRoleList); + LOG.debug("listPrincipalRolesAssigned returning: {}", principalRoles); + return Response.ok(principalRoles).build(); + } + + /** From PolarisPrincipalRolesApiService */ + @Override + public Response assignCatalogRoleToPrincipalRole( + String principalRoleName, + String catalogName, + GrantCatalogRoleRequest request, + SecurityContext securityContext) { + LOG.info( + "Assigning catalogRole {} in catalog {} to principalRole {}", + request.getCatalogRole().getName(), + catalogName, + principalRoleName); + PolarisAdminService adminService = newAdminService(securityContext); + adminService.assignCatalogRoleToPrincipalRole( + principalRoleName, catalogName, request.getCatalogRole().getName()); + return Response.status(Response.Status.CREATED).build(); + } + + /** From PolarisPrincipalRolesApiService */ + @Override + public Response revokeCatalogRoleFromPrincipalRole( + String principalRoleName, + String catalogName, + String catalogRoleName, + SecurityContext securityContext) { + LOG.info( + "Revoking catalogRole {} in catalog {} from principalRole {}", + catalogRoleName, + catalogName, + principalRoleName); + PolarisAdminService adminService = newAdminService(securityContext); + adminService.revokeCatalogRoleFromPrincipalRole( + principalRoleName, catalogName, catalogRoleName); + return Response.status(Response.Status.NO_CONTENT).build(); + } + + /** From PolarisPrincipalRolesApiService */ + @Override + public Response listAssigneePrincipalsForPrincipalRole( + String principalRoleName, SecurityContext securityContext) { + PolarisAdminService adminService = newAdminService(securityContext); + List principalList = + adminService.listAssigneePrincipalsForPrincipalRole(principalRoleName).stream() + .map(PrincipalEntity::new) + .map(PrincipalEntity::asPrincipal) + .toList(); + Principals principals = new Principals(principalList); + LOG.debug("listAssigneePrincipalsForPrincipalRole returning: {}", principals); + return Response.ok(principals).build(); + } + + /** From PolarisPrincipalRolesApiService */ + @Override + public Response listCatalogRolesForPrincipalRole( + String principalRoleName, String catalogName, SecurityContext securityContext) { + PolarisAdminService adminService = newAdminService(securityContext); + List catalogRoleList = + adminService.listCatalogRolesForPrincipalRole(principalRoleName, catalogName).stream() + .map(CatalogRoleEntity::new) + .map(CatalogRoleEntity::asCatalogRole) + .toList(); + CatalogRoles catalogRoles = new CatalogRoles(catalogRoleList); + LOG.debug("listCatalogRolesForPrincipalRole returning: {}", catalogRoles); + return Response.ok(catalogRoles).build(); + } + + /** From PolarisCatalogsApiService */ + @Override + public Response addGrantToCatalogRole( + String catalogName, + String catalogRoleName, + AddGrantRequest grantRequest, + SecurityContext securityContext) { + LOG.info( + "Adding grant {} to catalogRole {} in catalog {}", + grantRequest, + catalogRoleName, + catalogName); + PolarisAdminService adminService = newAdminService(securityContext); + switch (grantRequest.getGrant()) { + // The per-securable-type Privilege enums must be exact String match for a subset of all + // PolarisPrivilege values. + case ViewGrant viewGrant: + { + PolarisPrivilege privilege = + PolarisPrivilege.valueOf(viewGrant.getPrivilege().toString()); + String viewName = viewGrant.getViewName(); + String[] namespaceParts = viewGrant.getNamespace().toArray(new String[0]); + adminService.grantPrivilegeOnViewToRole( + catalogName, + catalogRoleName, + TableIdentifier.of(Namespace.of(namespaceParts), viewName), + privilege); + break; + } + case TableGrant tableGrant: + { + PolarisPrivilege privilege = + PolarisPrivilege.valueOf(tableGrant.getPrivilege().toString()); + String tableName = tableGrant.getTableName(); + String[] namespaceParts = tableGrant.getNamespace().toArray(new String[0]); + adminService.grantPrivilegeOnTableToRole( + catalogName, + catalogRoleName, + TableIdentifier.of(Namespace.of(namespaceParts), tableName), + privilege); + break; + } + case NamespaceGrant namespaceGrant: + { + PolarisPrivilege privilege = + PolarisPrivilege.valueOf(namespaceGrant.getPrivilege().toString()); + String[] namespaceParts = namespaceGrant.getNamespace().toArray(new String[0]); + adminService.grantPrivilegeOnNamespaceToRole( + catalogName, catalogRoleName, Namespace.of(namespaceParts), privilege); + break; + } + case CatalogGrant catalogGrant: + { + PolarisPrivilege privilege = + PolarisPrivilege.valueOf(catalogGrant.getPrivilege().toString()); + adminService.grantPrivilegeOnCatalogToRole(catalogName, catalogRoleName, privilege); + break; + } + default: + LOG.atWarn() + .addKeyValue("catalog", catalogName) + .addKeyValue("role", catalogRoleName) + .log("Don't know how to handle privilege grant: {}", grantRequest); + return Response.status(Response.Status.BAD_REQUEST).build(); + } + return Response.status(Response.Status.CREATED).build(); + } + + /** From PolarisCatalogsApiService */ + @Override + public Response revokeGrantFromCatalogRole( + String catalogName, + String catalogRoleName, + Boolean cascade, + RevokeGrantRequest grantRequest, + SecurityContext securityContext) { + LOG.info( + "Revoking grant {} from catalogRole {} in catalog {}", + grantRequest, + catalogRoleName, + catalogName); + if (cascade != null && cascade.booleanValue()) { + LOG.warn("Tried to use unimplemented 'cascade' feature when revoking grants."); + return Response.status(501).build(); // not implemented + } + + PolarisAdminService adminService = newAdminService(securityContext); + switch (grantRequest.getGrant()) { + // The per-securable-type Privilege enums must be exact String match for a subset of all + // PolarisPrivilege values. + case ViewGrant viewGrant: + { + PolarisPrivilege privilege = + PolarisPrivilege.valueOf(viewGrant.getPrivilege().toString()); + String viewName = viewGrant.getViewName(); + String[] namespaceParts = viewGrant.getNamespace().toArray(new String[0]); + adminService.revokePrivilegeOnViewFromRole( + catalogName, + catalogRoleName, + TableIdentifier.of(Namespace.of(namespaceParts), viewName), + privilege); + break; + } + case TableGrant tableGrant: + { + PolarisPrivilege privilege = + PolarisPrivilege.valueOf(tableGrant.getPrivilege().toString()); + String tableName = tableGrant.getTableName(); + String[] namespaceParts = tableGrant.getNamespace().toArray(new String[0]); + adminService.revokePrivilegeOnTableFromRole( + catalogName, + catalogRoleName, + TableIdentifier.of(Namespace.of(namespaceParts), tableName), + privilege); + break; + } + case NamespaceGrant namespaceGrant: + { + PolarisPrivilege privilege = + PolarisPrivilege.valueOf(namespaceGrant.getPrivilege().toString()); + String[] namespaceParts = namespaceGrant.getNamespace().toArray(new String[0]); + adminService.revokePrivilegeOnNamespaceFromRole( + catalogName, catalogRoleName, Namespace.of(namespaceParts), privilege); + break; + } + case CatalogGrant catalogGrant: + { + PolarisPrivilege privilege = + PolarisPrivilege.valueOf(catalogGrant.getPrivilege().toString()); + adminService.revokePrivilegeOnCatalogFromRole(catalogName, catalogRoleName, privilege); + break; + } + default: + LOG.atWarn() + .addKeyValue("catalog", catalogName) + .addKeyValue("role", catalogRoleName) + .log("Don't know how to handle privilege revocation: {}", grantRequest); + return Response.status(Response.Status.BAD_REQUEST).build(); + } + return Response.status(Response.Status.CREATED).build(); + } + + /** From PolarisCatalogsApiService */ + @Override + public Response listAssigneePrincipalRolesForCatalogRole( + String catalogName, String catalogRoleName, SecurityContext securityContext) { + PolarisAdminService adminService = newAdminService(securityContext); + List principalRoleList = + adminService.listAssigneePrincipalRolesForCatalogRole(catalogName, catalogRoleName).stream() + .map(PrincipalRoleEntity::new) + .map(PrincipalRoleEntity::asPrincipalRole) + .toList(); + PrincipalRoles principalRoles = new PrincipalRoles(principalRoleList); + LOG.debug("listAssigneePrincipalRolesForCatalogRole returning: {}", principalRoles); + return Response.ok(principalRoles).build(); + } + + /** From PolarisCatalogsApiService */ + @Override + public Response listGrantsForCatalogRole( + String catalogName, String catalogRoleName, SecurityContext securityContext) { + PolarisAdminService adminService = newAdminService(securityContext); + List grantList = + adminService.listGrantsForCatalogRole(catalogName, catalogRoleName); + GrantResources grantResources = new GrantResources(grantList); + return Response.ok(grantResources).build(); + } +} diff --git a/polaris-service/src/main/java/io/polaris/service/auth/BasePolarisAuthenticator.java b/polaris-service/src/main/java/io/polaris/service/auth/BasePolarisAuthenticator.java new file mode 100644 index 0000000000..18c9554780 --- /dev/null +++ b/polaris-service/src/main/java/io/polaris/service/auth/BasePolarisAuthenticator.java @@ -0,0 +1,102 @@ +package io.polaris.service.auth; + +import io.polaris.core.PolarisCallContext; +import io.polaris.core.auth.AuthenticatedPolarisPrincipal; +import io.polaris.core.context.CallContext; +import io.polaris.core.context.RealmContext; +import io.polaris.core.entity.PolarisEntity; +import io.polaris.core.entity.PolarisEntitySubType; +import io.polaris.core.entity.PolarisEntityType; +import io.polaris.core.entity.PrincipalEntity; +import io.polaris.core.persistence.PolarisMetaStoreManager; +import io.polaris.service.config.RealmEntityManagerFactory; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.apache.iceberg.exceptions.NotAuthorizedException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Base implementation of {@link DiscoverableAuthenticator} constructs a {@link + * AuthenticatedPolarisPrincipal} from the token parsed by subclasses. The {@link + * AuthenticatedPolarisPrincipal} is read from the {@link PolarisMetaStoreManager} for the current + * {@link RealmContext}. If the token defines a non-empty set of scopes, only the principal roles + * specified in the scopes will be active for the current principal. Only the grants assigned to + * these roles will be active in the current request. + */ +public abstract class BasePolarisAuthenticator + implements DiscoverableAuthenticator { + public static final String PRINCIPAL_ROLE_ALL = "PRINCIPAL_ROLE:ALL"; + public static final String PRINCIPAL_ROLE_PREFIX = "PRINCIPAL_ROLE:"; + private static final Logger LOGGER = LoggerFactory.getLogger(BasePolarisAuthenticator.class); + + protected RealmEntityManagerFactory entityManagerFactory; + + public void setEntityManagerFactory(RealmEntityManagerFactory entityManagerFactory) { + this.entityManagerFactory = entityManagerFactory; + } + + public PolarisCallContext getCurrentPolarisContext() { + return CallContext.getCurrentContext().getPolarisCallContext(); + } + + protected Optional getPrincipal(DecodedToken tokenInfo) { + LOGGER.debug("Resolving principal for tokenInfo client_id={}", tokenInfo.getClientId()); + RealmContext realmContext = CallContext.getCurrentContext().getRealmContext(); + PolarisMetaStoreManager metaStoreManager = + entityManagerFactory.getOrCreateEntityManager(realmContext).getMetaStoreManager(); + PolarisEntity principal; + try { + principal = + tokenInfo.getPrincipalId() > 0 + ? PolarisEntity.of( + metaStoreManager.loadEntity( + getCurrentPolarisContext(), 0L, tokenInfo.getPrincipalId())) + : PolarisEntity.of( + metaStoreManager.readEntityByName( + getCurrentPolarisContext(), + null, + PolarisEntityType.PRINCIPAL, + PolarisEntitySubType.NULL_SUBTYPE, + tokenInfo.getSub())); + } catch (Exception e) { + LoggerFactory.getLogger(BasePolarisAuthenticator.class) + .atError() + .addKeyValue("errMsg", e.getMessage()) + .addKeyValue("stackTrace", ExceptionUtils.getStackTrace(e)) + .log("Unable to authenticate user with token"); + throw new NotAuthorizedException("Unable to authenticate"); + } + if (principal == null) { + LOGGER.warn( + "Failed to resolve principal from tokenInfo client_id={}", tokenInfo.getClientId()); + throw new NotAuthorizedException("Unable to authenticate"); + } + + Set activatedPrincipalRoles = new HashSet<>(); + // TODO: Consolidate the divergent "scopes" logic between test-bearer-token and token-exchange. + if (tokenInfo.getScope() != null && !tokenInfo.getScope().equals(PRINCIPAL_ROLE_ALL)) { + activatedPrincipalRoles.addAll( + Arrays.stream(tokenInfo.getScope().split(" ")) + .map( + s -> // strip the principal_role prefix, if present + s.startsWith(PRINCIPAL_ROLE_PREFIX) + ? s.substring(PRINCIPAL_ROLE_PREFIX.length()) + : s) + .toList()); + } + + LOGGER.debug("Resolved principal: {}", principal); + + AuthenticatedPolarisPrincipal authenticatedPrincipal = + new AuthenticatedPolarisPrincipal(new PrincipalEntity(principal), activatedPrincipalRoles); + LOGGER.debug("Populating authenticatedPrincipal into CallContext: {}", authenticatedPrincipal); + CallContext.getCurrentContext() + .contextVariables() + .put(CallContext.AUTHENTICATED_PRINCIPAL, authenticatedPrincipal); + return Optional.of(authenticatedPrincipal); + } +} diff --git a/polaris-service/src/main/java/io/polaris/service/auth/DecodedToken.java b/polaris-service/src/main/java/io/polaris/service/auth/DecodedToken.java new file mode 100644 index 0000000000..2274ff12eb --- /dev/null +++ b/polaris-service/src/main/java/io/polaris/service/auth/DecodedToken.java @@ -0,0 +1,11 @@ +package io.polaris.service.auth; + +public interface DecodedToken { + Long getPrincipalId(); + + String getClientId(); + + String getSub(); + + String getScope(); +} diff --git a/polaris-service/src/main/java/io/polaris/service/auth/DefaultOAuth2ApiService.java b/polaris-service/src/main/java/io/polaris/service/auth/DefaultOAuth2ApiService.java new file mode 100644 index 0000000000..a1091a8ac0 --- /dev/null +++ b/polaris-service/src/main/java/io/polaris/service/auth/DefaultOAuth2ApiService.java @@ -0,0 +1,80 @@ +package io.polaris.service.auth; + +import com.fasterxml.jackson.annotation.JsonTypeName; +import io.polaris.core.context.CallContext; +import io.polaris.service.config.HasEntityManagerFactory; +import io.polaris.service.config.OAuth2ApiService; +import io.polaris.service.config.RealmEntityManagerFactory; +import io.polaris.service.types.TokenType; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.SecurityContext; +import org.apache.hadoop.hdfs.web.oauth2.OAuth2Constants; +import org.apache.iceberg.rest.responses.OAuthTokenResponse; + +/** + * Default implementation of the {@link OAuth2ApiService} that generates a JWT token for the client + * if the client secret matches. + */ +@JsonTypeName("default") +public class DefaultOAuth2ApiService implements OAuth2ApiService, HasEntityManagerFactory { + private TokenBrokerFactory tokenBrokerFactory; + + public DefaultOAuth2ApiService() {} + + @Override + public Response getToken( + String grantType, + String scope, + String clientId, + String clientSecret, + TokenType requestedTokenType, + String subjectToken, + TokenType subjectTokenType, + String actorToken, + TokenType actorTokenType, + SecurityContext securityContext) { + + TokenBroker tokenBroker = + tokenBrokerFactory.apply(CallContext.getCurrentContext().getRealmContext()); + if (!tokenBroker.supportsGrantType(grantType)) { + return OAuthUtils.getResponseFromError(OAuthTokenErrorResponse.Error.unsupported_grant_type); + } + if (!tokenBroker.supportsRequestedTokenType(requestedTokenType)) { + return OAuthUtils.getResponseFromError(OAuthTokenErrorResponse.Error.invalid_request); + } + TokenResponse tokenResponse = + switch (subjectTokenType) { + case TokenType.ID_TOKEN, + TokenType.REFRESH_TOKEN, + TokenType.JWT, + TokenType.SAML1, + TokenType.SAML2 -> + new TokenResponse(OAuthTokenErrorResponse.Error.invalid_request); + case TokenType.ACCESS_TOKEN -> + tokenBroker.generateFromToken(subjectTokenType, subjectToken, grantType, scope); + case null -> + tokenBroker.generateFromClientSecrets(clientId, clientSecret, grantType, scope); + }; + if (!tokenResponse.isValid()) { + return OAuthUtils.getResponseFromError(tokenResponse.getError()); + } + return Response.ok( + OAuthTokenResponse.builder() + .withToken(tokenResponse.getAccessToken()) + .withTokenType(OAuth2Constants.BEARER) + .setExpirationInSeconds(tokenResponse.getExpiresIn()) + .build()) + .build(); + } + + @Override + public void setEntityManagerFactory(RealmEntityManagerFactory entityManagerFactory) { + if (tokenBrokerFactory instanceof HasEntityManagerFactory hemf) { + hemf.setEntityManagerFactory(entityManagerFactory); + } + } + + public void setTokenBroker(TokenBrokerFactory tokenBrokerFactory) { + this.tokenBrokerFactory = tokenBrokerFactory; + } +} diff --git a/polaris-service/src/main/java/io/polaris/service/auth/DefaultPolarisAuthenticator.java b/polaris-service/src/main/java/io/polaris/service/auth/DefaultPolarisAuthenticator.java new file mode 100644 index 0000000000..8d5c7b47a2 --- /dev/null +++ b/polaris-service/src/main/java/io/polaris/service/auth/DefaultPolarisAuthenticator.java @@ -0,0 +1,33 @@ +package io.polaris.service.auth; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.polaris.core.auth.AuthenticatedPolarisPrincipal; +import io.polaris.core.context.CallContext; +import io.polaris.service.config.HasEntityManagerFactory; +import io.polaris.service.config.RealmEntityManagerFactory; +import java.util.Optional; + +public class DefaultPolarisAuthenticator extends BasePolarisAuthenticator { + private TokenBrokerFactory tokenBrokerFactory; + + @Override + public Optional authenticate(String credentials) { + TokenBroker handler = + tokenBrokerFactory.apply(CallContext.getCurrentContext().getRealmContext()); + DecodedToken decodedToken = handler.verify(credentials); + return getPrincipal(decodedToken); + } + + @Override + public void setEntityManagerFactory(RealmEntityManagerFactory entityManagerFactory) { + super.setEntityManagerFactory(entityManagerFactory); + if (tokenBrokerFactory instanceof HasEntityManagerFactory) { + ((HasEntityManagerFactory) tokenBrokerFactory).setEntityManagerFactory(entityManagerFactory); + } + } + + @JsonProperty("tokenBroker") + public void setTokenBroker(TokenBrokerFactory tokenBrokerFactory) { + this.tokenBrokerFactory = tokenBrokerFactory; + } +} diff --git a/polaris-service/src/main/java/io/polaris/service/auth/DiscoverableAuthenticator.java b/polaris-service/src/main/java/io/polaris/service/auth/DiscoverableAuthenticator.java new file mode 100644 index 0000000000..c5c1f1b73a --- /dev/null +++ b/polaris-service/src/main/java/io/polaris/service/auth/DiscoverableAuthenticator.java @@ -0,0 +1,20 @@ +package io.polaris.service.auth; + +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.dropwizard.auth.Authenticator; +import io.dropwizard.jackson.Discoverable; +import io.polaris.service.config.HasEntityManagerFactory; +import java.security.Principal; + +/** + * Extension of the {@link Authenticator} interface that extends {@link Discoverable} so + * implementations can be discovered using the mechanisms described in + * https://www.dropwizard.io/en/stable/manual/configuration.html#polymorphic-configuration . The + * default implementation is {@link TestInlineBearerTokenPolarisAuthenticator}. + * + * @param + * @param

+ */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, property = "class") +public interface DiscoverableAuthenticator + extends Authenticator, Discoverable, HasEntityManagerFactory {} diff --git a/polaris-service/src/main/java/io/polaris/service/auth/JWTBroker.java b/polaris-service/src/main/java/io/polaris/service/auth/JWTBroker.java new file mode 100644 index 0000000000..4f653ca905 --- /dev/null +++ b/polaris-service/src/main/java/io/polaris/service/auth/JWTBroker.java @@ -0,0 +1,149 @@ +package io.polaris.service.auth; + +import com.auth0.jwt.JWT; +import com.auth0.jwt.algorithms.Algorithm; +import com.auth0.jwt.interfaces.DecodedJWT; +import com.auth0.jwt.interfaces.JWTVerifier; +import io.polaris.core.context.CallContext; +import io.polaris.core.entity.PolarisEntityType; +import io.polaris.core.entity.PrincipalEntity; +import io.polaris.core.persistence.PolarisEntityManager; +import io.polaris.core.persistence.PolarisMetaStoreManager; +import io.polaris.service.types.TokenType; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Optional; +import java.util.UUID; +import org.apache.commons.lang3.StringUtils; +import org.apache.iceberg.exceptions.NotAuthorizedException; + +/** Generates a JWT Token. */ +abstract class JWTBroker implements TokenBroker { + + private static final String ISSUER_KEY = "polaris"; + private static final String CLAIM_KEY_ACTIVE = "active"; + private static final String CLAIM_KEY_CLIENT_ID = "client_id"; + private static final String CLAIM_KEY_PRINCIPAL_ID = "principalId"; + private static final String CLAIM_KEY_SCOPE = "scope"; + + private final PolarisEntityManager entityManager; + private final int maxTokenGenerationInSeconds; + + JWTBroker(PolarisEntityManager entityManager, int maxTokenGenerationInSeconds) { + this.entityManager = entityManager; + this.maxTokenGenerationInSeconds = maxTokenGenerationInSeconds; + } + + abstract Algorithm getAlgorithm(); + + public DecodedToken verify(String token) { + JWTVerifier verifier = JWT.require(getAlgorithm()).build(); + DecodedJWT decodedJWT = verifier.verify(token); + Boolean isActive = decodedJWT.getClaim(CLAIM_KEY_ACTIVE).asBoolean(); + if (isActive == null || !isActive) { + throw new NotAuthorizedException("Token is not active"); + } + if (decodedJWT.getExpiresAtAsInstant().isBefore(Instant.now())) { + throw new NotAuthorizedException("Token has expired"); + } + return new DecodedToken() { + @Override + public Long getPrincipalId() { + return decodedJWT.getClaim("principalId").asLong(); + } + + @Override + public String getClientId() { + return decodedJWT.getClaim("client_id").asString(); + } + + @Override + public String getSub() { + return decodedJWT.getSubject(); + } + + @Override + public String getScope() { + return decodedJWT.getClaim("scope").asString(); + } + }; + } + + @Override + public TokenResponse generateFromToken( + TokenType tokenType, String subjectToken, String grantType, String scope) { + if (!TokenType.ACCESS_TOKEN.equals(tokenType)) { + return new TokenResponse(OAuthTokenErrorResponse.Error.invalid_request); + } + if (StringUtils.isBlank(subjectToken)) { + return new TokenResponse(OAuthTokenErrorResponse.Error.invalid_request); + } + DecodedToken decodedToken = verify(subjectToken); + PolarisMetaStoreManager.EntityResult principalLookup = + entityManager + .getMetaStoreManager() + .loadEntity( + CallContext.getCurrentContext().getPolarisCallContext(), + 0L, + decodedToken.getPrincipalId()); + if (!principalLookup.isSuccess() + || principalLookup.getEntity().getType() != PolarisEntityType.PRINCIPAL) { + return new TokenResponse(OAuthTokenErrorResponse.Error.unauthorized_client); + } + String tokenString = + generateTokenString( + decodedToken.getClientId(), decodedToken.getScope(), decodedToken.getPrincipalId()); + return new TokenResponse( + tokenString, TokenType.ACCESS_TOKEN.getValue(), maxTokenGenerationInSeconds); + } + + @Override + public TokenResponse generateFromClientSecrets( + String clientId, String clientSecret, String grantType, String scope) { + // Initial sanity checks + TokenRequestValidator validator = new TokenRequestValidator(); + Optional initialValidationResponse = + validator.validateForClientCredentialsFlow(clientId, clientSecret, grantType, scope); + if (initialValidationResponse.isPresent()) { + return new TokenResponse(initialValidationResponse.get()); + } + + Optional principal = + TokenBroker.findPrincipalEntity(entityManager, clientId, clientSecret); + if (principal.isEmpty()) { + return new TokenResponse(OAuthTokenErrorResponse.Error.unauthorized_client); + } + String tokenString = generateTokenString(clientId, scope, principal.get().getId()); + return new TokenResponse( + tokenString, TokenType.ACCESS_TOKEN.getValue(), maxTokenGenerationInSeconds); + } + + private String generateTokenString(String clientId, String scope, Long principalId) { + Instant now = Instant.now(); + return JWT.create() + .withIssuer(ISSUER_KEY) + .withSubject(String.valueOf(principalId)) + .withIssuedAt(now) + .withExpiresAt(now.plus(maxTokenGenerationInSeconds, ChronoUnit.SECONDS)) + .withJWTId(UUID.randomUUID().toString()) + .withClaim(CLAIM_KEY_ACTIVE, true) + .withClaim(CLAIM_KEY_CLIENT_ID, clientId) + .withClaim(CLAIM_KEY_PRINCIPAL_ID, principalId) + .withClaim(CLAIM_KEY_SCOPE, scopes(scope)) + .sign(getAlgorithm()); + } + + @Override + public boolean supportsGrantType(String grantType) { + return TokenRequestValidator.ALLOWED_GRANT_TYPES.contains(grantType); + } + + @Override + public boolean supportsRequestedTokenType(TokenType tokenType) { + return tokenType == null || TokenType.ACCESS_TOKEN.equals(tokenType); + } + + private String scopes(String scope) { + return StringUtils.isNotBlank(scope) ? scope : BasePolarisAuthenticator.PRINCIPAL_ROLE_ALL; + } +} diff --git a/polaris-service/src/main/java/io/polaris/service/auth/JWTRSAKeyPair.java b/polaris-service/src/main/java/io/polaris/service/auth/JWTRSAKeyPair.java new file mode 100644 index 0000000000..9cf33cef04 --- /dev/null +++ b/polaris-service/src/main/java/io/polaris/service/auth/JWTRSAKeyPair.java @@ -0,0 +1,25 @@ +package io.polaris.service.auth; + +import com.auth0.jwt.algorithms.Algorithm; +import io.polaris.core.persistence.PolarisEntityManager; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; + +/** Generates a JWT using a Public/Private RSA Key */ +public class JWTRSAKeyPair extends JWTBroker { + + JWTRSAKeyPair(PolarisEntityManager entityManager, int maxTokenGenerationInSeconds) { + super(entityManager, maxTokenGenerationInSeconds); + } + + KeyProvider getKeyProvider() { + return new LocalRSAKeyProvider(); + } + + @Override + Algorithm getAlgorithm() { + KeyProvider keyProvider = getKeyProvider(); + return Algorithm.RSA256( + (RSAPublicKey) keyProvider.getPublicKey(), (RSAPrivateKey) keyProvider.getPrivateKey()); + } +} diff --git a/polaris-service/src/main/java/io/polaris/service/auth/JWTRSAKeyPairFactory.java b/polaris-service/src/main/java/io/polaris/service/auth/JWTRSAKeyPairFactory.java new file mode 100644 index 0000000000..26f5f0a70c --- /dev/null +++ b/polaris-service/src/main/java/io/polaris/service/auth/JWTRSAKeyPairFactory.java @@ -0,0 +1,28 @@ +package io.polaris.service.auth; + +import com.fasterxml.jackson.annotation.JsonTypeName; +import io.polaris.core.context.RealmContext; +import io.polaris.service.config.HasEntityManagerFactory; +import io.polaris.service.config.RealmEntityManagerFactory; + +@JsonTypeName("rsa-key-pair") +public class JWTRSAKeyPairFactory implements TokenBrokerFactory, HasEntityManagerFactory { + private int maxTokenGenerationInSeconds = 3600; + private RealmEntityManagerFactory realmEntityManagerFactory; + + public void setMaxTokenGenerationInSeconds(int maxTokenGenerationInSeconds) { + this.maxTokenGenerationInSeconds = maxTokenGenerationInSeconds; + } + + @Override + public TokenBroker apply(RealmContext realmContext) { + return new JWTRSAKeyPair( + realmEntityManagerFactory.getOrCreateEntityManager(realmContext), + maxTokenGenerationInSeconds); + } + + @Override + public void setEntityManagerFactory(RealmEntityManagerFactory entityManagerFactory) { + this.realmEntityManagerFactory = entityManagerFactory; + } +} diff --git a/polaris-service/src/main/java/io/polaris/service/auth/JWTSymmetricKeyBroker.java b/polaris-service/src/main/java/io/polaris/service/auth/JWTSymmetricKeyBroker.java new file mode 100644 index 0000000000..467e33e1d8 --- /dev/null +++ b/polaris-service/src/main/java/io/polaris/service/auth/JWTSymmetricKeyBroker.java @@ -0,0 +1,23 @@ +package io.polaris.service.auth; + +import com.auth0.jwt.algorithms.Algorithm; +import io.polaris.core.persistence.PolarisEntityManager; +import java.util.function.Supplier; + +/** Generates a JWT using a Symmetric Key. */ +public class JWTSymmetricKeyBroker extends JWTBroker { + private final Supplier secretSupplier; + + JWTSymmetricKeyBroker( + PolarisEntityManager entityManager, + int maxTokenGenerationInSeconds, + Supplier secretSupplier) { + super(entityManager, maxTokenGenerationInSeconds); + this.secretSupplier = secretSupplier; + } + + @Override + Algorithm getAlgorithm() { + return Algorithm.HMAC256(secretSupplier.get()); + } +} diff --git a/polaris-service/src/main/java/io/polaris/service/auth/JWTSymmetricKeyFactory.java b/polaris-service/src/main/java/io/polaris/service/auth/JWTSymmetricKeyFactory.java new file mode 100644 index 0000000000..a056c6bd74 --- /dev/null +++ b/polaris-service/src/main/java/io/polaris/service/auth/JWTSymmetricKeyFactory.java @@ -0,0 +1,57 @@ +package io.polaris.service.auth; + +import com.fasterxml.jackson.annotation.JsonTypeName; +import io.polaris.core.context.RealmContext; +import io.polaris.service.config.HasEntityManagerFactory; +import io.polaris.service.config.RealmEntityManagerFactory; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.function.Supplier; + +@JsonTypeName("symmetric-key") +public class JWTSymmetricKeyFactory implements TokenBrokerFactory, HasEntityManagerFactory { + private RealmEntityManagerFactory realmEntityManagerFactory; + private int maxTokenGenerationInSeconds = 3600; + private String file; + private String secret; + + @Override + public TokenBroker apply(RealmContext realmContext) { + if (file == null && secret == null) { + throw new IllegalStateException("Either file or secret must be set"); + } + Supplier secretSupplier = secret != null ? () -> secret : readSecretFromDisk(); + return new JWTSymmetricKeyBroker( + realmEntityManagerFactory.getOrCreateEntityManager(realmContext), + maxTokenGenerationInSeconds, + secretSupplier); + } + + private Supplier readSecretFromDisk() { + return () -> { + try { + return Files.readString(Paths.get(file)); + } catch (IOException e) { + throw new RuntimeException("Failed to read secret from file: " + file, e); + } + }; + } + + public void setMaxTokenGenerationInSeconds(int maxTokenGenerationInSeconds) { + this.maxTokenGenerationInSeconds = maxTokenGenerationInSeconds; + } + + public void setFile(String file) { + this.file = file; + } + + public void setSecret(String secret) { + this.secret = secret; + } + + @Override + public void setEntityManagerFactory(RealmEntityManagerFactory entityManagerFactory) { + this.realmEntityManagerFactory = entityManagerFactory; + } +} diff --git a/polaris-service/src/main/java/io/polaris/service/auth/KeyProvider.java b/polaris-service/src/main/java/io/polaris/service/auth/KeyProvider.java new file mode 100644 index 0000000000..8adb335bd4 --- /dev/null +++ b/polaris-service/src/main/java/io/polaris/service/auth/KeyProvider.java @@ -0,0 +1,13 @@ +package io.polaris.service.auth; + +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.dropwizard.jackson.Discoverable; +import java.security.PrivateKey; +import java.security.PublicKey; + +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") +public interface KeyProvider extends Discoverable { + PublicKey getPublicKey(); + + PrivateKey getPrivateKey(); +} diff --git a/polaris-service/src/main/java/io/polaris/service/auth/LocalRSAKeyProvider.java b/polaris-service/src/main/java/io/polaris/service/auth/LocalRSAKeyProvider.java new file mode 100644 index 0000000000..ce055a967b --- /dev/null +++ b/polaris-service/src/main/java/io/polaris/service/auth/LocalRSAKeyProvider.java @@ -0,0 +1,64 @@ +package io.polaris.service.auth; + +import io.polaris.core.PolarisCallContext; +import io.polaris.core.context.CallContext; +import java.io.IOException; +import java.security.PrivateKey; +import java.security.PublicKey; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Class that can load public / private keys stored on localhost. Meant to be a simple + * implementation for now where a PEM file is loaded off disk. + */ +public class LocalRSAKeyProvider implements KeyProvider { + + private static final String LOCAL_PRIVATE_KEY_LOCATION_KEY = "LOCAL_PRIVATE_KEY_LOCATION_KEY"; + private static final String LOCAL_PUBLIC_KEY_LOCATION_KEY = "LOCAL_PUBLIC_LOCATION_KEY"; + + private static final Logger LOGGER = LoggerFactory.getLogger(LocalRSAKeyProvider.class); + + private String getLocation(String configKey) { + CallContext callContext = CallContext.getCurrentContext(); + PolarisCallContext pCtx = callContext.getPolarisCallContext(); + String fileLocation = pCtx.getConfigurationStore().getConfiguration(pCtx, configKey); + if (fileLocation == null) { + throw new RuntimeException("Cannot find location for key " + configKey); + } + return fileLocation; + } + + /** + * Getter for the Public Key instance + * + * @return the Public Key instance + */ + @Override + public PublicKey getPublicKey() { + final String publicKeyFileLocation = getLocation(LOCAL_PUBLIC_KEY_LOCATION_KEY); + try { + return PemUtils.readPublicKeyFromFile(publicKeyFileLocation, "RSA"); + } catch (IOException e) { + LOGGER.error("Unable to read public key from file {}", publicKeyFileLocation, e); + throw new RuntimeException("Unable to read public key from file " + publicKeyFileLocation, e); + } + } + + /** + * Getter for the Private Key instance. Used to sign the content on the JWT signing stage. + * + * @return the Private Key instance + */ + @Override + public PrivateKey getPrivateKey() { + final String privateKeyFileLocation = getLocation(LOCAL_PRIVATE_KEY_LOCATION_KEY); + try { + return PemUtils.readPrivateKeyFromFile(privateKeyFileLocation, "RSA"); + } catch (IOException e) { + LOGGER.error("Unable to read private key from file {}", privateKeyFileLocation, e); + throw new RuntimeException( + "Unable to read private key from file " + privateKeyFileLocation, e); + } + } +} diff --git a/polaris-service/src/main/java/io/polaris/service/auth/OAuthTokenErrorResponse.java b/polaris-service/src/main/java/io/polaris/service/auth/OAuthTokenErrorResponse.java new file mode 100644 index 0000000000..c37d950d35 --- /dev/null +++ b/polaris-service/src/main/java/io/polaris/service/auth/OAuthTokenErrorResponse.java @@ -0,0 +1,57 @@ +package io.polaris.service.auth; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** An OAuth Error Token Response as defined by the Iceberg REST API OpenAPI Spec. */ +public class OAuthTokenErrorResponse { + + public enum Error { + invalid_request("The request is invalid"), + invalid_client("The Client is invalid"), + invalid_grant("The grant is invalid"), + unauthorized_client("The client is not authorized"), + unsupported_grant_type("The grant type is invalid"), + invalid_scope("The scope is invalid"), + ; + + String errorDescription; + + Error(String errorDescription) { + this.errorDescription = errorDescription; + } + + public String getErrorDescription() { + return errorDescription; + } + } + + private final String error; + private final String errorDescription; + private String errorUri; + + /** + * Initlaizes a response from one of the supported errors + * + * @param error + */ + public OAuthTokenErrorResponse(Error error) { + this.error = error.name(); + this.errorDescription = error.getErrorDescription(); + this.errorUri = null; // Not yet used + } + + @JsonProperty("error") + public String getError() { + return error; + } + + @JsonProperty("error_description") + public String getErrorDescription() { + return errorDescription; + } + + @JsonProperty("error_uri") + public String getErrorUri() { + return errorUri; + } +} diff --git a/polaris-service/src/main/java/io/polaris/service/auth/OAuthUtils.java b/polaris-service/src/main/java/io/polaris/service/auth/OAuthUtils.java new file mode 100644 index 0000000000..9ad5275fa5 --- /dev/null +++ b/polaris-service/src/main/java/io/polaris/service/auth/OAuthUtils.java @@ -0,0 +1,59 @@ +package io.polaris.service.auth; + +import jakarta.ws.rs.core.Response; +import java.nio.charset.StandardCharsets; +import org.apache.commons.codec.binary.Base64; + +/** Simple utility class to assist with OAuth operations */ +public class OAuthUtils { + + public static final String AUTHORIZATION_HEADER = "Authorization"; + + public static final String SF_HEADER_ACCOUNT_NAME = "Snowflake-Account"; + + public static final String POLARIS_ROLE_PREFIX = "PRINCIPAL_ROLE:"; + + public static final String SF_ACCOUNT_NAME_HEADER = "sf-account"; + public static final String SF_ACCOUNT_URL_HEADER = "sf-account-url"; + + /** + * @param clientId + * @param clientSecret + * @return basic Authorization Header of the form `base64_encode(client_id:client_secret) + */ + public static String getBasicAuthHeader(String clientId, String clientSecret) { + return Base64.encodeBase64String( + (clientId + ":" + clientSecret).getBytes(StandardCharsets.UTF_8)); + } + + public static Response getResponseFromError(OAuthTokenErrorResponse.Error error) { + return switch (error) { + case unauthorized_client -> + Response.status(Response.Status.UNAUTHORIZED) + .entity( + new OAuthTokenErrorResponse(OAuthTokenErrorResponse.Error.unauthorized_client)) + .build(); + case invalid_client -> + Response.status(Response.Status.BAD_REQUEST) + .entity(new OAuthTokenErrorResponse(OAuthTokenErrorResponse.Error.invalid_client)) + .build(); + case invalid_grant -> + Response.status(Response.Status.BAD_REQUEST) + .entity(new OAuthTokenErrorResponse(OAuthTokenErrorResponse.Error.invalid_grant)) + .build(); + case unsupported_grant_type -> + Response.status(Response.Status.BAD_REQUEST) + .entity( + new OAuthTokenErrorResponse(OAuthTokenErrorResponse.Error.unsupported_grant_type)) + .build(); + case invalid_scope -> + Response.status(Response.Status.BAD_REQUEST) + .entity(new OAuthTokenErrorResponse(OAuthTokenErrorResponse.Error.invalid_scope)) + .build(); + default -> + Response.status(Response.Status.BAD_REQUEST) + .entity(new OAuthTokenErrorResponse(OAuthTokenErrorResponse.Error.invalid_request)) + .build(); + }; + } +} diff --git a/polaris-service/src/main/java/io/polaris/service/auth/PemUtils.java b/polaris-service/src/main/java/io/polaris/service/auth/PemUtils.java new file mode 100644 index 0000000000..f44b3a5d22 --- /dev/null +++ b/polaris-service/src/main/java/io/polaris/service/auth/PemUtils.java @@ -0,0 +1,75 @@ +package io.polaris.service.auth; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.IOException; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.spec.EncodedKeySpec; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; +import org.bouncycastle.util.io.pem.PemObject; +import org.bouncycastle.util.io.pem.PemReader; + +public class PemUtils { + + private static byte[] parsePEMFile(File pemFile) throws IOException { + if (!pemFile.isFile() || !pemFile.exists()) { + throw new FileNotFoundException( + String.format("The file '%s' doesn't exist.", pemFile.getAbsolutePath())); + } + PemReader reader = new PemReader(new FileReader(pemFile)); + PemObject pemObject = reader.readPemObject(); + byte[] content = pemObject.getContent(); + reader.close(); + return content; + } + + private static PublicKey getPublicKey(byte[] keyBytes, String algorithm) { + PublicKey publicKey = null; + try { + KeyFactory kf = KeyFactory.getInstance(algorithm); + EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes); + publicKey = kf.generatePublic(keySpec); + } catch (NoSuchAlgorithmException e) { + System.out.println( + "Could not reconstruct the public key, the given algorithm could not be found."); + } catch (InvalidKeySpecException e) { + System.out.println("Could not reconstruct the public key"); + } + + return publicKey; + } + + private static PrivateKey getPrivateKey(byte[] keyBytes, String algorithm) { + PrivateKey privateKey = null; + try { + KeyFactory kf = KeyFactory.getInstance(algorithm); + EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes); + privateKey = kf.generatePrivate(keySpec); + } catch (NoSuchAlgorithmException e) { + System.out.println( + "Could not reconstruct the private key, the given algorithm could not be found."); + } catch (InvalidKeySpecException e) { + System.out.println("Could not reconstruct the private key"); + } + + return privateKey; + } + + public static PublicKey readPublicKeyFromFile(String filepath, String algorithm) + throws IOException { + byte[] bytes = PemUtils.parsePEMFile(new File(filepath)); + return PemUtils.getPublicKey(bytes, algorithm); + } + + public static PrivateKey readPrivateKeyFromFile(String filepath, String algorithm) + throws IOException { + byte[] bytes = PemUtils.parsePEMFile(new File(filepath)); + return PemUtils.getPrivateKey(bytes, algorithm); + } +} diff --git a/polaris-service/src/main/java/io/polaris/service/auth/TestInlineBearerTokenPolarisAuthenticator.java b/polaris-service/src/main/java/io/polaris/service/auth/TestInlineBearerTokenPolarisAuthenticator.java new file mode 100644 index 0000000000..f541fcb378 --- /dev/null +++ b/polaris-service/src/main/java/io/polaris/service/auth/TestInlineBearerTokenPolarisAuthenticator.java @@ -0,0 +1,72 @@ +package io.polaris.service.auth; + +import com.google.common.base.Splitter; +import io.dropwizard.auth.AuthenticationException; +import io.polaris.core.PolarisCallContext; +import io.polaris.core.auth.AuthenticatedPolarisPrincipal; +import io.polaris.core.context.CallContext; +import io.polaris.core.entity.PolarisPrincipalSecrets; +import io.polaris.core.persistence.PolarisMetaStoreManager; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * {@link io.dropwizard.auth.Authenticator} that parses a token as a sequence of key/value pairs. + * Specifically, we expect to find + * + *

    + *
  • principal - the clientId of the principal + *
  • realm - the current realm + *
+ * + * This class does not expect a client to be either present or correct. Lookup is delegated to the + * {@link PolarisMetaStoreManager} for the current realm. + */ +public class TestInlineBearerTokenPolarisAuthenticator extends BasePolarisAuthenticator { + private static final Logger LOGGER = + LoggerFactory.getLogger(TestInlineBearerTokenPolarisAuthenticator.class); + + @Override + public Optional authenticate(String credentials) + throws AuthenticationException { + Map properties = extractPrincipal(credentials); + PolarisMetaStoreManager metaStoreManager = + entityManagerFactory + .getOrCreateEntityManager(CallContext.getCurrentContext().getRealmContext()) + .getMetaStoreManager(); + PolarisCallContext callContext = CallContext.getCurrentContext().getPolarisCallContext(); + String principal = properties.get("principal"); + + LOGGER.info("Checking for existence of principal {} in map {}", principal, properties); + + TokenInfoExchangeResponse tokenInfo = new TokenInfoExchangeResponse(); + tokenInfo.setSub(principal); + tokenInfo.setScope(properties.get("role")); + + PolarisPrincipalSecrets secrets = + metaStoreManager.loadPrincipalSecrets(callContext, principal).getPrincipalSecrets(); + if (secrets == null) { + // For test scenarios, if we're allowing short-circuiting into the bearer flow, there may + // not be a clientId/clientSecret, and instead we'll let the BasePolarisAuthenticator + // resolve the principal by name from the persistence store. + LOGGER.warn("Failed to load secrets for principal {}", principal); + } else { + tokenInfo.setIntegrationId(secrets.getPrincipalId()); + } + + return getPrincipal(tokenInfo); + } + + private static Map extractPrincipal(String credentials) { + if (credentials.contains(";") || credentials.contains(":")) { + Map parsedProperties = new HashMap<>(); + parsedProperties.putAll( + Splitter.on(';').trimResults().withKeyValueSeparator(':').split(credentials)); + return parsedProperties; + } + return Map.of(); + } +} diff --git a/polaris-service/src/main/java/io/polaris/service/auth/TestOAuth2ApiService.java b/polaris-service/src/main/java/io/polaris/service/auth/TestOAuth2ApiService.java new file mode 100644 index 0000000000..4ad1c270ae --- /dev/null +++ b/polaris-service/src/main/java/io/polaris/service/auth/TestOAuth2ApiService.java @@ -0,0 +1,100 @@ +package io.polaris.service.auth; + +import com.fasterxml.jackson.annotation.JsonTypeName; +import io.polaris.core.PolarisCallContext; +import io.polaris.core.context.CallContext; +import io.polaris.core.entity.PolarisEntitySubType; +import io.polaris.core.entity.PolarisEntityType; +import io.polaris.core.persistence.PolarisEntityManager; +import io.polaris.core.persistence.PolarisMetaStoreManager; +import io.polaris.service.config.HasEntityManagerFactory; +import io.polaris.service.config.OAuth2ApiService; +import io.polaris.service.config.RealmEntityManagerFactory; +import io.polaris.service.types.TokenType; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.SecurityContext; +import java.util.HashMap; +import java.util.Map; +import org.apache.iceberg.exceptions.NotAuthorizedException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@JsonTypeName("test") +public class TestOAuth2ApiService implements OAuth2ApiService, HasEntityManagerFactory { + private static final Logger LOGGER = LoggerFactory.getLogger(TestOAuth2ApiService.class); + + private RealmEntityManagerFactory entityManagerFactory; + + @Override + public Response getToken( + String grantType, + String scope, + String clientId, + String clientSecret, + TokenType requestedTokenType, + String subjectToken, + TokenType subjectTokenType, + String actorToken, + TokenType actorTokenType, + SecurityContext securityContext) { + Map response = new HashMap<>(); + String principalName = getPrincipalName(clientId); + response.put( + "access_token", + "principal:" + + principalName + + ";password:" + + clientSecret + + ";realm:" + + CallContext.getCurrentContext().getRealmContext().getRealmIdentifier()); + response.put("token_type", "bearer"); + response.put("expires_in", 3600); + response.put("scope", "catalog"); + return Response.ok(response).build(); + } + + private String getPrincipalName(String clientId) { + PolarisEntityManager entityManager = + entityManagerFactory.getOrCreateEntityManager( + CallContext.getCurrentContext().getRealmContext()); + PolarisCallContext polarisCallContext = CallContext.getCurrentContext().getPolarisCallContext(); + PolarisMetaStoreManager.PrincipalSecretsResult secretsResult = + entityManager.getMetaStoreManager().loadPrincipalSecrets(polarisCallContext, clientId); + if (secretsResult.isSuccess()) { + LOGGER.debug("Found principal secrets for client id {}", clientId); + PolarisMetaStoreManager.EntityResult principalResult = + entityManager + .getMetaStoreManager() + .loadEntity( + polarisCallContext, 0L, secretsResult.getPrincipalSecrets().getPrincipalId()); + if (!principalResult.isSuccess()) { + throw new NotAuthorizedException("Failed to load principal entity"); + } + return principalResult.getEntity().getName(); + } else { + LOGGER.debug( + "Unable to find principal secrets for client id {} - trying as principal name", clientId); + PolarisMetaStoreManager.EntityResult principalResult = + entityManager + .getMetaStoreManager() + .readEntityByName( + polarisCallContext, + null, + PolarisEntityType.PRINCIPAL, + PolarisEntitySubType.NULL_SUBTYPE, + clientId); + if (!principalResult.isSuccess()) { + throw new NotAuthorizedException("Failed to read principal entity"); + } + return principalResult.getEntity().getName(); + } + } + + @Override + public void setEntityManagerFactory(RealmEntityManagerFactory entityManagerFactory) { + this.entityManagerFactory = entityManagerFactory; + } + + @Override + public void setTokenBroker(TokenBrokerFactory tokenBrokerFactory) {} +} diff --git a/polaris-service/src/main/java/io/polaris/service/auth/TokenBroker.java b/polaris-service/src/main/java/io/polaris/service/auth/TokenBroker.java new file mode 100644 index 0000000000..97bd6be8a5 --- /dev/null +++ b/polaris-service/src/main/java/io/polaris/service/auth/TokenBroker.java @@ -0,0 +1,50 @@ +package io.polaris.service.auth; + +import io.polaris.core.PolarisCallContext; +import io.polaris.core.context.CallContext; +import io.polaris.core.entity.PolarisEntityType; +import io.polaris.core.entity.PrincipalEntity; +import io.polaris.core.persistence.PolarisEntityManager; +import io.polaris.core.persistence.PolarisMetaStoreManager; +import io.polaris.service.types.TokenType; +import java.util.Optional; +import org.jetbrains.annotations.NotNull; + +/** Generic token class intended to be extended by different token types */ +public interface TokenBroker { + + boolean supportsGrantType(String grantType); + + boolean supportsRequestedTokenType(TokenType tokenType); + + TokenResponse generateFromClientSecrets( + final String clientId, final String clientSecret, final String grantType, final String scope); + + TokenResponse generateFromToken( + TokenType tokenType, String subjectToken, final String grantType, final String scope); + + DecodedToken verify(String token); + + static @NotNull Optional findPrincipalEntity( + PolarisEntityManager entityManager, String clientId, String clientSecret) { + // Validate the principal is present and secrets match + PolarisMetaStoreManager metaStoreManager = entityManager.getMetaStoreManager(); + PolarisCallContext polarisCallContext = CallContext.getCurrentContext().getPolarisCallContext(); + PolarisMetaStoreManager.PrincipalSecretsResult principalSecrets = + metaStoreManager.loadPrincipalSecrets(polarisCallContext, clientId); + if (!principalSecrets.isSuccess()) { + return Optional.empty(); + } + if (!principalSecrets.getPrincipalSecrets().getMainSecret().equals(clientSecret) + && !principalSecrets.getPrincipalSecrets().getSecondarySecret().equals(clientSecret)) { + return Optional.empty(); + } + PolarisMetaStoreManager.EntityResult result = + metaStoreManager.loadEntity( + polarisCallContext, 0L, principalSecrets.getPrincipalSecrets().getPrincipalId()); + if (!result.isSuccess() || result.getEntity().getType() != PolarisEntityType.PRINCIPAL) { + return Optional.empty(); + } + return Optional.of(PrincipalEntity.of(result.getEntity())); + } +} diff --git a/polaris-service/src/main/java/io/polaris/service/auth/TokenBrokerFactory.java b/polaris-service/src/main/java/io/polaris/service/auth/TokenBrokerFactory.java new file mode 100644 index 0000000000..8a6fc0d0b7 --- /dev/null +++ b/polaris-service/src/main/java/io/polaris/service/auth/TokenBrokerFactory.java @@ -0,0 +1,13 @@ +package io.polaris.service.auth; + +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.dropwizard.jackson.Discoverable; +import io.polaris.core.context.RealmContext; +import java.util.function.Function; + +/** + * Factory that creates a {@link TokenBroker} for generating and parsing. The {@link TokenBroker} is + * created based on the realm context. + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") +public interface TokenBrokerFactory extends Function, Discoverable {} diff --git a/polaris-service/src/main/java/io/polaris/service/auth/TokenInfoExchangeResponse.java b/polaris-service/src/main/java/io/polaris/service/auth/TokenInfoExchangeResponse.java new file mode 100644 index 0000000000..d01b96750d --- /dev/null +++ b/polaris-service/src/main/java/io/polaris/service/auth/TokenInfoExchangeResponse.java @@ -0,0 +1,132 @@ +package io.polaris.service.auth; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class TokenInfoExchangeResponse implements DecodedToken { + + private boolean active; + + @JsonProperty("active") + public boolean isActive() { + return active; + } + + @JsonProperty("active") + public void setActive(boolean active) { + this.active = active; + } + + private String scope; + + @JsonProperty("scope") + public String getScope() { + return scope; + } + + @JsonProperty("scope") + public void setScope(String scope) { + this.scope = scope; + } + + private String clientId; + + @JsonProperty("client_id") + public String getClientId() { + return clientId; + } + + @JsonProperty("client_id") + public void setClientId(String clientId) { + this.clientId = clientId; + } + + private String tokenType; + + @JsonProperty("token_type") + public String getTokenType() { + return tokenType; + } + + @JsonProperty("token_type") + public void setTokenType(String tokenType) { + this.tokenType = tokenType; + } + + private Long exp; + + @JsonProperty("exp") + public Long getExp() { + return exp; + } + + @JsonProperty("exp") + public void setExp(Long exp) { + this.exp = exp; + } + + private String sub; + + @JsonProperty("sub") + public String getSub() { + return sub; + } + + @JsonProperty("sub") + public void setSub(String sub) { + this.sub = sub; + } + + private String aud; + + @JsonProperty("aud") + public String getAud() { + return aud; + } + + @JsonProperty("aud") + public void setAud(String aud) { + this.aud = aud; + } + + @JsonProperty("iss") + private String iss; + + @JsonProperty("iss") + public String getIss() { + return iss; + } + + @JsonProperty("iss") + public void setIss(String iss) { + this.iss = iss; + } + + private String token; + + @JsonProperty("token") + public String getToken() { + return token; + } + + @JsonProperty("token") + public void setToken(String token) { + this.token = token; + } + + private long integrationId; + + public long getIntegrationId() { + return integrationId; + } + + @JsonProperty("integration_id") + public void setIntegrationId(long integrationId) { + this.integrationId = integrationId; + } + + /* integration ID is effectively principal ID */ + @Override + public Long getPrincipalId() { + return integrationId; + } +} diff --git a/polaris-service/src/main/java/io/polaris/service/auth/TokenRequestValidator.java b/polaris-service/src/main/java/io/polaris/service/auth/TokenRequestValidator.java new file mode 100644 index 0000000000..b36d105a4b --- /dev/null +++ b/polaris-service/src/main/java/io/polaris/service/auth/TokenRequestValidator.java @@ -0,0 +1,64 @@ +package io.polaris.service.auth; + +import java.util.Optional; +import java.util.Set; +import java.util.logging.Logger; + +public class TokenRequestValidator { + + static final Logger LOGGER = Logger.getLogger(TokenRequestValidator.class.getName()); + + public static final String TOKEN_EXCHANGE = "urn:ietf:params:oauth:grant-type:token-exchange"; + public static final String CLIENT_CREDENTIALS = "client_credentials"; + public static final Set ALLOWED_GRANT_TYPES = Set.of(CLIENT_CREDENTIALS, TOKEN_EXCHANGE); + + /** Default constructor */ + public TokenRequestValidator() {} + + /** + * Validates the incoming Client Credentials flow. + * + *
    + *
  • Non-null scope: while optional in the spec we make it required and expect it to conform + * to the format + *
+ * + * @param clientId + * @param clientSecret + * @param grantType + * @param scope while optional in the Iceberg REST API Spec we make it required and expect it to + * conform to the format "PRINCIPAL_ROLE:NAME PRINCIPAL_ROLE:NAME2 ..." + * @return + */ + public Optional validateForClientCredentialsFlow( + final String clientId, + final String clientSecret, + final String grantType, + final String scope) { + if (clientId == null || clientId.isEmpty() || clientSecret == null || clientSecret.isEmpty()) { + // TODO: Figure out how to get the authorization header from `securityContext` + LOGGER.info("Missing Client ID or Client Secret in Request Body"); + return Optional.of(OAuthTokenErrorResponse.Error.invalid_client); + } + if (grantType == null || grantType.isEmpty() || !ALLOWED_GRANT_TYPES.contains(grantType)) { + LOGGER.info("Invalid grant type: " + grantType); + return Optional.of(OAuthTokenErrorResponse.Error.invalid_grant); + } + if (scope == null || scope.isEmpty()) { + LOGGER.info("Missing scope in Request Body"); + return Optional.of(OAuthTokenErrorResponse.Error.invalid_scope); + } + String[] scopes = scope.split(" "); + for (String s : scopes) { + if (!s.startsWith(OAuthUtils.POLARIS_ROLE_PREFIX)) { + LOGGER.info("Invalid scope provided. scopes=" + s + "scopes=" + scope); + return Optional.of(OAuthTokenErrorResponse.Error.invalid_scope); + } + if (s.replaceFirst(OAuthUtils.POLARIS_ROLE_PREFIX, "").isEmpty()) { + LOGGER.info("Invalid scope provided. scopes=" + s + "scopes=" + scope); + return Optional.of(OAuthTokenErrorResponse.Error.invalid_scope); + } + } + return Optional.empty(); + } +} diff --git a/polaris-service/src/main/java/io/polaris/service/auth/TokenResponse.java b/polaris-service/src/main/java/io/polaris/service/auth/TokenResponse.java new file mode 100644 index 0000000000..bb171f40e2 --- /dev/null +++ b/polaris-service/src/main/java/io/polaris/service/auth/TokenResponse.java @@ -0,0 +1,41 @@ +package io.polaris.service.auth; + +import java.util.Optional; + +public class TokenResponse { + private final Optional error; + private String accessToken; + private String tokenType; + private Integer expiresIn; + + public TokenResponse(OAuthTokenErrorResponse.Error error) { + this.error = Optional.of(error); + } + + public TokenResponse(String accessToken, String tokenType, int expiresIn) { + this.accessToken = accessToken; + this.expiresIn = expiresIn; + this.tokenType = tokenType; + this.error = Optional.empty(); + } + + public boolean isValid() { + return error.isEmpty(); + } + + public OAuthTokenErrorResponse.Error getError() { + return error.get(); + } + + public String getAccessToken() { + return accessToken; + } + + public int getExpiresIn() { + return expiresIn; + } + + public String getTokenType() { + return tokenType; + } +} diff --git a/polaris-service/src/main/java/io/polaris/service/catalog/BasePolarisCatalog.java b/polaris-service/src/main/java/io/polaris/service/catalog/BasePolarisCatalog.java new file mode 100644 index 0000000000..01df54615c --- /dev/null +++ b/polaris-service/src/main/java/io/polaris/service/catalog/BasePolarisCatalog.java @@ -0,0 +1,1557 @@ +package io.polaris.service.catalog; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Joiner; +import com.google.common.base.Objects; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableMap; +import io.polaris.core.PolarisCallContext; +import io.polaris.core.catalog.PolarisCatalogHelpers; +import io.polaris.core.context.CallContext; +import io.polaris.core.entity.CatalogEntity; +import io.polaris.core.entity.NamespaceEntity; +import io.polaris.core.entity.PolarisEntity; +import io.polaris.core.entity.PolarisEntityConstants; +import io.polaris.core.entity.PolarisEntitySubType; +import io.polaris.core.entity.PolarisEntityType; +import io.polaris.core.entity.PolarisTaskConstants; +import io.polaris.core.entity.TableLikeEntity; +import io.polaris.core.persistence.PolarisEntityManager; +import io.polaris.core.persistence.PolarisMetaStoreManager; +import io.polaris.core.persistence.PolarisResolvedPathWrapper; +import io.polaris.core.persistence.resolver.PolarisResolutionManifestCatalogView; +import io.polaris.core.storage.InMemoryStorageIntegration; +import io.polaris.core.storage.PolarisStorageActions; +import io.polaris.core.storage.PolarisStorageConfigurationInfo; +import io.polaris.core.storage.PolarisStorageIntegration; +import io.polaris.core.storage.aws.PolarisS3FileIOClientFactory; +import io.polaris.service.task.TaskExecutor; +import io.polaris.service.types.NotificationRequest; +import io.polaris.service.types.NotificationType; +import jakarta.ws.rs.BadRequestException; +import java.io.Closeable; +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.apache.hadoop.conf.Configuration; +import org.apache.iceberg.BaseMetastoreTableOperations; +import org.apache.iceberg.CatalogProperties; +import org.apache.iceberg.CatalogUtil; +import org.apache.iceberg.Schema; +import org.apache.iceberg.TableMetadata; +import org.apache.iceberg.TableMetadataParser; +import org.apache.iceberg.TableOperations; +import org.apache.iceberg.aws.s3.S3FileIOProperties; +import org.apache.iceberg.catalog.Namespace; +import org.apache.iceberg.catalog.SupportsNamespaces; +import org.apache.iceberg.catalog.TableIdentifier; +import org.apache.iceberg.exceptions.AlreadyExistsException; +import org.apache.iceberg.exceptions.CommitFailedException; +import org.apache.iceberg.exceptions.ForbiddenException; +import org.apache.iceberg.exceptions.NamespaceNotEmptyException; +import org.apache.iceberg.exceptions.NoSuchNamespaceException; +import org.apache.iceberg.exceptions.NoSuchTableException; +import org.apache.iceberg.exceptions.NoSuchViewException; +import org.apache.iceberg.exceptions.NotFoundException; +import org.apache.iceberg.exceptions.UnprocessableEntityException; +import org.apache.iceberg.exceptions.ValidationException; +import org.apache.iceberg.io.CloseableGroup; +import org.apache.iceberg.io.FileIO; +import org.apache.iceberg.view.BaseMetastoreViewCatalog; +import org.apache.iceberg.view.BaseViewOperations; +import org.apache.iceberg.view.ViewBuilder; +import org.apache.iceberg.view.ViewMetadata; +import org.apache.iceberg.view.ViewMetadataParser; +import org.apache.iceberg.view.ViewUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.TestOnly; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import software.amazon.awssdk.core.exception.SdkException; + +/** Defines the relationship between PolarisEntities and Iceberg's business logic. */ +public class BasePolarisCatalog extends BaseMetastoreViewCatalog + implements SupportsNamespaces, SupportsNotifications, Closeable, SupportsCredentialDelegation { + private static final Logger LOG = LoggerFactory.getLogger(BasePolarisCatalog.class); + + private static final Joiner SLASH = Joiner.on("/"); + private static final Joiner DOT = Joiner.on("."); + + // Config key for whether to allow setting the FILE_IO_IMPL using catalog properties. Should + // only be allowed in dev/test environments. + static final String ALLOW_SPECIFYING_FILE_IO_IMPL = "ALLOW_SPECIFYING_FILE_IO_IMPL"; + private static final int MAX_RETRIES = 12; + + static final Predicate SHOULD_RETRY_REFRESH_PREDICATE = + new Predicate() { + @Override + public boolean test(Exception ex) { + // Default arguments from BaseMetastoreTableOperation only stop retries on + // NotFoundException. We should more carefully identify the set of retriable + // and non-retriable exceptions here. + return !(ex instanceof NotFoundException) + && !(ex instanceof IllegalArgumentException) + && !(ex instanceof AlreadyExistsException) + && !(ex instanceof ForbiddenException) + && !(ex instanceof UnprocessableEntityException) + && isStorageProviderRetryableException(ex); + } + }; + public static final String CLEANUP_ON_NAMESPACE_DROP = "CLEANUP_ON_NAMESPACE_DROP"; + + private final PolarisEntityManager entityManager; + private final CallContext callContext; + private final PolarisResolutionManifestCatalogView resolvedEntityView; + private final CatalogEntity catalogEntity; + private final TaskExecutor taskExecutor; + private String ioImplClassName; + private FileIO io; + private String catalogName; + private long catalogId = -1; + private String defaultBaseLocation; + private CloseableGroup closeableGroup; + private Map catalogProperties; + + /** + * @param entityManager provides handle to underlying PolarisMetaStoreManager with which to + * perform mutations on entities. + * @param callContext the current CallContext + * @param resolvedEntityView accessor to resolved entity paths that have been pre-vetted to ensure + * this catalog instance only interacts with authorized resolved paths. + * @param taskExecutor Executor we use to register cleanup task handlers + */ + public BasePolarisCatalog( + PolarisEntityManager entityManager, + CallContext callContext, + PolarisResolutionManifestCatalogView resolvedEntityView, + TaskExecutor taskExecutor) { + this.entityManager = entityManager; + this.callContext = callContext; + this.resolvedEntityView = resolvedEntityView; + this.catalogEntity = + CatalogEntity.of(resolvedEntityView.getResolvedReferenceCatalogEntity().getRawLeafEntity()); + + this.taskExecutor = taskExecutor; + this.catalogId = catalogEntity.getId(); + this.catalogName = catalogEntity.getName(); + } + + @Override + public String name() { + return catalogName; + } + + @TestOnly + FileIO getIo() { + return io; + } + + @Override + public void initialize(String name, Map properties) { + Preconditions.checkState( + this.catalogName.equals(name), + "Tried to initialize catalog as name %s but already constructed with name %s", + name, + this.catalogName); + + // Base location from catalogEntity is primary source of truth, otherwise fall through + // to the same key from the properties map, annd finally fall through to WAREHOUSE_LOCATION. + String baseLocation = + Optional.ofNullable(catalogEntity.getDefaultBaseLocation()) + .orElse( + properties.getOrDefault( + CatalogEntity.DEFAULT_BASE_LOCATION_KEY, + properties.getOrDefault(CatalogProperties.WAREHOUSE_LOCATION, ""))); + this.defaultBaseLocation = baseLocation.replaceAll("/*$", ""); + + Boolean allowSpecifyingFileIoImpl = + callContext + .getPolarisCallContext() + .getConfigurationStore() + .getConfiguration( + callContext.getPolarisCallContext(), ALLOW_SPECIFYING_FILE_IO_IMPL, false); + + if (properties.containsKey(CatalogProperties.FILE_IO_IMPL)) { + ioImplClassName = properties.get(CatalogProperties.FILE_IO_IMPL); + + if (!Boolean.TRUE.equals(allowSpecifyingFileIoImpl)) { + throw new ValidationException( + "Cannot set property '%s' to '%s' for this catalog.", + CatalogProperties.FILE_IO_IMPL, ioImplClassName); + } + LOG.debug( + "Allowing overriding ioImplClassName to {} for storageConfiguration {}", + ioImplClassName, + catalogEntity.getStorageConfigurationInfo()); + } else { + ioImplClassName = catalogEntity.getStorageConfigurationInfo().getFileIoImplClassName(); + LOG.debug( + "Resolved ioImplClassName {} for storageConfiguration {}", + ioImplClassName, + catalogEntity.getStorageConfigurationInfo()); + } + this.io = loadFileIO(ioImplClassName, properties); + + this.closeableGroup = CallContext.getCurrentContext().closeables(); + closeableGroup.addCloseable(metricsReporter()); + // TODO: FileIO initialization should should happen later depending on the operation so + // we'd also add it to the closeableGroup later. + closeableGroup.addCloseable(this.io); + closeableGroup.setSuppressCloseFailure(true); + catalogProperties = properties; + } + + @Override + protected Map properties() { + return catalogProperties == null ? ImmutableMap.of() : catalogProperties; + } + + @Override + public TableBuilder buildTable(TableIdentifier identifier, Schema schema) { + return new BasePolarisCatalogTableBuilder(identifier, schema); + } + + @Override + public ViewBuilder buildView(TableIdentifier identifier) { + return new BasePolarisCatalogViewBuilder(identifier); + } + + @Override + protected TableOperations newTableOps(TableIdentifier tableIdentifier) { + return new BasePolarisTableOperations(io, tableIdentifier); + } + + @Override + protected String defaultWarehouseLocation(TableIdentifier tableIdentifier) { + return SLASH.join( + defaultNamespaceLocation(tableIdentifier.namespace()), tableIdentifier.name()); + } + + private String defaultNamespaceLocation(Namespace namespace) { + if (namespace.isEmpty()) { + return defaultBaseLocation; + } else { + return SLASH.join(defaultBaseLocation, SLASH.join(namespace.levels())); + } + } + + @Override + public boolean dropTable(TableIdentifier tableIdentifier, boolean purge) { + TableOperations ops = newTableOps(tableIdentifier); + TableMetadata lastMetadata; + if (purge && ops.current() != null) { + lastMetadata = ops.current(); + } else { + lastMetadata = null; + } + + Optional storageInfoEntity = findStorageInfo(tableIdentifier); + if (purge && lastMetadata != null) { + Map credentialsMap = + storageInfoEntity + .map( + entity -> + refreshCredentials( + tableIdentifier, + Set.of(PolarisStorageActions.READ, PolarisStorageActions.WRITE), + lastMetadata.location(), + entity)) + .orElse(Map.of()); + Map tableProperties = new HashMap<>(lastMetadata.properties()); + tableProperties.putAll(credentialsMap); + if (!tableProperties.isEmpty()) { + io = loadFileIO(ioImplClassName, tableProperties); + // ensure the new fileIO is closed when the catalog is closed + closeableGroup.addCloseable(io); + } + } + Map storageProperties = + storageInfoEntity + .map(PolarisEntity::getInternalPropertiesAsMap) + .map( + properties -> { + if (lastMetadata == null) { + return Map.of(); + } + Map clone = new HashMap<>(properties); + clone.put(CatalogProperties.FILE_IO_IMPL, ioImplClassName); + try { + clone.putAll(io.properties()); + } catch (UnsupportedOperationException e) { + LOG.warn("FileIO doesn't implement properties()"); + } + clone.put(PolarisTaskConstants.STORAGE_LOCATION, lastMetadata.location()); + return clone; + }) + .orElse(Map.of()); + PolarisMetaStoreManager.DropEntityResult dropEntityResult = + dropTableLike( + catalogId, PolarisEntitySubType.TABLE, tableIdentifier, storageProperties, purge); + if (!dropEntityResult.isSuccess()) { + return false; + } + + if (purge && lastMetadata != null && dropEntityResult.getCleanupTaskId() != null) { + LOG.info( + "Scheduled cleanup task {} for table {}", + dropEntityResult.getCleanupTaskId(), + tableIdentifier); + taskExecutor.addTaskHandlerContext( + dropEntityResult.getCleanupTaskId(), CallContext.getCurrentContext()); + } + + return true; + } + + @Override + public List listTables(Namespace namespace) { + if (!namespaceExists(namespace) && !namespace.isEmpty()) { + throw new NoSuchNamespaceException( + "Cannot list tables for namespace. Namespace does not exist: %s", namespace); + } + + return listTableLike(catalogId, PolarisEntitySubType.TABLE, namespace); + } + + @Override + public void renameTable(TableIdentifier from, TableIdentifier to) { + if (from.equals(to)) { + return; + } + + renameTableLike(catalogId, PolarisEntitySubType.TABLE, from, to); + } + + @Override + public void createNamespace(Namespace namespace) { + createNamespace(namespace, Collections.emptyMap()); + } + + @Override + public void createNamespace(Namespace namespace, Map metadata) { + LOG.debug("Creating namespace {} with metadata {}", namespace, metadata); + if (namespace.isEmpty()) { + throw new AlreadyExistsException( + "Cannot create root namespace, as it already exists implicitly."); + } + + // TODO: These should really be helpers in core Iceberg Namespace. + Namespace parentNamespace = PolarisCatalogHelpers.getParentNamespace(namespace); + + PolarisResolvedPathWrapper resolvedParent = resolvedEntityView.getResolvedPath(parentNamespace); + if (resolvedParent == null) { + throw new NoSuchNamespaceException( + "Cannot create namespace %s. Parent namespace does not exist.", namespace); + } + createNamespaceInternal(namespace, metadata, resolvedParent); + } + + private void createNamespaceInternal( + Namespace namespace, + Map metadata, + PolarisResolvedPathWrapper resolvedParent) { + NamespaceEntity entity = + new NamespaceEntity.Builder(namespace) + .setCatalogId(getCatalogId()) + .setId( + entityManager + .getMetaStoreManager() + .generateNewEntityId(getCurrentPolarisContext()) + .getId()) + .setParentId(resolvedParent.getRawLeafEntity().getId()) + .setProperties(metadata) + .setCreateTimestamp(System.currentTimeMillis()) + .build(); + PolarisEntity returnedEntity = + PolarisEntity.of( + entityManager + .getMetaStoreManager() + .createEntityIfNotExists( + getCurrentPolarisContext(), + PolarisEntity.toCoreList(resolvedParent.getRawFullPath()), + entity)); + if (returnedEntity == null) { + throw new AlreadyExistsException( + "Cannot create namespace %s. Namespace already exists", namespace); + } + } + + @Override + public boolean namespaceExists(Namespace namespace) { + PolarisResolvedPathWrapper resolvedEntities = resolvedEntityView.getResolvedPath(namespace); + if (resolvedEntities == null) { + return false; + } + return true; + } + + @Override + public boolean dropNamespace(Namespace namespace) throws NamespaceNotEmptyException { + PolarisResolvedPathWrapper resolvedEntities = resolvedEntityView.getResolvedPath(namespace); + if (resolvedEntities == null) { + return false; + } + + List catalogPath = resolvedEntities.getRawParentPath(); + PolarisEntity leafEntity = resolvedEntities.getRawLeafEntity(); + + // drop if exists and is empty + PolarisCallContext polarisCallContext = callContext.getPolarisCallContext(); + PolarisMetaStoreManager.DropEntityResult dropEntityResult = + entityManager + .getMetaStoreManager() + .dropEntityIfExists( + getCurrentPolarisContext(), + PolarisEntity.toCoreList(catalogPath), + leafEntity, + Map.of(), + polarisCallContext + .getConfigurationStore() + .getConfiguration(polarisCallContext, CLEANUP_ON_NAMESPACE_DROP, false)); + + if (!dropEntityResult.isSuccess() && dropEntityResult.failedBecauseNotEmpty()) { + throw new NamespaceNotEmptyException("Namespace %s is not empty", namespace); + } + + // return status of drop operation + return dropEntityResult.isSuccess(); + } + + @Override + public boolean setProperties(Namespace namespace, Map properties) + throws NoSuchNamespaceException { + PolarisResolvedPathWrapper resolvedEntities = resolvedEntityView.getResolvedPath(namespace); + if (resolvedEntities == null) { + throw new NoSuchNamespaceException("Namespace does not exist: %s", namespace); + } + PolarisEntity entity = resolvedEntities.getRawLeafEntity(); + Map newProperties = new HashMap<>(entity.getPropertiesAsMap()); + + // Merge new properties into existing map. + newProperties.putAll(properties); + PolarisEntity updatedEntity = + new PolarisEntity.Builder(entity).setProperties(newProperties).build(); + + List parentPath = resolvedEntities.getRawFullPath(); + PolarisEntity returnedEntity = + Optional.ofNullable( + entityManager + .getMetaStoreManager() + .updateEntityPropertiesIfNotChanged( + getCurrentPolarisContext(), + PolarisEntity.toCoreList(parentPath), + updatedEntity) + .getEntity()) + .map(PolarisEntity::new) + .orElse(null); + if (returnedEntity == null) { + throw new RuntimeException("Concurrent modification of namespace: " + namespace); + } + return true; + } + + @Override + public boolean removeProperties(Namespace namespace, Set properties) + throws NoSuchNamespaceException { + PolarisResolvedPathWrapper resolvedEntities = resolvedEntityView.getResolvedPath(namespace); + if (resolvedEntities == null) { + throw new NoSuchNamespaceException("Namespace does not exist: %s", namespace); + } + PolarisEntity entity = resolvedEntities.getRawLeafEntity(); + + Map updatedProperties = new HashMap<>(entity.getPropertiesAsMap()); + properties.forEach(updatedProperties::remove); + + PolarisEntity updatedEntity = + new PolarisEntity.Builder(entity).setProperties(updatedProperties).build(); + + List parentPath = resolvedEntities.getRawFullPath(); + PolarisEntity returnedEntity = + Optional.ofNullable( + entityManager + .getMetaStoreManager() + .updateEntityPropertiesIfNotChanged( + getCurrentPolarisContext(), + PolarisEntity.toCoreList(parentPath), + updatedEntity) + .getEntity()) + .map(PolarisEntity::new) + .orElse(null); + if (returnedEntity == null) { + throw new RuntimeException("Concurrent modification of namespace: " + namespace); + } + return true; + } + + @Override + public Map loadNamespaceMetadata(Namespace namespace) + throws NoSuchNamespaceException { + PolarisResolvedPathWrapper resolvedEntities = resolvedEntityView.getResolvedPath(namespace); + if (resolvedEntities == null) { + throw new NoSuchNamespaceException("Namespace does not exist: %s", namespace); + } + NamespaceEntity entity = NamespaceEntity.of(resolvedEntities.getRawLeafEntity()); + Preconditions.checkState( + entity.getParentNamespace().equals(PolarisCatalogHelpers.getParentNamespace(namespace)), + "Mismatched stored parentNamespace '%s' vs looked up parentNamespace '%s", + entity.getParentNamespace(), + PolarisCatalogHelpers.getParentNamespace(namespace)); + + return entity.getPropertiesAsMap(); + } + + @Override + public List listNamespaces() { + return listNamespaces(Namespace.empty()); + } + + @Override + public List listNamespaces(Namespace namespace) throws NoSuchNamespaceException { + PolarisResolvedPathWrapper resolvedEntities = resolvedEntityView.getResolvedPath(namespace); + if (resolvedEntities == null) { + throw new NoSuchNamespaceException("Namespace does not exist: %s", namespace); + } + + List catalogPath = resolvedEntities.getRawFullPath(); + List entities = + PolarisEntity.toNameAndIdList( + entityManager + .getMetaStoreManager() + .listEntities( + getCurrentPolarisContext(), + PolarisEntity.toCoreList(catalogPath), + PolarisEntityType.NAMESPACE, + PolarisEntitySubType.NULL_SUBTYPE) + .getEntities()); + return PolarisCatalogHelpers.nameAndIdToNamespaces(catalogPath, entities); + } + + @Override + public void close() throws IOException {} + + @Override + public List listViews(Namespace namespace) { + if (!namespaceExists(namespace) && !namespace.isEmpty()) { + throw new NoSuchNamespaceException( + "Cannot list views for namespace. Namespace does not exist: %s", namespace); + } + + return listTableLike(catalogId, PolarisEntitySubType.VIEW, namespace); + } + + @Override + protected BasePolarisViewOperations newViewOps(TableIdentifier identifier) { + return new BasePolarisViewOperations(io, identifier); + } + + @Override + public boolean dropView(TableIdentifier identifier) { + return dropTableLike(catalogId, PolarisEntitySubType.VIEW, identifier, Map.of(), true) + .isSuccess(); + } + + @Override + public void renameView(TableIdentifier from, TableIdentifier to) { + if (from.equals(to)) { + return; + } + + renameTableLike(catalogId, PolarisEntitySubType.VIEW, from, to); + } + + @Override + public boolean sendNotification( + TableIdentifier identifier, NotificationRequest notificationRequest) { + return sendNotificationForTableLike( + catalogId, PolarisEntitySubType.TABLE, identifier, notificationRequest); + } + + @Override + public Map getCredentialConfig( + TableIdentifier tableIdentifier, + TableMetadata tableMetadata, + Set storageActions) { + Optional storageInfo = findStorageInfo(tableIdentifier); + if (storageInfo.isEmpty()) { + LOG.atWarn() + .addKeyValue("tableIdentifier", tableIdentifier) + .log("Table entity has no storage configuration in its hierarchy"); + return Map.of(); + } + return refreshCredentials( + tableIdentifier, storageActions, tableMetadata.location(), storageInfo.get()); + } + + /** + * Based on configuration settings, for callsites that need to handle potentially setting a new + * base location for a TableLike entity, produces the transformed location if applicable, or else + * the unaltered specified location. + */ + public String transformTableLikeLocation(String specifiedTableLikeLocation) { + String replaceNewLocationPrefix = catalogEntity.getReplaceNewLocationPrefixWithCatalogDefault(); + if (specifiedTableLikeLocation != null + && replaceNewLocationPrefix != null + && specifiedTableLikeLocation.startsWith(replaceNewLocationPrefix)) { + String modifiedLocation = + defaultBaseLocation + + specifiedTableLikeLocation.substring(replaceNewLocationPrefix.length()); + LOG.atDebug() + .addKeyValue("specifiedTableLikeLocation", specifiedTableLikeLocation) + .addKeyValue("modifiedLocation", modifiedLocation) + .log("Translating specifiedTableLikeLocation based on config"); + return modifiedLocation; + } + return specifiedTableLikeLocation; + } + + private @NotNull Optional findStorageInfo(TableIdentifier tableIdentifier) { + PolarisResolvedPathWrapper resolvedTableEntities = + resolvedEntityView.getResolvedPath(tableIdentifier, PolarisEntitySubType.TABLE); + + PolarisResolvedPathWrapper resolvedStorageEntity = + resolvedTableEntities == null + ? resolvedEntityView.getResolvedPath(tableIdentifier.namespace()) + : resolvedTableEntities; + + return findStorageInfoFromHierarchy(resolvedStorageEntity); + } + + private Map refreshCredentials( + TableIdentifier tableIdentifier, + Set storageActions, + String tableLocation, + PolarisEntity entity) { + // Important: Any locations added to the set of requested locations need to be validated + // prior to requested subscoped credentials. + validateLocationForTableLike(tableIdentifier, tableLocation); + + boolean allowList = + storageActions.contains(PolarisStorageActions.LIST) + || storageActions.contains(PolarisStorageActions.ALL); + Set writeLocations = + storageActions.contains(PolarisStorageActions.WRITE) + || storageActions.contains(PolarisStorageActions.DELETE) + || storageActions.contains(PolarisStorageActions.ALL) + ? Set.of(tableLocation) + : Set.of(); + Map credentialsMap = + entityManager + .getCredentialCache() + .getOrGenerateSubScopeCreds( + entityManager.getMetaStoreManager(), + callContext.getPolarisCallContext(), + entity, + allowList, + Set.of(tableLocation), + writeLocations); + LOG.atDebug() + .addKeyValue("tableIdentifier", tableIdentifier) + .addKeyValue("credentialKeys", credentialsMap.keySet()) + .log("Loaded scoped credentials for table"); + if (credentialsMap.isEmpty()) { + LOG.debug("No credentials found for table"); + } + return credentialsMap; + } + + /** + * Validates that the specified {@code location} is valid for whatever storage config is found for + * this TableLike's parent hierarchy. + */ + private void validateLocationForTableLike(TableIdentifier identifier, String location) { + PolarisResolvedPathWrapper resolvedStorageEntity = + resolvedEntityView.getResolvedPath(identifier, PolarisEntitySubType.ANY_SUBTYPE); + if (resolvedStorageEntity == null) { + resolvedStorageEntity = resolvedEntityView.getResolvedPath(identifier.namespace()); + } + + validateLocationForTableLike(identifier, location, resolvedStorageEntity); + } + + /** + * Validates that the specified {@code location} is valid for whatever storage config is found for + * this TableLike's parent hierarchy. + */ + private void validateLocationForTableLike( + TableIdentifier identifier, + String location, + PolarisResolvedPathWrapper resolvedStorageEntity) { + Optional storageInfoHolder = findStorageInfoFromHierarchy(resolvedStorageEntity); + storageInfoHolder.ifPresentOrElse( + storageInfoHolderEntity -> { + // Though the storage entity may not actually be a CatalogEntity, we just use the + // CatalogEntity wrapper here for a convenient deserializer helper method. + PolarisStorageConfigurationInfo storageConfigInfo = + new CatalogEntity(storageInfoHolderEntity).getStorageConfigurationInfo(); + Map> + validationResults = + InMemoryStorageIntegration.validateSubpathsOfAllowedLocations( + storageConfigInfo, Set.of(PolarisStorageActions.ALL), Set.of(location)); + validationResults.values().stream() + .forEach( + actionResult -> + actionResult.values().stream() + .forEach( + result -> { + if (!result.isSuccess()) { + throw new ForbiddenException( + "Invalid location '%s' for identifier '%s': %s", + location, identifier, result.getMessage()); + } + })); + + // TODO: Consider exposing a property to control whether to use the explicit default + // in-memory PolarisStorageIntegration implementation to perform validation or + // whether to delegate to PolarisMetaStoreManager::validateAccessToLocations. + // Usually the validation is better to perform with local business logic, but if + // there are additional rules to be evaluated by a custom PolarisMetaStoreManager + // implementation, then the validation should go through that API instead as follows: + // + // PolarisMetaStoreManager.ValidateAccessResult validateResult = + // entityManager.getMetaStoreManager().validateAccessToLocations( + // getCurrentPolarisContext(), + // storageInfoHolderEntity.getCatalogId(), + // storageInfoHolderEntity.getId(), + // Set.of(PolarisStorageActions.ALL), + // Set.of(location)); + // if (!validateResult.isSuccess()) { + // throw new ForbiddenException("Invalid location '%s' for identifier '%s': %s", + // location, identifier, validateResult.getExtraInformation()); + // } + }, + () -> { + if (location.startsWith("file:") || location.startsWith("http")) { + throw new ForbiddenException( + "Invalid location '%s' for identifier '%s': File locations are not allowed", + location, identifier); + } + }); + } + + private class BasePolarisCatalogTableBuilder + extends BaseMetastoreViewCatalog.BaseMetastoreViewCatalogTableBuilder { + private final TableIdentifier identifier; + + public BasePolarisCatalogTableBuilder(TableIdentifier identifier, Schema schema) { + super(identifier, schema); + this.identifier = identifier; + } + + @Override + public TableBuilder withLocation(String newLocation) { + return super.withLocation(transformTableLikeLocation(newLocation)); + } + } + + private class BasePolarisCatalogViewBuilder extends BaseMetastoreViewCatalog.BaseViewBuilder { + private final TableIdentifier identifier; + + public BasePolarisCatalogViewBuilder(TableIdentifier identifier) { + super(identifier); + this.identifier = identifier; + } + + @Override + public ViewBuilder withLocation(String newLocation) { + return super.withLocation(transformTableLikeLocation(newLocation)); + } + } + + private class BasePolarisTableOperations extends BaseMetastoreTableOperations { + private final TableIdentifier tableIdentifier; + private final String fullTableName; + private FileIO fileIO; + + BasePolarisTableOperations(FileIO defaultFileIO, TableIdentifier tableIdentifier) { + LOG.debug("new BasePolarisTableOperations for {}", tableIdentifier); + this.tableIdentifier = tableIdentifier; + this.fullTableName = fullTableName(catalogName, tableIdentifier); + this.fileIO = defaultFileIO; + } + + @Override + public void doRefresh() { + LOG.debug("doRefresh for tableIdentifier {}", tableIdentifier); + // While doing refresh/commit protocols, we must fetch the fresh "passthrough" resolved + // table entity instead of the statically-resolved authz resolution set. + PolarisResolvedPathWrapper resolvedEntities = + resolvedEntityView.getPassthroughResolvedPath( + tableIdentifier, PolarisEntitySubType.TABLE); + TableLikeEntity entity = null; + + if (resolvedEntities != null) { + entity = TableLikeEntity.of(resolvedEntities.getRawLeafEntity()); + if (!tableIdentifier.equals(entity.getTableIdentifier())) { + LOG.atError() + .addKeyValue("entity.getTableIdentifier()", entity.getTableIdentifier()) + .addKeyValue("tableIdentifier", tableIdentifier) + .log("Stored entity identifier mismatches requested identifier"); + } + } + + String latestLocation = entity != null ? entity.getMetadataLocation() : null; + LOG.debug("Refreshing latestLocation: {}", latestLocation); + if (latestLocation == null) { + disableRefresh(); + } else { + refreshFromMetadataLocation( + latestLocation, + SHOULD_RETRY_REFRESH_PREDICATE, + MAX_RETRIES, + metadataLocation -> { + FileIO fileIO = this.fileIO; + boolean closeFileIO = false; + PolarisResolvedPathWrapper resolvedStorageEntity = + resolvedEntities == null + ? resolvedEntityView.getResolvedPath(tableIdentifier.namespace()) + : resolvedEntities; + String latestLocationDir = + latestLocation.substring(0, latestLocation.lastIndexOf('/')); + Optional storageInfoEntity = + findStorageInfoFromHierarchy(resolvedStorageEntity); + Map credentialsMap = + storageInfoEntity + .map( + storageInfo -> + refreshCredentials( + tableIdentifier, + Set.of(PolarisStorageActions.READ), + latestLocationDir, + storageInfo)) + .orElse(Map.of()); + if (!credentialsMap.isEmpty()) { + String ioImpl = fileIO.getClass().getName(); + fileIO = loadFileIO(ioImpl, credentialsMap); + closeFileIO = true; + } + try { + return TableMetadataParser.read(fileIO, metadataLocation); + } finally { + if (closeFileIO) { + fileIO.close(); + } + } + }); + } + } + + @Override + public void doCommit(TableMetadata base, TableMetadata metadata) { + LOG.debug("doCommit for {} with base {}, metadata {}", tableIdentifier, base, metadata); + // TODO: Maybe avoid writing metadata if there's definitely a transaction conflict + if (null == base && !namespaceExists(tableIdentifier.namespace())) { + throw new NoSuchNamespaceException( + "Cannot create table %s. Namespace does not exist: %s", + tableIdentifier, tableIdentifier.namespace()); + } + + PolarisResolvedPathWrapper resolvedTableEntities = + resolvedEntityView.getPassthroughResolvedPath( + tableIdentifier, PolarisEntitySubType.TABLE); + + // Fetch credentials for the resolved entity. The entity could be the table itself (if it has + // already been stored and credentials have been configured directly) or it could be the + // table's namespace or catalog. + PolarisResolvedPathWrapper resolvedStorageEntity = + resolvedTableEntities == null + ? resolvedEntityView.getResolvedPath(tableIdentifier.namespace()) + : resolvedTableEntities; + + if (base == null || !metadata.location().equals(base.location())) { + // If location is changing then we must validate that the requested location is valid + // for the storage configuration inherited under this entity's path. + validateLocationForTableLike(tableIdentifier, metadata.location(), resolvedStorageEntity); + } + + Optional storageInfoEntity = + findStorageInfoFromHierarchy(resolvedStorageEntity); + Map credentialsMap = + storageInfoEntity + .map( + storageInfo -> + refreshCredentials( + tableIdentifier, + Set.of(PolarisStorageActions.READ, PolarisStorageActions.WRITE), + metadata.location(), + storageInfo)) + .orElse(Map.of()); + + // Update the FileIO before we write the new metadata file + // update with table properties in case there are table-level overrides + // the credentials should always override table-level properties, since + // storage configuration will be found at whatever entity defines it + Map tableProperties = new HashMap<>(metadata.properties()); + tableProperties.putAll(credentialsMap); + if (!tableProperties.isEmpty()) { + String ioImpl = fileIO.getClass().getName(); + fileIO = loadFileIO(ioImpl, tableProperties); + // ensure the new fileIO is closed when the catalog is closed + closeableGroup.addCloseable(fileIO); + } + + String newLocation = writeNewMetadataIfRequired(base == null, metadata); + String oldLocation = base == null ? null : base.metadataFileLocation(); + + PolarisResolvedPathWrapper resolvedView = + resolvedEntityView.getPassthroughResolvedPath(tableIdentifier, PolarisEntitySubType.VIEW); + if (resolvedView != null) { + throw new AlreadyExistsException("View with same name already exists: %s", tableIdentifier); + } + + // TODO: Consider using the entity from doRefresh() directly to do the conflict detection + // instead of a two-layer CAS (checking metadataLocation to detect concurrent modification + // between doRefresh() and doCommit(), and then updateEntityPropertiesIfNotChanged to detect + // concurrent + // modification between our checking of unchanged metadataLocation here and actual + // persistence-layer commit). + PolarisResolvedPathWrapper resolvedEntities = + resolvedEntityView.getPassthroughResolvedPath( + tableIdentifier, PolarisEntitySubType.TABLE); + TableLikeEntity entity = + TableLikeEntity.of(resolvedEntities == null ? null : resolvedEntities.getRawLeafEntity()); + String existingLocation; + if (null == entity) { + existingLocation = null; + entity = + new TableLikeEntity.Builder(tableIdentifier, newLocation) + .setCatalogId(getCatalogId()) + .setSubType(PolarisEntitySubType.TABLE) + .setId( + entityManager + .getMetaStoreManager() + .generateNewEntityId(getCurrentPolarisContext()) + .getId()) + .build(); + } else { + existingLocation = entity.getMetadataLocation(); + entity = new TableLikeEntity.Builder(entity).setMetadataLocation(newLocation).build(); + } + if (!Objects.equal(existingLocation, oldLocation)) { + if (null == base) { + throw new AlreadyExistsException("Table already exists: %s", tableName()); + } + + if (null == existingLocation) { + throw new NoSuchTableException("Table does not exist: %s", tableName()); + } + + throw new CommitFailedException( + "Cannot commit to table %s metadata location from %s to %s " + + "because it has been concurrently modified to %s", + tableIdentifier, oldLocation, newLocation, existingLocation); + } + if (null == existingLocation) { + createTableLike(catalogId, tableIdentifier, entity); + } else { + updateTableLike(catalogId, tableIdentifier, entity); + } + } + + @Override + public FileIO io() { + return fileIO; + } + + @Override + protected String tableName() { + return fullTableName; + } + } + + private static @NotNull Optional findStorageInfoFromHierarchy( + PolarisResolvedPathWrapper resolvedStorageEntity) { + Optional storageInfoEntity = + resolvedStorageEntity.getRawFullPath().reversed().stream() + .filter( + e -> + e.getInternalPropertiesAsMap() + .containsKey(PolarisEntityConstants.getStorageConfigInfoPropertyName())) + .findFirst(); + return storageInfoEntity; + } + + private class BasePolarisViewOperations extends BaseViewOperations { + private final TableIdentifier identifier; + private final String fullViewName; + private FileIO io; + + BasePolarisViewOperations(FileIO io, TableIdentifier identifier) { + this.io = io; + this.identifier = identifier; + this.fullViewName = ViewUtil.fullViewName(catalogName, identifier); + } + + @Override + public void doRefresh() { + PolarisResolvedPathWrapper resolvedEntities = + resolvedEntityView.getPassthroughResolvedPath(identifier, PolarisEntitySubType.VIEW); + TableLikeEntity entity = null; + + if (resolvedEntities != null) { + entity = TableLikeEntity.of(resolvedEntities.getRawLeafEntity()); + if (!identifier.equals(entity.getTableIdentifier())) { + LOG.atError() + .addKeyValue("entity.getTableIdentifier()", entity.getTableIdentifier()) + .addKeyValue("identifier", identifier) + .log("Stored entity identifier mismatches requested identifier"); + } + } + + String latestLocation = entity != null ? entity.getMetadataLocation() : null; + LOG.debug("Refreshing view latestLocation: {}", latestLocation); + if (latestLocation == null) { + disableRefresh(); + } else { + refreshFromMetadataLocation( + latestLocation, + SHOULD_RETRY_REFRESH_PREDICATE, + MAX_RETRIES, + metadataLocation -> { + FileIO fileIO = this.io; + boolean closeFileIO = false; + PolarisResolvedPathWrapper resolvedStorageEntity = + resolvedEntities == null + ? resolvedEntityView.getResolvedPath(identifier.namespace()) + : resolvedEntities; + String latestLocationDir = + latestLocation.substring(0, latestLocation.lastIndexOf('/')); + Optional storageInfoEntity = + findStorageInfoFromHierarchy(resolvedStorageEntity); + Map credentialsMap = + storageInfoEntity + .map( + storageInfo -> + refreshCredentials( + identifier, + Set.of(PolarisStorageActions.READ), + latestLocationDir, + storageInfo)) + .orElse(Map.of()); + if (!credentialsMap.isEmpty()) { + String ioImpl = fileIO.getClass().getName(); + fileIO = loadFileIO(ioImpl, credentialsMap); + closeFileIO = true; + } + try { + return ViewMetadataParser.read(fileIO.newInputFile(metadataLocation)); + } finally { + if (closeFileIO) { + fileIO.close(); + } + } + }); + } + } + + @Override + public void doCommit(ViewMetadata base, ViewMetadata metadata) { + // TODO: Maybe avoid writing metadata if there's definitely a transaction conflict + LOG.debug("doCommit for {} with base {}, metadata {}", identifier, base, metadata); + if (null == base && !namespaceExists(identifier.namespace())) { + throw new NoSuchNamespaceException( + "Cannot create view %s. Namespace does not exist: %s", + identifier, identifier.namespace()); + } + + PolarisResolvedPathWrapper resolvedTable = + resolvedEntityView.getPassthroughResolvedPath(identifier, PolarisEntitySubType.TABLE); + if (resolvedTable != null) { + throw new AlreadyExistsException("Table with same name already exists: %s", identifier); + } + + PolarisResolvedPathWrapper resolvedEntities = + resolvedEntityView.getPassthroughResolvedPath(identifier, PolarisEntitySubType.VIEW); + + // Fetch credentials for the resolved entity. The entity could be the view itself (if it has + // already been stored and credentials have been configured directly) or it could be the + // table's namespace or catalog. + PolarisResolvedPathWrapper resolvedStorageEntity = + resolvedEntities == null + ? resolvedEntityView.getResolvedPath(identifier.namespace()) + : resolvedEntities; + + if (base == null || !metadata.location().equals(base.location())) { + // If location is changing then we must validate that the requested location is valid + // for the storage configuration inherited under this entity's path. + validateLocationForTableLike(identifier, metadata.location(), resolvedStorageEntity); + } + + Optional storageInfoEntity = + findStorageInfoFromHierarchy(resolvedStorageEntity); + Map credentialsMap = + storageInfoEntity + .map( + storageInfo -> + refreshCredentials( + identifier, + Set.of(PolarisStorageActions.READ, PolarisStorageActions.WRITE), + metadata.location(), + storageInfo)) + .orElse(Map.of()); + + // Update the FileIO before we write the new metadata file + // update with table properties in case there are table-level overrides + // the credentials should always override table-level properties, since + // storage configuration will be found at whatever entity defines it + Map tableProperties = new HashMap<>(metadata.properties()); + tableProperties.putAll(credentialsMap); + if (!tableProperties.isEmpty()) { + String ioImpl = io.getClass().getName(); + io = loadFileIO(ioImpl, tableProperties); + // ensure the new fileIO is closed when the catalog is closed + closeableGroup.addCloseable(io); + } + String newLocation = writeNewMetadataIfRequired(metadata); + String oldLocation = base == null ? null : currentMetadataLocation(); + + if (null == base && !namespaceExists(identifier.namespace())) { + throw new NoSuchNamespaceException( + "Cannot create view %s. Namespace does not exist: %s", + identifier, identifier.namespace()); + } + + TableLikeEntity entity = + TableLikeEntity.of(resolvedEntities == null ? null : resolvedEntities.getRawLeafEntity()); + String existingLocation; + if (null == entity) { + existingLocation = null; + entity = + new TableLikeEntity.Builder(identifier, newLocation) + .setCatalogId(getCatalogId()) + .setSubType(PolarisEntitySubType.VIEW) + .setId( + entityManager + .getMetaStoreManager() + .generateNewEntityId(getCurrentPolarisContext()) + .getId()) + .build(); + } else { + existingLocation = entity.getMetadataLocation(); + entity = new TableLikeEntity.Builder(entity).setMetadataLocation(newLocation).build(); + } + if (!Objects.equal(existingLocation, oldLocation)) { + if (null == base) { + throw new AlreadyExistsException("View already exists: %s", identifier); + } + + if (null == existingLocation) { + throw new NoSuchViewException("View does not exist: %s", identifier); + } + + throw new CommitFailedException( + "Cannot commit to view %s metadata location from %s to %s " + + "because it has been concurrently modified to %s", + identifier, oldLocation, newLocation, existingLocation); + } + if (null == existingLocation) { + createTableLike(catalogId, identifier, entity); + } else { + updateTableLike(catalogId, identifier, entity); + } + } + + @Override + public FileIO io() { + return io; + } + + @Override + protected String viewName() { + return fullViewName; + } + } + + private PolarisCallContext getCurrentPolarisContext() { + return callContext.getPolarisCallContext(); + } + + @VisibleForTesting + long getCatalogId() { + // TODO: Properly handle initialization + if (catalogId <= 0) { + throw new RuntimeException( + "Failed to initialize catalogId before using catalog with name: " + catalogName); + } + return catalogId; + } + + private void renameTableLike( + long catalogId, PolarisEntitySubType subType, TableIdentifier from, TableIdentifier to) { + LOG.debug("Renaming tableLike from {} to {}", from, to); + PolarisResolvedPathWrapper resolvedEntities = resolvedEntityView.getResolvedPath(from, subType); + if (resolvedEntities == null) { + if (subType == PolarisEntitySubType.VIEW) { + throw new NoSuchViewException("Cannot rename %s to %s. View does not exist", from, to); + } else { + throw new NoSuchTableException("Cannot rename %s to %s. Table does not exist", from, to); + } + } + List catalogPath = resolvedEntities.getRawParentPath(); + PolarisEntity leafEntity = resolvedEntities.getRawLeafEntity(); + final TableLikeEntity toEntity; + List newCatalogPath = null; + if (!from.namespace().equals(to.namespace())) { + PolarisResolvedPathWrapper resolvedNewParentEntities = + resolvedEntityView.getResolvedPath(to.namespace()); + if (resolvedNewParentEntities == null) { + throw new NoSuchNamespaceException( + "Cannot rename %s to %s. Namespace does not exist: %s", from, to, to.namespace()); + } + newCatalogPath = resolvedNewParentEntities.getRawFullPath(); + + // the "to" table has a new parent and a new name / namespace path + toEntity = + new TableLikeEntity.Builder(TableLikeEntity.of(leafEntity)) + .setTableIdentifier(to) + .setParentId(resolvedNewParentEntities.getResolvedLeafEntity().getEntity().getId()) + .build(); + } else { + // only the name of the entity is changed + toEntity = + new TableLikeEntity.Builder(TableLikeEntity.of(leafEntity)) + .setTableIdentifier(to) + .build(); + } + + // rename the entity now + PolarisMetaStoreManager.EntityResult returnedEntityResult = + entityManager + .getMetaStoreManager() + .renameEntity( + getCurrentPolarisContext(), + PolarisEntity.toCoreList(catalogPath), + leafEntity, + PolarisEntity.toCoreList(newCatalogPath), + toEntity); + + // handle error + if (!returnedEntityResult.isSuccess()) { + LOG.debug( + "Rename error {} trying to rename {} to {}. Checking existing object.", + returnedEntityResult.getReturnStatus(), + from, + to); + switch (returnedEntityResult.getReturnStatus()) { + case PolarisMetaStoreManager.ReturnStatus.ENTITY_ALREADY_EXISTS: + { + PolarisEntitySubType existingEntitySubType = + returnedEntityResult.getAlreadyExistsEntitySubType(); + if (existingEntitySubType == null) { + // this code path is unexpected + throw new AlreadyExistsException( + "Cannot rename %s to %s. Object %s already exists", from, to); + } else if (existingEntitySubType == PolarisEntitySubType.TABLE) { + throw new AlreadyExistsException( + "Cannot rename %s to %s. Table already exists", from, to); + } else if (existingEntitySubType == PolarisEntitySubType.VIEW) { + throw new AlreadyExistsException( + "Cannot rename %s to %s. View already exists", from, to); + } + } + + case PolarisMetaStoreManager.ReturnStatus.ENTITY_NOT_FOUND: + throw new NotFoundException("Cannot rename %s to %s. %s does not exist", from, to, from); + + // this is temporary. Should throw a special error that will be caught and retried + case PolarisMetaStoreManager.ReturnStatus.TARGET_ENTITY_CONCURRENTLY_MODIFIED: + case PolarisMetaStoreManager.ReturnStatus.ENTITY_CANNOT_BE_RESOLVED: + throw new RuntimeException("concurrent update detected, please retry"); + + // some entities cannot be renamed + case PolarisMetaStoreManager.ReturnStatus.ENTITY_CANNOT_BE_RENAMED: + throw new BadRequestException("Cannot rename built-in object " + leafEntity.getName()); + + // some entities cannot be renamed + default: + throw new IllegalStateException( + "Unknown error status " + returnedEntityResult.getReturnStatus()); + } + } else { + TableLikeEntity returnedEntity = TableLikeEntity.of(returnedEntityResult.getEntity()); + if (!toEntity.getTableIdentifier().equals(returnedEntity.getTableIdentifier())) { + // As long as there are older deployments which don't support the atomic update of the + // internalProperties during rename, we can log and then patch it up explicitly + // in a best-effort way. + LOG.atError() + .addKeyValue("toEntity.getTableIdentifier()", toEntity.getTableIdentifier()) + .addKeyValue("returnedEntity.getTableIdentifier()", returnedEntity.getTableIdentifier()) + .log("Returned entity identifier doesn't match toEntity identifier"); + entityManager + .getMetaStoreManager() + .updateEntityPropertiesIfNotChanged( + getCurrentPolarisContext(), + PolarisEntity.toCoreList(newCatalogPath), + new TableLikeEntity.Builder(returnedEntity).setTableIdentifier(to).build()); + } + } + } + + /** + * Caller must fill in all entity fields except parentId, since the caller may not want to + * duplicate the logic to try to reolve parentIds before constructing the proposed entity. This + * method will fill in the parentId if needed upon resolution. + */ + private void createTableLike(long catalogId, TableIdentifier identifier, PolarisEntity entity) { + PolarisResolvedPathWrapper resolvedParent = + resolvedEntityView.getResolvedPath(identifier.namespace()); + if (resolvedParent == null) { + // Illegal state because the namespace should've already been in the static resolution set. + throw new IllegalStateException( + String.format("Failed to fetch resolved parent for TableIdentifier '%s'", identifier)); + } + + createTableLike(catalogId, identifier, entity, resolvedParent); + } + + private void createTableLike( + long catalogId, + TableIdentifier identifier, + PolarisEntity entity, + PolarisResolvedPathWrapper resolvedParent) { + // Make sure the metadata file is valid for our allowed locations. + String metadataLocation = TableLikeEntity.of(entity).getMetadataLocation(); + validateLocationForTableLike(identifier, metadataLocation, resolvedParent); + + List catalogPath = resolvedParent.getRawFullPath(); + + if (entity.getParentId() <= 0) { + // TODO: Validate catalogPath size is at least 1 for catalog entity? + entity = + new PolarisEntity.Builder(entity) + .setParentId(resolvedParent.getRawLeafEntity().getId()) + .build(); + } + entity = + new PolarisEntity.Builder(entity).setCreateTimestamp(System.currentTimeMillis()).build(); + + PolarisEntity returnedEntity = + PolarisEntity.of( + entityManager + .getMetaStoreManager() + .createEntityIfNotExists( + getCurrentPolarisContext(), PolarisEntity.toCoreList(catalogPath), entity)); + LOG.debug("Created TableLike entity {} with TableIdentifier {}", entity, identifier); + if (returnedEntity == null) { + // TODO: Error or retry? + } + } + + private void updateTableLike(long catalogId, TableIdentifier identifier, PolarisEntity entity) { + PolarisResolvedPathWrapper resolvedEntities = + resolvedEntityView.getResolvedPath(identifier, entity.getSubType()); + if (resolvedEntities == null) { + // Illegal state because the identifier should've already been in the static resolution set. + throw new IllegalStateException( + String.format("Failed to fetch resolved TableIdentifier '%s'", identifier)); + } + + // Make sure the metadata file is valid for our allowed locations. + String metadataLocation = TableLikeEntity.of(entity).getMetadataLocation(); + validateLocationForTableLike(identifier, metadataLocation, resolvedEntities); + + List catalogPath = resolvedEntities.getRawParentPath(); + PolarisEntity returnedEntity = + Optional.ofNullable( + entityManager + .getMetaStoreManager() + .updateEntityPropertiesIfNotChanged( + getCurrentPolarisContext(), PolarisEntity.toCoreList(catalogPath), entity) + .getEntity()) + .map(PolarisEntity::new) + .orElse(null); + if (returnedEntity == null) { + // TODO: Error or retry? + } + } + + private @NotNull PolarisMetaStoreManager.DropEntityResult dropTableLike( + long catalogId, + PolarisEntitySubType subType, + TableIdentifier identifier, + Map storageProperties, + boolean purge) { + PolarisResolvedPathWrapper resolvedEntities = + resolvedEntityView.getResolvedPath(identifier, subType); + if (resolvedEntities == null) { + // TODO: Error? + return new PolarisMetaStoreManager.DropEntityResult( + PolarisMetaStoreManager.ReturnStatus.ENTITY_NOT_FOUND, null); + } + + List catalogPath = resolvedEntities.getRawParentPath(); + PolarisEntity leafEntity = resolvedEntities.getRawLeafEntity(); + return entityManager + .getMetaStoreManager() + .dropEntityIfExists( + getCurrentPolarisContext(), + PolarisEntity.toCoreList(catalogPath), + leafEntity, + storageProperties, + purge); + } + + private boolean sendNotificationForTableLike( + long catalogId, + PolarisEntitySubType subType, + TableIdentifier tableIdentifier, + NotificationRequest request) { + LOG.debug("Handling notification request {} for tableIdentifier {}", request, tableIdentifier); + PolarisResolvedPathWrapper resolvedEntities = + resolvedEntityView.getPassthroughResolvedPath(tableIdentifier, subType); + + NotificationType notificationType = request.getNotificationType(); + + Preconditions.checkNotNull(notificationType, "Expected a valid notification type."); + + if (notificationType == NotificationType.DROP) { + return dropTableLike( + catalogId, PolarisEntitySubType.TABLE, tableIdentifier, Map.of(), false /* purge */) + .isSuccess(); + } else if (notificationType == NotificationType.CREATE + || notificationType == NotificationType.UPDATE) { + + Namespace ns = tableIdentifier.namespace(); + createNonExistingNamespaces(ns); + + PolarisResolvedPathWrapper resolvedParent = resolvedEntityView.getPassthroughResolvedPath(ns); + + TableLikeEntity entity = + TableLikeEntity.of(resolvedEntities == null ? null : resolvedEntities.getRawLeafEntity()); + + String existingLocation; + String newLocation = transformTableLikeLocation(request.getPayload().getMetadataLocation()); + if (null == entity) { + existingLocation = null; + entity = + new TableLikeEntity.Builder(tableIdentifier, newLocation) + .setCatalogId(getCatalogId()) + .setSubType(PolarisEntitySubType.TABLE) + .setId( + entityManager + .getMetaStoreManager() + .generateNewEntityId(getCurrentPolarisContext()) + .getId()) + .build(); + } else { + existingLocation = entity.getMetadataLocation(); + entity = new TableLikeEntity.Builder(entity).setMetadataLocation(newLocation).build(); + } + // TODO: These might fail due to concurrent update; we need to do a retry in those cases. + if (null == existingLocation) { + LOG.debug( + "Creating table {} for notification with metadataLocation {}", + tableIdentifier, + newLocation); + createTableLike(catalogId, tableIdentifier, entity, resolvedParent); + } else { + LOG.debug( + "Updating table {} for notification with metadataLocation {}", + tableIdentifier, + newLocation); + updateTableLike(catalogId, tableIdentifier, entity); + } + } + return true; + } + + private void createNonExistingNamespaces(Namespace namespace) { + // Pre-create namespaces if they don't exist + for (int i = 1; i <= namespace.length(); i++) { + Namespace nsLevel = + Namespace.of( + Arrays.stream(namespace.levels()) + .limit(i) + .collect(Collectors.toList()) + .toArray(String[]::new)); + if (resolvedEntityView.getPassthroughResolvedPath(nsLevel) == null) { + Namespace parentNamespace = PolarisCatalogHelpers.getParentNamespace(nsLevel); + PolarisResolvedPathWrapper resolvedParent = + resolvedEntityView.getPassthroughResolvedPath(parentNamespace); + createNamespaceInternal(nsLevel, Collections.emptyMap(), resolvedParent); + } + } + } + + private List listTableLike( + long catalogId, PolarisEntitySubType subType, Namespace namespace) { + PolarisResolvedPathWrapper resolvedEntities = resolvedEntityView.getResolvedPath(namespace); + if (resolvedEntities == null) { + // Illegal state because the namespace should've already been in the static resolution set. + throw new IllegalStateException( + String.format("Failed to fetch resolved namespace '%s'", namespace)); + } + + List catalogPath = resolvedEntities.getRawFullPath(); + List entities = + PolarisEntity.toNameAndIdList( + entityManager + .getMetaStoreManager() + .listEntities( + getCurrentPolarisContext(), + PolarisEntity.toCoreList(catalogPath), + PolarisEntityType.TABLE_LIKE, + subType) + .getEntities()); + return PolarisCatalogHelpers.nameAndIdToTableIdentifiers(catalogPath, entities); + } + + /** + * Load FileIO with provided impl and properties + * + * @param ioImpl full class name of a custom FileIO implementation + * @param properties used to initialize the FileIO implementation + * @return FileIO object + */ + private FileIO loadFileIO(String ioImpl, Map properties) { + Map propertiesWithS3CustomizedClientFactory = new HashMap<>(properties); + propertiesWithS3CustomizedClientFactory.put( + S3FileIOProperties.CLIENT_FACTORY, PolarisS3FileIOClientFactory.class.getName()); + return CatalogUtil.loadFileIO( + ioImpl, propertiesWithS3CustomizedClientFactory, new Configuration()); + } + + /** + * Check if the exception is retryable for the storage provider + * + * @param ex exception + * @return true if the exception is retryable + */ + private static boolean isStorageProviderRetryableException(Exception ex) { + // For S3/Azure, the exception is not wrapped, while for GCP the exception is wrapped as a + // RuntimeException + Throwable rootCause = ExceptionUtils.getRootCause(ex); + if (rootCause == null) { + // no root cause, let it retry + return true; + } + // only S3 SdkException has this retryable property + if (rootCause instanceof SdkException && ((SdkException) rootCause).retryable()) { + return true; + } + // add more cases here if needed + // AccessDenied is not retryable + return !isAccessDenied(rootCause.getMessage()); + } + + private static boolean isAccessDenied(String errorMsg) { + // corresponding error messages for storage providers Aws/Azure/Gcp + boolean isAccessDenied = + errorMsg != null + && (errorMsg.contains("Access Denied") + || errorMsg.contains("This request is not authorized to perform this operation") + || errorMsg.contains("Forbidden")); + if (isAccessDenied) { + LOG.debug("Access Denied or Forbidden error: {}", errorMsg); + return true; + } + return false; + } +} diff --git a/polaris-service/src/main/java/io/polaris/service/catalog/IcebergCatalogAdapter.java b/polaris-service/src/main/java/io/polaris/service/catalog/IcebergCatalogAdapter.java new file mode 100644 index 0000000000..662dd7e03d --- /dev/null +++ b/polaris-service/src/main/java/io/polaris/service/catalog/IcebergCatalogAdapter.java @@ -0,0 +1,463 @@ +package io.polaris.service.catalog; + +import com.google.common.base.Preconditions; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableMap; +import io.polaris.core.auth.AuthenticatedPolarisPrincipal; +import io.polaris.core.auth.PolarisAuthorizer; +import io.polaris.core.context.CallContext; +import io.polaris.core.context.RealmContext; +import io.polaris.core.entity.PolarisEntity; +import io.polaris.core.persistence.PolarisEntityManager; +import io.polaris.core.persistence.cache.EntityCacheEntry; +import io.polaris.core.persistence.resolver.Resolver; +import io.polaris.core.persistence.resolver.ResolverStatus; +import io.polaris.service.catalog.api.IcebergRestCatalogApiService; +import io.polaris.service.catalog.api.IcebergRestConfigurationApiService; +import io.polaris.service.config.RealmEntityManagerFactory; +import io.polaris.service.context.CallContextCatalogFactory; +import io.polaris.service.types.CommitTableRequest; +import io.polaris.service.types.CommitViewRequest; +import io.polaris.service.types.NotificationRequest; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.SecurityContext; +import java.net.URLEncoder; +import java.nio.charset.Charset; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; +import org.apache.iceberg.UpdateRequirement; +import org.apache.iceberg.catalog.Catalog; +import org.apache.iceberg.catalog.Namespace; +import org.apache.iceberg.catalog.TableIdentifier; +import org.apache.iceberg.exceptions.BadRequestException; +import org.apache.iceberg.exceptions.NotAuthorizedException; +import org.apache.iceberg.exceptions.NotFoundException; +import org.apache.iceberg.rest.RESTUtil; +import org.apache.iceberg.rest.requests.CommitTransactionRequest; +import org.apache.iceberg.rest.requests.CreateNamespaceRequest; +import org.apache.iceberg.rest.requests.CreateTableRequest; +import org.apache.iceberg.rest.requests.CreateViewRequest; +import org.apache.iceberg.rest.requests.RegisterTableRequest; +import org.apache.iceberg.rest.requests.RenameTableRequest; +import org.apache.iceberg.rest.requests.ReportMetricsRequest; +import org.apache.iceberg.rest.requests.UpdateNamespacePropertiesRequest; +import org.apache.iceberg.rest.requests.UpdateTableRequest; +import org.apache.iceberg.rest.responses.ConfigResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * {@link IcebergRestCatalogApiService} implementation that delegates operations to {@link + * org.apache.iceberg.rest.CatalogHandlers} after finding the appropriate {@link Catalog} for the + * current {@link RealmContext}. + */ +public class IcebergCatalogAdapter + implements IcebergRestCatalogApiService, IcebergRestConfigurationApiService { + private static final Logger LOG = LoggerFactory.getLogger(IcebergCatalogAdapter.class); + + private final CallContextCatalogFactory catalogFactory; + private final RealmEntityManagerFactory entityManagerFactory; + private PolarisAuthorizer polarisAuthorizer; + + public IcebergCatalogAdapter( + CallContextCatalogFactory catalogFactory, + RealmEntityManagerFactory entityManagerFactory, + PolarisAuthorizer polarisAuthorizer) { + this.catalogFactory = catalogFactory; + this.entityManagerFactory = entityManagerFactory; + this.polarisAuthorizer = polarisAuthorizer; + } + + private PolarisCatalogHandlerWrapper newHandlerWrapper( + SecurityContext securityContext, String catalogName) { + CallContext callContext = CallContext.getCurrentContext(); + AuthenticatedPolarisPrincipal authenticatedPrincipal = + (AuthenticatedPolarisPrincipal) securityContext.getUserPrincipal(); + if (authenticatedPrincipal == null) { + throw new NotAuthorizedException("Failed to find authenticatedPrincipal in SecurityContext"); + } + + PolarisEntityManager entityManager = + entityManagerFactory.getOrCreateEntityManager(callContext.getRealmContext()); + + return new PolarisCatalogHandlerWrapper( + callContext, + entityManager, + authenticatedPrincipal, + catalogFactory, + catalogName, + polarisAuthorizer); + } + + @Override + public Response createNamespace( + String prefix, + CreateNamespaceRequest createNamespaceRequest, + SecurityContext securityContext) { + return Response.ok( + newHandlerWrapper(securityContext, prefix).createNamespace(createNamespaceRequest)) + .build(); + } + + @Override + public Response listNamespaces( + String prefix, + String pageToken, + Integer pageSize, + String parent, + SecurityContext securityContext) { + Optional namespaceOptional = + Optional.ofNullable(parent).map(IcebergCatalogAdapter::decodeNamespace); + return Response.ok( + newHandlerWrapper(securityContext, prefix) + .listNamespaces(namespaceOptional.orElse(Namespace.of()))) + .build(); + } + + @Override + public Response loadNamespaceMetadata( + String prefix, String namespace, SecurityContext securityContext) { + Namespace ns = decodeNamespace(namespace); + return Response.ok(newHandlerWrapper(securityContext, prefix).loadNamespaceMetadata(ns)) + .build(); + } + + private static Namespace decodeNamespace(String namespace) { + return RESTUtil.decodeNamespace(URLEncoder.encode(namespace, Charset.defaultCharset())); + } + + @Override + public Response namespaceExists( + String prefix, String namespace, SecurityContext securityContext) { + Namespace ns = decodeNamespace(namespace); + newHandlerWrapper(securityContext, prefix).namespaceExists(ns); + return Response.ok().build(); + } + + @Override + public Response dropNamespace(String prefix, String namespace, SecurityContext securityContext) { + Namespace ns = decodeNamespace(namespace); + newHandlerWrapper(securityContext, prefix).dropNamespace(ns); + return Response.ok(Response.Status.NO_CONTENT).build(); + } + + @Override + public Response updateProperties( + String prefix, + String namespace, + UpdateNamespacePropertiesRequest updateNamespacePropertiesRequest, + SecurityContext securityContext) { + Namespace ns = decodeNamespace(namespace); + return Response.ok( + newHandlerWrapper(securityContext, prefix) + .updateNamespaceProperties(ns, updateNamespacePropertiesRequest)) + .build(); + } + + @Override + public Response createTable( + String prefix, + String namespace, + CreateTableRequest createTableRequest, + String xIcebergAccessDelegation, + SecurityContext securityContext) { + Namespace ns = decodeNamespace(namespace); + if (createTableRequest.stageCreate()) { + if (Strings.isNullOrEmpty(xIcebergAccessDelegation)) { + return Response.ok( + newHandlerWrapper(securityContext, prefix) + .createTableStaged(ns, createTableRequest)) + .build(); + } else { + return Response.ok( + newHandlerWrapper(securityContext, prefix) + .createTableStagedWithWriteDelegation( + ns, createTableRequest, xIcebergAccessDelegation)) + .build(); + } + } else if (Strings.isNullOrEmpty(xIcebergAccessDelegation)) { + return Response.ok( + newHandlerWrapper(securityContext, prefix).createTableDirect(ns, createTableRequest)) + .build(); + } else { + return Response.ok( + newHandlerWrapper(securityContext, prefix) + .createTableDirectWithWriteDelegation(ns, createTableRequest)) + .build(); + } + } + + @Override + public Response listTables( + String prefix, + String namespace, + String pageToken, + Integer pageSize, + SecurityContext securityContext) { + Namespace ns = decodeNamespace(namespace); + return Response.ok(newHandlerWrapper(securityContext, prefix).listTables(ns)).build(); + } + + @Override + public Response loadTable( + String prefix, + String namespace, + String table, + String xIcebergAccessDelegation, + String snapshots, + SecurityContext securityContext) { + Namespace ns = decodeNamespace(namespace); + TableIdentifier tableIdentifier = TableIdentifier.of(ns, RESTUtil.decodeString(table)); + if (Strings.isNullOrEmpty(xIcebergAccessDelegation)) { + return Response.ok( + newHandlerWrapper(securityContext, prefix).loadTable(tableIdentifier, snapshots)) + .build(); + } else { + return Response.ok( + newHandlerWrapper(securityContext, prefix) + .loadTableWithAccessDelegation( + tableIdentifier, xIcebergAccessDelegation, snapshots)) + .build(); + } + } + + @Override + public Response tableExists( + String prefix, String namespace, String table, SecurityContext securityContext) { + Namespace ns = decodeNamespace(namespace); + TableIdentifier tableIdentifier = TableIdentifier.of(ns, RESTUtil.decodeString(table)); + newHandlerWrapper(securityContext, prefix).tableExists(tableIdentifier); + return Response.ok().build(); + } + + @Override + public Response dropTable( + String prefix, + String namespace, + String table, + Boolean purgeRequested, + SecurityContext securityContext) { + Namespace ns = decodeNamespace(namespace); + TableIdentifier tableIdentifier = TableIdentifier.of(ns, RESTUtil.decodeString(table)); + + if (purgeRequested != null && purgeRequested.booleanValue()) { + newHandlerWrapper(securityContext, prefix).dropTableWithPurge(tableIdentifier); + } else { + newHandlerWrapper(securityContext, prefix).dropTableWithoutPurge(tableIdentifier); + } + return Response.ok(Response.Status.NO_CONTENT).build(); + } + + @Override + public Response registerTable( + String prefix, + String namespace, + RegisterTableRequest registerTableRequest, + SecurityContext securityContext) { + Namespace ns = decodeNamespace(namespace); + return Response.ok( + newHandlerWrapper(securityContext, prefix).registerTable(ns, registerTableRequest)) + .build(); + } + + @Override + public Response renameTable( + String prefix, RenameTableRequest renameTableRequest, SecurityContext securityContext) { + newHandlerWrapper(securityContext, prefix).renameTable(renameTableRequest); + return Response.ok(javax.ws.rs.core.Response.Status.NO_CONTENT).build(); + } + + @Override + public Response updateTable( + String prefix, + String namespace, + String table, + CommitTableRequest commitTableRequest, + SecurityContext securityContext) { + Namespace ns = decodeNamespace(namespace); + TableIdentifier tableIdentifier = TableIdentifier.of(ns, RESTUtil.decodeString(table)); + + if (isCreate(commitTableRequest)) { + return Response.ok( + newHandlerWrapper(securityContext, prefix) + .updateTableForStagedCreate(tableIdentifier, commitTableRequest)) + .build(); + } else { + return Response.ok( + newHandlerWrapper(securityContext, prefix) + .updateTable(tableIdentifier, commitTableRequest)) + .build(); + } + } + + /** + * TODO: Make the helper in org.apache.iceberg.rest.CatalogHandlers public instead of needing to + * copy/pastehere. + */ + private static boolean isCreate(UpdateTableRequest request) { + boolean isCreate = + request.requirements().stream() + .anyMatch(UpdateRequirement.AssertTableDoesNotExist.class::isInstance); + + if (isCreate) { + List invalidRequirements = + request.requirements().stream() + .filter(req -> !(req instanceof UpdateRequirement.AssertTableDoesNotExist)) + .collect(Collectors.toList()); + Preconditions.checkArgument( + invalidRequirements.isEmpty(), "Invalid create requirements: %s", invalidRequirements); + } + + return isCreate; + } + + @Override + public Response createView( + String prefix, + String namespace, + CreateViewRequest createViewRequest, + SecurityContext securityContext) { + Namespace ns = decodeNamespace(namespace); + return Response.ok(newHandlerWrapper(securityContext, prefix).createView(ns, createViewRequest)) + .build(); + } + + @Override + public Response listViews( + String prefix, + String namespace, + String pageToken, + Integer pageSize, + SecurityContext securityContext) { + Namespace ns = decodeNamespace(namespace); + return Response.ok(newHandlerWrapper(securityContext, prefix).listViews(ns)).build(); + } + + @Override + public Response loadView( + String prefix, String namespace, String view, SecurityContext securityContext) { + Namespace ns = decodeNamespace(namespace); + TableIdentifier tableIdentifier = TableIdentifier.of(ns, RESTUtil.decodeString(view)); + return Response.ok(newHandlerWrapper(securityContext, prefix).loadView(tableIdentifier)) + .build(); + } + + @Override + public Response viewExists( + String prefix, String namespace, String view, SecurityContext securityContext) { + Namespace ns = decodeNamespace(namespace); + TableIdentifier tableIdentifier = TableIdentifier.of(ns, RESTUtil.decodeString(view)); + newHandlerWrapper(securityContext, prefix).viewExists(tableIdentifier); + return Response.ok().build(); + } + + @Override + public Response dropView( + String prefix, String namespace, String view, SecurityContext securityContext) { + Namespace ns = decodeNamespace(namespace); + TableIdentifier tableIdentifier = TableIdentifier.of(ns, RESTUtil.decodeString(view)); + newHandlerWrapper(securityContext, prefix).dropView(tableIdentifier); + return Response.ok(Response.Status.NO_CONTENT).build(); + } + + @Override + public Response renameView( + String prefix, RenameTableRequest renameTableRequest, SecurityContext securityContext) { + newHandlerWrapper(securityContext, prefix).renameView(renameTableRequest); + return Response.ok(Response.Status.NO_CONTENT).build(); + } + + @Override + public Response replaceView( + String prefix, + String namespace, + String view, + CommitViewRequest commitViewRequest, + SecurityContext securityContext) { + Namespace ns = decodeNamespace(namespace); + TableIdentifier tableIdentifier = TableIdentifier.of(ns, RESTUtil.decodeString(view)); + return Response.ok( + newHandlerWrapper(securityContext, prefix) + .replaceView(tableIdentifier, commitViewRequest)) + .build(); + } + + @Override + public Response commitTransaction( + String prefix, + CommitTransactionRequest commitTransactionRequest, + SecurityContext securityContext) { + newHandlerWrapper(securityContext, prefix).commitTransaction(commitTransactionRequest); + return Response.status(Response.Status.NO_CONTENT).build(); + } + + @Override + public Response reportMetrics( + String prefix, + String namespace, + String table, + ReportMetricsRequest reportMetricsRequest, + SecurityContext securityContext) { + return Response.status(Response.Status.NO_CONTENT).build(); + } + + @Override + public Response sendNotification( + String prefix, + String namespace, + String table, + NotificationRequest notificationRequest, + SecurityContext securityContext) { + Namespace ns = decodeNamespace(namespace); + TableIdentifier tableIdentifier = TableIdentifier.of(ns, RESTUtil.decodeString(table)); + newHandlerWrapper(securityContext, prefix) + .sendNotification(tableIdentifier, notificationRequest); + return Response.status(Response.Status.NO_CONTENT).build(); + } + + /** From IcebergRestConfigurationApiService. */ + @Override + public Response getConfig(String warehouse, SecurityContext securityContext) { + // 'warehouse' as an input here is catalogName. + // 'warehouse' as an output will be treated by the client as a default catalog + // storage + // base location. + // 'prefix' as an output is the REST subpath that routes to the catalog + // resource, + // which may be URL-escaped catalogName or potentially a different unique + // identifier for + // the catalog being accessed. + // TODO: Push this down into PolarisCatalogHandlerWrapper for authorizing "any" catalog + // role in this catalog. + PolarisEntityManager entityManager = + entityManagerFactory.getOrCreateEntityManager( + CallContext.getCurrentContext().getRealmContext()); + AuthenticatedPolarisPrincipal authenticatedPrincipal = + (AuthenticatedPolarisPrincipal) securityContext.getUserPrincipal(); + if (authenticatedPrincipal == null) { + throw new NotAuthorizedException("Failed to find authenticatedPrincipal in SecurityContext"); + } + if (warehouse == null) { + throw new BadRequestException("Please specify a warehouse"); + } + Resolver resolver = + entityManager.prepareResolver( + CallContext.getCurrentContext(), authenticatedPrincipal, warehouse); + ResolverStatus resolverStatus = resolver.resolveAll(); + if (!resolverStatus.getStatus().equals(ResolverStatus.StatusEnum.SUCCESS)) { + throw new NotFoundException("Unable to find warehouse " + warehouse); + } + EntityCacheEntry resolvedReferenceCatalog = resolver.getResolvedReferenceCatalog(); + Map properties = + PolarisEntity.of(resolvedReferenceCatalog.getEntity()).getPropertiesAsMap(); + + return Response.ok( + ConfigResponse.builder() + .withDefaults(properties) // catalog properties are defaults + .withOverrides(ImmutableMap.of("prefix", warehouse)) + .build()) + .build(); + } +} diff --git a/polaris-service/src/main/java/io/polaris/service/catalog/PolarisCatalogHandlerWrapper.java b/polaris-service/src/main/java/io/polaris/service/catalog/PolarisCatalogHandlerWrapper.java new file mode 100644 index 0000000000..b3a291aacf --- /dev/null +++ b/polaris-service/src/main/java/io/polaris/service/catalog/PolarisCatalogHandlerWrapper.java @@ -0,0 +1,1045 @@ +package io.polaris.service.catalog; + +import com.google.common.collect.Maps; +import io.polaris.core.auth.AuthenticatedPolarisPrincipal; +import io.polaris.core.auth.PolarisAuthorizableOperation; +import io.polaris.core.auth.PolarisAuthorizer; +import io.polaris.core.catalog.PolarisCatalogHelpers; +import io.polaris.core.context.CallContext; +import io.polaris.core.entity.CatalogEntity; +import io.polaris.core.entity.PolarisEntitySubType; +import io.polaris.core.entity.PolarisEntityType; +import io.polaris.core.persistence.PolarisEntityManager; +import io.polaris.core.persistence.PolarisResolvedPathWrapper; +import io.polaris.core.persistence.resolver.PolarisResolutionManifest; +import io.polaris.core.persistence.resolver.ResolverPath; +import io.polaris.core.persistence.resolver.ResolverStatus; +import io.polaris.core.storage.PolarisStorageActions; +import io.polaris.service.context.CallContextCatalogFactory; +import io.polaris.service.types.NotificationRequest; +import java.io.Closeable; +import java.io.IOException; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import org.apache.iceberg.BaseMetadataTable; +import org.apache.iceberg.BaseTable; +import org.apache.iceberg.BaseTransaction; +import org.apache.iceberg.MetadataUpdate; +import org.apache.iceberg.PartitionSpec; +import org.apache.iceberg.SortOrder; +import org.apache.iceberg.Table; +import org.apache.iceberg.TableMetadata; +import org.apache.iceberg.Transaction; +import org.apache.iceberg.Transactions; +import org.apache.iceberg.UpdateRequirement; +import org.apache.iceberg.catalog.Catalog; +import org.apache.iceberg.catalog.Namespace; +import org.apache.iceberg.catalog.SupportsNamespaces; +import org.apache.iceberg.catalog.TableIdentifier; +import org.apache.iceberg.catalog.ViewCatalog; +import org.apache.iceberg.exceptions.AlreadyExistsException; +import org.apache.iceberg.exceptions.BadRequestException; +import org.apache.iceberg.exceptions.ForbiddenException; +import org.apache.iceberg.exceptions.NoSuchNamespaceException; +import org.apache.iceberg.exceptions.NoSuchTableException; +import org.apache.iceberg.exceptions.NoSuchViewException; +import org.apache.iceberg.rest.CatalogHandlers; +import org.apache.iceberg.rest.requests.CommitTransactionRequest; +import org.apache.iceberg.rest.requests.CreateNamespaceRequest; +import org.apache.iceberg.rest.requests.CreateTableRequest; +import org.apache.iceberg.rest.requests.CreateViewRequest; +import org.apache.iceberg.rest.requests.RegisterTableRequest; +import org.apache.iceberg.rest.requests.RenameTableRequest; +import org.apache.iceberg.rest.requests.UpdateNamespacePropertiesRequest; +import org.apache.iceberg.rest.requests.UpdateTableRequest; +import org.apache.iceberg.rest.responses.CreateNamespaceResponse; +import org.apache.iceberg.rest.responses.GetNamespaceResponse; +import org.apache.iceberg.rest.responses.ListNamespacesResponse; +import org.apache.iceberg.rest.responses.ListTablesResponse; +import org.apache.iceberg.rest.responses.LoadTableResponse; +import org.apache.iceberg.rest.responses.LoadViewResponse; +import org.apache.iceberg.rest.responses.UpdateNamespacePropertiesResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Authorization-aware adapter between REST stubs and shared Iceberg SDK CatalogHandlers. + * + *

We must make authorization decisions based on entity resolution at this layer instead of the + * underlying BasePolarisCatalog layer, because this REST-adjacent layer captures intent of + * different REST calls that share underlying catalog calls (e.g. updateTable will call loadTable + * under the hood), and some features of the REST API aren't expressed at all in the underlying + * Catalog interfaces (e.g. credential-vending in createTable/loadTable). + * + *

We also want this layer to be independent of API-endpoint-specific idioms, such as dealing + * with jakarta.ws.rs.core.Response objects, and other implementations that expose different HTTP + * stubs or even tunnel the protocol over something like gRPC can still normalize on the Iceberg + * model objects used in this layer to still benefit from the shared implementation of + * authorization-aware catalog protocols. + */ +public class PolarisCatalogHandlerWrapper { + private static final Logger LOG = LoggerFactory.getLogger(PolarisCatalogHandlerWrapper.class); + + private final CallContext callContext; + private final PolarisEntityManager entityManager; + private final String catalogName; + private final AuthenticatedPolarisPrincipal authenticatedPrincipal; + private final PolarisAuthorizer authorizer; + private final CallContextCatalogFactory catalogFactory; + + // Initialized in the authorize methods. + private PolarisResolutionManifest resolutionManifest = null; + + // Catalog instance will be initialized after authorizing resolver successfully resolves + // the catalog entity. + private Catalog baseCatalog = null; + private SupportsNamespaces namespaceCatalog = null; + private ViewCatalog viewCatalog = null; + + public PolarisCatalogHandlerWrapper( + CallContext callContext, + PolarisEntityManager entityManager, + AuthenticatedPolarisPrincipal authenticatedPrincipal, + CallContextCatalogFactory catalogFactory, + String catalogName, + PolarisAuthorizer authorizer) { + this.callContext = callContext; + this.entityManager = entityManager; + this.catalogName = catalogName; + this.authenticatedPrincipal = authenticatedPrincipal; + this.authorizer = authorizer; + this.catalogFactory = catalogFactory; + } + + private void initializeCatalog() { + this.baseCatalog = catalogFactory.createCallContextCatalog(callContext, resolutionManifest); + this.namespaceCatalog = + (baseCatalog instanceof SupportsNamespaces) ? (SupportsNamespaces) baseCatalog : null; + this.viewCatalog = (baseCatalog instanceof ViewCatalog) ? (ViewCatalog) baseCatalog : null; + } + + private void authorizeBasicNamespaceOperationOrThrow( + PolarisAuthorizableOperation op, Namespace namespace) { + authorizeBasicNamespaceOperationOrThrow(op, namespace, null, null); + } + + private void authorizeBasicNamespaceOperationOrThrow( + PolarisAuthorizableOperation op, + Namespace namespace, + List extraPassthroughNamespaces, + List extraPassthroughTableLikes) { + resolutionManifest = + entityManager.prepareResolutionManifest(callContext, authenticatedPrincipal, catalogName); + resolutionManifest.addPath( + new ResolverPath(Arrays.asList(namespace.levels()), PolarisEntityType.NAMESPACE), + namespace); + + if (extraPassthroughNamespaces != null) { + for (Namespace ns : extraPassthroughNamespaces) { + resolutionManifest.addPassthroughPath( + new ResolverPath( + Arrays.asList(ns.levels()), PolarisEntityType.NAMESPACE, true /* optional */), + ns); + } + } + if (extraPassthroughTableLikes != null) { + for (TableIdentifier id : extraPassthroughTableLikes) { + resolutionManifest.addPassthroughPath( + new ResolverPath( + PolarisCatalogHelpers.tableIdentifierToList(id), + PolarisEntityType.TABLE_LIKE, + true /* optional */), + id); + } + } + resolutionManifest.resolveAll(); + PolarisResolvedPathWrapper target = resolutionManifest.getResolvedPath(namespace, true); + if (target == null) { + throw new NoSuchNamespaceException("Namespace does not exist: %s", namespace); + } + authorizer.authorizeOrThrow( + authenticatedPrincipal, + resolutionManifest.getAllActivatedCatalogRoleAndPrincipalRoleIds(), + op, + target, + null /* secondary */); + + initializeCatalog(); + } + + private void authorizeCreateNamespaceUnderNamespaceOperationOrThrow( + PolarisAuthorizableOperation op, Namespace namespace) { + resolutionManifest = + entityManager.prepareResolutionManifest(callContext, authenticatedPrincipal, catalogName); + + Namespace parentNamespace = PolarisCatalogHelpers.getParentNamespace(namespace); + resolutionManifest.addPath( + new ResolverPath(Arrays.asList(parentNamespace.levels()), PolarisEntityType.NAMESPACE), + parentNamespace); + + // When creating an entity under a namespace, the authz target is the parentNamespace, but we + // must also add the actual path that will be created as an "optional" passthrough resolution + // path to indicate that the underlying catalog is "allowed" to check the creation path for + // a conflicting entity. + resolutionManifest.addPassthroughPath( + new ResolverPath( + Arrays.asList(namespace.levels()), PolarisEntityType.NAMESPACE, true /* optional */), + namespace); + resolutionManifest.resolveAll(); + PolarisResolvedPathWrapper target = resolutionManifest.getResolvedPath(parentNamespace, true); + if (target == null) { + throw new NoSuchNamespaceException("Namespace does not exist: %s", parentNamespace); + } + authorizer.authorizeOrThrow( + authenticatedPrincipal, + resolutionManifest.getAllActivatedCatalogRoleAndPrincipalRoleIds(), + op, + target, + null /* secondary */); + + initializeCatalog(); + } + + private void authorizeCreateTableLikeUnderNamespaceOperationOrThrow( + PolarisAuthorizableOperation op, TableIdentifier identifier) { + Namespace namespace = identifier.namespace(); + + resolutionManifest = + entityManager.prepareResolutionManifest(callContext, authenticatedPrincipal, catalogName); + resolutionManifest.addPath( + new ResolverPath(Arrays.asList(namespace.levels()), PolarisEntityType.NAMESPACE), + namespace); + + // When creating an entity under a namespace, the authz target is the namespace, but we must + // also + // add the actual path that will be created as an "optional" passthrough resolution path to + // indicate that the underlying catalog is "allowed" to check the creation path for a + // conflicting + // entity. + resolutionManifest.addPassthroughPath( + new ResolverPath( + PolarisCatalogHelpers.tableIdentifierToList(identifier), + PolarisEntityType.TABLE_LIKE, + true /* optional */), + identifier); + resolutionManifest.resolveAll(); + PolarisResolvedPathWrapper target = resolutionManifest.getResolvedPath(namespace, true); + if (target == null) { + throw new NoSuchNamespaceException("Namespace does not exist: %s", namespace); + } + authorizer.authorizeOrThrow( + authenticatedPrincipal, + resolutionManifest.getAllActivatedCatalogRoleAndPrincipalRoleIds(), + op, + target, + null /* secondary */); + + initializeCatalog(); + } + + private void authorizeBasicTableLikeOperationOrThrow( + PolarisAuthorizableOperation op, PolarisEntitySubType subType, TableIdentifier identifier) { + resolutionManifest = + entityManager.prepareResolutionManifest(callContext, authenticatedPrincipal, catalogName); + + // The underlying Catalog is also allowed to fetch "fresh" versions of the target entity. + resolutionManifest.addPassthroughPath( + new ResolverPath( + PolarisCatalogHelpers.tableIdentifierToList(identifier), + PolarisEntityType.TABLE_LIKE, + true /* optional */), + identifier); + resolutionManifest.resolveAll(); + PolarisResolvedPathWrapper target = + resolutionManifest.getResolvedPath(identifier, subType, true); + if (target == null) { + if (subType == PolarisEntitySubType.TABLE) { + throw new NoSuchTableException("Table does not exist: %s", identifier); + } else { + throw new NoSuchViewException("View does not exist: %s", identifier); + } + } + authorizer.authorizeOrThrow( + authenticatedPrincipal, + resolutionManifest.getAllActivatedCatalogRoleAndPrincipalRoleIds(), + op, + target, + null /* secondary */); + + initializeCatalog(); + } + + private void authorizeCollectionOfTableLikeOperationOrThrow( + PolarisAuthorizableOperation op, + final PolarisEntitySubType subType, + List ids) { + resolutionManifest = + entityManager.prepareResolutionManifest(callContext, authenticatedPrincipal, catalogName); + ids.stream() + .forEach( + identifier -> + resolutionManifest.addPassthroughPath( + new ResolverPath( + PolarisCatalogHelpers.tableIdentifierToList(identifier), + PolarisEntityType.TABLE_LIKE), + identifier)); + + ResolverStatus status = resolutionManifest.resolveAll(); + + // If one of the paths failed to resolve, throw exception based on the one that + // we first failed to resolve. + if (status.getStatus() == ResolverStatus.StatusEnum.PATH_COULD_NOT_BE_FULLY_RESOLVED) { + TableIdentifier identifier = + PolarisCatalogHelpers.listToTableIdentifier( + status.getFailedToResolvePath().getEntityNames()); + if (subType == PolarisEntitySubType.TABLE) { + throw new NoSuchTableException("Table does not exist: %s", identifier); + } else { + throw new NoSuchViewException("View does not exist: %s", identifier); + } + } + + List targets = + ids.stream() + .map( + identifier -> + Optional.ofNullable( + resolutionManifest.getResolvedPath(identifier, subType, true)) + .orElseThrow( + () -> + subType == PolarisEntitySubType.TABLE + ? new NoSuchTableException( + "Table does not exist: %s", identifier) + : new NoSuchViewException( + "View does not exist: %s", identifier))) + .toList(); + authorizer.authorizeOrThrow( + authenticatedPrincipal, + resolutionManifest.getAllActivatedCatalogRoleAndPrincipalRoleIds(), + op, + targets, + null /* secondaries */); + + initializeCatalog(); + } + + private void authorizeRenameTableLikeOperationOrThrow( + PolarisAuthorizableOperation op, + PolarisEntitySubType subType, + TableIdentifier src, + TableIdentifier dst) { + resolutionManifest = + entityManager.prepareResolutionManifest(callContext, authenticatedPrincipal, catalogName); + // Add src, dstParent, and dst(optional) + resolutionManifest.addPath( + new ResolverPath( + PolarisCatalogHelpers.tableIdentifierToList(src), PolarisEntityType.TABLE_LIKE), + src); + resolutionManifest.addPath( + new ResolverPath(Arrays.asList(dst.namespace().levels()), PolarisEntityType.NAMESPACE), + dst.namespace()); + resolutionManifest.addPath( + new ResolverPath( + PolarisCatalogHelpers.tableIdentifierToList(dst), + PolarisEntityType.TABLE_LIKE, + true /* optional */), + dst); + ResolverStatus status = resolutionManifest.resolveAll(); + if (status.getStatus() == ResolverStatus.StatusEnum.PATH_COULD_NOT_BE_FULLY_RESOLVED + && status.getFailedToResolvePath().getLastEntityType() == PolarisEntityType.NAMESPACE) { + throw new NoSuchNamespaceException("Namespace does not exist: %s", dst.namespace()); + } else if (resolutionManifest.getResolvedPath(src, subType) == null) { + if (subType == PolarisEntitySubType.TABLE) { + throw new NoSuchTableException("Table does not exist: %s", src); + } else { + throw new NoSuchViewException("View does not exist: %s", src); + } + } + + // Normally, since we added the dst as an optional path, we'd expect it to only get resolved + // up to its parent namespace, and for there to be no TABLE_LIKE already in the dst in which + // case the leafSubType will be NULL_SUBTYPE. + // If there is a conflicting TABLE or VIEW, this leafSubType will indicate that conflicting + // type. + // TODO: Possibly modify the exception thrown depending on whether the caller has privileges + // on the parent namespace. + PolarisEntitySubType dstLeafSubType = resolutionManifest.getLeafSubType(dst); + if (dstLeafSubType == PolarisEntitySubType.TABLE) { + throw new AlreadyExistsException("Cannot rename %s to %s. Table already exists", src, dst); + } else if (dstLeafSubType == PolarisEntitySubType.VIEW) { + throw new AlreadyExistsException("Cannot rename %s to %s. View already exists", src, dst); + } + + PolarisResolvedPathWrapper target = resolutionManifest.getResolvedPath(src, subType, true); + PolarisResolvedPathWrapper secondary = + resolutionManifest.getResolvedPath(dst.namespace(), true); + authorizer.authorizeOrThrow( + authenticatedPrincipal, + resolutionManifest.getAllActivatedCatalogRoleAndPrincipalRoleIds(), + op, + target, + secondary); + + initializeCatalog(); + } + + public ListNamespacesResponse listNamespaces(Namespace parent) { + PolarisAuthorizableOperation op = PolarisAuthorizableOperation.LIST_NAMESPACES; + authorizeBasicNamespaceOperationOrThrow(op, parent); + + return doCatalogOperation(() -> CatalogHandlers.listNamespaces(namespaceCatalog, parent)); + } + + public CreateNamespaceResponse createNamespace(CreateNamespaceRequest request) { + PolarisAuthorizableOperation op = PolarisAuthorizableOperation.CREATE_NAMESPACE; + + Namespace namespace = request.namespace(); + if (namespace.length() == 0) { + throw new AlreadyExistsException( + "Cannot create root namespace, as it already exists implicitly."); + } + authorizeCreateNamespaceUnderNamespaceOperationOrThrow(op, namespace); + + if (namespaceCatalog instanceof BasePolarisCatalog) { + // Note: The CatalogHandlers' default implementation will non-atomically create the + // namespace and then fetch its properties using loadNamespaceMetadata for the response. + // However, the latest namespace metadata technically isn't the same authorized instance, + // so we don't want all cals to loadNamespaceMetadata to automatically use the manifest + // in "passthrough" mode. + // + // For CreateNamespace, we consider this a special case in that the creator is able to + // retrieve the latest namespace metadata for the duration of the CreateNamespace + // operation, even if the entityVersion and/or grantsVersion update in the interim. + return doCatalogOperation( + () -> { + namespaceCatalog.createNamespace(namespace, request.properties()); + return CreateNamespaceResponse.builder() + .withNamespace(namespace) + .setProperties( + resolutionManifest + .getPassthroughResolvedPath(namespace) + .getRawLeafEntity() + .getPropertiesAsMap()) + .build(); + }); + } else { + return doCatalogOperation(() -> CatalogHandlers.createNamespace(namespaceCatalog, request)); + } + } + + private static boolean isExternal(CatalogEntity catalog) { + return io.polaris.core.admin.model.Catalog.TypeEnum.EXTERNAL.equals(catalog.getCatalogType()); + } + + private void doCatalogOperation(Runnable handler) { + doCatalogOperation( + () -> { + handler.run(); + return null; + }); + } + + /** + * Execute a catalog function and ensure we close the BaseCatalog afterward. This will typically + * ensure the underlying FileIO is closed + * + * @param handler + * @return + * @param + */ + private T doCatalogOperation(Supplier handler) { + try { + return handler.get(); + } finally { + if (baseCatalog instanceof Closeable closeable) { + try { + closeable.close(); + } catch (IOException e) { + LOG.error("Error while closing BaseCatalog", e); + } + } + } + } + + public GetNamespaceResponse loadNamespaceMetadata(Namespace namespace) { + PolarisAuthorizableOperation op = PolarisAuthorizableOperation.LOAD_NAMESPACE_METADATA; + authorizeBasicNamespaceOperationOrThrow(op, namespace); + + return doCatalogOperation(() -> CatalogHandlers.loadNamespace(namespaceCatalog, namespace)); + } + + public void namespaceExists(Namespace namespace) { + PolarisAuthorizableOperation op = PolarisAuthorizableOperation.NAMESPACE_EXISTS; + + // TODO: This authz check doesn't accomplish true authz in terms of blocking the ability + // for a caller to ascertain whether the namespace exists or not, but instead just behaves + // according to convention -- if existence is going to be privileged, we must instead + // add a base layer that throws NotFound exceptions instead of NotAuthorizedException + // for *all* operations in which we determine that the basic privilege for determining + // existence is also missing. + authorizeBasicNamespaceOperationOrThrow(op, namespace); + + // TODO: Just skip CatalogHandlers for this one maybe + doCatalogOperation(() -> CatalogHandlers.loadNamespace(namespaceCatalog, namespace)); + } + + public void dropNamespace(Namespace namespace) { + PolarisAuthorizableOperation op = PolarisAuthorizableOperation.DROP_NAMESPACE; + authorizeBasicNamespaceOperationOrThrow(op, namespace); + + doCatalogOperation(() -> CatalogHandlers.dropNamespace(namespaceCatalog, namespace)); + } + + public UpdateNamespacePropertiesResponse updateNamespaceProperties( + Namespace namespace, UpdateNamespacePropertiesRequest request) { + PolarisAuthorizableOperation op = PolarisAuthorizableOperation.UPDATE_NAMESPACE_PROPERTIES; + authorizeBasicNamespaceOperationOrThrow(op, namespace); + + return doCatalogOperation( + () -> CatalogHandlers.updateNamespaceProperties(namespaceCatalog, namespace, request)); + } + + public ListTablesResponse listTables(Namespace namespace) { + PolarisAuthorizableOperation op = PolarisAuthorizableOperation.LIST_TABLES; + authorizeBasicNamespaceOperationOrThrow(op, namespace); + + return doCatalogOperation(() -> CatalogHandlers.listTables(baseCatalog, namespace)); + } + + public LoadTableResponse createTableDirect(Namespace namespace, CreateTableRequest request) { + PolarisAuthorizableOperation op = PolarisAuthorizableOperation.CREATE_TABLE_DIRECT; + authorizeCreateTableLikeUnderNamespaceOperationOrThrow( + op, TableIdentifier.of(namespace, request.name())); + + CatalogEntity catalog = + CatalogEntity.of( + resolutionManifest + .getResolvedReferenceCatalogEntity() + .getResolvedLeafEntity() + .getEntity()); + if (isExternal(catalog)) { + throw new BadRequestException("Cannot create table on external catalogs."); + } + return doCatalogOperation(() -> CatalogHandlers.createTable(baseCatalog, namespace, request)); + } + + public LoadTableResponse createTableDirectWithWriteDelegation( + Namespace namespace, CreateTableRequest request) { + PolarisAuthorizableOperation op = PolarisAuthorizableOperation.CREATE_TABLE_DIRECT; + authorizeCreateTableLikeUnderNamespaceOperationOrThrow( + op, TableIdentifier.of(namespace, request.name())); + + CatalogEntity catalog = + CatalogEntity.of( + resolutionManifest + .getResolvedReferenceCatalogEntity() + .getResolvedLeafEntity() + .getEntity()); + if (isExternal(catalog)) { + throw new BadRequestException("Cannot create table on external catalogs."); + } + return doCatalogOperation( + () -> { + request.validate(); + + TableIdentifier tableIdentifier = TableIdentifier.of(namespace, request.name()); + if (baseCatalog.tableExists(tableIdentifier)) { + throw new AlreadyExistsException("Table already exists: %s", tableIdentifier); + } + + Map properties = Maps.newHashMap(); + properties.put("created-at", OffsetDateTime.now(ZoneOffset.UTC).toString()); + properties.putAll(request.properties()); + + Table table = + baseCatalog + .buildTable(tableIdentifier, request.schema()) + .withLocation(request.location()) + .withPartitionSpec(request.spec()) + .withSortOrder(request.writeOrder()) + .withProperties(request.properties()) + .create(); + + if (table instanceof BaseTable baseTable) { + TableMetadata tableMetadata = baseTable.operations().current(); + LoadTableResponse.Builder responseBuilder = + LoadTableResponse.builder().withTableMetadata(tableMetadata); + if (baseCatalog instanceof SupportsCredentialDelegation credentialDelegation) { + LOG.atDebug() + .addKeyValue("tableIdentifier", tableIdentifier) + .addKeyValue("tableLocation", tableMetadata.location()) + .log("Fetching client credentials for table"); + responseBuilder.addAllConfig( + credentialDelegation.getCredentialConfig( + tableIdentifier, + tableMetadata, + Set.of( + PolarisStorageActions.READ, + PolarisStorageActions.WRITE, + PolarisStorageActions.LIST))); + } + return responseBuilder.build(); + } else if (table instanceof BaseMetadataTable) { + // metadata tables are loaded on the client side, return NoSuchTableException for now + throw new NoSuchTableException("Table does not exist: %s", tableIdentifier.toString()); + } + + throw new IllegalStateException("Cannot wrap catalog that does not produce BaseTable"); + }); + } + + private TableMetadata stageTableCreateHelper(Namespace namespace, CreateTableRequest request) { + request.validate(); + + TableIdentifier ident = TableIdentifier.of(namespace, request.name()); + if (baseCatalog.tableExists(ident)) { + throw new AlreadyExistsException("Table already exists: %s", ident); + } + + Map properties = Maps.newHashMap(); + properties.put("created-at", OffsetDateTime.now(ZoneOffset.UTC).toString()); + properties.putAll(request.properties()); + + String location; + if (request.location() != null) { + // Even if the request provides a location, run it through the catalog's TableBuilder + // to inherit any override behaviors if applicable. + if (baseCatalog instanceof BasePolarisCatalog) { + location = + ((BasePolarisCatalog) baseCatalog).transformTableLikeLocation(request.location()); + } else { + location = request.location(); + } + } else { + location = + baseCatalog + .buildTable(ident, request.schema()) + .withPartitionSpec(request.spec()) + .withSortOrder(request.writeOrder()) + .withProperties(properties) + .createTransaction() + .table() + .location(); + } + + TableMetadata metadata = + TableMetadata.newTableMetadata( + request.schema(), + request.spec() != null ? request.spec() : PartitionSpec.unpartitioned(), + request.writeOrder() != null ? request.writeOrder() : SortOrder.unsorted(), + location, + properties); + return metadata; + } + + public LoadTableResponse createTableStaged(Namespace namespace, CreateTableRequest request) { + PolarisAuthorizableOperation op = PolarisAuthorizableOperation.CREATE_TABLE_STAGED; + authorizeCreateTableLikeUnderNamespaceOperationOrThrow( + op, TableIdentifier.of(namespace, request.name())); + + CatalogEntity catalog = + CatalogEntity.of( + resolutionManifest + .getResolvedReferenceCatalogEntity() + .getResolvedLeafEntity() + .getEntity()); + if (isExternal(catalog)) { + throw new BadRequestException("Cannot create table on external catalogs."); + } + return doCatalogOperation( + () -> { + TableMetadata metadata = stageTableCreateHelper(namespace, request); + return LoadTableResponse.builder().withTableMetadata(metadata).build(); + }); + } + + public LoadTableResponse createTableStagedWithWriteDelegation( + Namespace namespace, CreateTableRequest request, String xIcebergAccessDelegation) { + PolarisAuthorizableOperation op = + PolarisAuthorizableOperation.CREATE_TABLE_STAGED_WITH_WRITE_DELEGATION; + authorizeCreateTableLikeUnderNamespaceOperationOrThrow( + op, TableIdentifier.of(namespace, request.name())); + + CatalogEntity catalog = + CatalogEntity.of( + resolutionManifest + .getResolvedReferenceCatalogEntity() + .getResolvedLeafEntity() + .getEntity()); + if (isExternal(catalog)) { + throw new BadRequestException("Cannot create table on external catalogs."); + } + return doCatalogOperation( + () -> { + TableIdentifier ident = TableIdentifier.of(namespace, request.name()); + TableMetadata metadata = stageTableCreateHelper(namespace, request); + + LoadTableResponse.Builder responseBuilder = + LoadTableResponse.builder().withTableMetadata(metadata); + + if (baseCatalog instanceof SupportsCredentialDelegation credentialDelegation) { + LOG.atDebug() + .addKeyValue("tableIdentifier", ident) + .addKeyValue("tableLocation", metadata.location()) + .log("Fetching client credentials for table"); + responseBuilder.addAllConfig( + credentialDelegation.getCredentialConfig( + ident, metadata, Set.of(PolarisStorageActions.ALL))); + } + return responseBuilder.build(); + }); + } + + public LoadTableResponse registerTable(Namespace namespace, RegisterTableRequest request) { + PolarisAuthorizableOperation op = PolarisAuthorizableOperation.REGISTER_TABLE; + authorizeCreateTableLikeUnderNamespaceOperationOrThrow( + op, TableIdentifier.of(namespace, request.name())); + + return doCatalogOperation(() -> CatalogHandlers.registerTable(baseCatalog, namespace, request)); + } + + public boolean sendNotification(TableIdentifier identifier, NotificationRequest request) { + PolarisAuthorizableOperation op = PolarisAuthorizableOperation.SEND_NOTIFICATIONS; + + // For now, just require the full set of privileges on the base Catalog entity, which we can + // also express just as the "root" Namespace for purposes of the BasePolarisCatalog being + // able to fetch Namespace.empty() as path key. + List extraPassthroughTableLikes = List.of(identifier); + List extraPassthroughNamespaces = new ArrayList<>(); + extraPassthroughNamespaces.add(Namespace.empty()); + for (int i = 1; i <= identifier.namespace().length(); i++) { + Namespace nsLevel = + Namespace.of( + Arrays.stream(identifier.namespace().levels()) + .limit(i) + .collect(Collectors.toList()) + .toArray(String[]::new)); + extraPassthroughNamespaces.add(nsLevel); + } + authorizeBasicNamespaceOperationOrThrow( + op, Namespace.empty(), extraPassthroughNamespaces, extraPassthroughTableLikes); + + CatalogEntity catalog = + CatalogEntity.of( + resolutionManifest + .getResolvedReferenceCatalogEntity() + .getResolvedLeafEntity() + .getEntity()); + if (catalog.getCatalogType().equals(io.polaris.core.admin.model.Catalog.TypeEnum.INTERNAL)) { + LOG.atWarn() + .addKeyValue("catalog", catalog) + .addKeyValue("notification", request) + .log("Attempted notification on internal catalog"); + throw new BadRequestException("Cannot update internal catalog via notifications"); + } + if (!(baseCatalog instanceof SupportsNotifications)) { + return false; + } + SupportsNotifications notificationCatalog = (SupportsNotifications) baseCatalog; + return notificationCatalog.sendNotification(identifier, request); + } + + public LoadTableResponse loadTable(TableIdentifier tableIdentifier, String snapshots) { + PolarisAuthorizableOperation op = PolarisAuthorizableOperation.LOAD_TABLE; + authorizeBasicTableLikeOperationOrThrow(op, PolarisEntitySubType.TABLE, tableIdentifier); + + return doCatalogOperation(() -> CatalogHandlers.loadTable(baseCatalog, tableIdentifier)); + } + + public LoadTableResponse loadTableWithAccessDelegation( + TableIdentifier tableIdentifier, String xIcebergAccessDelegation, String snapshots) { + // Here we have a single method that falls through multiple candidate + // PolarisAuthorizableOperations because instead of identifying the desired operation up-front + // and + // failing the authz check if grants aren't found, we find the first most-privileged authz match + // and respond according to that. + PolarisAuthorizableOperation op1 = PolarisAuthorizableOperation.LOAD_TABLE_WITH_READ_DELEGATION; + PolarisAuthorizableOperation op2 = + PolarisAuthorizableOperation.LOAD_TABLE_WITH_WRITE_DELEGATION; + + Set actionsRequested = + new HashSet<>(Set.of(PolarisStorageActions.READ, PolarisStorageActions.LIST)); + try { + // TODO: Refactor to have a boolean-return version of the helpers so we can fallthrough + // easily. + authorizeBasicTableLikeOperationOrThrow(op2, PolarisEntitySubType.TABLE, tableIdentifier); + actionsRequested.add(PolarisStorageActions.WRITE); + } catch (ForbiddenException e) { + LOG.atDebug() + .addKeyValue("tableIdentifier", tableIdentifier) + .log("Authz failed for LOAD_TABLE_WITH_WRITE_DELEGATION so attempting READ only"); + authorizeBasicTableLikeOperationOrThrow(op1, PolarisEntitySubType.TABLE, tableIdentifier); + } + + // TODO: Find a way for the configuration or caller to better express whether to fail or omit + // when data-access is specified but access delegation grants are not found. + + return doCatalogOperation( + () -> { + Table table = baseCatalog.loadTable(tableIdentifier); + + if (table instanceof BaseTable baseTable) { + TableMetadata tableMetadata = baseTable.operations().current(); + LoadTableResponse.Builder responseBuilder = + LoadTableResponse.builder().withTableMetadata(tableMetadata); + if (baseCatalog instanceof SupportsCredentialDelegation credentialDelegation) { + LOG.atDebug() + .addKeyValue("tableIdentifier", tableIdentifier) + .addKeyValue("tableLocation", tableMetadata.location()) + .log("Fetching client credentials for table"); + responseBuilder.addAllConfig( + credentialDelegation.getCredentialConfig( + tableIdentifier, tableMetadata, actionsRequested)); + } + return responseBuilder.build(); + } else if (table instanceof BaseMetadataTable) { + // metadata tables are loaded on the client side, return NoSuchTableException for now + throw new NoSuchTableException("Table does not exist: %s", tableIdentifier.toString()); + } + + throw new IllegalStateException("Cannot wrap catalog that does not produce BaseTable"); + }); + } + + private UpdateTableRequest applyUpdateFilters(UpdateTableRequest request) { + // Certain MetadataUpdates need to be explicitly transformed to achieve the same behavior + // as using a local Catalog client via TableBuilder. + TableIdentifier identifier = request.identifier(); + List requirements = request.requirements(); + List updates = + request.updates().stream() + .map( + update -> { + if (baseCatalog instanceof BasePolarisCatalog + && update instanceof MetadataUpdate.SetLocation) { + String requestedLocation = ((MetadataUpdate.SetLocation) update).location(); + String filteredLocation = + ((BasePolarisCatalog) baseCatalog) + .transformTableLikeLocation(requestedLocation); + return new MetadataUpdate.SetLocation(filteredLocation); + } else { + return update; + } + }) + .toList(); + return UpdateTableRequest.create(identifier, requirements, updates); + } + + public LoadTableResponse updateTable( + TableIdentifier tableIdentifier, UpdateTableRequest request) { + PolarisAuthorizableOperation op = PolarisAuthorizableOperation.UPDATE_TABLE; + authorizeBasicTableLikeOperationOrThrow(op, PolarisEntitySubType.TABLE, tableIdentifier); + + CatalogEntity catalog = + CatalogEntity.of( + resolutionManifest + .getResolvedReferenceCatalogEntity() + .getResolvedLeafEntity() + .getEntity()); + if (isExternal(catalog)) { + throw new BadRequestException("Cannot update table on external catalogs."); + } + return doCatalogOperation( + () -> + CatalogHandlers.updateTable(baseCatalog, tableIdentifier, applyUpdateFilters(request))); + } + + public LoadTableResponse updateTableForStagedCreate( + TableIdentifier tableIdentifier, UpdateTableRequest request) { + PolarisAuthorizableOperation op = PolarisAuthorizableOperation.UPDATE_TABLE_FOR_STAGED_CREATE; + authorizeCreateTableLikeUnderNamespaceOperationOrThrow(op, tableIdentifier); + + CatalogEntity catalog = + CatalogEntity.of( + resolutionManifest + .getResolvedReferenceCatalogEntity() + .getResolvedLeafEntity() + .getEntity()); + if (isExternal(catalog)) { + throw new BadRequestException("Cannot update table on external catalogs."); + } + return doCatalogOperation( + () -> + CatalogHandlers.updateTable(baseCatalog, tableIdentifier, applyUpdateFilters(request))); + } + + public void dropTableWithoutPurge(TableIdentifier tableIdentifier) { + PolarisAuthorizableOperation op = PolarisAuthorizableOperation.DROP_TABLE_WITHOUT_PURGE; + authorizeBasicTableLikeOperationOrThrow(op, PolarisEntitySubType.TABLE, tableIdentifier); + + doCatalogOperation(() -> CatalogHandlers.dropTable(baseCatalog, tableIdentifier)); + } + + public void dropTableWithPurge(TableIdentifier tableIdentifier) { + PolarisAuthorizableOperation op = PolarisAuthorizableOperation.DROP_TABLE_WITH_PURGE; + authorizeBasicTableLikeOperationOrThrow(op, PolarisEntitySubType.TABLE, tableIdentifier); + + CatalogEntity catalog = + CatalogEntity.of( + resolutionManifest + .getResolvedReferenceCatalogEntity() + .getResolvedLeafEntity() + .getEntity()); + if (isExternal(catalog)) { + throw new BadRequestException("Cannot drop table on external catalogs."); + } + doCatalogOperation(() -> CatalogHandlers.purgeTable(baseCatalog, tableIdentifier)); + } + + public void tableExists(TableIdentifier tableIdentifier) { + PolarisAuthorizableOperation op = PolarisAuthorizableOperation.TABLE_EXISTS; + authorizeBasicTableLikeOperationOrThrow(op, PolarisEntitySubType.TABLE, tableIdentifier); + + // TODO: Just skip CatalogHandlers for this one maybe + doCatalogOperation(() -> CatalogHandlers.loadTable(baseCatalog, tableIdentifier)); + } + + public void renameTable(RenameTableRequest request) { + PolarisAuthorizableOperation op = PolarisAuthorizableOperation.RENAME_TABLE; + authorizeRenameTableLikeOperationOrThrow( + op, PolarisEntitySubType.TABLE, request.source(), request.destination()); + + CatalogEntity catalog = + CatalogEntity.of( + resolutionManifest + .getResolvedReferenceCatalogEntity() + .getResolvedLeafEntity() + .getEntity()); + if (isExternal(catalog)) { + throw new BadRequestException("Cannot rename table on external catalogs."); + } + doCatalogOperation(() -> CatalogHandlers.renameTable(baseCatalog, request)); + } + + public void commitTransaction(CommitTransactionRequest commitTransactionRequest) { + PolarisAuthorizableOperation op = PolarisAuthorizableOperation.COMMIT_TRANSACTION; + // TODO: The authz actually needs to detect hidden updateForStagedCreate UpdateTableRequests + // and have some kind of per-item conditional privilege requirement if we want to make it + // so that only the stageCreate updates need TABLE_CREATE whereas everything else only + // needs TABLE_WRITE_PROPERTIES. + authorizeCollectionOfTableLikeOperationOrThrow( + op, + PolarisEntitySubType.TABLE, + commitTransactionRequest.tableChanges().stream().map(t -> t.identifier()).toList()); + CatalogEntity catalog = + CatalogEntity.of( + resolutionManifest + .getResolvedReferenceCatalogEntity() + .getResolvedLeafEntity() + .getEntity()); + if (isExternal(catalog)) { + throw new BadRequestException("Cannot update table on external catalogs."); + } + + // TODO: Implement this properly + List transactions = + commitTransactionRequest.tableChanges().stream() + .map( + change -> { + Table table = baseCatalog.loadTable(change.identifier()); + if (!(table instanceof BaseTable)) { + throw new IllegalStateException( + "Cannot wrap catalog that does not produce BaseTable"); + } + Transaction transaction = + Transactions.newTransaction( + change.identifier().toString(), ((BaseTable) table).operations()); + BaseTransaction.TransactionTable txTable = + (BaseTransaction.TransactionTable) transaction.table(); + CatalogHandlers.updateTable(baseCatalog, change.identifier(), change); + return transaction; + }) + .toList(); + + transactions.forEach(Transaction::commitTransaction); + } + + public ListTablesResponse listViews(Namespace namespace) { + PolarisAuthorizableOperation op = PolarisAuthorizableOperation.LIST_VIEWS; + authorizeBasicNamespaceOperationOrThrow(op, namespace); + + return doCatalogOperation(() -> CatalogHandlers.listViews(viewCatalog, namespace)); + } + + public LoadViewResponse createView(Namespace namespace, CreateViewRequest request) { + PolarisAuthorizableOperation op = PolarisAuthorizableOperation.CREATE_VIEW; + authorizeCreateTableLikeUnderNamespaceOperationOrThrow( + op, TableIdentifier.of(namespace, request.name())); + + CatalogEntity catalog = + CatalogEntity.of( + resolutionManifest + .getResolvedReferenceCatalogEntity() + .getResolvedLeafEntity() + .getEntity()); + if (isExternal(catalog)) { + throw new BadRequestException("Cannot create view on external catalogs."); + } + return doCatalogOperation(() -> CatalogHandlers.createView(viewCatalog, namespace, request)); + } + + public LoadViewResponse loadView(TableIdentifier viewIdentifier) { + PolarisAuthorizableOperation op = PolarisAuthorizableOperation.LOAD_VIEW; + authorizeBasicTableLikeOperationOrThrow(op, PolarisEntitySubType.VIEW, viewIdentifier); + + return doCatalogOperation(() -> CatalogHandlers.loadView(viewCatalog, viewIdentifier)); + } + + public LoadViewResponse replaceView(TableIdentifier viewIdentifier, UpdateTableRequest request) { + PolarisAuthorizableOperation op = PolarisAuthorizableOperation.REPLACE_VIEW; + authorizeBasicTableLikeOperationOrThrow(op, PolarisEntitySubType.VIEW, viewIdentifier); + + CatalogEntity catalog = + CatalogEntity.of( + resolutionManifest + .getResolvedReferenceCatalogEntity() + .getResolvedLeafEntity() + .getEntity()); + if (isExternal(catalog)) { + throw new BadRequestException("Cannot replace view on external catalogs."); + } + return doCatalogOperation( + () -> CatalogHandlers.updateView(viewCatalog, viewIdentifier, applyUpdateFilters(request))); + } + + public void dropView(TableIdentifier viewIdentifier) { + PolarisAuthorizableOperation op = PolarisAuthorizableOperation.DROP_VIEW; + authorizeBasicTableLikeOperationOrThrow(op, PolarisEntitySubType.VIEW, viewIdentifier); + + doCatalogOperation(() -> CatalogHandlers.dropView(viewCatalog, viewIdentifier)); + } + + public void viewExists(TableIdentifier viewIdentifier) { + PolarisAuthorizableOperation op = PolarisAuthorizableOperation.VIEW_EXISTS; + authorizeBasicTableLikeOperationOrThrow(op, PolarisEntitySubType.VIEW, viewIdentifier); + + // TODO: Just skip CatalogHandlers for this one maybe + doCatalogOperation(() -> CatalogHandlers.loadView(viewCatalog, viewIdentifier)); + } + + public void renameView(RenameTableRequest request) { + PolarisAuthorizableOperation op = PolarisAuthorizableOperation.RENAME_VIEW; + authorizeRenameTableLikeOperationOrThrow( + op, PolarisEntitySubType.VIEW, request.source(), request.destination()); + + CatalogEntity catalog = + CatalogEntity.of( + resolutionManifest + .getResolvedReferenceCatalogEntity() + .getResolvedLeafEntity() + .getEntity()); + if (isExternal(catalog)) { + throw new BadRequestException("Cannot rename view on external catalogs."); + } + doCatalogOperation(() -> CatalogHandlers.renameView(viewCatalog, request)); + } +} diff --git a/polaris-service/src/main/java/io/polaris/service/catalog/SupportsCredentialDelegation.java b/polaris-service/src/main/java/io/polaris/service/catalog/SupportsCredentialDelegation.java new file mode 100644 index 0000000000..87fc55e09a --- /dev/null +++ b/polaris-service/src/main/java/io/polaris/service/catalog/SupportsCredentialDelegation.java @@ -0,0 +1,21 @@ +package io.polaris.service.catalog; + +import io.polaris.core.storage.PolarisStorageActions; +import java.util.Map; +import java.util.Set; +import org.apache.iceberg.TableMetadata; +import org.apache.iceberg.catalog.TableIdentifier; + +/** + * Adds support for credential vending for (typically) {@link org.apache.iceberg.TableOperations} to + * fetch access credentials that are inserted into the {@link + * org.apache.iceberg.rest.responses.LoadTableResponse#config} property. See the + * rest-catalog-open-api.yaml spec for details on the expected format of vended credential + * configuration. + */ +public interface SupportsCredentialDelegation { + Map getCredentialConfig( + TableIdentifier tableIdentifier, + TableMetadata tableMetadata, + Set storageActions); +} diff --git a/polaris-service/src/main/java/io/polaris/service/catalog/SupportsNotifications.java b/polaris-service/src/main/java/io/polaris/service/catalog/SupportsNotifications.java new file mode 100644 index 0000000000..ca9059033d --- /dev/null +++ b/polaris-service/src/main/java/io/polaris/service/catalog/SupportsNotifications.java @@ -0,0 +1,9 @@ +package io.polaris.service.catalog; + +import io.polaris.service.types.NotificationRequest; +import org.apache.iceberg.catalog.TableIdentifier; + +public interface SupportsNotifications { + + public boolean sendNotification(TableIdentifier table, NotificationRequest notificationRequest); +} diff --git a/polaris-service/src/main/java/io/polaris/service/config/ConfigurationStoreAware.java b/polaris-service/src/main/java/io/polaris/service/config/ConfigurationStoreAware.java new file mode 100644 index 0000000000..a0a383e5a4 --- /dev/null +++ b/polaris-service/src/main/java/io/polaris/service/config/ConfigurationStoreAware.java @@ -0,0 +1,9 @@ +package io.polaris.service.config; + +import io.polaris.core.PolarisConfigurationStore; + +/** Interface allows injection of a {@link PolarisConfigurationStore} */ +public interface ConfigurationStoreAware { + + void setConfigurationStore(PolarisConfigurationStore configurationStore); +} diff --git a/polaris-service/src/main/java/io/polaris/service/config/CorsConfiguration.java b/polaris-service/src/main/java/io/polaris/service/config/CorsConfiguration.java new file mode 100644 index 0000000000..cef7c326cc --- /dev/null +++ b/polaris-service/src/main/java/io/polaris/service/config/CorsConfiguration.java @@ -0,0 +1,77 @@ +package io.polaris.service.config; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; + +public class CorsConfiguration { + private List allowedOrigins = List.of("*"); + private List allowedTimingOrigins = List.of("*"); + private List allowedMethods = List.of("*"); + private List allowedHeaders = List.of("*"); + private List exposedHeaders = List.of("*"); + private Integer preflightMaxAge = 600; + private String allowCredentials = "true"; + + public List getAllowedOrigins() { + return allowedOrigins; + } + + @JsonProperty("allowed-origins") + public void setAllowedOrigins(List allowedOrigins) { + this.allowedOrigins = allowedOrigins; + } + + public void setAllowedTimingOrigins(List allowedTimingOrigins) { + this.allowedTimingOrigins = allowedTimingOrigins; + } + + @JsonProperty("allowed-timing-origins") + public List getAllowedTimingOrigins() { + return allowedTimingOrigins; + } + + public List getAllowedMethods() { + return allowedMethods; + } + + @JsonProperty("allowed-methods") + public void setAllowedMethods(List allowedMethods) { + this.allowedMethods = allowedMethods; + } + + public List getAllowedHeaders() { + return allowedHeaders; + } + + @JsonProperty("allowed-headers") + public void setAllowedHeaders(List allowedHeaders) { + this.allowedHeaders = allowedHeaders; + } + + public List getExposedHeaders() { + return exposedHeaders; + } + + @JsonProperty("exposed-headers") + public void setExposedHeaders(List exposedHeaders) { + this.exposedHeaders = exposedHeaders; + } + + public Integer getPreflightMaxAge() { + return preflightMaxAge; + } + + @JsonProperty("preflight-max-age") + public void setPreflightMaxAge(Integer preflightMaxAge) { + this.preflightMaxAge = preflightMaxAge; + } + + public String getAllowCredentials() { + return allowCredentials; + } + + @JsonProperty("allowed-credentials") + public void setAllowCredentials(String allowCredentials) { + this.allowCredentials = allowCredentials; + } +} diff --git a/polaris-service/src/main/java/io/polaris/service/config/DefaultConfigurationStore.java b/polaris-service/src/main/java/io/polaris/service/config/DefaultConfigurationStore.java new file mode 100644 index 0000000000..fad9ec4177 --- /dev/null +++ b/polaris-service/src/main/java/io/polaris/service/config/DefaultConfigurationStore.java @@ -0,0 +1,19 @@ +package io.polaris.service.config; + +import io.polaris.core.PolarisCallContext; +import io.polaris.core.PolarisConfigurationStore; +import java.util.Map; +import org.jetbrains.annotations.Nullable; + +public class DefaultConfigurationStore implements PolarisConfigurationStore { + private final Map properties; + + public DefaultConfigurationStore(Map properties) { + this.properties = properties; + } + + @Override + public @Nullable T getConfiguration(PolarisCallContext ctx, String configName) { + return (T) properties.get(configName); + } +} diff --git a/polaris-service/src/main/java/io/polaris/service/config/HasEntityManagerFactory.java b/polaris-service/src/main/java/io/polaris/service/config/HasEntityManagerFactory.java new file mode 100644 index 0000000000..71a48fbd04 --- /dev/null +++ b/polaris-service/src/main/java/io/polaris/service/config/HasEntityManagerFactory.java @@ -0,0 +1,5 @@ +package io.polaris.service.config; + +public interface HasEntityManagerFactory { + void setEntityManagerFactory(RealmEntityManagerFactory entityManagerFactory); +} diff --git a/polaris-service/src/main/java/io/polaris/service/config/OAuth2ApiService.java b/polaris-service/src/main/java/io/polaris/service/config/OAuth2ApiService.java new file mode 100644 index 0000000000..488c2c5f5c --- /dev/null +++ b/polaris-service/src/main/java/io/polaris/service/config/OAuth2ApiService.java @@ -0,0 +1,11 @@ +package io.polaris.service.config; + +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.dropwizard.jackson.Discoverable; +import io.polaris.service.auth.TokenBrokerFactory; +import io.polaris.service.catalog.api.IcebergRestOAuth2ApiService; + +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type") +public interface OAuth2ApiService extends Discoverable, IcebergRestOAuth2ApiService { + void setTokenBroker(TokenBrokerFactory tokenBrokerFactory); +} diff --git a/polaris-service/src/main/java/io/polaris/service/config/PolarisApplicationConfig.java b/polaris-service/src/main/java/io/polaris/service/config/PolarisApplicationConfig.java new file mode 100644 index 0000000000..3bcc927714 --- /dev/null +++ b/polaris-service/src/main/java/io/polaris/service/config/PolarisApplicationConfig.java @@ -0,0 +1,144 @@ +package io.polaris.service.config; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.dropwizard.core.Configuration; +import io.polaris.core.PolarisConfigurationStore; +import io.polaris.core.auth.AuthenticatedPolarisPrincipal; +import io.polaris.core.persistence.MetaStoreManagerFactory; +import io.polaris.service.auth.DiscoverableAuthenticator; +import io.polaris.service.context.CallContextResolver; +import io.polaris.service.context.RealmContextResolver; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Configuration specific to a Polaris REST Service. Place these entries in a YML file for them to + * be picked up, i.e. `iceberg-rest-server.yml` + */ +public class PolarisApplicationConfig extends Configuration { + private Map sqlLiteCatalogDirs = new HashMap<>(); + + private String baseCatalogType; + private MetaStoreManagerFactory metaStoreManagerFactory; + private String defaultRealm = "default-realm"; + private RealmContextResolver realmContextResolver; + private CallContextResolver callContextResolver; + private DiscoverableAuthenticator polarisAuthenticator; + private CorsConfiguration corsConfiguration = new CorsConfiguration(); + private TaskHandlerConfiguration taskHandler = new TaskHandlerConfiguration(); + private PolarisConfigurationStore configurationStore = + new DefaultConfigurationStore(new HashMap<>()); + private List defaultRealms; + + public Map getSqlLiteCatalogDirs() { + return sqlLiteCatalogDirs; + } + + public void setSqlLiteCatalogDirs(Map sqlLiteCatalogDirs) { + this.sqlLiteCatalogDirs = sqlLiteCatalogDirs; + } + + @JsonProperty("baseCatalogType") + public void setBaseCatalogType(String baseCatalogType) { + this.baseCatalogType = baseCatalogType; + } + + @JsonProperty("baseCatalogType") + public String getBaseCatalogType() { + return baseCatalogType; + } + + @JsonProperty("metaStoreManager") + public void setMetaStoreManagerFactory(MetaStoreManagerFactory metaStoreManagerFactory) { + this.metaStoreManagerFactory = metaStoreManagerFactory; + } + + @JsonProperty("metaStoreManager") + public MetaStoreManagerFactory getMetaStoreManagerFactory() { + return metaStoreManagerFactory; + } + + @JsonProperty("authenticator") + public void setPolarisAuthenticator( + DiscoverableAuthenticator polarisAuthenticator) { + this.polarisAuthenticator = polarisAuthenticator; + } + + public DiscoverableAuthenticator + getPolarisAuthenticator() { + return polarisAuthenticator; + } + + public RealmContextResolver getRealmContextResolver() { + return realmContextResolver; + } + + public void setRealmContextResolver(RealmContextResolver realmContextResolver) { + this.realmContextResolver = realmContextResolver; + } + + public CallContextResolver getCallContextResolver() { + return callContextResolver; + } + + @JsonProperty("callContextResolver") + public void setCallContextResolver(CallContextResolver callContextResolver) { + this.callContextResolver = callContextResolver; + } + + private OAuth2ApiService oauth2Service; + + @JsonProperty("oauth2") + public void setOauth2Service(OAuth2ApiService oauth2Service) { + this.oauth2Service = oauth2Service; + } + + public OAuth2ApiService getOauth2Service() { + return oauth2Service; + } + + public String getDefaultRealm() { + return defaultRealm; + } + + @JsonProperty("defaultRealm") + public void setDefaultRealm(String defaultRealm) { + this.defaultRealm = defaultRealm; + } + + @JsonProperty("cors") + public CorsConfiguration getCorsConfiguration() { + return corsConfiguration; + } + + @JsonProperty("cors") + public void setCorsConfiguration(CorsConfiguration corsConfiguration) { + this.corsConfiguration = corsConfiguration; + } + + public void setTaskHandler(TaskHandlerConfiguration taskHandler) { + this.taskHandler = taskHandler; + } + + public TaskHandlerConfiguration getTaskHandler() { + return taskHandler; + } + + @JsonProperty("featureConfiguration") + public void setFeatureConfiguration(Map featureConfiguration) { + this.configurationStore = new DefaultConfigurationStore(featureConfiguration); + } + + public PolarisConfigurationStore getConfigurationStore() { + return configurationStore; + } + + public List getDefaultRealms() { + return defaultRealms; + } + + public void setDefaultRealms(List defaultRealms) { + this.defaultRealms = defaultRealms; + } +} diff --git a/polaris-service/src/main/java/io/polaris/service/config/RealmEntityManagerFactory.java b/polaris-service/src/main/java/io/polaris/service/config/RealmEntityManagerFactory.java new file mode 100644 index 0000000000..b95beb91f0 --- /dev/null +++ b/polaris-service/src/main/java/io/polaris/service/config/RealmEntityManagerFactory.java @@ -0,0 +1,46 @@ +package io.polaris.service.config; + +import io.polaris.core.context.RealmContext; +import io.polaris.core.persistence.MetaStoreManagerFactory; +import io.polaris.core.persistence.PolarisEntityManager; +import java.util.HashMap; +import java.util.Map; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** Gets or creates PolarisEntityManager instances based on config values and RealmContext. */ +public class RealmEntityManagerFactory { + private static final Logger LOG = LoggerFactory.getLogger(RealmEntityManagerFactory.class); + private final MetaStoreManagerFactory metaStoreManagerFactory; + + // Key: realmIdentifier + private Map cachedEntityManagers = new HashMap<>(); + + // Subclasses for test injection. + protected RealmEntityManagerFactory() { + this.metaStoreManagerFactory = null; + } + + public RealmEntityManagerFactory(MetaStoreManagerFactory metaStoreManagerFactory) { + this.metaStoreManagerFactory = metaStoreManagerFactory; + } + + public PolarisEntityManager getOrCreateEntityManager(RealmContext context) { + String realm = context.getRealmIdentifier(); + + LOG.debug("Looking up PolarisEntityManager for realm {}", realm); + PolarisEntityManager entityManagerInstance = cachedEntityManagers.get(realm); + if (entityManagerInstance == null) { + LOG.info("Initializing new PolarisEntityManager for realm {}", realm); + + entityManagerInstance = + new PolarisEntityManager( + metaStoreManagerFactory.getOrCreateMetaStoreManager(context), + metaStoreManagerFactory.getOrCreateSessionSupplier(context), + metaStoreManagerFactory.getOrCreateStorageCredentialCache(context)); + + cachedEntityManagers.put(realm, entityManagerInstance); + } + return entityManagerInstance; + } +} diff --git a/polaris-service/src/main/java/io/polaris/service/config/Serializers.java b/polaris-service/src/main/java/io/polaris/service/config/Serializers.java new file mode 100644 index 0000000000..59eb158710 --- /dev/null +++ b/polaris-service/src/main/java/io/polaris/service/config/Serializers.java @@ -0,0 +1,228 @@ +package io.polaris.service.config; + +import com.fasterxml.jackson.core.JacksonException; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.TreeNode; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.node.ObjectNode; +import io.polaris.core.admin.model.AddGrantRequest; +import io.polaris.core.admin.model.Catalog; +import io.polaris.core.admin.model.CatalogRole; +import io.polaris.core.admin.model.CreateCatalogRequest; +import io.polaris.core.admin.model.CreateCatalogRoleRequest; +import io.polaris.core.admin.model.CreatePrincipalRequest; +import io.polaris.core.admin.model.CreatePrincipalRoleRequest; +import io.polaris.core.admin.model.GrantCatalogRoleRequest; +import io.polaris.core.admin.model.GrantPrincipalRoleRequest; +import io.polaris.core.admin.model.GrantResource; +import io.polaris.core.admin.model.Principal; +import io.polaris.core.admin.model.PrincipalRole; +import io.polaris.core.admin.model.RevokeGrantRequest; +import java.io.IOException; + +public final class Serializers { + private Serializers() {} + + public static void registerSerializers(ObjectMapper mapper) { + SimpleModule module = new SimpleModule(); + module.addDeserializer(CreateCatalogRequest.class, new CreateCatalogRequestDeserializer()); + module.addDeserializer(CreatePrincipalRequest.class, new CreatePrincipalRequestDeserializer()); + module.addDeserializer( + CreatePrincipalRoleRequest.class, new CreatePrincipalRoleRequestDeserializer()); + module.addDeserializer( + GrantPrincipalRoleRequest.class, new GrantPrincipalRoleRequestDeserializer()); + module.addDeserializer( + CreateCatalogRoleRequest.class, new CreateCatalogRoleRequestDeserializer()); + module.addDeserializer( + GrantCatalogRoleRequest.class, new GrantCatalogRoleRequestDeserializer()); + module.addDeserializer(AddGrantRequest.class, new AddGrantRequestDeserializer()); + module.addDeserializer(RevokeGrantRequest.class, new RevokeGrantRequestDeserializer()); + mapper.registerModule(module); + } + + /** + * Deserializer for {@link CreateCatalogRequest}. Backward compatible with the previous version of + * the api + */ + public static final class CreateCatalogRequestDeserializer + extends JsonDeserializer { + @Override + public CreateCatalogRequest deserialize(JsonParser p, DeserializationContext ctxt) + throws IOException, JacksonException { + TreeNode treeNode = p.readValueAsTree(); + if (treeNode.isObject() && ((ObjectNode) treeNode).has("catalog")) { + return CreateCatalogRequest.builder() + .setCatalog(ctxt.readTreeAsValue((JsonNode) treeNode.get("catalog"), Catalog.class)) + .build(); + } else { + return CreateCatalogRequest.builder() + .setCatalog(ctxt.readTreeAsValue((JsonNode) treeNode, Catalog.class)) + .build(); + } + } + } + + /** + * Deserializer for {@link CreatePrincipalRequest}. Backward compatible with the previous version + * of the api + */ + public static final class CreatePrincipalRequestDeserializer + extends JsonDeserializer { + @Override + public CreatePrincipalRequest deserialize(JsonParser p, DeserializationContext ctxt) + throws IOException, JacksonException { + TreeNode treeNode = p.readValueAsTree(); + if (treeNode.isObject() && ((ObjectNode) treeNode).has("principal")) { + return CreatePrincipalRequest.builder() + .setPrincipal( + ctxt.readTreeAsValue((JsonNode) treeNode.get("principal"), Principal.class)) + .setCredentialRotationRequired( + ctxt.readTreeAsValue( + (JsonNode) treeNode.get("credentialRotationRequired"), Boolean.class)) + .build(); + } else { + return CreatePrincipalRequest.builder() + .setPrincipal(ctxt.readTreeAsValue((JsonNode) treeNode, Principal.class)) + .build(); + } + } + } + + /** + * Deserializer for {@link CreatePrincipalRoleRequest}. Backward compatible with the previous + * version of the api + */ + public static final class CreatePrincipalRoleRequestDeserializer + extends JsonDeserializer { + @Override + public CreatePrincipalRoleRequest deserialize(JsonParser p, DeserializationContext ctxt) + throws IOException, JacksonException { + TreeNode treeNode = p.readValueAsTree(); + if (treeNode.isObject() && ((ObjectNode) treeNode).has("principalRole")) { + return CreatePrincipalRoleRequest.builder() + .setPrincipalRole( + ctxt.readTreeAsValue((JsonNode) treeNode.get("principalRole"), PrincipalRole.class)) + .build(); + } else { + return CreatePrincipalRoleRequest.builder() + .setPrincipalRole(ctxt.readTreeAsValue((JsonNode) treeNode, PrincipalRole.class)) + .build(); + } + } + } + + /** + * Deserializer for {@link GrantPrincipalRoleRequest}. Backward compatible with the previous + * version of the api + */ + public static final class GrantPrincipalRoleRequestDeserializer + extends JsonDeserializer { + @Override + public GrantPrincipalRoleRequest deserialize(JsonParser p, DeserializationContext ctxt) + throws IOException, JacksonException { + TreeNode treeNode = p.readValueAsTree(); + if (treeNode.isObject() && ((ObjectNode) treeNode).has("principalRole")) { + return GrantPrincipalRoleRequest.builder() + .setPrincipalRole( + ctxt.readTreeAsValue((JsonNode) treeNode.get("principalRole"), PrincipalRole.class)) + .build(); + } else { + return GrantPrincipalRoleRequest.builder() + .setPrincipalRole(ctxt.readTreeAsValue((JsonNode) treeNode, PrincipalRole.class)) + .build(); + } + } + } + + /** + * Deserializer for {@link CreateCatalogRoleRequest} Backward compatible with the previous version + * of the api + */ + public static final class CreateCatalogRoleRequestDeserializer + extends JsonDeserializer { + @Override + public CreateCatalogRoleRequest deserialize(JsonParser p, DeserializationContext ctxt) + throws IOException, JacksonException { + TreeNode treeNode = p.readValueAsTree(); + if (treeNode.isObject() && ((ObjectNode) treeNode).has("catalogRole")) { + return CreateCatalogRoleRequest.builder() + .setCatalogRole( + ctxt.readTreeAsValue((JsonNode) treeNode.get("catalogRole"), CatalogRole.class)) + .build(); + } else { + return CreateCatalogRoleRequest.builder() + .setCatalogRole(ctxt.readTreeAsValue((JsonNode) treeNode, CatalogRole.class)) + .build(); + } + } + } + + /** + * Deserializer for {@link GrantCatalogRoleRequest} Backward compatible with the previous version + * of the api + */ + public static final class GrantCatalogRoleRequestDeserializer + extends JsonDeserializer { + @Override + public GrantCatalogRoleRequest deserialize(JsonParser p, DeserializationContext ctxt) + throws IOException, JacksonException { + TreeNode treeNode = p.readValueAsTree(); + if (treeNode.isObject() && ((ObjectNode) treeNode).has("catalogRole")) { + return GrantCatalogRoleRequest.builder() + .setCatalogRole( + ctxt.readTreeAsValue((JsonNode) treeNode.get("catalogRole"), CatalogRole.class)) + .build(); + } else { + return GrantCatalogRoleRequest.builder() + .setCatalogRole(ctxt.readTreeAsValue((JsonNode) treeNode, CatalogRole.class)) + .build(); + } + } + } + + /** + * Deserializer for {@link AddGrantRequest} Backward compatible with previous version of the api + */ + public static final class AddGrantRequestDeserializer extends JsonDeserializer { + @Override + public AddGrantRequest deserialize(JsonParser p, DeserializationContext ctxt) + throws IOException, JacksonException { + TreeNode treeNode = p.readValueAsTree(); + if (treeNode.isObject() && ((ObjectNode) treeNode).has("grant")) { + return AddGrantRequest.builder() + .setGrant(ctxt.readTreeAsValue((JsonNode) treeNode.get("grant"), GrantResource.class)) + .build(); + } else { + return AddGrantRequest.builder() + .setGrant(ctxt.readTreeAsValue((JsonNode) treeNode, GrantResource.class)) + .build(); + } + } + } + + /** + * Deserializer for {@link RevokeGrantRequest} Backward compatible with previous version of the + * api + */ + public static final class RevokeGrantRequestDeserializer + extends JsonDeserializer { + @Override + public RevokeGrantRequest deserialize(JsonParser p, DeserializationContext ctxt) + throws IOException, JacksonException { + TreeNode treeNode = p.readValueAsTree(); + if (treeNode.isObject() && ((ObjectNode) treeNode).has("grant")) { + return RevokeGrantRequest.builder() + .setGrant(ctxt.readTreeAsValue((JsonNode) treeNode.get("grant"), GrantResource.class)) + .build(); + } else { + return RevokeGrantRequest.builder() + .setGrant(ctxt.readTreeAsValue((JsonNode) treeNode, GrantResource.class)) + .build(); + } + } + } +} diff --git a/polaris-service/src/main/java/io/polaris/service/config/TaskHandlerConfiguration.java b/polaris-service/src/main/java/io/polaris/service/config/TaskHandlerConfiguration.java new file mode 100644 index 0000000000..410145d05b --- /dev/null +++ b/polaris-service/src/main/java/io/polaris/service/config/TaskHandlerConfiguration.java @@ -0,0 +1,34 @@ +package io.polaris.service.config; + +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; + +public class TaskHandlerConfiguration { + private int poolSize = 10; + private boolean fixedSize = true; + private String threadNamePattern = "taskHandler-%d"; + + public void setPoolSize(int poolSize) { + this.poolSize = poolSize; + } + + public void setFixedSize(boolean fixedSize) { + this.fixedSize = fixedSize; + } + + public void setThreadNamePattern(String threadNamePattern) { + this.threadNamePattern = threadNamePattern; + } + + public ExecutorService executorService() { + return fixedSize + ? Executors.newFixedThreadPool(poolSize, threadFactory()) + : Executors.newCachedThreadPool(threadFactory()); + } + + private ThreadFactory threadFactory() { + return new ThreadFactoryBuilder().setNameFormat(threadNamePattern).setDaemon(true).build(); + } +} diff --git a/polaris-service/src/main/java/io/polaris/service/context/CallContextCatalogFactory.java b/polaris-service/src/main/java/io/polaris/service/context/CallContextCatalogFactory.java new file mode 100644 index 0000000000..0615f62bc3 --- /dev/null +++ b/polaris-service/src/main/java/io/polaris/service/context/CallContextCatalogFactory.java @@ -0,0 +1,9 @@ +package io.polaris.service.context; + +import io.polaris.core.context.CallContext; +import io.polaris.core.persistence.resolver.PolarisResolutionManifest; +import org.apache.iceberg.catalog.Catalog; + +public interface CallContextCatalogFactory { + Catalog createCallContextCatalog(CallContext context, PolarisResolutionManifest resolvedManifest); +} diff --git a/polaris-service/src/main/java/io/polaris/service/context/CallContextResolver.java b/polaris-service/src/main/java/io/polaris/service/context/CallContextResolver.java new file mode 100644 index 0000000000..ea541c97c2 --- /dev/null +++ b/polaris-service/src/main/java/io/polaris/service/context/CallContextResolver.java @@ -0,0 +1,19 @@ +package io.polaris.service.context; + +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.dropwizard.jackson.Discoverable; +import io.polaris.core.context.CallContext; +import io.polaris.core.context.RealmContext; +import io.polaris.service.config.HasEntityManagerFactory; +import java.util.Map; + +/** Uses the resolved RealmContext to further resolve elements of the CallContext. */ +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type") +public interface CallContextResolver extends HasEntityManagerFactory, Discoverable { + CallContext resolveCallContext( + RealmContext realmContext, + String method, + String path, + Map queryParams, + Map headers); +} diff --git a/polaris-service/src/main/java/io/polaris/service/context/DefaultContextResolver.java b/polaris-service/src/main/java/io/polaris/service/context/DefaultContextResolver.java new file mode 100644 index 0000000000..47b08ebd36 --- /dev/null +++ b/polaris-service/src/main/java/io/polaris/service/context/DefaultContextResolver.java @@ -0,0 +1,153 @@ +package io.polaris.service.context; + +import com.fasterxml.jackson.annotation.JsonTypeName; +import com.google.common.base.Splitter; +import io.polaris.core.PolarisCallContext; +import io.polaris.core.PolarisConfigurationStore; +import io.polaris.core.PolarisDefaultDiagServiceImpl; +import io.polaris.core.PolarisDiagnostics; +import io.polaris.core.context.CallContext; +import io.polaris.core.context.RealmContext; +import io.polaris.core.persistence.PolarisEntityManager; +import io.polaris.core.persistence.PolarisMetaStoreSession; +import io.polaris.service.config.ConfigurationStoreAware; +import io.polaris.service.config.RealmEntityManagerFactory; +import java.time.Clock; +import java.time.ZoneId; +import java.util.HashMap; +import java.util.Map; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * For local/dev testing, this resolver simply expects a custom bearer-token format that is a + * semicolon-separated list of colon-separated key/value pairs that constitute the realm properties. + * + *

Example: principal:data-engineer;password:test;realm:acct123 + */ +@JsonTypeName("default") +public class DefaultContextResolver + implements RealmContextResolver, CallContextResolver, ConfigurationStoreAware { + private static final Logger LOG = LoggerFactory.getLogger(DefaultContextResolver.class); + + public static final String REALM_PROPERTY_KEY = "realm"; + public static final String REALM_PROPERTY_DEFAULT_VALUE = "default-realm"; + + public static final String PRINCIPAL_PROPERTY_KEY = "principal"; + public static final String PRINCIPAL_PROPERTY_DEFAULT_VALUE = "default-principal"; + + private RealmEntityManagerFactory entityManagerFactory; + private PolarisConfigurationStore configurationStore; + + /** + * During CallContext resolution that might depend on RealmContext, the {@code + * entityManagerFactory} will be used to resolve elements of the CallContext which require + * additional information from an underlying entity store. + */ + @Override + public void setEntityManagerFactory(RealmEntityManagerFactory entityManagerFactory) { + this.entityManagerFactory = entityManagerFactory; + } + + @Override + public RealmContext resolveRealmContext( + String requestURL, + String method, + String path, + Map queryParams, + Map headers) { + // Since this default resolver is strictly for use in test/dev environments, we'll consider + // it safe to log all contents. Any "real" resolver used in a prod environment should make + // sure to only log non-sensitive contents. + LOG.debug( + "Resolving RealmContext for method: {}, path: {}, queryParams: {}, headers: {}", + method, + path, + queryParams, + headers); + final Map parsedProperties = parseBearerTokenAsKvPairs(headers); + + if (!parsedProperties.containsKey(REALM_PROPERTY_KEY) + && headers.containsKey(REALM_PROPERTY_KEY)) { + parsedProperties.put(REALM_PROPERTY_KEY, headers.get(REALM_PROPERTY_KEY)); + } + + if (!parsedProperties.containsKey(REALM_PROPERTY_KEY)) { + LOG.warn( + "Failed to parse {} from headers; using {}", + REALM_PROPERTY_KEY, + REALM_PROPERTY_DEFAULT_VALUE); + parsedProperties.put(REALM_PROPERTY_KEY, REALM_PROPERTY_DEFAULT_VALUE); + } + return new RealmContext() { + @Override + public String getRealmIdentifier() { + return parsedProperties.get(REALM_PROPERTY_KEY); + } + }; + } + + @Override + public CallContext resolveCallContext( + final RealmContext realmContext, + String method, + String path, + Map queryParams, + Map headers) { + LOG.atDebug() + .addKeyValue("realmContext", realmContext.getRealmIdentifier()) + .addKeyValue("method", method) + .addKeyValue("path", path) + .addKeyValue("queryParams", queryParams) + .addKeyValue("headers", headers) + .log("Resolving CallContext"); + final Map parsedProperties = parseBearerTokenAsKvPairs(headers); + + if (!parsedProperties.containsKey(PRINCIPAL_PROPERTY_KEY)) { + LOG.warn( + "Failed to parse {} from headers ({}); using {}", + PRINCIPAL_PROPERTY_KEY, + headers, + PRINCIPAL_PROPERTY_DEFAULT_VALUE); + parsedProperties.put(PRINCIPAL_PROPERTY_KEY, PRINCIPAL_PROPERTY_DEFAULT_VALUE); + } + + PolarisEntityManager entityManager = + entityManagerFactory.getOrCreateEntityManager(realmContext); + PolarisDiagnostics diagServices = new PolarisDefaultDiagServiceImpl(); + PolarisMetaStoreSession metaStoreSession = entityManager.newMetaStoreSession(); + PolarisCallContext polarisContext = + new PolarisCallContext( + metaStoreSession, + diagServices, + configurationStore, + Clock.system(ZoneId.systemDefault())); + return CallContext.of(realmContext, polarisContext); + } + + /** + * Returns kv pairs parsed from the "Authorization: Bearer k1:v1;k2:v2;k3:v3" header if it exists; + * if missing, returns empty map. + */ + private static Map parseBearerTokenAsKvPairs(Map headers) { + Map parsedProperties = new HashMap<>(); + if (headers != null) { + String authHeader = headers.get("Authorization"); + if (authHeader != null) { + String[] parts = authHeader.split(" "); + if (parts.length == 2 && "Bearer".equalsIgnoreCase(parts[0])) { + if (parts[1].matches("[\\w\\d=_+-]+:[\\w\\d=+_-]+(?:;[\\w\\d=+_-]+:[\\w\\d=+_-]+)*")) { + parsedProperties.putAll( + Splitter.on(';').trimResults().withKeyValueSeparator(':').split(parts[1])); + } + } + } + } + return parsedProperties; + } + + @Override + public void setConfigurationStore(PolarisConfigurationStore configurationStore) { + this.configurationStore = configurationStore; + } +} diff --git a/polaris-service/src/main/java/io/polaris/service/context/PolarisCallContextCatalogFactory.java b/polaris-service/src/main/java/io/polaris/service/context/PolarisCallContextCatalogFactory.java new file mode 100644 index 0000000000..ff7637f3fd --- /dev/null +++ b/polaris-service/src/main/java/io/polaris/service/context/PolarisCallContextCatalogFactory.java @@ -0,0 +1,71 @@ +package io.polaris.service.context; + +import io.polaris.core.context.CallContext; +import io.polaris.core.entity.CatalogEntity; +import io.polaris.core.entity.PolarisBaseEntity; +import io.polaris.core.persistence.PolarisEntityManager; +import io.polaris.core.persistence.resolver.PolarisResolutionManifest; +import io.polaris.service.catalog.BasePolarisCatalog; +import io.polaris.service.config.RealmEntityManagerFactory; +import io.polaris.service.task.TaskExecutor; +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.Map; +import org.apache.iceberg.CatalogProperties; +import org.apache.iceberg.catalog.Catalog; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class PolarisCallContextCatalogFactory implements CallContextCatalogFactory { + private static final Logger LOG = LoggerFactory.getLogger(PolarisCallContextCatalogFactory.class); + + private static final String WAREHOUSE_LOCATION_BASEDIR = + "/tmp/iceberg_rest_server_warehouse_data/"; + + private final RealmEntityManagerFactory entityManagerFactory; + private final TaskExecutor taskExecutor; + + public PolarisCallContextCatalogFactory( + RealmEntityManagerFactory entityManagerFactory, TaskExecutor taskExecutor) { + this.entityManagerFactory = entityManagerFactory; + this.taskExecutor = taskExecutor; + } + + @Override + public Catalog createCallContextCatalog( + CallContext context, final PolarisResolutionManifest resolvedManifest) { + PolarisBaseEntity baseCatalogEntity = + resolvedManifest.getResolvedReferenceCatalogEntity().getRawLeafEntity(); + String catalogName = baseCatalogEntity.getName(); + + String realm = context.getRealmContext().getRealmIdentifier(); + String catalogKey = realm + "/" + catalogName; + LOG.info("Initializing new BasePolarisCatalog for key: {}", catalogKey); + + PolarisEntityManager entityManager = + entityManagerFactory.getOrCreateEntityManager(context.getRealmContext()); + + BasePolarisCatalog catalogInstance = + new BasePolarisCatalog(entityManager, context, resolvedManifest, taskExecutor); + + context.contextVariables().put(CallContext.REQUEST_PATH_CATALOG_INSTANCE_KEY, catalogInstance); + + CatalogEntity catalog = CatalogEntity.of(baseCatalogEntity); + Map catalogProperties = new HashMap<>(catalog.getPropertiesAsMap()); + String defaultBaseLocation = catalog.getDefaultBaseLocation(); + LOG.info("Looked up defaultBaseLocation {} for catalog {}", defaultBaseLocation, catalogKey); + if (defaultBaseLocation != null) { + catalogProperties.put(CatalogProperties.WAREHOUSE_LOCATION, defaultBaseLocation); + } else { + catalogProperties.put( + CatalogProperties.WAREHOUSE_LOCATION, + Paths.get(WAREHOUSE_LOCATION_BASEDIR, catalogKey).toString()); + } + + // TODO: The initialize properties might need to take more from CallContext and the + // CatalogEntity. + catalogInstance.initialize(catalogName, catalogProperties); + + return catalogInstance; + } +} diff --git a/polaris-service/src/main/java/io/polaris/service/context/RealmContextResolver.java b/polaris-service/src/main/java/io/polaris/service/context/RealmContextResolver.java new file mode 100644 index 0000000000..389383f1d4 --- /dev/null +++ b/polaris-service/src/main/java/io/polaris/service/context/RealmContextResolver.java @@ -0,0 +1,17 @@ +package io.polaris.service.context; + +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.dropwizard.jackson.Discoverable; +import io.polaris.core.context.RealmContext; +import io.polaris.service.config.HasEntityManagerFactory; +import java.util.Map; + +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type") +public interface RealmContextResolver extends Discoverable, HasEntityManagerFactory { + RealmContext resolveRealmContext( + String requestURL, + String method, + String path, + Map queryParams, + Map headers); +} diff --git a/polaris-service/src/main/java/io/polaris/service/context/SqlliteCallContextCatalogFactory.java b/polaris-service/src/main/java/io/polaris/service/context/SqlliteCallContextCatalogFactory.java new file mode 100644 index 0000000000..bd2c39466d --- /dev/null +++ b/polaris-service/src/main/java/io/polaris/service/context/SqlliteCallContextCatalogFactory.java @@ -0,0 +1,89 @@ +package io.polaris.service.context; + +import io.polaris.core.context.CallContext; +import io.polaris.core.persistence.resolver.PolarisResolutionManifest; +import java.io.IOException; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.Map; +import org.apache.hadoop.conf.Configuration; +import org.apache.iceberg.CatalogProperties; +import org.apache.iceberg.CatalogUtil; +import org.apache.iceberg.catalog.Catalog; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * For local/dev testing, this RealmCallContextFactory uses Sqllite as the backing store for Catalog + * metadata using local-filesystem-based files as the persistence layer. + * + *

Realms will reside in different subdirectories under a shared base directory on the local + * filesystem. Each Catalog in the realm will be a different sqllite file. + */ +public class SqlliteCallContextCatalogFactory implements CallContextCatalogFactory { + private static final String DEFAULT_METASTORE_STATE_BASEDIR = + "/tmp/iceberg_rest_server_sqlitestate_basedir/"; + private static final String WAREHOUSE_LOCATION_BASEDIR = + "/tmp/iceberg_rest_server_warehouse_data/"; + + private static final Logger LOG = LoggerFactory.getLogger(SqlliteCallContextCatalogFactory.class); + + private Map cachedCatalogs = new HashMap<>(); + private final Map catalogBaseDirs; + + public SqlliteCallContextCatalogFactory(Map catalogBaseDirs) { + this.catalogBaseDirs = catalogBaseDirs; + } + + @Override + public Catalog createCallContextCatalog( + CallContext context, PolarisResolutionManifest resolvedManifest) { + String catalogName = + resolvedManifest.getResolvedReferenceCatalogEntity().getRawLeafEntity().getName(); + if (catalogName == null) { + catalogName = "default"; + } + + String realm = context.getRealmContext().getRealmIdentifier(); + String catalogKey = realm + "/" + catalogName; + LOG.debug("Looking up catalogKey: {}", catalogKey); + + Catalog catalogInstance = cachedCatalogs.get(catalogKey); + if (catalogInstance == null) { + Map catalogProperties = new HashMap<>(); + catalogProperties.put(CatalogProperties.CATALOG_IMPL, "org.apache.iceberg.jdbc.JdbcCatalog"); + catalogProperties.put("jdbc.schema-version", "V1"); + + // TODO: Do sanitization in case this ever runs in an exposed environment to avoid + // injection attacks. + String baseDir = catalogBaseDirs.getOrDefault(realm, DEFAULT_METASTORE_STATE_BASEDIR); + + String realmDir = Paths.get(baseDir, realm).toString(); + String catalogFile = Paths.get(realmDir, catalogName).toString(); + + // Ensure parent directories of metastore-state base directory exists. + LOG.info("Creating metastore state directory: " + realmDir); + try { + Path result = Files.createDirectories(FileSystems.getDefault().getPath(realmDir)); + } catch (IOException ioe) { + throw new RuntimeException(ioe); + } + + catalogProperties.put(CatalogProperties.URI, "jdbc:sqlite:file:" + catalogFile); + + // TODO: Derive warehouse location from realm configs. + catalogProperties.put( + CatalogProperties.WAREHOUSE_LOCATION, + Paths.get(WAREHOUSE_LOCATION_BASEDIR, catalogKey).toString()); + + catalogInstance = + CatalogUtil.buildIcebergCatalog( + "catalog_" + catalogKey, catalogProperties, new Configuration()); + cachedCatalogs.put(catalogKey, catalogInstance); + } + return catalogInstance; + } +} diff --git a/polaris-service/src/main/java/io/polaris/service/logging/PolarisJsonLayoutFactory.java b/polaris-service/src/main/java/io/polaris/service/logging/PolarisJsonLayoutFactory.java new file mode 100644 index 0000000000..94e8cc8aa4 --- /dev/null +++ b/polaris-service/src/main/java/io/polaris/service/logging/PolarisJsonLayoutFactory.java @@ -0,0 +1,224 @@ +package io.polaris.service.logging; + +import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.classic.pattern.ExtendedThrowableProxyConverter; +import ch.qos.logback.classic.pattern.RootCauseFirstThrowableProxyConverter; +import ch.qos.logback.classic.pattern.ThrowableHandlingConverter; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.LayoutBase; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeName; +import com.google.common.collect.ImmutableMap; +import io.dropwizard.logging.json.AbstractJsonLayoutBaseFactory; +import io.dropwizard.logging.json.EventAttribute; +import io.dropwizard.logging.json.layout.EventJsonLayout; +import io.dropwizard.logging.json.layout.ExceptionFormat; +import io.dropwizard.logging.json.layout.JsonFormatter; +import io.dropwizard.logging.json.layout.TimestampFormatter; +import java.util.ArrayList; +import java.util.Collections; +import java.util.EnumSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TimeZone; +import java.util.stream.Collectors; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * Basically a direct copy of {@link io.dropwizard.logging.json.EventJsonLayoutBaseFactory} that + * adds support for {@link ILoggingEvent#getKeyValuePairs()} in the output. By default, additional + * key/value pairs are included as the `params` field of the json output, but they can optionally be + * flattened into the log event output. + * + *

To use this appender, change the appender type to `polaris` + * loggers: + * org.apache.iceberg.rest: DEBUG + * org.apache.iceberg.polaris: DEBUG + * appenders: + * - type: console + * threshold: ALL + * layout: + * type: polaris + * flattenKeyValues: false + * includeKeyValues: true + * + */ +@JsonTypeName("polaris") +public class PolarisJsonLayoutFactory extends AbstractJsonLayoutBaseFactory { + private EnumSet includes = + EnumSet.of( + EventAttribute.LEVEL, + EventAttribute.THREAD_NAME, + EventAttribute.MDC, + EventAttribute.MARKER, + EventAttribute.LOGGER_NAME, + EventAttribute.MESSAGE, + EventAttribute.EXCEPTION, + EventAttribute.TIMESTAMP); + + private Set includesMdcKeys = Collections.emptySet(); + private boolean flattenMdc = false; + private boolean includeKeyValues = true; + private boolean flattenKeyValues = false; + + @Nullable private ExceptionFormat exceptionFormat; + + @JsonProperty + public EnumSet getIncludes() { + return includes; + } + + @JsonProperty + public void setIncludes(EnumSet includes) { + this.includes = includes; + } + + @JsonProperty + public Set getIncludesMdcKeys() { + return includesMdcKeys; + } + + @JsonProperty + public void setIncludesMdcKeys(Set includesMdcKeys) { + this.includesMdcKeys = includesMdcKeys; + } + + @JsonProperty + public boolean isFlattenMdc() { + return flattenMdc; + } + + @JsonProperty + public void setFlattenMdc(boolean flattenMdc) { + this.flattenMdc = flattenMdc; + } + + @JsonProperty + public boolean isIncludeKeyValues() { + return includeKeyValues; + } + + @JsonProperty + public void setIncludeKeyValues(boolean includeKeyValues) { + this.includeKeyValues = includeKeyValues; + } + + @JsonProperty + public boolean isFlattenKeyValues() { + return flattenKeyValues; + } + + @JsonProperty + public void setFlattenKeyValues(boolean flattenKeyValues) { + this.flattenKeyValues = flattenKeyValues; + } + + /** + * @since 2.0 + */ + @JsonProperty("exception") + public void setExceptionFormat(ExceptionFormat exceptionFormat) { + this.exceptionFormat = exceptionFormat; + } + + /** + * @since 2.0 + */ + @JsonProperty("exception") + @Nullable + public ExceptionFormat getExceptionFormat() { + return exceptionFormat; + } + + @Override + public LayoutBase build(LoggerContext context, TimeZone timeZone) { + final PolarisJsonLayout jsonLayout = + new PolarisJsonLayout( + createDropwizardJsonFormatter(), + createTimestampFormatter(timeZone), + createThrowableProxyConverter(context), + includes, + getCustomFieldNames(), + getAdditionalFields(), + includesMdcKeys, + flattenMdc, + includeKeyValues, + flattenKeyValues); + jsonLayout.setContext(context); + return jsonLayout; + } + + public static class PolarisJsonLayout extends EventJsonLayout { + private final boolean includeKeyValues; + private final boolean flattenKeyValues; + + public PolarisJsonLayout( + JsonFormatter jsonFormatter, + TimestampFormatter timestampFormatter, + ThrowableHandlingConverter throwableProxyConverter, + Set includes, + Map customFieldNames, + Map additionalFields, + Set includesMdcKeys, + boolean flattenMdc, + boolean includeKeyValues, + boolean flattenKeyValues) { + super( + jsonFormatter, + timestampFormatter, + throwableProxyConverter, + includes, + customFieldNames, + additionalFields, + includesMdcKeys, + flattenMdc); + this.includeKeyValues = includeKeyValues; + this.flattenKeyValues = flattenKeyValues; + } + + @Override + protected Map toJsonMap(ILoggingEvent event) { + Map jsonMap = super.toJsonMap(event); + if (!includeKeyValues) { + return jsonMap; + } + Map keyValueMap = + event.getKeyValuePairs() == null + ? Map.of() + : event.getKeyValuePairs().stream() + .collect(Collectors.toMap(kv -> kv.key, kv -> kv.value)); + ImmutableMap.Builder builder = + ImmutableMap.builder().putAll(jsonMap); + if (flattenKeyValues) { + builder.putAll(keyValueMap); + } else { + builder.put("params", keyValueMap); + } + return builder.build(); + } + } + + protected ThrowableHandlingConverter createThrowableProxyConverter(LoggerContext context) { + if (exceptionFormat == null) { + return new RootCauseFirstThrowableProxyConverter(); + } + + ThrowableHandlingConverter throwableHandlingConverter; + if (exceptionFormat.isRootFirst()) { + throwableHandlingConverter = new RootCauseFirstThrowableProxyConverter(); + } else { + throwableHandlingConverter = new ExtendedThrowableProxyConverter(); + } + + List options = new ArrayList<>(); + // depth must be added first + options.add(exceptionFormat.getDepth()); + options.addAll(exceptionFormat.getEvaluators()); + + throwableHandlingConverter.setOptionList(options); + throwableHandlingConverter.setContext(context); + + return throwableHandlingConverter; + } +} diff --git a/polaris-service/src/main/java/io/polaris/service/persistence/InMemoryPolarisMetaStoreManagerFactory.java b/polaris-service/src/main/java/io/polaris/service/persistence/InMemoryPolarisMetaStoreManagerFactory.java new file mode 100644 index 0000000000..dc77d5793b --- /dev/null +++ b/polaris-service/src/main/java/io/polaris/service/persistence/InMemoryPolarisMetaStoreManagerFactory.java @@ -0,0 +1,70 @@ +package io.polaris.service.persistence; + +import com.fasterxml.jackson.annotation.JsonTypeName; +import io.polaris.core.PolarisDiagnostics; +import io.polaris.core.context.RealmContext; +import io.polaris.core.persistence.LocalPolarisMetaStoreManagerFactory; +import io.polaris.core.persistence.PolarisMetaStoreManager; +import io.polaris.core.persistence.PolarisMetaStoreSession; +import io.polaris.core.persistence.PolarisTreeMapMetaStoreSessionImpl; +import io.polaris.core.persistence.PolarisTreeMapStore; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.function.Supplier; +import org.jetbrains.annotations.NotNull; + +@JsonTypeName("in-memory") +public class InMemoryPolarisMetaStoreManagerFactory + extends LocalPolarisMetaStoreManagerFactory< + PolarisTreeMapStore, PolarisTreeMapMetaStoreSessionImpl> { + Set bootstrappedRealms = new HashSet<>(); + + @Override + protected PolarisTreeMapStore createBackingStore(@NotNull PolarisDiagnostics diagnostics) { + return new PolarisTreeMapStore(diagnostics); + } + + @Override + protected PolarisMetaStoreSession createMetaStoreSession( + @NotNull PolarisTreeMapStore store, @NotNull RealmContext realmContext) { + return new PolarisTreeMapMetaStoreSessionImpl(store, storageIntegration); + } + + @Override + public synchronized PolarisMetaStoreManager getOrCreateMetaStoreManager( + RealmContext realmContext) { + String realmId = realmContext.getRealmIdentifier(); + if (!bootstrappedRealms.contains(realmId)) { + bootstrapRealmAndPrintCredentials(realmId); + } + return super.getOrCreateMetaStoreManager(realmContext); + } + + @Override + public synchronized Supplier getOrCreateSessionSupplier( + RealmContext realmContext) { + String realmId = realmContext.getRealmIdentifier(); + if (!bootstrappedRealms.contains(realmId)) { + bootstrapRealmAndPrintCredentials(realmId); + } + return super.getOrCreateSessionSupplier(realmContext); + } + + private void bootstrapRealmAndPrintCredentials(String realmId) { + Map results = + this.bootstrapRealms(Arrays.asList(realmId)); + bootstrappedRealms.add(realmId); + + PolarisMetaStoreManager.PrincipalSecretsResult principalSecrets = results.get(realmId); + + String msg = + String.format( + "realm: %1s root principal credentials: %2s:%3s", + realmId, + principalSecrets.getPrincipalSecrets().getPrincipalClientId(), + principalSecrets.getPrincipalSecrets().getMainSecret()); + System.out.println(msg); + } +} diff --git a/polaris-service/src/main/java/io/polaris/service/resource/TimedApi.java b/polaris-service/src/main/java/io/polaris/service/resource/TimedApi.java new file mode 100644 index 0000000000..e99b186872 --- /dev/null +++ b/polaris-service/src/main/java/io/polaris/service/resource/TimedApi.java @@ -0,0 +1,22 @@ +package io.polaris.service.resource; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation to mark methods for timing API calls and counting errors. The backing logic is + * controlled by {@link io.polaris.service.TimedApplicationEventListener}, therefore this annotation + * is only effective for Jersey resource methods. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface TimedApi { + /** + * The name of the metric to be recorded. + * + * @return the metric name + */ + String value(); +} diff --git a/polaris-service/src/main/java/io/polaris/service/storage/PolarisStorageIntegrationProviderImpl.java b/polaris-service/src/main/java/io/polaris/service/storage/PolarisStorageIntegrationProviderImpl.java new file mode 100644 index 0000000000..56bff16b1b --- /dev/null +++ b/polaris-service/src/main/java/io/polaris/service/storage/PolarisStorageIntegrationProviderImpl.java @@ -0,0 +1,94 @@ +package io.polaris.service.storage; + +import com.google.api.client.http.javanet.NetHttpTransport; +import com.google.auth.http.HttpTransportFactory; +import com.google.auth.oauth2.GoogleCredentials; +import com.google.cloud.ServiceOptions; +import io.polaris.core.PolarisDiagnostics; +import io.polaris.core.storage.PolarisCredentialProperty; +import io.polaris.core.storage.PolarisStorageActions; +import io.polaris.core.storage.PolarisStorageConfigurationInfo; +import io.polaris.core.storage.PolarisStorageIntegration; +import io.polaris.core.storage.PolarisStorageIntegrationProvider; +import io.polaris.core.storage.aws.AwsCredentialsStorageIntegration; +import io.polaris.core.storage.azure.AzureCredentialsStorageIntegration; +import io.polaris.core.storage.gcp.GcpCredentialsStorageIntegration; +import java.io.IOException; +import java.util.EnumMap; +import java.util.Map; +import java.util.Set; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import software.amazon.awssdk.services.sts.StsClient; + +public class PolarisStorageIntegrationProviderImpl implements PolarisStorageIntegrationProvider { + public PolarisStorageIntegrationProviderImpl() {} + + @Override + @SuppressWarnings("unchecked") + public @Nullable + PolarisStorageIntegration getStorageIntegrationForConfig( + PolarisStorageConfigurationInfo polarisStorageConfigurationInfo) { + if (polarisStorageConfigurationInfo == null) { + return null; + } + PolarisStorageIntegration storageIntegration; + switch (polarisStorageConfigurationInfo.getStorageType()) { + case S3: + storageIntegration = + (PolarisStorageIntegration) new AwsCredentialsStorageIntegration(StsClient.create()); + break; + case GCS: + try { + storageIntegration = + (PolarisStorageIntegration) + new GcpCredentialsStorageIntegration( + GoogleCredentials.getApplicationDefault(), + ServiceOptions.getFromServiceLoader( + HttpTransportFactory.class, NetHttpTransport::new)); + } catch (IOException e) { + throw new RuntimeException( + "Error initializing default google credentials" + e.getMessage()); + } + break; + case AZURE: + storageIntegration = + (PolarisStorageIntegration) new AzureCredentialsStorageIntegration(); + break; + case FILE: + storageIntegration = + new PolarisStorageIntegration("file") { + @Override + public EnumMap getSubscopedCreds( + @NotNull PolarisDiagnostics diagnostics, + @NotNull T storageConfig, + boolean allowListOperation, + @NotNull Set allowedReadLocations, + @NotNull Set allowedWriteLocations) { + return new EnumMap<>(PolarisCredentialProperty.class); + } + + @Override + public EnumMap + descPolarisStorageConfiguration( + @NotNull PolarisStorageConfigurationInfo storageConfigInfo) { + return new EnumMap<>(PolarisStorageConfigurationInfo.DescribeProperty.class); + } + + @Override + public @NotNull Map> + validateAccessToLocations( + @NotNull T storageConfig, + @NotNull Set actions, + @NotNull Set locations) { + return Map.of(); + } + }; + break; + default: + throw new IllegalArgumentException( + "Unknown storage type " + polarisStorageConfigurationInfo.getStorageType()); + } + return storageIntegration; + } +} diff --git a/polaris-service/src/main/java/io/polaris/service/task/ManifestFileCleanupTaskHandler.java b/polaris-service/src/main/java/io/polaris/service/task/ManifestFileCleanupTaskHandler.java new file mode 100644 index 0000000000..58c0a1bbea --- /dev/null +++ b/polaris-service/src/main/java/io/polaris/service/task/ManifestFileCleanupTaskHandler.java @@ -0,0 +1,206 @@ +package io.polaris.service.task; + +import io.polaris.core.entity.AsyncTaskType; +import io.polaris.core.entity.TaskEntity; +import java.io.IOException; +import java.util.List; +import java.util.Objects; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.stream.StreamSupport; +import org.apache.commons.codec.binary.Base64; +import org.apache.iceberg.DataFile; +import org.apache.iceberg.ManifestFile; +import org.apache.iceberg.ManifestFiles; +import org.apache.iceberg.ManifestReader; +import org.apache.iceberg.catalog.TableIdentifier; +import org.apache.iceberg.io.FileIO; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * {@link TaskHandler} responsible for deleting all of the files in a manifest and the manifest + * itself. Since data files may be present in multiple manifests across different snapshots, we + * assume a data file that doesn't exist is missing because it was already deleted by another task. + */ +public class ManifestFileCleanupTaskHandler implements TaskHandler { + public static final int MAX_ATTEMPTS = 3; + public static final int FILE_DELETION_RETRY_MILLIS = 100; + private final Logger LOGGER = LoggerFactory.getLogger(ManifestFileCleanupTaskHandler.class); + private final Function fileIOSupplier; + private final ExecutorService executorService; + + public ManifestFileCleanupTaskHandler( + Function fileIOSupplier, ExecutorService executorService) { + this.fileIOSupplier = fileIOSupplier; + this.executorService = executorService; + } + + @Override + public boolean canHandleTask(TaskEntity task) { + return task.getTaskType() == AsyncTaskType.FILE_CLEANUP; + } + + @Override + public boolean handleTask(TaskEntity task) { + ManifestCleanupTask cleanupTask = task.readData(ManifestCleanupTask.class); + ManifestFile manifestFile = decodeManifestData(cleanupTask.getManifestFileData()); + TableIdentifier tableId = cleanupTask.getTableId(); + try (FileIO authorizedFileIO = fileIOSupplier.apply(task)) { + + // if the file doesn't exist, we assume that another task execution was successful, but failed + // to drop the task entity. Log a warning and return success + if (!TaskUtils.exists(manifestFile.path(), authorizedFileIO)) { + LOGGER + .atWarn() + .addKeyValue("manifestFile", manifestFile.path()) + .addKeyValue("tableId", tableId) + .log("Manifest cleanup task scheduled, but manifest file doesn't exist"); + return true; + } + + ManifestReader dataFiles = ManifestFiles.read(manifestFile, authorizedFileIO); + List> dataFileDeletes = + StreamSupport.stream( + Spliterators.spliteratorUnknownSize(dataFiles.iterator(), Spliterator.IMMUTABLE), + false) + .map( + file -> + tryDelete( + tableId, authorizedFileIO, manifestFile, file.path().toString(), null, 1)) + .toList(); + LOGGER.debug( + "Scheduled {} data files to be deleted from manifest {}", + dataFileDeletes.size(), + manifestFile.path()); + try { + // wait for all data files to be deleted, then wait for the manifest itself to be deleted + CompletableFuture.allOf(dataFileDeletes.toArray(CompletableFuture[]::new)) + .thenCompose( + (v) -> { + LOGGER + .atInfo() + .addKeyValue("manifestFile", manifestFile.path()) + .log("All data files in manifest deleted - deleting manifest"); + return tryDelete( + tableId, authorizedFileIO, manifestFile, manifestFile.path(), null, 1); + }) + .get(); + return true; + } catch (InterruptedException e) { + LOGGER.error( + "Interrupted exception deleting data files from manifest {}", manifestFile.path(), e); + throw new RuntimeException(e); + } catch (ExecutionException e) { + LOGGER.error("Unable to delete data files from manifest {}", manifestFile.path(), e); + return false; + } + } + } + + private static ManifestFile decodeManifestData(String manifestFileData) { + try { + return ManifestFiles.decode(Base64.decodeBase64(manifestFileData)); + } catch (IOException e) { + throw new RuntimeException("Unable to decode base64 encoded manifest", e); + } + } + + private CompletableFuture tryDelete( + TableIdentifier tableId, + FileIO fileIO, + ManifestFile manifestFile, + String dataFile, + Throwable e, + int attempt) { + if (e != null && attempt <= MAX_ATTEMPTS) { + LOGGER + .atWarn() + .addKeyValue("dataFile", dataFile) + .addKeyValue("attempt", attempt) + .addKeyValue("error", e.getMessage()) + .log("Error encountered attempting to delete data file"); + } + if (attempt > MAX_ATTEMPTS && e != null) { + return CompletableFuture.failedFuture(e); + } + return CompletableFuture.runAsync( + () -> { + // totally normal for a file to already be missing, as a data file + // may be in multiple manifests. There's a possibility we check the + // file's existence, but then it is deleted before we have a chance to + // send the delete request. In such a case, we should retry + // and find + if (TaskUtils.exists(dataFile.toString(), fileIO)) { + fileIO.deleteFile(dataFile.toString()); + } else { + LOGGER + .atInfo() + .addKeyValue("dataFile", dataFile) + .addKeyValue("manifestFile", manifestFile.path()) + .addKeyValue("tableId", tableId) + .log("Manifest cleanup task scheduled, but data file doesn't exist"); + } + }, + executorService) + .exceptionallyComposeAsync( + newEx -> { + LOGGER + .atWarn() + .addKeyValue("dataFile", dataFile) + .addKeyValue("tableIdentifer", tableId) + .addKeyValue("manifestFile", manifestFile.path()) + .log("Exception caught deleting data file from manifest", newEx); + return tryDelete(tableId, fileIO, manifestFile, dataFile, newEx, attempt + 1); + }, + CompletableFuture.delayedExecutor( + FILE_DELETION_RETRY_MILLIS, TimeUnit.MILLISECONDS, executorService)); + } + + /** Serialized Task data sent from the {@link TableCleanupTaskHandler} */ + public static final class ManifestCleanupTask { + private TableIdentifier tableId; + private String manifestFileData; + + public ManifestCleanupTask(TableIdentifier tableId, String manifestFileData) { + this.tableId = tableId; + this.manifestFileData = manifestFileData; + } + + public ManifestCleanupTask() {} + + public TableIdentifier getTableId() { + return tableId; + } + + public void setTableId(TableIdentifier tableId) { + this.tableId = tableId; + } + + public String getManifestFileData() { + return manifestFileData; + } + + public void setManifestFileData(String manifestFileData) { + this.manifestFileData = manifestFileData; + } + + @Override + public boolean equals(Object object) { + if (this == object) return true; + if (!(object instanceof ManifestCleanupTask that)) return false; + return Objects.equals(tableId, that.tableId) + && Objects.equals(manifestFileData, that.manifestFileData); + } + + @Override + public int hashCode() { + return Objects.hash(tableId, manifestFileData); + } + } +} diff --git a/polaris-service/src/main/java/io/polaris/service/task/TableCleanupTaskHandler.java b/polaris-service/src/main/java/io/polaris/service/task/TableCleanupTaskHandler.java new file mode 100644 index 0000000000..97d321e666 --- /dev/null +++ b/polaris-service/src/main/java/io/polaris/service/task/TableCleanupTaskHandler.java @@ -0,0 +1,153 @@ +package io.polaris.service.task; + +import io.polaris.core.PolarisCallContext; +import io.polaris.core.context.CallContext; +import io.polaris.core.entity.AsyncTaskType; +import io.polaris.core.entity.PolarisBaseEntity; +import io.polaris.core.entity.PolarisEntity; +import io.polaris.core.entity.PolarisEntityType; +import io.polaris.core.entity.TableLikeEntity; +import io.polaris.core.entity.TaskEntity; +import io.polaris.core.persistence.MetaStoreManagerFactory; +import io.polaris.core.persistence.PolarisMetaStoreManager; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.function.Function; +import java.util.stream.Collectors; +import org.apache.iceberg.ManifestFile; +import org.apache.iceberg.TableMetadata; +import org.apache.iceberg.TableMetadataParser; +import org.apache.iceberg.io.FileIO; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Table cleanup handler resolves the latest {@link TableMetadata} file for a dropped table and + * schedules a deletion task for each Snapshot found in the {@link TableMetadata}. Manifest + * cleanup tasks are scheduled in a batch so tasks should be stored atomically. + */ +public class TableCleanupTaskHandler implements TaskHandler { + private final Logger LOGGER = LoggerFactory.getLogger(TableCleanupTaskHandler.class); + private final TaskExecutor taskExecutor; + private final MetaStoreManagerFactory metaStoreManagerFactory; + private final Function fileIOSupplier; + private final ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor(); + + public TableCleanupTaskHandler( + TaskExecutor taskExecutor, + MetaStoreManagerFactory metaStoreManagerFactory, + Function fileIOSupplier) { + this.taskExecutor = taskExecutor; + this.metaStoreManagerFactory = metaStoreManagerFactory; + this.fileIOSupplier = fileIOSupplier; + } + + @Override + public boolean canHandleTask(TaskEntity task) { + return task.getTaskType() == AsyncTaskType.ENTITY_CLEANUP_SCHEDULER && taskEntityIsTable(task); + } + + private boolean taskEntityIsTable(TaskEntity task) { + PolarisEntity entity = PolarisEntity.of((task.readData(PolarisBaseEntity.class))); + return entity.getType().equals(PolarisEntityType.TABLE_LIKE); + } + + @Override + public boolean handleTask(TaskEntity cleanupTask) { + PolarisBaseEntity entity = cleanupTask.readData(PolarisBaseEntity.class); + PolarisMetaStoreManager metaStoreManager = + metaStoreManagerFactory.getOrCreateMetaStoreManager( + CallContext.getCurrentContext().getRealmContext()); + TableLikeEntity tableEntity = TableLikeEntity.of(entity); + PolarisCallContext polarisCallContext = CallContext.getCurrentContext().getPolarisCallContext(); + LOGGER + .atInfo() + .addKeyValue("tableIdentifier", tableEntity.getTableIdentifier()) + .addKeyValue("metadataLocation", tableEntity.getMetadataLocation()) + .log("Handling table metadata cleanup task"); + + // It's likely the cleanupTask has already been completed, but wasn't dropped successfully. + // Log a + // warning and move on + try (FileIO fileIO = fileIOSupplier.apply(cleanupTask)) { + if (!TaskUtils.exists(tableEntity.getMetadataLocation(), fileIO)) { + LOGGER + .atWarn() + .addKeyValue("tableIdentifier", tableEntity.getTableIdentifier()) + .addKeyValue("metadataLocation", tableEntity.getMetadataLocation()) + .log("Table metadata cleanup scheduled, but metadata file does not exist"); + return true; + } + + TableMetadata tableMetadata = + TableMetadataParser.read(fileIO, tableEntity.getMetadataLocation()); + + // read the manifest list for each snapshot. dedupe the manifest files and schedule a + // cleanupTask + // for each manifest file and its data files to be deleted + List taskEntities = + tableMetadata.snapshots().stream() + .flatMap(sn -> sn.allManifests(fileIO).stream()) + // distinct by manifest path, since multiple snapshots will contain the same + // manifest + .collect(Collectors.toMap(ManifestFile::path, Function.identity(), (mf1, mf2) -> mf1)) + .values() + .stream() + .filter(mf -> TaskUtils.exists(mf.path(), fileIO)) + .map( + mf -> { + // append a random uuid to the task name to avoid any potential conflict + // when + // storing the task entity. It's better to have duplicate tasks than to risk + // not storing the rest of the task entities. If a duplicate deletion task + // is + // queued, it will check for the manifest file's existence and simply exit + // if + // the task has already been handled. + String taskName = + cleanupTask.getName() + "_" + mf.path() + "_" + UUID.randomUUID(); + LOGGER + .atDebug() + .addKeyValue("taskName", taskName) + .addKeyValue("tableIdentifier", tableEntity.getTableIdentifier()) + .addKeyValue("metadataLocation", tableEntity.getMetadataLocation()) + .addKeyValue("manifestFile", mf.path()) + .log("Queueing task to delete manifest file"); + return new TaskEntity.Builder() + .setName(taskName) + .setId(metaStoreManager.generateNewEntityId(polarisCallContext).getId()) + .setCreateTimestamp(polarisCallContext.getClock().millis()) + .withTaskType(AsyncTaskType.FILE_CLEANUP) + .withData( + new ManifestFileCleanupTaskHandler.ManifestCleanupTask( + tableEntity.getTableIdentifier(), TaskUtils.encodeManifestFile(mf))) + .setId(metaStoreManager.generateNewEntityId(polarisCallContext).getId()) + // copy the internal properties, which will have storage info + .setInternalProperties(cleanupTask.getInternalPropertiesAsMap()) + .build(); + }) + .toList(); + List createdTasks = + metaStoreManager + .createEntitiesIfNotExist(polarisCallContext, null, taskEntities) + .getEntities(); + if (createdTasks != null) { + LOGGER + .atInfo() + .addKeyValue("tableIdentifier", tableEntity.getTableIdentifier()) + .addKeyValue("metadataLocation", tableEntity.getMetadataLocation()) + .addKeyValue("taskCount", taskEntities.size()) + .log("Successfully queued tasks to delete manifests - deleting table metadata file"); + for (PolarisBaseEntity createdTask : createdTasks) { + taskExecutor.addTaskHandlerContext(createdTask.getId(), CallContext.getCurrentContext()); + } + fileIO.deleteFile(tableEntity.getMetadataLocation()); + + return true; + } + } + return false; + } +} diff --git a/polaris-service/src/main/java/io/polaris/service/task/TaskExecutor.java b/polaris-service/src/main/java/io/polaris/service/task/TaskExecutor.java new file mode 100644 index 0000000000..c75c6fca8c --- /dev/null +++ b/polaris-service/src/main/java/io/polaris/service/task/TaskExecutor.java @@ -0,0 +1,11 @@ +package io.polaris.service.task; + +import io.polaris.core.context.CallContext; + +/** + * Execute a task asynchronously with a provided context. The context must be cloned so that callers + * can close their own context and closables + */ +public interface TaskExecutor { + void addTaskHandlerContext(long taskEntityId, CallContext callContext); +} diff --git a/polaris-service/src/main/java/io/polaris/service/task/TaskExecutorImpl.java b/polaris-service/src/main/java/io/polaris/service/task/TaskExecutorImpl.java new file mode 100644 index 0000000000..315e4fefed --- /dev/null +++ b/polaris-service/src/main/java/io/polaris/service/task/TaskExecutorImpl.java @@ -0,0 +1,123 @@ +package io.polaris.service.task; + +import io.polaris.core.context.CallContext; +import io.polaris.core.entity.PolarisBaseEntity; +import io.polaris.core.entity.PolarisEntity; +import io.polaris.core.entity.PolarisEntityType; +import io.polaris.core.entity.TaskEntity; +import io.polaris.core.persistence.MetaStoreManagerFactory; +import io.polaris.core.persistence.PolarisMetaStoreManager; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Given a list of registered {@link TaskHandler}s, execute tasks asynchronously with the provided + * {@link CallContext}. + */ +public class TaskExecutorImpl implements TaskExecutor { + private static final Logger LOGGER = LoggerFactory.getLogger(TaskExecutorImpl.class); + public static final long TASK_RETRY_DELAY = 1000; + private final ExecutorService executorService; + private final MetaStoreManagerFactory metaStoreManagerFactory; + private final List taskHandlers = new ArrayList<>(); + + public TaskExecutorImpl( + ExecutorService executorService, MetaStoreManagerFactory metaStoreManagerFactory) { + this.executorService = executorService; + this.metaStoreManagerFactory = metaStoreManagerFactory; + } + + /** + * Add a {@link TaskHandler}. {@link TaskEntity}s will be tested against the {@link + * TaskHandler#canHandleTask(TaskEntity)} method and will be handled by the first handler that + * responds true. + * + * @param taskHandler + */ + public void addTaskHandler(TaskHandler taskHandler) { + taskHandlers.add(taskHandler); + } + + /** + * Register a {@link CallContext} for a specific task id. That task will be loaded and executed + * asynchronously with a clone of the provided {@link CallContext}. + * + * @param taskEntityId + * @param callContext + */ + @Override + public void addTaskHandlerContext(long taskEntityId, CallContext callContext) { + CallContext clone = CallContext.copyOf(callContext); + tryHandleTask(taskEntityId, clone, null, 1); + } + + private @NotNull CompletableFuture tryHandleTask( + long taskEntityId, CallContext clone, Throwable e, int attempt) { + if (attempt > 3) { + return CompletableFuture.failedFuture(e); + } + return CompletableFuture.runAsync( + () -> { + // set the call context INSIDE the async task + try (CallContext ctx = CallContext.setCurrentContext(CallContext.copyOf(clone))) { + PolarisMetaStoreManager metaStoreManager = + metaStoreManagerFactory.getOrCreateMetaStoreManager(ctx.getRealmContext()); + PolarisBaseEntity taskEntity = + metaStoreManager + .loadEntity(ctx.getPolarisCallContext(), 0L, taskEntityId) + .getEntity(); + if (!PolarisEntityType.TASK.equals(taskEntity.getType())) { + throw new IllegalArgumentException("Provided taskId must be a task entity type"); + } + TaskEntity task = TaskEntity.of(taskEntity); + Optional handlerOpt = + taskHandlers.stream().filter(th -> th.canHandleTask(task)).findFirst(); + if (handlerOpt.isEmpty()) { + LOGGER + .atWarn() + .addKeyValue("taskEntityId", taskEntityId) + .addKeyValue("taskType", task.getTaskType()) + .log("Unable to find handler for task type"); + return; + } + TaskHandler handler = handlerOpt.get(); + boolean success = handler.handleTask(task); + if (success) { + LOGGER + .atInfo() + .addKeyValue("taskEntityId", taskEntityId) + .addKeyValue("handlerClass", handler.getClass()) + .log("Task successfully handled"); + metaStoreManager.dropEntityIfExists( + ctx.getPolarisCallContext(), + null, + PolarisEntity.toCore(taskEntity), + Map.of(), + false); + } else { + LOGGER + .atWarn() + .addKeyValue("taskEntityId", taskEntityId) + .addKeyValue("taskEntityName", taskEntity.getName()) + .log("Unable to execute async task"); + } + } + }, + executorService) + .exceptionallyComposeAsync( + (t) -> { + LOGGER.warn("Failed to handle task entity id {}", taskEntityId, t); + return tryHandleTask(taskEntityId, clone, t, attempt + 1); + }, + CompletableFuture.delayedExecutor( + TASK_RETRY_DELAY * (long) attempt, TimeUnit.MILLISECONDS, executorService)); + } +} diff --git a/polaris-service/src/main/java/io/polaris/service/task/TaskFileIOSupplier.java b/polaris-service/src/main/java/io/polaris/service/task/TaskFileIOSupplier.java new file mode 100644 index 0000000000..49d6db2b3b --- /dev/null +++ b/polaris-service/src/main/java/io/polaris/service/task/TaskFileIOSupplier.java @@ -0,0 +1,47 @@ +package io.polaris.service.task; + +import io.polaris.core.context.CallContext; +import io.polaris.core.entity.PolarisTaskConstants; +import io.polaris.core.entity.TaskEntity; +import io.polaris.core.persistence.MetaStoreManagerFactory; +import io.polaris.core.persistence.PolarisMetaStoreManager; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import org.apache.hadoop.conf.Configuration; +import org.apache.iceberg.CatalogProperties; +import org.apache.iceberg.CatalogUtil; +import org.apache.iceberg.io.FileIO; + +public class TaskFileIOSupplier implements Function { + private final MetaStoreManagerFactory metaStoreManagerFactory; + + public TaskFileIOSupplier(MetaStoreManagerFactory metaStoreManagerFactory) { + this.metaStoreManagerFactory = metaStoreManagerFactory; + } + + @Override + public FileIO apply(TaskEntity task) { + Map internalProperties = task.getInternalPropertiesAsMap(); + String location = internalProperties.get(PolarisTaskConstants.STORAGE_LOCATION); + PolarisMetaStoreManager metaStoreManager = + metaStoreManagerFactory.getOrCreateMetaStoreManager( + CallContext.getCurrentContext().getRealmContext()); + Map properties = new HashMap<>(internalProperties); + properties.putAll( + metaStoreManagerFactory + .getOrCreateStorageCredentialCache(CallContext.getCurrentContext().getRealmContext()) + .getOrGenerateSubScopeCreds( + metaStoreManager, + CallContext.getCurrentContext().getPolarisCallContext(), + task, + true, + Set.of(location), + Set.of(location))); + String ioImpl = + properties.getOrDefault( + CatalogProperties.FILE_IO_IMPL, "org.apache.iceberg.io.ResolvingFileIO"); + return CatalogUtil.loadFileIO(ioImpl, properties, new Configuration()); + } +} diff --git a/polaris-service/src/main/java/io/polaris/service/task/TaskHandler.java b/polaris-service/src/main/java/io/polaris/service/task/TaskHandler.java new file mode 100644 index 0000000000..af73423977 --- /dev/null +++ b/polaris-service/src/main/java/io/polaris/service/task/TaskHandler.java @@ -0,0 +1,9 @@ +package io.polaris.service.task; + +import io.polaris.core.entity.TaskEntity; + +public interface TaskHandler { + boolean canHandleTask(TaskEntity task); + + boolean handleTask(TaskEntity task); +} diff --git a/polaris-service/src/main/java/io/polaris/service/task/TaskUtils.java b/polaris-service/src/main/java/io/polaris/service/task/TaskUtils.java new file mode 100644 index 0000000000..c4f431bb2b --- /dev/null +++ b/polaris-service/src/main/java/io/polaris/service/task/TaskUtils.java @@ -0,0 +1,38 @@ +package io.polaris.service.task; + +import java.io.IOException; +import org.apache.commons.codec.binary.Base64; +import org.apache.iceberg.ManifestFile; +import org.apache.iceberg.ManifestFiles; +import org.apache.iceberg.exceptions.NotFoundException; +import org.apache.iceberg.io.FileIO; + +public class TaskUtils { + static boolean exists(String path, FileIO fileIO) { + try { + return fileIO.newInputFile(path).exists(); + } catch (NotFoundException e) { + // in-memory FileIO throws this exception + return false; + } catch (Exception e) { + // typically, clients will catch a 404 and simply return false, so any other exception + // means something probably went wrong + throw new RuntimeException(e); + } + } + + /** + * base64 encode the serialized manifest file entry so we can deserialize it and read the manifest + * in the {@link ManifestFileCleanupTaskHandler} + * + * @param mf + * @return + */ + static String encodeManifestFile(ManifestFile mf) { + try { + return Base64.encodeBase64String(ManifestFiles.encode(mf)); + } catch (IOException e) { + throw new RuntimeException("Unable to encode binary data in memory", e); + } + } +} diff --git a/polaris-service/src/main/java/io/polaris/service/tracing/HeadersMapAccessor.java b/polaris-service/src/main/java/io/polaris/service/tracing/HeadersMapAccessor.java new file mode 100644 index 0000000000..c3694cd8b3 --- /dev/null +++ b/polaris-service/src/main/java/io/polaris/service/tracing/HeadersMapAccessor.java @@ -0,0 +1,39 @@ +package io.polaris.service.tracing; + +import io.opentelemetry.context.propagation.TextMapGetter; +import io.opentelemetry.context.propagation.TextMapSetter; +import jakarta.servlet.http.HttpServletRequest; +import java.net.http.HttpRequest; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.stream.StreamSupport; +import org.jetbrains.annotations.Nullable; + +/** + * Implementation of {@link TextMapSetter} and {@link TextMapGetter} that can handle an {@link + * HttpServletRequest} for extracting headers and sets headers on a {@link HttpRequest.Builder}. + */ +public class HeadersMapAccessor + implements TextMapSetter, TextMapGetter { + @Override + public Iterable keys(HttpServletRequest carrier) { + return StreamSupport.stream( + Spliterators.spliteratorUnknownSize( + carrier.getHeaderNames().asIterator(), Spliterator.IMMUTABLE), + false) + .toList(); + } + + @Nullable + @Override + public String get(@Nullable HttpServletRequest carrier, String key) { + return carrier == null ? null : carrier.getHeader(key); + } + + @Override + public void set(@Nullable HttpRequest.Builder carrier, String key, String value) { + if (carrier != null) { + carrier.setHeader(key, value); + } + } +} diff --git a/polaris-service/src/main/java/io/polaris/service/tracing/OpenTelemetryAware.java b/polaris-service/src/main/java/io/polaris/service/tracing/OpenTelemetryAware.java new file mode 100644 index 0000000000..f05db40ee8 --- /dev/null +++ b/polaris-service/src/main/java/io/polaris/service/tracing/OpenTelemetryAware.java @@ -0,0 +1,8 @@ +package io.polaris.service.tracing; + +import io.opentelemetry.api.OpenTelemetry; + +/** Allows setting a configured instance of {@link OpenTelemetry} */ +public interface OpenTelemetryAware { + void setOpenTelemetry(OpenTelemetry openTelemetry); +} diff --git a/polaris-service/src/main/java/io/polaris/service/tracing/TracingFilter.java b/polaris-service/src/main/java/io/polaris/service/tracing/TracingFilter.java new file mode 100644 index 0000000000..1cde0c402d --- /dev/null +++ b/polaris-service/src/main/java/io/polaris/service/tracing/TracingFilter.java @@ -0,0 +1,82 @@ +package io.polaris.service.tracing; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.semconv.HttpAttributes; +import io.opentelemetry.semconv.ServerAttributes; +import io.opentelemetry.semconv.UrlAttributes; +import io.polaris.core.context.CallContext; +import jakarta.annotation.Priority; +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.ws.rs.Priorities; +import java.io.IOException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.MDC; + +/** + * Servlet {@link Filter} that starts an OpenTracing {@link Span}, propagating the calling context + * from HTTP headers, if present. "spanId" and "traceId" are added to the logging MDC so that all + * logs recorded in the request will contain the current span and trace id. Downstream HTTP calls + * should use the OpenTelementry {@link io.opentelemetry.context.propagation.ContextPropagators} to + * include the current trace id in the request headers. + */ +@Priority(Priorities.AUTHENTICATION - 1) +public class TracingFilter implements Filter { + private final Logger LOGGER = LoggerFactory.getLogger(TracingFilter.class); + private final OpenTelemetry openTelemetry; + + public TracingFilter(OpenTelemetry openTelemetry) { + this.openTelemetry = openTelemetry; + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + HttpServletRequest httpRequest = (HttpServletRequest) request; + Context extractedContext = + openTelemetry + .getPropagators() + .getTextMapPropagator() + .extract(Context.current(), httpRequest, new HeadersMapAccessor()); + try (Scope scope = extractedContext.makeCurrent()) { + Tracer tracer = openTelemetry.getTracer(httpRequest.getPathInfo()); + Span span = + tracer + .spanBuilder(httpRequest.getMethod() + " " + httpRequest.getPathInfo()) + .setSpanKind(SpanKind.SERVER) + .setAttribute( + "realm", CallContext.getCurrentContext().getRealmContext().getRealmIdentifier()) + .startSpan(); + + try (Scope ignored = span.makeCurrent(); + MDC.MDCCloseable spanId = MDC.putCloseable("spanId", span.getSpanContext().getSpanId()); + MDC.MDCCloseable traceId = + MDC.putCloseable("traceId", span.getSpanContext().getTraceId()); ) { + LOGGER + .atInfo() + .addKeyValue("spanId", span.getSpanContext().getSpanId()) + .addKeyValue("traceId", span.getSpanContext().getTraceId()) + .addKeyValue("parentContext", extractedContext) + .log("Started span with parent"); + span.setAttribute(HttpAttributes.HTTP_REQUEST_METHOD, httpRequest.getMethod()); + span.setAttribute(ServerAttributes.SERVER_ADDRESS, httpRequest.getServerName()); + span.setAttribute(UrlAttributes.URL_SCHEME, httpRequest.getScheme()); + span.setAttribute(UrlAttributes.URL_PATH, httpRequest.getPathInfo()); + + chain.doFilter(request, response); + } finally { + span.end(); + } + } + } +} diff --git a/polaris-service/src/main/java/io/polaris/service/types/CommitTableRequest.java b/polaris-service/src/main/java/io/polaris/service/types/CommitTableRequest.java new file mode 100644 index 0000000000..c3ccdcab52 --- /dev/null +++ b/polaris-service/src/main/java/io/polaris/service/types/CommitTableRequest.java @@ -0,0 +1,5 @@ +package io.polaris.service.types; + +import org.apache.iceberg.rest.requests.UpdateTableRequest; + +public class CommitTableRequest extends UpdateTableRequest {} diff --git a/polaris-service/src/main/java/io/polaris/service/types/CommitViewRequest.java b/polaris-service/src/main/java/io/polaris/service/types/CommitViewRequest.java new file mode 100644 index 0000000000..3cc8f262e5 --- /dev/null +++ b/polaris-service/src/main/java/io/polaris/service/types/CommitViewRequest.java @@ -0,0 +1,5 @@ +package io.polaris.service.types; + +import org.apache.iceberg.rest.requests.UpdateTableRequest; + +public class CommitViewRequest extends UpdateTableRequest {} diff --git a/polaris-service/src/main/java/io/polaris/service/types/NotificationRequest.java b/polaris-service/src/main/java/io/polaris/service/types/NotificationRequest.java new file mode 100644 index 0000000000..8c404cdb9a --- /dev/null +++ b/polaris-service/src/main/java/io/polaris/service/types/NotificationRequest.java @@ -0,0 +1,76 @@ +package io.polaris.service.types; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModelProperty; +import java.util.Objects; + +@jakarta.annotation.Generated( + value = "org.openapitools.codegen.languages.JavaResteasyServerCodegen", + date = "2024-05-25T00:53:53.298853423Z[UTC]", + comments = "Generator version: 7.5.0") +public class NotificationRequest { + + private NotificationType notificationType; + private TableUpdateNotification payload; + + /** */ + @ApiModelProperty(required = true, value = "") + @JsonProperty("notification-type") + public NotificationType getNotificationType() { + return notificationType; + } + + public void setNotificationType(NotificationType notificationType) { + this.notificationType = notificationType; + } + + /** */ + @ApiModelProperty(value = "") + @JsonProperty("payload") + public TableUpdateNotification getPayload() { + return payload; + } + + public void setPayload(TableUpdateNotification payload) { + this.payload = payload; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + NotificationRequest notificationRequest = (NotificationRequest) o; + return Objects.equals(this.notificationType, notificationRequest.notificationType) + && Objects.equals(this.payload, notificationRequest.payload); + } + + @Override + public int hashCode() { + return Objects.hash(notificationType, payload); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class NotificationRequest {\n"); + + sb.append(" notificationType: ").append(toIndentedString(notificationType)).append("\n"); + sb.append(" payload: ").append(toIndentedString(payload)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } +} diff --git a/polaris-service/src/main/java/io/polaris/service/types/NotificationType.java b/polaris-service/src/main/java/io/polaris/service/types/NotificationType.java new file mode 100644 index 0000000000..6b1fa2bb76 --- /dev/null +++ b/polaris-service/src/main/java/io/polaris/service/types/NotificationType.java @@ -0,0 +1,74 @@ +package io.polaris.service.types; + +import java.util.Arrays; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +public enum NotificationType { + + /** Supported notification types for the update table notification. */ + UNKNOWN(0, "UNKNOWN"), + CREATE(1, "CREATE"), + UPDATE(2, "UPDATE"), + DROP(3, "DROP"); + + NotificationType(int id, String displayName) { + this.id = id; + this.displayName = displayName; + } + + /** Internal id of the notification type. */ + private final int id; + + /** Display name of the notification type */ + private final String displayName; + + /** Internal ids and their corresponding sources of notification types. */ + private static final Map idToNotificationTypeMap = + Arrays.stream(NotificationType.values()) + .collect(Collectors.toMap(NotificationType::getId, tf -> tf)); + + /** + * Lookup a notification type using its internal id representation + * + * @param id internal id of the notification type + * @return The notification type, if it exists, or empty + */ + public static Optional lookupById(int id) { + return Optional.ofNullable(idToNotificationTypeMap.get(id)); + } + + /** + * Return the internal id of the notification type + * + * @return id + */ + public int getId() { + return id; + } + + /** Return the display name of the notification type */ + public String getDisplayName() { + return displayName; + } + + /** + * Find the notification type by name, or return an empty optional + * + * @param name name of the notification type + * @return The notification type, if it exists, or empty + */ + public static Optional lookupByName(String name) { + if (name == null) { + return Optional.empty(); + } + + for (NotificationType NotificationType : NotificationType.values()) { + if (name.toUpperCase().equals(NotificationType.name())) { + return Optional.of(NotificationType); + } + } + return Optional.empty(); + } +} diff --git a/polaris-service/src/main/java/io/polaris/service/types/TableUpdateNotification.java b/polaris-service/src/main/java/io/polaris/service/types/TableUpdateNotification.java new file mode 100644 index 0000000000..6b21f841c9 --- /dev/null +++ b/polaris-service/src/main/java/io/polaris/service/types/TableUpdateNotification.java @@ -0,0 +1,181 @@ +package io.polaris.service.types; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.base.Preconditions; +import io.swagger.annotations.ApiModelProperty; +import java.util.Objects; +import org.apache.iceberg.TableMetadata; + +public class TableUpdateNotification { + + private String tableName; + private Long timestamp; + private String tableUuid; + private String metadataLocation; + private TableMetadata metadata; + + /** */ + @ApiModelProperty(required = true, value = "") + @JsonProperty("table-name") + public String getTableName() { + return tableName; + } + + public void setTableName(String tableName) { + this.tableName = tableName; + } + + /** */ + @ApiModelProperty(required = true, value = "") + @JsonProperty("timestamp") + public Long getTimestamp() { + return timestamp; + } + + public void setTimestamp(Long timestamp) { + this.timestamp = timestamp; + } + + /** */ + @ApiModelProperty(required = true, value = "") + @JsonProperty("table-uuid") + public String getTableUuid() { + return tableUuid; + } + + public void setTableUuid(String tableUuid) { + this.tableUuid = tableUuid; + } + + /** */ + @ApiModelProperty(required = true, value = "") + @JsonProperty("metadata-location") + public String getMetadataLocation() { + return metadataLocation; + } + + public void setMetadataLocation(String metadataLocation) { + this.metadataLocation = metadataLocation; + } + + /** */ + @ApiModelProperty(required = true, value = "") + @JsonProperty("metadata") + public TableMetadata getMetadata() { + return metadata; + } + + public void setMetadata(TableMetadata metadata) { + this.metadata = metadata; + } + + public TableUpdateNotification() {} + + public TableUpdateNotification( + String tableName, + Long timestamp, + String tableUuid, + String metadataLocation, + TableMetadata metadata) { + this.tableName = tableName; + this.timestamp = timestamp; + this.tableUuid = tableUuid; + this.metadataLocation = metadataLocation; + this.metadata = metadata; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + TableUpdateNotification tableUpdateNotification = (TableUpdateNotification) o; + return Objects.equals(this.tableName, tableUpdateNotification.tableName) + && Objects.equals(this.timestamp, tableUpdateNotification.timestamp) + && Objects.equals(this.tableUuid, tableUpdateNotification.tableUuid) + && Objects.equals(this.metadataLocation, tableUpdateNotification.metadataLocation) + && Objects.equals(this.metadata, tableUpdateNotification.metadata); + } + + @Override + public int hashCode() { + return Objects.hash(tableName, timestamp, tableUuid, metadataLocation, metadata); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class TableUpdateNotification {\n"); + + sb.append(" tableName: ").append(toIndentedString(tableName)).append("\n"); + sb.append(" timestamp: ").append(toIndentedString(timestamp)).append("\n"); + sb.append(" tableUuid: ").append(toIndentedString(tableUuid)).append("\n"); + sb.append(" metadataLocation: ").append(toIndentedString(metadataLocation)).append("\n"); + sb.append(" metadata: ").append(toIndentedString(metadata)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + + private String tableName; + private Long timestamp; + private String tableUuid; + private String metadataLocation; + private TableMetadata metadata; + + private Builder() {} + + public final Builder tableName(String tableName) { + Preconditions.checkArgument(tableName != null, "Null table name supplied"); + this.tableName = tableName; + return this; + } + + public final Builder timestamp(Long timestamp) { + Preconditions.checkArgument(timestamp != null, "timestamp can't be null"); + this.timestamp = timestamp; + return this; + } + + public final Builder metadataLocation(String metadataLocation) { + Preconditions.checkArgument(metadataLocation != null, "metadataLocation can't be null"); + this.metadataLocation = metadataLocation; + return this; + } + + public final Builder metadata(TableMetadata metadata) { + this.metadata = metadata; + return this; + } + + public final Builder tableUuid(String tableUuid) { + Preconditions.checkArgument(tableUuid != null, "timestamp can't be null"); + this.tableUuid = tableUuid; + return this; + } + + public TableUpdateNotification build() { + + return new TableUpdateNotification( + tableName, timestamp, tableUuid, metadataLocation, metadata); + } + } +} diff --git a/polaris-service/src/main/java/io/polaris/service/types/TokenType.java b/polaris-service/src/main/java/io/polaris/service/types/TokenType.java new file mode 100644 index 0000000000..2a6f218a3e --- /dev/null +++ b/polaris-service/src/main/java/io/polaris/service/types/TokenType.java @@ -0,0 +1,48 @@ +package io.polaris.service.types; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * Token type identifier, from RFC 8693 Section 3 See + * https://datatracker.ietf.org/doc/html/rfc8693#section-3 + */ +public enum TokenType { + ACCESS_TOKEN("urn:ietf:params:oauth:token-type:access_token"), + + REFRESH_TOKEN("urn:ietf:params:oauth:token-type:refresh_token"), + + ID_TOKEN("urn:ietf:params:oauth:token-type:id_token"), + + SAML1("urn:ietf:params:oauth:token-type:saml1"), + + SAML2("urn:ietf:params:oauth:token-type:saml2"), + + JWT("urn:ietf:params:oauth:token-type:jwt"); + + private String value; + + TokenType(String value) { + this.value = value; + } + + @JsonValue + public String getValue() { + return value; + } + + @Override + public String toString() { + return String.valueOf(value); + } + + @JsonCreator + public static TokenType fromValue(String value) { + for (TokenType b : TokenType.values()) { + if (b.value.equals(value)) { + return b; + } + } + throw new IllegalArgumentException("Unexpected value '" + value + "'"); + } +} diff --git a/polaris-service/src/main/resources/META-INF/persistence.xml b/polaris-service/src/main/resources/META-INF/persistence.xml new file mode 100644 index 0000000000..640fe1175a --- /dev/null +++ b/polaris-service/src/main/resources/META-INF/persistence.xml @@ -0,0 +1,27 @@ + + + + + + org.eclipse.persistence.jpa.PersistenceProvider + io.polaris.core.persistence.models.ModelEntity + io.polaris.core.persistence.models.ModelEntityActive + io.polaris.core.persistence.models.ModelEntityChangeTracking + io.polaris.core.persistence.models.ModelEntityDropped + io.polaris.core.persistence.models.ModelGrantRecord + io.polaris.core.persistence.models.ModelPrincipalSecrets + io.polaris.core.persistence.models.ModelSequenceId + NONE + + + + + + + + + + + \ No newline at end of file diff --git a/polaris-service/src/main/resources/META-INF/services/io.dropwizard.jackson.Discoverable b/polaris-service/src/main/resources/META-INF/services/io.dropwizard.jackson.Discoverable new file mode 100644 index 0000000000..8aa3e90116 --- /dev/null +++ b/polaris-service/src/main/resources/META-INF/services/io.dropwizard.jackson.Discoverable @@ -0,0 +1,6 @@ +io.polaris.service.auth.DiscoverableAuthenticator +io.polaris.core.persistence.MetaStoreManagerFactory +io.polaris.service.config.OAuth2ApiService +io.polaris.service.context.RealmContextResolver +io.polaris.service.context.CallContextResolver +io.polaris.service.auth.TokenBrokerFactory \ No newline at end of file diff --git a/polaris-service/src/main/resources/META-INF/services/io.dropwizard.logging.common.layout.DiscoverableLayoutFactory b/polaris-service/src/main/resources/META-INF/services/io.dropwizard.logging.common.layout.DiscoverableLayoutFactory new file mode 100644 index 0000000000..cf8f596130 --- /dev/null +++ b/polaris-service/src/main/resources/META-INF/services/io.dropwizard.logging.common.layout.DiscoverableLayoutFactory @@ -0,0 +1 @@ +io.polaris.service.logging.PolarisJsonLayoutFactory \ No newline at end of file diff --git a/polaris-service/src/main/resources/META-INF/services/io.polaris.core.persistence.MetaStoreManagerFactory b/polaris-service/src/main/resources/META-INF/services/io.polaris.core.persistence.MetaStoreManagerFactory new file mode 100644 index 0000000000..80fb2d5d40 --- /dev/null +++ b/polaris-service/src/main/resources/META-INF/services/io.polaris.core.persistence.MetaStoreManagerFactory @@ -0,0 +1,4 @@ +io.polaris.extension.persistence.impl.hibernate.HibernatePolarisMetaStoreManagerFactory +io.polaris.service.persistence.InMemoryPolarisMetaStoreManagerFactory +com.snowflake.polaris.persistence.impl.remote.RemotePolarisMetaStoreManagerFactory +io.polaris.extension.persistence.impl.eclipselink.EclipseLinkPolarisMetaStoreManagerFactory diff --git a/polaris-service/src/main/resources/META-INF/services/io.polaris.service.auth.TokenBrokerFactory b/polaris-service/src/main/resources/META-INF/services/io.polaris.service.auth.TokenBrokerFactory new file mode 100644 index 0000000000..b5a14e5dcc --- /dev/null +++ b/polaris-service/src/main/resources/META-INF/services/io.polaris.service.auth.TokenBrokerFactory @@ -0,0 +1,3 @@ +com.snowflake.polaris.auth.SnowflakeOAuth2TokenBrokerFactory +io.polaris.service.auth.JWTRSAKeyPairFactory +io.polaris.service.auth.JWTSymmetricKeyFactory \ No newline at end of file diff --git a/polaris-service/src/main/resources/META-INF/services/io.polaris.service.config.OAuth2ApiService b/polaris-service/src/main/resources/META-INF/services/io.polaris.service.config.OAuth2ApiService new file mode 100644 index 0000000000..4c74cd07e3 --- /dev/null +++ b/polaris-service/src/main/resources/META-INF/services/io.polaris.service.config.OAuth2ApiService @@ -0,0 +1,2 @@ +io.polaris.service.auth.TestOAuth2ApiService +io.polaris.service.auth.DefaultOAuth2ApiService \ No newline at end of file diff --git a/polaris-service/src/main/resources/META-INF/services/io.polaris.service.context.CallContextResolver b/polaris-service/src/main/resources/META-INF/services/io.polaris.service.context.CallContextResolver new file mode 100644 index 0000000000..4b0118d637 --- /dev/null +++ b/polaris-service/src/main/resources/META-INF/services/io.polaris.service.context.CallContextResolver @@ -0,0 +1 @@ +io.polaris.service.context.DefaultContextResolver \ No newline at end of file diff --git a/polaris-service/src/main/resources/META-INF/services/io.polaris.service.context.RealmContextResolver b/polaris-service/src/main/resources/META-INF/services/io.polaris.service.context.RealmContextResolver new file mode 100644 index 0000000000..4b0118d637 --- /dev/null +++ b/polaris-service/src/main/resources/META-INF/services/io.polaris.service.context.RealmContextResolver @@ -0,0 +1 @@ +io.polaris.service.context.DefaultContextResolver \ No newline at end of file diff --git a/polaris-service/src/main/resources/log4j.properties b/polaris-service/src/main/resources/log4j.properties new file mode 100644 index 0000000000..fc63d9a2e2 --- /dev/null +++ b/polaris-service/src/main/resources/log4j.properties @@ -0,0 +1,5 @@ +log4j.rootLogger=INFO, stdout +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.Target=System.out +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd'T'HH:mm:ss.SSS} %-5p [%c] - %m%n diff --git a/polaris-service/src/test/java/io/polaris/service/PolarisApplicationIntegrationTest.java b/polaris-service/src/test/java/io/polaris/service/PolarisApplicationIntegrationTest.java new file mode 100644 index 0000000000..d42753cddb --- /dev/null +++ b/polaris-service/src/test/java/io/polaris/service/PolarisApplicationIntegrationTest.java @@ -0,0 +1,609 @@ +package io.polaris.service; + +import static io.polaris.service.context.DefaultContextResolver.REALM_PROPERTY_KEY; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + +import io.dropwizard.testing.ConfigOverride; +import io.dropwizard.testing.ResourceHelpers; +import io.dropwizard.testing.junit5.DropwizardAppExtension; +import io.dropwizard.testing.junit5.DropwizardExtensionsSupport; +import io.polaris.core.admin.model.AwsStorageConfigInfo; +import io.polaris.core.admin.model.Catalog; +import io.polaris.core.admin.model.CatalogProperties; +import io.polaris.core.admin.model.CatalogRole; +import io.polaris.core.admin.model.ExternalCatalog; +import io.polaris.core.admin.model.FileStorageConfigInfo; +import io.polaris.core.admin.model.PolarisCatalog; +import io.polaris.core.admin.model.PrincipalRole; +import io.polaris.core.admin.model.StorageConfigInfo; +import io.polaris.core.entity.CatalogEntity; +import io.polaris.core.entity.PolarisEntityConstants; +import io.polaris.service.auth.BasePolarisAuthenticator; +import io.polaris.service.config.PolarisApplicationConfig; +import io.polaris.service.test.PolarisConnectionExtension; +import io.polaris.service.test.SnowmanCredentialsExtension; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.core.Response; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import org.apache.hadoop.conf.Configuration; +import org.apache.iceberg.BaseTable; +import org.apache.iceberg.PartitionData; +import org.apache.iceberg.PartitionSpec; +import org.apache.iceberg.Schema; +import org.apache.iceberg.SortOrder; +import org.apache.iceberg.Table; +import org.apache.iceberg.TableMetadata; +import org.apache.iceberg.TableMetadataParser; +import org.apache.iceberg.TestHelpers; +import org.apache.iceberg.catalog.Namespace; +import org.apache.iceberg.catalog.SessionCatalog; +import org.apache.iceberg.catalog.TableIdentifier; +import org.apache.iceberg.exceptions.BadRequestException; +import org.apache.iceberg.exceptions.NoSuchNamespaceException; +import org.apache.iceberg.exceptions.NoSuchTableException; +import org.apache.iceberg.exceptions.RESTException; +import org.apache.iceberg.exceptions.ServiceFailureException; +import org.apache.iceberg.hadoop.HadoopFileIO; +import org.apache.iceberg.io.ResolvingFileIO; +import org.apache.iceberg.rest.RESTSessionCatalog; +import org.apache.iceberg.rest.auth.OAuth2Properties; +import org.apache.iceberg.types.Types; +import org.apache.iceberg.util.EnvironmentUtil; +import org.assertj.core.api.Assertions; +import org.assertj.core.api.InstanceOfAssertFactories; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; +import org.junit.jupiter.api.extension.ExtendWith; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@ExtendWith({ + DropwizardExtensionsSupport.class, + PolarisConnectionExtension.class, + SnowmanCredentialsExtension.class +}) +public class PolarisApplicationIntegrationTest { + public static final String PRINCIPAL_ROLE_NAME = "admin"; + public static final Logger LOGGER = + LoggerFactory.getLogger(PolarisApplicationIntegrationTest.class); + private static DropwizardAppExtension EXT = + new DropwizardAppExtension<>( + PolarisApplication.class, + ResourceHelpers.resourceFilePath("polaris-server-integrationtest.yml"), + ConfigOverride.config( + "server.applicationConnectors[0].port", + "0"), // Bind to random port to support parallelism + ConfigOverride.config( + "server.adminConnectors[0].port", "0")); // Bind to random port to support parallelism + + private static String userToken; + private static SnowmanCredentialsExtension.SnowmanCredentials snowmanCredentials; + private static Path testDir; + private static String realm; + + @BeforeAll + public static void setup( + PolarisConnectionExtension.PolarisToken userToken, + SnowmanCredentialsExtension.SnowmanCredentials snowmanCredentials) + throws IOException { + realm = PolarisConnectionExtension.getTestRealm(PolarisApplicationIntegrationTest.class); + + testDir = Path.of("build/test_data/iceberg/" + realm); + if (Files.exists(testDir)) { + if (Files.isDirectory(testDir)) { + Files.walk(testDir) + .sorted(Comparator.reverseOrder()) + .forEach( + path -> { + try { + Files.delete(path); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + + } else { + Files.delete(testDir); + } + } + Files.createDirectories(testDir); + PolarisApplicationIntegrationTest.userToken = userToken.token(); + PolarisApplicationIntegrationTest.snowmanCredentials = snowmanCredentials; + + PrincipalRole principalRole = new PrincipalRole(PRINCIPAL_ROLE_NAME); + try (Response createPrResponse = + EXT.client() + .target( + String.format( + "http://localhost:%d/api/management/v1/principal-roles", EXT.getLocalPort())) + .request("application/json") + .header("Authorization", "Bearer " + userToken.token()) + .header(REALM_PROPERTY_KEY, realm) + .post(Entity.json(principalRole))) { + assertThat(createPrResponse) + .returns(Response.Status.CREATED.getStatusCode(), Response::getStatus); + } + + try (Response assignPrResponse = + EXT.client() + .target( + String.format( + "http://localhost:%d/api/management/v1/principals/snowman/principal-roles", + EXT.getLocalPort())) + .request("application/json") + .header("Authorization", "Bearer " + PolarisApplicationIntegrationTest.userToken) + .header(REALM_PROPERTY_KEY, realm) + .put(Entity.json(principalRole))) { + assertThat(assignPrResponse) + .returns(Response.Status.CREATED.getStatusCode(), Response::getStatus); + } + } + + @AfterAll + public static void deletePrincipalRole() { + try (Response deletePrResponse = + EXT.client() + .target( + String.format( + "http://localhost:%d/api/management/v1/principal-roles/%s", + EXT.getLocalPort(), PRINCIPAL_ROLE_NAME)) + .request("application/json") + .header("Authorization", "Bearer " + userToken) + .header(REALM_PROPERTY_KEY, realm) + .delete()) {} + } + + /** + * Create a new catalog for each test case. Assign the snowman catalog-admin principal role the + * admin role of the new catalog. + * + * @param testInfo + */ + @BeforeEach + public void before(TestInfo testInfo) { + testInfo + .getTestMethod() + .ifPresent( + method -> { + String catalogName = method.getName(); + Catalog.TypeEnum catalogType = Catalog.TypeEnum.INTERNAL; + createCatalog(catalogName, catalogType, PRINCIPAL_ROLE_NAME); + }); + } + + private static void createCatalog( + String catalogName, Catalog.TypeEnum catalogType, String principalRoleName) { + createCatalog( + catalogName, + catalogType, + principalRoleName, + AwsStorageConfigInfo.builder() + .setRoleArn("arn:aws:iam::123456789012:role/my-role") + .setExternalId("externalId") + .setStorageType(StorageConfigInfo.StorageTypeEnum.S3) + .setAllowedLocations(List.of("s3://my-old-bucket/path/to/data")) + .build(), + "s3://my-bucket/path/to/data"); + } + + private static void createCatalog( + String catalogName, + Catalog.TypeEnum catalogType, + String principalRoleName, + StorageConfigInfo storageConfig, + String defaultBaseLocation) { + CatalogProperties props = + CatalogProperties.builder(defaultBaseLocation) + .addProperty( + CatalogEntity.REPLACE_NEW_LOCATION_PREFIX_WITH_CATALOG_DEFAULT_KEY, "file:/") + .build(); + Catalog catalog = + catalogType.equals(Catalog.TypeEnum.INTERNAL) + ? PolarisCatalog.builder() + .setName(catalogName) + .setType(catalogType) + .setProperties(props) + .setStorageConfigInfo(storageConfig) + .build() + : ExternalCatalog.builder() + .setRemoteUrl("http://faraway.com") + .setName(catalogName) + .setType(catalogType) + .setProperties(props) + .setStorageConfigInfo(storageConfig) + .build(); + try (Response response = + EXT.client() + .target( + String.format("http://localhost:%d/api/management/v1/catalogs", EXT.getLocalPort())) + .request("application/json") + .header("Authorization", "Bearer " + userToken) + .header(REALM_PROPERTY_KEY, realm) + .post(Entity.json(catalog))) { + assertThat(response).returns(Response.Status.CREATED.getStatusCode(), Response::getStatus); + } + try (Response response = + EXT.client() + .target( + String.format( + "http://localhost:%d/api/management/v1/catalogs/%s/catalog-roles/%s", + EXT.getLocalPort(), + catalogName, + PolarisEntityConstants.getNameOfCatalogAdminRole())) + .request("application/json") + .header("Authorization", "Bearer " + userToken) + .header(REALM_PROPERTY_KEY, realm) + .get()) { + assertThat(response).returns(Response.Status.OK.getStatusCode(), Response::getStatus); + CatalogRole catalogRole = response.readEntity(CatalogRole.class); + + try (Response assignResponse = + EXT.client() + .target( + String.format( + "http://localhost:%d/api/management/v1/principal-roles/%s/catalog-roles/%s", + EXT.getLocalPort(), principalRoleName, catalogName)) + .request("application/json") + .header("Authorization", "Bearer " + userToken) + .header(REALM_PROPERTY_KEY, realm) + .put(Entity.json(catalogRole))) { + assertThat(assignResponse) + .returns(Response.Status.CREATED.getStatusCode(), Response::getStatus); + } + } + } + + private static RESTSessionCatalog newSessionCatalog(String catalog) { + RESTSessionCatalog sessionCatalog = new RESTSessionCatalog(); + sessionCatalog.initialize( + "snowflake", + Map.of( + "uri", + "http://localhost:" + EXT.getLocalPort() + "/api/catalog", + OAuth2Properties.CREDENTIAL, + snowmanCredentials.clientId() + ":" + snowmanCredentials.clientSecret(), + OAuth2Properties.SCOPE, + BasePolarisAuthenticator.PRINCIPAL_ROLE_ALL, + "warehouse", + catalog, + "header." + REALM_PROPERTY_KEY, + realm)); + return sessionCatalog; + } + + @Test + public void testIcebergListNamespaces() throws IOException { + try (RESTSessionCatalog sessionCatalog = newSessionCatalog("testIcebergListNamespaces")) { + SessionCatalog.SessionContext sessionContext = SessionCatalog.SessionContext.createEmpty(); + List namespaces = sessionCatalog.listNamespaces(sessionContext); + assertThat(namespaces).isNotNull().isEmpty(); + } + } + + @Test + public void testConfigureCatalogCaseSensitive() throws IOException { + try { + RESTSessionCatalog sessionCatalog = newSessionCatalog("TESTCONFIGURECATALOGCASESENSITIVE"); + fail("Expected exception connecting to catalog"); + } catch (ServiceFailureException e) { + fail("Unexpected service failure exception", e); + } catch (RESTException e) { + LoggerFactory.getLogger(getClass()).info("Caught expected rest exception", e); + } + } + + @Test + public void testIcebergListNamespacesNotFound() throws IOException { + try (RESTSessionCatalog sessionCatalog = + newSessionCatalog("testIcebergListNamespacesNotFound")) { + SessionCatalog.SessionContext sessionContext = SessionCatalog.SessionContext.createEmpty(); + try { + sessionCatalog.listNamespaces(sessionContext, Namespace.of("whoops")); + fail("Expected exception to be thrown"); + } catch (NoSuchNamespaceException e) { + // we expect this! + Assertions.assertThat(e).isNotNull(); + } catch (Exception e) { + fail("Unexpected exception", e); + } + } + } + + @Test + public void testIcebergListNamespacesNestedNotFound() throws IOException { + try (RESTSessionCatalog sessionCatalog = + newSessionCatalog("testIcebergListNamespacesNestedNotFound")) { + SessionCatalog.SessionContext sessionContext = SessionCatalog.SessionContext.createEmpty(); + Namespace topLevelNamespace = Namespace.of("top_level"); + sessionCatalog.createNamespace(sessionContext, topLevelNamespace); + sessionCatalog.loadNamespaceMetadata(sessionContext, Namespace.of("top_level")); + try { + sessionCatalog.listNamespaces(sessionContext, Namespace.of("top_level", "whoops")); + fail("Expected exception to be thrown"); + } catch (NoSuchNamespaceException e) { + // we expect this! + Assertions.assertThat(e).isNotNull(); + } catch (Exception e) { + fail("Unexpected exception", e); + } + } + } + + @Test + public void testIcebergListTablesNamespaceNotFound() throws IOException { + try (RESTSessionCatalog sessionCatalog = + newSessionCatalog("testIcebergListTablesNamespaceNotFound")) { + SessionCatalog.SessionContext sessionContext = SessionCatalog.SessionContext.createEmpty(); + try { + sessionCatalog.listTables(sessionContext, Namespace.of("whoops")); + fail("Expected exception to be thrown"); + } catch (NoSuchNamespaceException e) { + // we expect this! + Assertions.assertThat(e).isNotNull(); + } catch (Exception e) { + fail("Unexpected exception", e); + } + } + } + + @Test + public void testIcebergCreateNamespace() throws IOException { + try (RESTSessionCatalog sessionCatalog = newSessionCatalog("testIcebergCreateNamespace")) { + SessionCatalog.SessionContext sessionContext = SessionCatalog.SessionContext.createEmpty(); + Namespace topLevelNamespace = Namespace.of("top_level"); + sessionCatalog.createNamespace(sessionContext, topLevelNamespace); + List namespaces = sessionCatalog.listNamespaces(sessionContext); + assertThat(namespaces).isNotNull().hasSize(1).containsExactly(topLevelNamespace); + Namespace nestedNamespace = Namespace.of("top_level", "second_level"); + sessionCatalog.createNamespace(sessionContext, nestedNamespace); + namespaces = sessionCatalog.listNamespaces(sessionContext, topLevelNamespace); + assertThat(namespaces).isNotNull().hasSize(1).containsExactly(nestedNamespace); + } + } + + @Test + public void testIcebergCreateNamespaceInExternalCatalog(TestInfo testInfo) throws IOException { + String catalogName = testInfo.getTestMethod().get().getName() + "External"; + createCatalog(catalogName, Catalog.TypeEnum.EXTERNAL, PRINCIPAL_ROLE_NAME); + try (RESTSessionCatalog sessionCatalog = newSessionCatalog(catalogName)) { + SessionCatalog.SessionContext sessionContext = SessionCatalog.SessionContext.createEmpty(); + Namespace ns = Namespace.of("db1"); + sessionCatalog.createNamespace(sessionContext, ns); + List namespaces = sessionCatalog.listNamespaces(sessionContext); + assertThat(namespaces).isNotNull().hasSize(1).containsExactly(ns); + Map metadata = sessionCatalog.loadNamespaceMetadata(sessionContext, ns); + assertThat(metadata).isNotNull().isEmpty(); + } + } + + @Test + public void testIcebergDropNamespaceInExternalCatalog(TestInfo testInfo) throws IOException { + String catalogName = testInfo.getTestMethod().get().getName() + "External"; + createCatalog(catalogName, Catalog.TypeEnum.EXTERNAL, PRINCIPAL_ROLE_NAME); + try (RESTSessionCatalog sessionCatalog = newSessionCatalog(catalogName)) { + SessionCatalog.SessionContext sessionContext = SessionCatalog.SessionContext.createEmpty(); + Namespace ns = Namespace.of("db1"); + sessionCatalog.createNamespace(sessionContext, ns); + List namespaces = sessionCatalog.listNamespaces(sessionContext); + assertThat(namespaces).isNotNull().hasSize(1).containsExactly(ns); + sessionCatalog.dropNamespace(sessionContext, ns); + try { + sessionCatalog.loadNamespaceMetadata(sessionContext, ns); + Assertions.fail("Expected exception when loading namespace after drop"); + } catch (NoSuchNamespaceException e) { + LOGGER.info("Received expected exception " + e.getMessage()); + } + } + } + + @Test + public void testIcebergCreateTablesInExternalCatalog(TestInfo testInfo) throws IOException { + String catalogName = testInfo.getTestMethod().get().getName() + "External"; + createCatalog(catalogName, Catalog.TypeEnum.EXTERNAL, PRINCIPAL_ROLE_NAME); + try (RESTSessionCatalog sessionCatalog = newSessionCatalog(catalogName)) { + SessionCatalog.SessionContext sessionContext = SessionCatalog.SessionContext.createEmpty(); + Namespace ns = Namespace.of("db1"); + sessionCatalog.createNamespace(sessionContext, ns); + try { + sessionCatalog + .buildTable( + sessionContext, + TableIdentifier.of(ns, "the_table"), + new Schema( + List.of(Types.NestedField.of(1, false, "theField", Types.StringType.get())))) + .withLocation("file:///tmp/tables") + .withSortOrder(SortOrder.unsorted()) + .withPartitionSpec(PartitionSpec.unpartitioned()) + .create(); + Assertions.fail("Expected failure calling create table in external catalog"); + } catch (BadRequestException e) { + LOGGER.info("Received expected exception " + e.getMessage()); + } + } + } + + @Test + public void testIcebergRegisterTableInExternalCatalog(TestInfo testInfo) throws IOException { + String catalogName = testInfo.getTestMethod().get().getName() + "External"; + createCatalog( + catalogName, + Catalog.TypeEnum.EXTERNAL, + PRINCIPAL_ROLE_NAME, + FileStorageConfigInfo.builder(StorageConfigInfo.StorageTypeEnum.FILE) + .setAllowedLocations(List.of("file://" + testDir.toFile().getAbsolutePath())) + .build(), + "file://" + testDir.toFile().getAbsolutePath()); + try (RESTSessionCatalog sessionCatalog = newSessionCatalog(catalogName); + HadoopFileIO fileIo = new HadoopFileIO(new Configuration()); ) { + SessionCatalog.SessionContext sessionContext = SessionCatalog.SessionContext.createEmpty(); + Namespace ns = Namespace.of("db1"); + sessionCatalog.createNamespace(sessionContext, ns); + TableIdentifier tableIdentifier = TableIdentifier.of(ns, "the_table"); + String location = + "file://" + + testDir.toFile().getAbsolutePath() + + "/" + + testInfo.getTestMethod().get().getName(); + String metadataLocation = location + "/metadata/000001-494949494949494949.metadata.json"; + + TableMetadata tableMetadata = + TableMetadata.buildFromEmpty() + .setLocation(location) + .assignUUID() + .addPartitionSpec(PartitionSpec.unpartitioned()) + .addSortOrder(SortOrder.unsorted()) + .addSchema( + new Schema(Types.NestedField.of(1, false, "col1", Types.StringType.get())), 1) + .build(); + TableMetadataParser.write(tableMetadata, fileIo.newOutputFile(metadataLocation)); + + sessionCatalog.registerTable(sessionContext, tableIdentifier, metadataLocation); + Table table = sessionCatalog.loadTable(sessionContext, tableIdentifier); + assertThat(table) + .isNotNull() + .isInstanceOf(BaseTable.class) + .asInstanceOf(InstanceOfAssertFactories.type(BaseTable.class)) + .returns(tableMetadata.location(), BaseTable::location) + .returns(tableMetadata.uuid(), bt -> bt.uuid().toString()) + .returns(tableMetadata.schema().columns(), bt -> bt.schema().columns()); + } + } + + @Test + public void testIcebergUpdateTableInExternalCatalog(TestInfo testInfo) throws IOException { + String catalogName = testInfo.getTestMethod().get().getName() + "External"; + createCatalog( + catalogName, + Catalog.TypeEnum.EXTERNAL, + PRINCIPAL_ROLE_NAME, + FileStorageConfigInfo.builder(StorageConfigInfo.StorageTypeEnum.FILE) + .setAllowedLocations(List.of("file://" + testDir.toFile().getAbsolutePath())) + .build(), + "file://" + testDir.toFile().getAbsolutePath()); + try (RESTSessionCatalog sessionCatalog = newSessionCatalog(catalogName); + HadoopFileIO fileIo = new HadoopFileIO(new Configuration()); ) { + SessionCatalog.SessionContext sessionContext = SessionCatalog.SessionContext.createEmpty(); + Namespace ns = Namespace.of("db1"); + sessionCatalog.createNamespace(sessionContext, ns); + TableIdentifier tableIdentifier = TableIdentifier.of(ns, "the_table"); + String location = + "file://" + + testDir.toFile().getAbsolutePath() + + "/" + + testInfo.getTestMethod().get().getName(); + String metadataLocation = location + "/metadata/000001-494949494949494949.metadata.json"; + + Types.NestedField col1 = Types.NestedField.of(1, false, "col1", Types.StringType.get()); + TableMetadata tableMetadata = + TableMetadata.buildFromEmpty() + .setLocation(location) + .assignUUID() + .addPartitionSpec(PartitionSpec.unpartitioned()) + .addSortOrder(SortOrder.unsorted()) + .addSchema(new Schema(col1), 1) + .build(); + TableMetadataParser.write(tableMetadata, fileIo.newOutputFile(metadataLocation)); + + sessionCatalog.registerTable(sessionContext, tableIdentifier, metadataLocation); + Table table = sessionCatalog.loadTable(sessionContext, tableIdentifier); + ((ResolvingFileIO) table.io()).setConf(new Configuration()); + try { + table + .newAppend() + .appendFile( + new TestHelpers.TestDataFile( + location + "/path/to/file.parquet", + new PartitionData(PartitionSpec.unpartitioned().partitionType()), + 10L)) + .commit(); + Assertions.fail("Should fail when committing an update to external catalog"); + } catch (BadRequestException e) { + LOGGER.info("Received expected exception " + e.getMessage()); + } + } + } + + @Test + public void testIcebergDropTableInExternalCatalog(TestInfo testInfo) throws IOException { + String catalogName = testInfo.getTestMethod().get().getName() + "External"; + createCatalog( + catalogName, + Catalog.TypeEnum.EXTERNAL, + PRINCIPAL_ROLE_NAME, + FileStorageConfigInfo.builder(StorageConfigInfo.StorageTypeEnum.FILE) + .setAllowedLocations(List.of("file://" + testDir.toFile().getAbsolutePath())) + .build(), + "file://" + testDir.toFile().getAbsolutePath()); + try (RESTSessionCatalog sessionCatalog = newSessionCatalog(catalogName); + HadoopFileIO fileIo = new HadoopFileIO(new Configuration()); ) { + SessionCatalog.SessionContext sessionContext = SessionCatalog.SessionContext.createEmpty(); + Namespace ns = Namespace.of("db1"); + sessionCatalog.createNamespace(sessionContext, ns); + TableIdentifier tableIdentifier = TableIdentifier.of(ns, "the_table"); + String location = + "file://" + + testDir.toFile().getAbsolutePath() + + "/" + + testInfo.getTestMethod().get().getName(); + String metadataLocation = location + "/metadata/000001-494949494949494949.metadata.json"; + + TableMetadata tableMetadata = + TableMetadata.buildFromEmpty() + .setLocation(location) + .assignUUID() + .addPartitionSpec(PartitionSpec.unpartitioned()) + .addSortOrder(SortOrder.unsorted()) + .addSchema( + new Schema(Types.NestedField.of(1, false, "col1", Types.StringType.get())), 1) + .build(); + TableMetadataParser.write(tableMetadata, fileIo.newOutputFile(metadataLocation)); + + sessionCatalog.registerTable(sessionContext, tableIdentifier, metadataLocation); + Table table = sessionCatalog.loadTable(sessionContext, tableIdentifier); + assertThat(table).isNotNull(); + sessionCatalog.dropTable(sessionContext, tableIdentifier); + try { + sessionCatalog.loadTable(sessionContext, tableIdentifier); + Assertions.fail("Expected failure loading table after drop"); + } catch (NoSuchTableException e) { + LOGGER.info("Received expected exception " + e.getMessage()); + } + } + } + + @Test + public void testWarehouseNotSpecified() throws IOException { + try (RESTSessionCatalog sessionCatalog = new RESTSessionCatalog()) { + String emptyEnvironmentVariable = "env:__NULL_ENV_VARIABLE__"; + assertThat(EnvironmentUtil.resolveAll(Map.of("", emptyEnvironmentVariable)).get("")).isNull(); + sessionCatalog.initialize( + "snowflake", + Map.of( + "uri", + "http://localhost:" + EXT.getLocalPort() + "/api/catalog", + OAuth2Properties.CREDENTIAL, + snowmanCredentials.clientId() + ":" + snowmanCredentials.clientSecret(), + OAuth2Properties.SCOPE, + BasePolarisAuthenticator.PRINCIPAL_ROLE_ALL, + "warehouse", + emptyEnvironmentVariable, + "header." + REALM_PROPERTY_KEY, + realm)); + fail("Expected exception due to null warehouse"); + } catch (ServiceFailureException e) { + fail("Unexpected service failure exception", e); + } catch (RESTException e) { + LoggerFactory.getLogger(getClass()).info("Caught expected rest exception", e); + assertThat(e).isInstanceOf(BadRequestException.class); + } + } +} diff --git a/polaris-service/src/test/java/io/polaris/service/admin/PolarisAdminServiceAuthzTest.java b/polaris-service/src/test/java/io/polaris/service/admin/PolarisAdminServiceAuthzTest.java new file mode 100644 index 0000000000..e8a1b3be0f --- /dev/null +++ b/polaris-service/src/test/java/io/polaris/service/admin/PolarisAdminServiceAuthzTest.java @@ -0,0 +1,1027 @@ +package io.polaris.service.admin; + +import io.polaris.core.admin.model.UpdateCatalogRequest; +import io.polaris.core.admin.model.UpdateCatalogRoleRequest; +import io.polaris.core.admin.model.UpdatePrincipalRequest; +import io.polaris.core.admin.model.UpdatePrincipalRoleRequest; +import io.polaris.core.auth.AuthenticatedPolarisPrincipal; +import io.polaris.core.entity.CatalogEntity; +import io.polaris.core.entity.CatalogRoleEntity; +import io.polaris.core.entity.PolarisPrivilege; +import io.polaris.core.entity.PrincipalEntity; +import io.polaris.core.entity.PrincipalRoleEntity; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +public class PolarisAdminServiceAuthzTest extends PolarisAuthzTestBase { + private PolarisAdminService newTestAdminService() { + return newTestAdminService(Set.of()); + } + + private PolarisAdminService newTestAdminService(Set activatedPrincipalRoles) { + final AuthenticatedPolarisPrincipal authenticatedPrincipal = + new AuthenticatedPolarisPrincipal(principalEntity, activatedPrincipalRoles); + return new PolarisAdminService( + callContext, entityManager, authenticatedPrincipal, polarisAuthorizer); + } + + private void doTestSufficientPrivileges( + List sufficientPrivileges, + Runnable action, + Runnable cleanupAction, + Function grantAction, + Function revokeAction) { + doTestSufficientPrivilegeSets( + sufficientPrivileges.stream().map(priv -> Set.of(priv)).toList(), + action, + cleanupAction, + PRINCIPAL_NAME, + grantAction, + revokeAction); + } + + private void doTestInsufficientPrivileges( + List insufficientPrivileges, + Runnable action, + Function grantAction, + Function revokeAction) { + doTestInsufficientPrivileges( + insufficientPrivileges, PRINCIPAL_NAME, action, grantAction, revokeAction); + } + + @Test + public void testListCatalogsSufficientPrivileges() { + doTestSufficientPrivileges( + List.of( + PolarisPrivilege.SERVICE_MANAGE_ACCESS, + PolarisPrivilege.CATALOG_LIST, + PolarisPrivilege.CATALOG_READ_PROPERTIES, + PolarisPrivilege.CATALOG_WRITE_PROPERTIES, + PolarisPrivilege.CATALOG_CREATE, + PolarisPrivilege.CATALOG_FULL_METADATA, + PolarisPrivilege.CATALOG_MANAGE_METADATA, + PolarisPrivilege.CATALOG_MANAGE_CONTENT), + () -> newTestAdminService().listCatalogs(), + null, // cleanupAction + (privilege) -> + adminService.grantPrivilegeOnRootContainerToPrincipalRole(PRINCIPAL_ROLE1, privilege), + (privilege) -> + adminService.revokePrivilegeOnRootContainerFromPrincipalRole( + PRINCIPAL_ROLE1, privilege)); + } + + @Test + public void testListCatalogsInsufficientPrivileges() { + doTestInsufficientPrivileges( + List.of( + PolarisPrivilege.NAMESPACE_FULL_METADATA, + PolarisPrivilege.TABLE_FULL_METADATA, + PolarisPrivilege.VIEW_FULL_METADATA, + PolarisPrivilege.VIEW_FULL_METADATA, + PolarisPrivilege.PRINCIPAL_FULL_METADATA, + PolarisPrivilege.PRINCIPAL_ROLE_FULL_METADATA, + PolarisPrivilege.CATALOG_DROP, + PolarisPrivilege.CATALOG_ROLE_FULL_METADATA, + PolarisPrivilege.CATALOG_MANAGE_ACCESS), + () -> newTestAdminService().listCatalogs(), + (privilege) -> + adminService.grantPrivilegeOnRootContainerToPrincipalRole(PRINCIPAL_ROLE1, privilege), + (privilege) -> + adminService.revokePrivilegeOnRootContainerFromPrincipalRole( + PRINCIPAL_ROLE1, privilege)); + } + + @Test + public void testCreateCatalogSufficientPrivileges() { + // Cleanup with PRINCIPAL_ROLE2 + Assertions.assertThat( + adminService.grantPrivilegeOnRootContainerToPrincipalRole( + PRINCIPAL_ROLE2, PolarisPrivilege.CATALOG_DROP)) + .isTrue(); + final CatalogEntity newCatalog = new CatalogEntity.Builder().setName("new_catalog").build(); + + doTestSufficientPrivileges( + List.of( + PolarisPrivilege.SERVICE_MANAGE_ACCESS, + PolarisPrivilege.CATALOG_CREATE, + PolarisPrivilege.CATALOG_FULL_METADATA), + () -> newTestAdminService(Set.of(PRINCIPAL_ROLE1)).createCatalog(newCatalog), + () -> newTestAdminService(Set.of(PRINCIPAL_ROLE2)).deleteCatalog(newCatalog.getName()), + (privilege) -> + adminService.grantPrivilegeOnRootContainerToPrincipalRole(PRINCIPAL_ROLE1, privilege), + (privilege) -> + adminService.revokePrivilegeOnRootContainerFromPrincipalRole( + PRINCIPAL_ROLE1, privilege)); + } + + @Test + public void testCreateCatalogInsufficientPrivileges() { + final CatalogEntity newCatalog = new CatalogEntity.Builder().setName("new_catalog").build(); + + doTestInsufficientPrivileges( + List.of( + PolarisPrivilege.NAMESPACE_FULL_METADATA, + PolarisPrivilege.TABLE_FULL_METADATA, + PolarisPrivilege.VIEW_FULL_METADATA, + PolarisPrivilege.PRINCIPAL_FULL_METADATA, + PolarisPrivilege.PRINCIPAL_ROLE_FULL_METADATA, + PolarisPrivilege.CATALOG_ROLE_FULL_METADATA, + PolarisPrivilege.CATALOG_LIST, + PolarisPrivilege.CATALOG_DROP, + PolarisPrivilege.CATALOG_READ_PROPERTIES, + PolarisPrivilege.CATALOG_WRITE_PROPERTIES, + PolarisPrivilege.CATALOG_MANAGE_METADATA, + PolarisPrivilege.CATALOG_MANAGE_CONTENT, + PolarisPrivilege.CATALOG_MANAGE_ACCESS), + () -> newTestAdminService(Set.of(PRINCIPAL_ROLE1)).createCatalog(newCatalog), + (privilege) -> + adminService.grantPrivilegeOnRootContainerToPrincipalRole(PRINCIPAL_ROLE1, privilege), + (privilege) -> + adminService.revokePrivilegeOnRootContainerFromPrincipalRole( + PRINCIPAL_ROLE1, privilege)); + } + + @Test + public void testGetCatalogSufficientPrivileges() { + doTestSufficientPrivileges( + List.of( + PolarisPrivilege.SERVICE_MANAGE_ACCESS, + PolarisPrivilege.CATALOG_READ_PROPERTIES, + PolarisPrivilege.CATALOG_WRITE_PROPERTIES, + PolarisPrivilege.CATALOG_FULL_METADATA, + PolarisPrivilege.CATALOG_MANAGE_METADATA, + PolarisPrivilege.CATALOG_MANAGE_CONTENT), + () -> newTestAdminService().getCatalog(CATALOG_NAME), + null, // cleanupAction + (privilege) -> + adminService.grantPrivilegeOnRootContainerToPrincipalRole(PRINCIPAL_ROLE1, privilege), + (privilege) -> + adminService.revokePrivilegeOnRootContainerFromPrincipalRole( + PRINCIPAL_ROLE1, privilege)); + } + + @Test + public void testGetCatalogInsufficientPrivileges() { + doTestInsufficientPrivileges( + List.of( + PolarisPrivilege.NAMESPACE_FULL_METADATA, + PolarisPrivilege.TABLE_FULL_METADATA, + PolarisPrivilege.VIEW_FULL_METADATA, + PolarisPrivilege.VIEW_FULL_METADATA, + PolarisPrivilege.PRINCIPAL_FULL_METADATA, + PolarisPrivilege.PRINCIPAL_ROLE_FULL_METADATA, + PolarisPrivilege.CATALOG_LIST, + PolarisPrivilege.CATALOG_CREATE, + PolarisPrivilege.CATALOG_DROP, + PolarisPrivilege.CATALOG_ROLE_FULL_METADATA, + PolarisPrivilege.CATALOG_MANAGE_ACCESS), + () -> newTestAdminService().getCatalog(CATALOG_NAME), + (privilege) -> + adminService.grantPrivilegeOnRootContainerToPrincipalRole(PRINCIPAL_ROLE1, privilege), + (privilege) -> + adminService.revokePrivilegeOnRootContainerFromPrincipalRole( + PRINCIPAL_ROLE1, privilege)); + } + + @Test + public void testUpdateCatalogSufficientPrivileges() { + doTestSufficientPrivileges( + List.of( + PolarisPrivilege.SERVICE_MANAGE_ACCESS, + PolarisPrivilege.CATALOG_WRITE_PROPERTIES, + PolarisPrivilege.CATALOG_FULL_METADATA, + PolarisPrivilege.CATALOG_MANAGE_METADATA, + PolarisPrivilege.CATALOG_MANAGE_CONTENT), + () -> { + // Use the test-permission admin service instead of the root adminService to also + // perform the initial GET to illustrate that the actual user workflow for update + // *must* also encompass GET privileges to be able to set entityVersion properly. + UpdateCatalogRequest updateRequest = + UpdateCatalogRequest.builder() + .setCurrentEntityVersion( + newTestAdminService().getCatalog(CATALOG_NAME).getEntityVersion()) + .setProperties(Map.of("foo", Long.toString(System.currentTimeMillis()))) + .build(); + newTestAdminService().updateCatalog(CATALOG_NAME, updateRequest); + }, + null, // cleanupAction + (privilege) -> + adminService.grantPrivilegeOnRootContainerToPrincipalRole(PRINCIPAL_ROLE1, privilege), + (privilege) -> + adminService.revokePrivilegeOnRootContainerFromPrincipalRole( + PRINCIPAL_ROLE1, privilege)); + } + + @Test + public void testUpdateCatalogInsufficientPrivileges() { + doTestInsufficientPrivileges( + List.of( + PolarisPrivilege.NAMESPACE_FULL_METADATA, + PolarisPrivilege.TABLE_FULL_METADATA, + PolarisPrivilege.VIEW_FULL_METADATA, + PolarisPrivilege.VIEW_FULL_METADATA, + PolarisPrivilege.PRINCIPAL_FULL_METADATA, + PolarisPrivilege.PRINCIPAL_ROLE_FULL_METADATA, + PolarisPrivilege.CATALOG_READ_PROPERTIES, + PolarisPrivilege.CATALOG_LIST, + PolarisPrivilege.CATALOG_CREATE, + PolarisPrivilege.CATALOG_DROP, + PolarisPrivilege.CATALOG_ROLE_FULL_METADATA, + PolarisPrivilege.CATALOG_MANAGE_ACCESS), + () -> { + UpdateCatalogRequest updateRequest = + UpdateCatalogRequest.builder() + .setCurrentEntityVersion( + newTestAdminService().getCatalog(CATALOG_NAME).getEntityVersion()) + .setProperties(Map.of("foo", Long.toString(System.currentTimeMillis()))) + .build(); + newTestAdminService().updateCatalog(CATALOG_NAME, updateRequest); + }, + (privilege) -> + adminService.grantPrivilegeOnRootContainerToPrincipalRole(PRINCIPAL_ROLE1, privilege), + (privilege) -> + adminService.revokePrivilegeOnRootContainerFromPrincipalRole( + PRINCIPAL_ROLE1, privilege)); + } + + @Test + public void testDeleteCatalogSufficientPrivileges() { + // Cleanup with PRINCIPAL_ROLE2 + Assertions.assertThat( + adminService.grantPrivilegeOnRootContainerToPrincipalRole( + PRINCIPAL_ROLE2, PolarisPrivilege.CATALOG_CREATE)) + .isTrue(); + final CatalogEntity newCatalog = new CatalogEntity.Builder().setName("new_catalog").build(); + adminService.createCatalog(newCatalog); + + doTestSufficientPrivileges( + List.of( + PolarisPrivilege.SERVICE_MANAGE_ACCESS, + PolarisPrivilege.CATALOG_DROP, + PolarisPrivilege.CATALOG_FULL_METADATA), + () -> newTestAdminService(Set.of(PRINCIPAL_ROLE1)).deleteCatalog(newCatalog.getName()), + () -> newTestAdminService(Set.of(PRINCIPAL_ROLE2)).createCatalog(newCatalog), + (privilege) -> + adminService.grantPrivilegeOnRootContainerToPrincipalRole(PRINCIPAL_ROLE1, privilege), + (privilege) -> + adminService.revokePrivilegeOnRootContainerFromPrincipalRole( + PRINCIPAL_ROLE1, privilege)); + } + + @Test + public void testDeleteCatalogInsufficientPrivileges() { + final CatalogEntity newCatalog = new CatalogEntity.Builder().setName("new_catalog").build(); + adminService.createCatalog(newCatalog); + + doTestInsufficientPrivileges( + List.of( + PolarisPrivilege.NAMESPACE_FULL_METADATA, + PolarisPrivilege.TABLE_FULL_METADATA, + PolarisPrivilege.VIEW_FULL_METADATA, + PolarisPrivilege.PRINCIPAL_FULL_METADATA, + PolarisPrivilege.PRINCIPAL_ROLE_FULL_METADATA, + PolarisPrivilege.CATALOG_ROLE_FULL_METADATA, + PolarisPrivilege.CATALOG_CREATE, + PolarisPrivilege.CATALOG_LIST, + PolarisPrivilege.CATALOG_READ_PROPERTIES, + PolarisPrivilege.CATALOG_WRITE_PROPERTIES, + PolarisPrivilege.CATALOG_MANAGE_METADATA, + PolarisPrivilege.CATALOG_MANAGE_CONTENT, + PolarisPrivilege.CATALOG_MANAGE_ACCESS), + () -> newTestAdminService(Set.of(PRINCIPAL_ROLE1)).deleteCatalog(newCatalog.getName()), + (privilege) -> + adminService.grantPrivilegeOnRootContainerToPrincipalRole(PRINCIPAL_ROLE1, privilege), + (privilege) -> + adminService.revokePrivilegeOnRootContainerFromPrincipalRole( + PRINCIPAL_ROLE1, privilege)); + } + + @Test + public void testListPrincipalsSufficientPrivileges() { + doTestSufficientPrivileges( + List.of( + PolarisPrivilege.SERVICE_MANAGE_ACCESS, + PolarisPrivilege.PRINCIPAL_LIST, + PolarisPrivilege.PRINCIPAL_READ_PROPERTIES, + PolarisPrivilege.PRINCIPAL_WRITE_PROPERTIES, + PolarisPrivilege.PRINCIPAL_CREATE, + PolarisPrivilege.PRINCIPAL_FULL_METADATA), + () -> newTestAdminService().listPrincipals(), + null, // cleanupAction + (privilege) -> + adminService.grantPrivilegeOnRootContainerToPrincipalRole(PRINCIPAL_ROLE1, privilege), + (privilege) -> + adminService.revokePrivilegeOnRootContainerFromPrincipalRole( + PRINCIPAL_ROLE1, privilege)); + } + + @Test + public void testListPrincipalsInsufficientPrivileges() { + doTestInsufficientPrivileges( + List.of( + PolarisPrivilege.NAMESPACE_FULL_METADATA, + PolarisPrivilege.TABLE_FULL_METADATA, + PolarisPrivilege.VIEW_FULL_METADATA, + PolarisPrivilege.VIEW_FULL_METADATA, + PolarisPrivilege.CATALOG_FULL_METADATA, + PolarisPrivilege.PRINCIPAL_ROLE_FULL_METADATA, + PolarisPrivilege.PRINCIPAL_DROP, + PolarisPrivilege.CATALOG_ROLE_FULL_METADATA), + () -> newTestAdminService().listPrincipals(), + (privilege) -> + adminService.grantPrivilegeOnRootContainerToPrincipalRole(PRINCIPAL_ROLE1, privilege), + (privilege) -> + adminService.revokePrivilegeOnRootContainerFromPrincipalRole( + PRINCIPAL_ROLE1, privilege)); + } + + @Test + public void testCreatePrincipalSufficientPrivileges() { + // Cleanup with PRINCIPAL_ROLE2 + Assertions.assertThat( + adminService.grantPrivilegeOnRootContainerToPrincipalRole( + PRINCIPAL_ROLE2, PolarisPrivilege.PRINCIPAL_DROP)) + .isTrue(); + final PrincipalEntity newPrincipal = + new PrincipalEntity.Builder().setName("new_principal").build(); + + doTestSufficientPrivileges( + List.of( + PolarisPrivilege.SERVICE_MANAGE_ACCESS, + PolarisPrivilege.PRINCIPAL_CREATE, + PolarisPrivilege.PRINCIPAL_FULL_METADATA), + () -> newTestAdminService(Set.of(PRINCIPAL_ROLE1)).createPrincipal(newPrincipal), + () -> newTestAdminService(Set.of(PRINCIPAL_ROLE2)).deletePrincipal(newPrincipal.getName()), + (privilege) -> + adminService.grantPrivilegeOnRootContainerToPrincipalRole(PRINCIPAL_ROLE1, privilege), + (privilege) -> + adminService.revokePrivilegeOnRootContainerFromPrincipalRole( + PRINCIPAL_ROLE1, privilege)); + } + + @Test + public void testCreatePrincipalInsufficientPrivileges() { + final PrincipalEntity newPrincipal = + new PrincipalEntity.Builder().setName("new_principal").build(); + + doTestInsufficientPrivileges( + List.of( + PolarisPrivilege.NAMESPACE_FULL_METADATA, + PolarisPrivilege.TABLE_FULL_METADATA, + PolarisPrivilege.VIEW_FULL_METADATA, + PolarisPrivilege.CATALOG_FULL_METADATA, + PolarisPrivilege.PRINCIPAL_ROLE_FULL_METADATA, + PolarisPrivilege.CATALOG_ROLE_FULL_METADATA, + PolarisPrivilege.PRINCIPAL_LIST, + PolarisPrivilege.PRINCIPAL_DROP, + PolarisPrivilege.PRINCIPAL_READ_PROPERTIES, + PolarisPrivilege.PRINCIPAL_WRITE_PROPERTIES), + () -> newTestAdminService(Set.of(PRINCIPAL_ROLE1)).createPrincipal(newPrincipal), + (privilege) -> + adminService.grantPrivilegeOnRootContainerToPrincipalRole(PRINCIPAL_ROLE1, privilege), + (privilege) -> + adminService.revokePrivilegeOnRootContainerFromPrincipalRole( + PRINCIPAL_ROLE1, privilege)); + } + + @Test + public void testGetPrincipalSufficientPrivileges() { + doTestSufficientPrivileges( + List.of( + PolarisPrivilege.SERVICE_MANAGE_ACCESS, + PolarisPrivilege.PRINCIPAL_READ_PROPERTIES, + PolarisPrivilege.PRINCIPAL_WRITE_PROPERTIES, + PolarisPrivilege.PRINCIPAL_FULL_METADATA), + () -> newTestAdminService().getPrincipal(PRINCIPAL_NAME), + null, // cleanupAction + (privilege) -> + adminService.grantPrivilegeOnRootContainerToPrincipalRole(PRINCIPAL_ROLE1, privilege), + (privilege) -> + adminService.revokePrivilegeOnRootContainerFromPrincipalRole( + PRINCIPAL_ROLE1, privilege)); + } + + @Test + public void testGetPrincipalInsufficientPrivileges() { + doTestInsufficientPrivileges( + List.of( + PolarisPrivilege.NAMESPACE_FULL_METADATA, + PolarisPrivilege.TABLE_FULL_METADATA, + PolarisPrivilege.VIEW_FULL_METADATA, + PolarisPrivilege.VIEW_FULL_METADATA, + PolarisPrivilege.CATALOG_FULL_METADATA, + PolarisPrivilege.PRINCIPAL_ROLE_FULL_METADATA, + PolarisPrivilege.PRINCIPAL_LIST, + PolarisPrivilege.PRINCIPAL_CREATE, + PolarisPrivilege.PRINCIPAL_DROP, + PolarisPrivilege.CATALOG_ROLE_FULL_METADATA), + () -> newTestAdminService().getPrincipal(PRINCIPAL_NAME), + (privilege) -> + adminService.grantPrivilegeOnRootContainerToPrincipalRole(PRINCIPAL_ROLE1, privilege), + (privilege) -> + adminService.revokePrivilegeOnRootContainerFromPrincipalRole( + PRINCIPAL_ROLE1, privilege)); + } + + @Test + public void testUpdatePrincipalSufficientPrivileges() { + doTestSufficientPrivileges( + List.of( + PolarisPrivilege.SERVICE_MANAGE_ACCESS, + PolarisPrivilege.PRINCIPAL_WRITE_PROPERTIES, + PolarisPrivilege.PRINCIPAL_FULL_METADATA), + () -> { + // Use the test-permission admin service instead of the root adminService to also + // perform the initial GET to illustrate that the actual user workflow for update + // *must* also encompass GET privileges to be able to set entityVersion properly. + UpdatePrincipalRequest updateRequest = + UpdatePrincipalRequest.builder() + .setCurrentEntityVersion( + newTestAdminService().getPrincipal(PRINCIPAL_NAME).getEntityVersion()) + .setProperties(Map.of("foo", Long.toString(System.currentTimeMillis()))) + .build(); + newTestAdminService().updatePrincipal(PRINCIPAL_NAME, updateRequest); + }, + null, // cleanupAction + (privilege) -> + adminService.grantPrivilegeOnRootContainerToPrincipalRole(PRINCIPAL_ROLE1, privilege), + (privilege) -> + adminService.revokePrivilegeOnRootContainerFromPrincipalRole( + PRINCIPAL_ROLE1, privilege)); + } + + @Test + public void testUpdatePrincipalInsufficientPrivileges() { + doTestInsufficientPrivileges( + List.of( + PolarisPrivilege.NAMESPACE_FULL_METADATA, + PolarisPrivilege.TABLE_FULL_METADATA, + PolarisPrivilege.VIEW_FULL_METADATA, + PolarisPrivilege.VIEW_FULL_METADATA, + PolarisPrivilege.CATALOG_FULL_METADATA, + PolarisPrivilege.PRINCIPAL_ROLE_FULL_METADATA, + PolarisPrivilege.PRINCIPAL_READ_PROPERTIES, + PolarisPrivilege.PRINCIPAL_LIST, + PolarisPrivilege.PRINCIPAL_CREATE, + PolarisPrivilege.PRINCIPAL_DROP, + PolarisPrivilege.CATALOG_ROLE_FULL_METADATA), + () -> { + UpdatePrincipalRequest updateRequest = + UpdatePrincipalRequest.builder() + .setCurrentEntityVersion( + newTestAdminService().getPrincipal(PRINCIPAL_NAME).getEntityVersion()) + .setProperties(Map.of("foo", Long.toString(System.currentTimeMillis()))) + .build(); + newTestAdminService().updatePrincipal(PRINCIPAL_NAME, updateRequest); + }, + (privilege) -> + adminService.grantPrivilegeOnRootContainerToPrincipalRole(PRINCIPAL_ROLE1, privilege), + (privilege) -> + adminService.revokePrivilegeOnRootContainerFromPrincipalRole( + PRINCIPAL_ROLE1, privilege)); + } + + @Test + public void testDeletePrincipalSufficientPrivileges() { + // Cleanup with PRINCIPAL_ROLE2 + Assertions.assertThat( + adminService.grantPrivilegeOnRootContainerToPrincipalRole( + PRINCIPAL_ROLE2, PolarisPrivilege.PRINCIPAL_CREATE)) + .isTrue(); + final PrincipalEntity newPrincipal = + new PrincipalEntity.Builder().setName("new_principal").build(); + adminService.createPrincipal(newPrincipal); + + doTestSufficientPrivileges( + List.of( + PolarisPrivilege.SERVICE_MANAGE_ACCESS, + PolarisPrivilege.PRINCIPAL_DROP, + PolarisPrivilege.PRINCIPAL_FULL_METADATA), + () -> newTestAdminService(Set.of(PRINCIPAL_ROLE1)).deletePrincipal(newPrincipal.getName()), + () -> newTestAdminService(Set.of(PRINCIPAL_ROLE2)).createPrincipal(newPrincipal), + (privilege) -> + adminService.grantPrivilegeOnRootContainerToPrincipalRole(PRINCIPAL_ROLE1, privilege), + (privilege) -> + adminService.revokePrivilegeOnRootContainerFromPrincipalRole( + PRINCIPAL_ROLE1, privilege)); + } + + @Test + public void testDeletePrincipalInsufficientPrivileges() { + final PrincipalEntity newPrincipal = + new PrincipalEntity.Builder().setName("new_principal").build(); + adminService.createPrincipal(newPrincipal); + + doTestInsufficientPrivileges( + List.of( + PolarisPrivilege.NAMESPACE_FULL_METADATA, + PolarisPrivilege.TABLE_FULL_METADATA, + PolarisPrivilege.VIEW_FULL_METADATA, + PolarisPrivilege.CATALOG_FULL_METADATA, + PolarisPrivilege.PRINCIPAL_ROLE_FULL_METADATA, + PolarisPrivilege.CATALOG_ROLE_FULL_METADATA, + PolarisPrivilege.PRINCIPAL_CREATE, + PolarisPrivilege.PRINCIPAL_LIST, + PolarisPrivilege.PRINCIPAL_READ_PROPERTIES, + PolarisPrivilege.PRINCIPAL_WRITE_PROPERTIES), + () -> newTestAdminService(Set.of(PRINCIPAL_ROLE1)).deletePrincipal(newPrincipal.getName()), + (privilege) -> + adminService.grantPrivilegeOnRootContainerToPrincipalRole(PRINCIPAL_ROLE1, privilege), + (privilege) -> + adminService.revokePrivilegeOnRootContainerFromPrincipalRole( + PRINCIPAL_ROLE1, privilege)); + } + + @Test + public void testListPrincipalRolesSufficientPrivileges() { + doTestSufficientPrivileges( + List.of( + PolarisPrivilege.SERVICE_MANAGE_ACCESS, + PolarisPrivilege.PRINCIPAL_ROLE_LIST, + PolarisPrivilege.PRINCIPAL_ROLE_READ_PROPERTIES, + PolarisPrivilege.PRINCIPAL_ROLE_WRITE_PROPERTIES, + PolarisPrivilege.PRINCIPAL_ROLE_CREATE, + PolarisPrivilege.PRINCIPAL_ROLE_FULL_METADATA), + () -> newTestAdminService().listPrincipalRoles(), + null, // cleanupAction + (privilege) -> + adminService.grantPrivilegeOnRootContainerToPrincipalRole(PRINCIPAL_ROLE1, privilege), + (privilege) -> + adminService.revokePrivilegeOnRootContainerFromPrincipalRole( + PRINCIPAL_ROLE1, privilege)); + } + + @Test + public void testListPrincipalRolesInsufficientPrivileges() { + doTestInsufficientPrivileges( + List.of( + PolarisPrivilege.NAMESPACE_FULL_METADATA, + PolarisPrivilege.TABLE_FULL_METADATA, + PolarisPrivilege.VIEW_FULL_METADATA, + PolarisPrivilege.VIEW_FULL_METADATA, + PolarisPrivilege.CATALOG_FULL_METADATA, + PolarisPrivilege.PRINCIPAL_FULL_METADATA, + PolarisPrivilege.PRINCIPAL_ROLE_DROP, + PolarisPrivilege.CATALOG_ROLE_FULL_METADATA), + () -> newTestAdminService().listPrincipalRoles(), + (privilege) -> + adminService.grantPrivilegeOnRootContainerToPrincipalRole(PRINCIPAL_ROLE1, privilege), + (privilege) -> + adminService.revokePrivilegeOnRootContainerFromPrincipalRole( + PRINCIPAL_ROLE1, privilege)); + } + + @Test + public void testCreatePrincipalRoleSufficientPrivileges() { + // Cleanup with PRINCIPAL_ROLE2 + Assertions.assertThat( + adminService.grantPrivilegeOnRootContainerToPrincipalRole( + PRINCIPAL_ROLE2, PolarisPrivilege.PRINCIPAL_ROLE_DROP)) + .isTrue(); + final PrincipalRoleEntity newPrincipalRole = + new PrincipalRoleEntity.Builder().setName("new_principal_role").build(); + + doTestSufficientPrivileges( + List.of( + PolarisPrivilege.SERVICE_MANAGE_ACCESS, + PolarisPrivilege.PRINCIPAL_ROLE_CREATE, + PolarisPrivilege.PRINCIPAL_ROLE_FULL_METADATA), + () -> newTestAdminService(Set.of(PRINCIPAL_ROLE1)).createPrincipalRole(newPrincipalRole), + () -> + newTestAdminService(Set.of(PRINCIPAL_ROLE2)) + .deletePrincipalRole(newPrincipalRole.getName()), + (privilege) -> + adminService.grantPrivilegeOnRootContainerToPrincipalRole(PRINCIPAL_ROLE1, privilege), + (privilege) -> + adminService.revokePrivilegeOnRootContainerFromPrincipalRole( + PRINCIPAL_ROLE1, privilege)); + } + + @Test + public void testCreatePrincipalRoleInsufficientPrivileges() { + final PrincipalRoleEntity newPrincipalRole = + new PrincipalRoleEntity.Builder().setName("new_principal_role").build(); + + doTestInsufficientPrivileges( + List.of( + PolarisPrivilege.NAMESPACE_FULL_METADATA, + PolarisPrivilege.TABLE_FULL_METADATA, + PolarisPrivilege.VIEW_FULL_METADATA, + PolarisPrivilege.CATALOG_FULL_METADATA, + PolarisPrivilege.PRINCIPAL_FULL_METADATA, + PolarisPrivilege.CATALOG_ROLE_FULL_METADATA, + PolarisPrivilege.PRINCIPAL_ROLE_LIST, + PolarisPrivilege.PRINCIPAL_ROLE_DROP, + PolarisPrivilege.PRINCIPAL_ROLE_READ_PROPERTIES, + PolarisPrivilege.PRINCIPAL_ROLE_WRITE_PROPERTIES), + () -> newTestAdminService(Set.of(PRINCIPAL_ROLE1)).createPrincipalRole(newPrincipalRole), + (privilege) -> + adminService.grantPrivilegeOnRootContainerToPrincipalRole(PRINCIPAL_ROLE1, privilege), + (privilege) -> + adminService.revokePrivilegeOnRootContainerFromPrincipalRole( + PRINCIPAL_ROLE1, privilege)); + } + + @Test + public void testGetPrincipalRoleSufficientPrivileges() { + doTestSufficientPrivileges( + List.of( + PolarisPrivilege.SERVICE_MANAGE_ACCESS, + PolarisPrivilege.PRINCIPAL_ROLE_READ_PROPERTIES, + PolarisPrivilege.PRINCIPAL_ROLE_WRITE_PROPERTIES, + PolarisPrivilege.PRINCIPAL_ROLE_FULL_METADATA), + () -> newTestAdminService().getPrincipalRole(PRINCIPAL_ROLE2), + null, // cleanupAction + (privilege) -> + adminService.grantPrivilegeOnRootContainerToPrincipalRole(PRINCIPAL_ROLE1, privilege), + (privilege) -> + adminService.revokePrivilegeOnRootContainerFromPrincipalRole( + PRINCIPAL_ROLE1, privilege)); + } + + @Test + public void testGetPrincipalRoleInsufficientPrivileges() { + doTestInsufficientPrivileges( + List.of( + PolarisPrivilege.NAMESPACE_FULL_METADATA, + PolarisPrivilege.TABLE_FULL_METADATA, + PolarisPrivilege.VIEW_FULL_METADATA, + PolarisPrivilege.VIEW_FULL_METADATA, + PolarisPrivilege.CATALOG_FULL_METADATA, + PolarisPrivilege.PRINCIPAL_FULL_METADATA, + PolarisPrivilege.PRINCIPAL_ROLE_LIST, + PolarisPrivilege.PRINCIPAL_ROLE_CREATE, + PolarisPrivilege.PRINCIPAL_ROLE_DROP, + PolarisPrivilege.CATALOG_ROLE_FULL_METADATA), + () -> newTestAdminService().getPrincipalRole(PRINCIPAL_ROLE2), + (privilege) -> + adminService.grantPrivilegeOnRootContainerToPrincipalRole(PRINCIPAL_ROLE1, privilege), + (privilege) -> + adminService.revokePrivilegeOnRootContainerFromPrincipalRole( + PRINCIPAL_ROLE1, privilege)); + } + + @Test + public void testUpdatePrincipalRoleSufficientPrivileges() { + doTestSufficientPrivileges( + List.of( + PolarisPrivilege.SERVICE_MANAGE_ACCESS, + PolarisPrivilege.PRINCIPAL_ROLE_WRITE_PROPERTIES, + PolarisPrivilege.PRINCIPAL_ROLE_FULL_METADATA), + () -> { + // Use the test-permission admin service instead of the root adminService to also + // perform the initial GET to illustrate that the actual user workflow for update + // *must* also encompass GET privileges to be able to set entityVersion properly. + UpdatePrincipalRoleRequest updateRequest = + UpdatePrincipalRoleRequest.builder() + .setCurrentEntityVersion( + newTestAdminService().getPrincipalRole(PRINCIPAL_ROLE2).getEntityVersion()) + .setProperties(Map.of("foo", Long.toString(System.currentTimeMillis()))) + .build(); + newTestAdminService().updatePrincipalRole(PRINCIPAL_ROLE2, updateRequest); + }, + null, // cleanupAction + (privilege) -> + adminService.grantPrivilegeOnRootContainerToPrincipalRole(PRINCIPAL_ROLE1, privilege), + (privilege) -> + adminService.revokePrivilegeOnRootContainerFromPrincipalRole( + PRINCIPAL_ROLE1, privilege)); + } + + @Test + public void testUpdatePrincipalRoleInsufficientPrivileges() { + doTestInsufficientPrivileges( + List.of( + PolarisPrivilege.NAMESPACE_FULL_METADATA, + PolarisPrivilege.TABLE_FULL_METADATA, + PolarisPrivilege.VIEW_FULL_METADATA, + PolarisPrivilege.VIEW_FULL_METADATA, + PolarisPrivilege.CATALOG_FULL_METADATA, + PolarisPrivilege.PRINCIPAL_FULL_METADATA, + PolarisPrivilege.PRINCIPAL_ROLE_READ_PROPERTIES, + PolarisPrivilege.PRINCIPAL_ROLE_LIST, + PolarisPrivilege.PRINCIPAL_ROLE_CREATE, + PolarisPrivilege.PRINCIPAL_ROLE_DROP, + PolarisPrivilege.CATALOG_ROLE_FULL_METADATA), + () -> { + UpdatePrincipalRoleRequest updateRequest = + UpdatePrincipalRoleRequest.builder() + .setCurrentEntityVersion( + newTestAdminService().getPrincipalRole(PRINCIPAL_ROLE2).getEntityVersion()) + .setProperties(Map.of("foo", Long.toString(System.currentTimeMillis()))) + .build(); + newTestAdminService().updatePrincipalRole(PRINCIPAL_ROLE2, updateRequest); + }, + (privilege) -> + adminService.grantPrivilegeOnRootContainerToPrincipalRole(PRINCIPAL_ROLE1, privilege), + (privilege) -> + adminService.revokePrivilegeOnRootContainerFromPrincipalRole( + PRINCIPAL_ROLE1, privilege)); + } + + @Test + public void testDeletePrincipalRoleSufficientPrivileges() { + // Cleanup with PRINCIPAL_ROLE2 + Assertions.assertThat( + adminService.grantPrivilegeOnRootContainerToPrincipalRole( + PRINCIPAL_ROLE2, PolarisPrivilege.PRINCIPAL_ROLE_CREATE)) + .isTrue(); + final PrincipalRoleEntity newPrincipalRole = + new PrincipalRoleEntity.Builder().setName("new_principal_role").build(); + adminService.createPrincipalRole(newPrincipalRole); + + doTestSufficientPrivileges( + List.of( + PolarisPrivilege.SERVICE_MANAGE_ACCESS, + PolarisPrivilege.PRINCIPAL_ROLE_DROP, + PolarisPrivilege.PRINCIPAL_ROLE_FULL_METADATA), + () -> + newTestAdminService(Set.of(PRINCIPAL_ROLE1)) + .deletePrincipalRole(newPrincipalRole.getName()), + () -> newTestAdminService(Set.of(PRINCIPAL_ROLE2)).createPrincipalRole(newPrincipalRole), + (privilege) -> + adminService.grantPrivilegeOnRootContainerToPrincipalRole(PRINCIPAL_ROLE1, privilege), + (privilege) -> + adminService.revokePrivilegeOnRootContainerFromPrincipalRole( + PRINCIPAL_ROLE1, privilege)); + } + + @Test + public void testDeletePrincipalRoleInsufficientPrivileges() { + final PrincipalRoleEntity newPrincipalRole = + new PrincipalRoleEntity.Builder().setName("new_principal_role").build(); + adminService.createPrincipalRole(newPrincipalRole); + + doTestInsufficientPrivileges( + List.of( + PolarisPrivilege.NAMESPACE_FULL_METADATA, + PolarisPrivilege.TABLE_FULL_METADATA, + PolarisPrivilege.VIEW_FULL_METADATA, + PolarisPrivilege.CATALOG_FULL_METADATA, + PolarisPrivilege.PRINCIPAL_FULL_METADATA, + PolarisPrivilege.CATALOG_ROLE_FULL_METADATA, + PolarisPrivilege.PRINCIPAL_ROLE_CREATE, + PolarisPrivilege.PRINCIPAL_ROLE_LIST, + PolarisPrivilege.PRINCIPAL_ROLE_READ_PROPERTIES, + PolarisPrivilege.PRINCIPAL_ROLE_WRITE_PROPERTIES), + () -> + newTestAdminService(Set.of(PRINCIPAL_ROLE1)) + .deletePrincipalRole(newPrincipalRole.getName()), + (privilege) -> + adminService.grantPrivilegeOnRootContainerToPrincipalRole(PRINCIPAL_ROLE1, privilege), + (privilege) -> + adminService.revokePrivilegeOnRootContainerFromPrincipalRole( + PRINCIPAL_ROLE1, privilege)); + } + + @Test + public void testListCatalogRolesSufficientPrivileges() { + doTestSufficientPrivileges( + List.of( + PolarisPrivilege.CATALOG_MANAGE_ACCESS, + PolarisPrivilege.CATALOG_ROLE_LIST, + PolarisPrivilege.CATALOG_ROLE_READ_PROPERTIES, + PolarisPrivilege.CATALOG_ROLE_WRITE_PROPERTIES, + PolarisPrivilege.CATALOG_ROLE_CREATE, + PolarisPrivilege.CATALOG_ROLE_FULL_METADATA), + () -> newTestAdminService().listCatalogRoles(CATALOG_NAME), + null, // cleanupAction + (privilege) -> + adminService.grantPrivilegeOnCatalogToRole(CATALOG_NAME, CATALOG_ROLE1, privilege), + (privilege) -> + adminService.revokePrivilegeOnCatalogFromRole(CATALOG_NAME, CATALOG_ROLE1, privilege)); + } + + @Test + public void testListCatalogRolesInsufficientPrivileges() { + doTestInsufficientPrivileges( + List.of( + PolarisPrivilege.SERVICE_MANAGE_ACCESS, + PolarisPrivilege.NAMESPACE_FULL_METADATA, + PolarisPrivilege.TABLE_FULL_METADATA, + PolarisPrivilege.VIEW_FULL_METADATA, + PolarisPrivilege.VIEW_FULL_METADATA, + PolarisPrivilege.CATALOG_FULL_METADATA, + PolarisPrivilege.PRINCIPAL_FULL_METADATA, + PolarisPrivilege.CATALOG_ROLE_DROP, + PolarisPrivilege.PRINCIPAL_ROLE_FULL_METADATA), + () -> newTestAdminService().listCatalogRoles(CATALOG_NAME), + (privilege) -> + adminService.grantPrivilegeOnCatalogToRole(CATALOG_NAME, CATALOG_ROLE1, privilege), + (privilege) -> + adminService.revokePrivilegeOnCatalogFromRole(CATALOG_NAME, CATALOG_ROLE1, privilege)); + } + + @Test + public void testCreateCatalogRoleSufficientPrivileges() { + // Cleanup with CATALOG_ROLE2 + Assertions.assertThat( + adminService.grantPrivilegeOnCatalogToRole( + CATALOG_NAME, CATALOG_ROLE2, PolarisPrivilege.CATALOG_ROLE_DROP)) + .isTrue(); + final CatalogRoleEntity newCatalogRole = + new CatalogRoleEntity.Builder().setName("new_catalog_role").build(); + + doTestSufficientPrivileges( + List.of( + PolarisPrivilege.CATALOG_MANAGE_ACCESS, + PolarisPrivilege.CATALOG_ROLE_CREATE, + PolarisPrivilege.CATALOG_ROLE_FULL_METADATA), + () -> + newTestAdminService(Set.of(PRINCIPAL_ROLE1)) + .createCatalogRole(CATALOG_NAME, newCatalogRole), + () -> + newTestAdminService(Set.of(PRINCIPAL_ROLE2)) + .deleteCatalogRole(CATALOG_NAME, newCatalogRole.getName()), + (privilege) -> + adminService.grantPrivilegeOnCatalogToRole(CATALOG_NAME, CATALOG_ROLE1, privilege), + (privilege) -> + adminService.revokePrivilegeOnCatalogFromRole(CATALOG_NAME, CATALOG_ROLE1, privilege)); + } + + @Test + public void testCreateCatalogRoleInsufficientPrivileges() { + final CatalogRoleEntity newCatalogRole = + new CatalogRoleEntity.Builder().setName("new_catalog_role").build(); + + doTestInsufficientPrivileges( + List.of( + PolarisPrivilege.SERVICE_MANAGE_ACCESS, + PolarisPrivilege.NAMESPACE_FULL_METADATA, + PolarisPrivilege.TABLE_FULL_METADATA, + PolarisPrivilege.VIEW_FULL_METADATA, + PolarisPrivilege.CATALOG_FULL_METADATA, + PolarisPrivilege.PRINCIPAL_FULL_METADATA, + PolarisPrivilege.PRINCIPAL_ROLE_FULL_METADATA, + PolarisPrivilege.CATALOG_ROLE_LIST, + PolarisPrivilege.CATALOG_ROLE_DROP, + PolarisPrivilege.CATALOG_ROLE_READ_PROPERTIES, + PolarisPrivilege.CATALOG_ROLE_WRITE_PROPERTIES), + () -> + newTestAdminService(Set.of(PRINCIPAL_ROLE1)) + .createCatalogRole(CATALOG_NAME, newCatalogRole), + (privilege) -> + adminService.grantPrivilegeOnCatalogToRole(CATALOG_NAME, CATALOG_ROLE1, privilege), + (privilege) -> + adminService.revokePrivilegeOnCatalogFromRole(CATALOG_NAME, CATALOG_ROLE1, privilege)); + } + + @Test + public void testGetCatalogRoleSufficientPrivileges() { + doTestSufficientPrivileges( + List.of( + PolarisPrivilege.CATALOG_MANAGE_ACCESS, + PolarisPrivilege.CATALOG_ROLE_READ_PROPERTIES, + PolarisPrivilege.CATALOG_ROLE_WRITE_PROPERTIES, + PolarisPrivilege.CATALOG_ROLE_FULL_METADATA), + () -> newTestAdminService().getCatalogRole(CATALOG_NAME, CATALOG_ROLE2), + null, // cleanupAction + (privilege) -> + adminService.grantPrivilegeOnCatalogToRole(CATALOG_NAME, CATALOG_ROLE1, privilege), + (privilege) -> + adminService.revokePrivilegeOnCatalogFromRole(CATALOG_NAME, CATALOG_ROLE1, privilege)); + } + + @Test + public void testGetCatalogRoleInsufficientPrivileges() { + doTestInsufficientPrivileges( + List.of( + PolarisPrivilege.SERVICE_MANAGE_ACCESS, + PolarisPrivilege.NAMESPACE_FULL_METADATA, + PolarisPrivilege.TABLE_FULL_METADATA, + PolarisPrivilege.VIEW_FULL_METADATA, + PolarisPrivilege.VIEW_FULL_METADATA, + PolarisPrivilege.CATALOG_FULL_METADATA, + PolarisPrivilege.PRINCIPAL_FULL_METADATA, + PolarisPrivilege.CATALOG_ROLE_LIST, + PolarisPrivilege.CATALOG_ROLE_CREATE, + PolarisPrivilege.CATALOG_ROLE_DROP, + PolarisPrivilege.PRINCIPAL_ROLE_FULL_METADATA), + () -> newTestAdminService().getCatalogRole(CATALOG_NAME, CATALOG_ROLE2), + (privilege) -> + adminService.grantPrivilegeOnCatalogToRole(CATALOG_NAME, CATALOG_ROLE1, privilege), + (privilege) -> + adminService.revokePrivilegeOnCatalogFromRole(CATALOG_NAME, CATALOG_ROLE1, privilege)); + } + + @Test + public void testUpdateCatalogRoleSufficientPrivileges() { + doTestSufficientPrivileges( + List.of( + PolarisPrivilege.CATALOG_MANAGE_ACCESS, + PolarisPrivilege.CATALOG_ROLE_WRITE_PROPERTIES, + PolarisPrivilege.CATALOG_ROLE_FULL_METADATA), + () -> { + // Use the test-permission admin service instead of the root adminService to also + // perform the initial GET to illustrate that the actual user workflow for update + // *must* also encompass GET privileges to be able to set entityVersion properly. + UpdateCatalogRoleRequest updateRequest = + UpdateCatalogRoleRequest.builder() + .setCurrentEntityVersion( + newTestAdminService() + .getCatalogRole(CATALOG_NAME, CATALOG_ROLE2) + .getEntityVersion()) + .setProperties(Map.of("foo", Long.toString(System.currentTimeMillis()))) + .build(); + newTestAdminService().updateCatalogRole(CATALOG_NAME, CATALOG_ROLE2, updateRequest); + }, + null, // cleanupAction + (privilege) -> + adminService.grantPrivilegeOnCatalogToRole(CATALOG_NAME, CATALOG_ROLE1, privilege), + (privilege) -> + adminService.revokePrivilegeOnCatalogFromRole(CATALOG_NAME, CATALOG_ROLE1, privilege)); + } + + @Test + public void testUpdateCatalogRoleInsufficientPrivileges() { + doTestInsufficientPrivileges( + List.of( + PolarisPrivilege.SERVICE_MANAGE_ACCESS, + PolarisPrivilege.NAMESPACE_FULL_METADATA, + PolarisPrivilege.TABLE_FULL_METADATA, + PolarisPrivilege.VIEW_FULL_METADATA, + PolarisPrivilege.VIEW_FULL_METADATA, + PolarisPrivilege.CATALOG_FULL_METADATA, + PolarisPrivilege.PRINCIPAL_FULL_METADATA, + PolarisPrivilege.CATALOG_ROLE_READ_PROPERTIES, + PolarisPrivilege.CATALOG_ROLE_LIST, + PolarisPrivilege.CATALOG_ROLE_CREATE, + PolarisPrivilege.CATALOG_ROLE_DROP, + PolarisPrivilege.PRINCIPAL_ROLE_FULL_METADATA), + () -> { + UpdateCatalogRoleRequest updateRequest = + UpdateCatalogRoleRequest.builder() + .setCurrentEntityVersion( + newTestAdminService() + .getCatalogRole(CATALOG_NAME, CATALOG_ROLE2) + .getEntityVersion()) + .setProperties(Map.of("foo", Long.toString(System.currentTimeMillis()))) + .build(); + newTestAdminService().updateCatalogRole(CATALOG_NAME, CATALOG_ROLE2, updateRequest); + }, + (privilege) -> + adminService.grantPrivilegeOnCatalogToRole(CATALOG_NAME, CATALOG_ROLE1, privilege), + (privilege) -> + adminService.revokePrivilegeOnCatalogFromRole(CATALOG_NAME, CATALOG_ROLE1, privilege)); + } + + @Test + public void testDeleteCatalogRoleSufficientPrivileges() { + // Cleanup with CATALOG_ROLE2 + Assertions.assertThat( + adminService.grantPrivilegeOnCatalogToRole( + CATALOG_NAME, CATALOG_ROLE2, PolarisPrivilege.CATALOG_ROLE_CREATE)) + .isTrue(); + final CatalogRoleEntity newCatalogRole = + new CatalogRoleEntity.Builder().setName("new_catalog_role").build(); + adminService.createCatalogRole(CATALOG_NAME, newCatalogRole); + + doTestSufficientPrivileges( + List.of( + PolarisPrivilege.CATALOG_MANAGE_ACCESS, + PolarisPrivilege.CATALOG_ROLE_DROP, + PolarisPrivilege.CATALOG_ROLE_FULL_METADATA), + () -> + newTestAdminService(Set.of(PRINCIPAL_ROLE1)) + .deleteCatalogRole(CATALOG_NAME, newCatalogRole.getName()), + () -> + newTestAdminService(Set.of(PRINCIPAL_ROLE2)) + .createCatalogRole(CATALOG_NAME, newCatalogRole), + (privilege) -> + adminService.grantPrivilegeOnCatalogToRole(CATALOG_NAME, CATALOG_ROLE1, privilege), + (privilege) -> + adminService.revokePrivilegeOnCatalogFromRole(CATALOG_NAME, CATALOG_ROLE1, privilege)); + } + + @Test + public void testDeleteCatalogRoleInsufficientPrivileges() { + final CatalogRoleEntity newCatalogRole = + new CatalogRoleEntity.Builder().setName("new_catalog_role").build(); + adminService.createCatalogRole(CATALOG_NAME, newCatalogRole); + + doTestInsufficientPrivileges( + List.of( + PolarisPrivilege.SERVICE_MANAGE_ACCESS, + PolarisPrivilege.NAMESPACE_FULL_METADATA, + PolarisPrivilege.TABLE_FULL_METADATA, + PolarisPrivilege.VIEW_FULL_METADATA, + PolarisPrivilege.CATALOG_FULL_METADATA, + PolarisPrivilege.PRINCIPAL_FULL_METADATA, + PolarisPrivilege.PRINCIPAL_ROLE_FULL_METADATA, + PolarisPrivilege.CATALOG_ROLE_CREATE, + PolarisPrivilege.CATALOG_ROLE_LIST, + PolarisPrivilege.CATALOG_ROLE_READ_PROPERTIES, + PolarisPrivilege.CATALOG_ROLE_WRITE_PROPERTIES), + () -> + newTestAdminService(Set.of(PRINCIPAL_ROLE1)) + .deleteCatalogRole(CATALOG_NAME, newCatalogRole.getName()), + (privilege) -> + adminService.grantPrivilegeOnCatalogToRole(CATALOG_NAME, CATALOG_ROLE1, privilege), + (privilege) -> + adminService.revokePrivilegeOnCatalogFromRole(CATALOG_NAME, CATALOG_ROLE1, privilege)); + } +} diff --git a/polaris-service/src/test/java/io/polaris/service/admin/PolarisAuthzTestBase.java b/polaris-service/src/test/java/io/polaris/service/admin/PolarisAuthzTestBase.java new file mode 100644 index 0000000000..80ad0bdf1a --- /dev/null +++ b/polaris-service/src/test/java/io/polaris/service/admin/PolarisAuthzTestBase.java @@ -0,0 +1,496 @@ +package io.polaris.service.admin; + +import static org.apache.iceberg.types.Types.NestedField.required; + +import com.google.common.collect.ImmutableMap; +import io.polaris.core.PolarisCallContext; +import io.polaris.core.PolarisConfiguration; +import io.polaris.core.PolarisConfigurationStore; +import io.polaris.core.PolarisDefaultDiagServiceImpl; +import io.polaris.core.PolarisDiagnostics; +import io.polaris.core.admin.model.FileStorageConfigInfo; +import io.polaris.core.admin.model.PrincipalWithCredentials; +import io.polaris.core.admin.model.PrincipalWithCredentialsCredentials; +import io.polaris.core.admin.model.StorageConfigInfo; +import io.polaris.core.auth.AuthenticatedPolarisPrincipal; +import io.polaris.core.auth.PolarisAuthorizer; +import io.polaris.core.context.CallContext; +import io.polaris.core.context.RealmContext; +import io.polaris.core.entity.CatalogEntity; +import io.polaris.core.entity.CatalogRoleEntity; +import io.polaris.core.entity.PolarisBaseEntity; +import io.polaris.core.entity.PolarisEntity; +import io.polaris.core.entity.PolarisEntitySubType; +import io.polaris.core.entity.PolarisEntityType; +import io.polaris.core.entity.PolarisPrivilege; +import io.polaris.core.entity.PrincipalEntity; +import io.polaris.core.entity.PrincipalRoleEntity; +import io.polaris.core.persistence.PolarisEntityManager; +import io.polaris.core.persistence.PolarisMetaStoreManager; +import io.polaris.core.persistence.PolarisTreeMapStore; +import io.polaris.core.persistence.resolver.PolarisResolutionManifest; +import io.polaris.core.storage.cache.StorageCredentialCache; +import io.polaris.service.catalog.BasePolarisCatalog; +import io.polaris.service.catalog.PolarisPassthroughResolutionView; +import io.polaris.service.config.DefaultConfigurationStore; +import io.polaris.service.config.RealmEntityManagerFactory; +import io.polaris.service.context.PolarisCallContextCatalogFactory; +import io.polaris.service.persistence.InMemoryPolarisMetaStoreManagerFactory; +import io.polaris.service.storage.PolarisStorageIntegrationProviderImpl; +import java.io.IOException; +import java.time.Clock; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import org.apache.iceberg.CatalogProperties; +import org.apache.iceberg.Schema; +import org.apache.iceberg.catalog.Catalog; +import org.apache.iceberg.catalog.Namespace; +import org.apache.iceberg.catalog.TableIdentifier; +import org.apache.iceberg.exceptions.ForbiddenException; +import org.apache.iceberg.types.Types; +import org.assertj.core.api.Assertions; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.mockito.Mockito; + +/** Base class for shared test setup logic used by various Polaris authz-related tests. */ +public abstract class PolarisAuthzTestBase { + protected static final String CATALOG_NAME = "polaris-catalog"; + protected static final String PRINCIPAL_NAME = "snowman"; + + // catalog_role1 will be assigned only to principal_role1 and + // catalog_role2 will be assigned only to principal_role2 + protected static final String PRINCIPAL_ROLE1 = "principal_role1"; + protected static final String PRINCIPAL_ROLE2 = "principal_role2"; + protected static final String CATALOG_ROLE1 = "catalog_role1"; + protected static final String CATALOG_ROLE2 = "catalog_role2"; + protected static final String CATALOG_ROLE_SHARED = "catalog_role_shared"; + + protected static final Namespace NS1 = Namespace.of("ns1"); + protected static final Namespace NS2 = Namespace.of("ns2"); + protected static final Namespace NS1A = Namespace.of("ns1", "ns1a"); + protected static final Namespace NS1AA = Namespace.of("ns1", "ns1a", "ns1aa"); + protected static final Namespace NS1B = Namespace.of("ns1", "ns1b"); + + // One table directly under ns1 + protected static final TableIdentifier TABLE_NS1_1 = TableIdentifier.of(NS1, "layer1_table"); + + // Two tables under ns1a + protected static final TableIdentifier TABLE_NS1A_1 = TableIdentifier.of(NS1A, "table1"); + protected static final TableIdentifier TABLE_NS1A_2 = TableIdentifier.of(NS1A, "table2"); + + // One table under ns1b with same name as one under ns1a + protected static final TableIdentifier TABLE_NS1B_1 = TableIdentifier.of(NS1B, "table1"); + + // One table directly under ns2 + protected static final TableIdentifier TABLE_NS2_1 = TableIdentifier.of(NS2, "table1"); + + // One view directly under ns1 + protected static final TableIdentifier VIEW_NS1_1 = TableIdentifier.of(NS1, "layer1_view"); + + // Two views under ns1a + protected static final TableIdentifier VIEW_NS1A_1 = TableIdentifier.of(NS1A, "view1"); + protected static final TableIdentifier VIEW_NS1A_2 = TableIdentifier.of(NS1A, "view2"); + + // One view under ns1b with same name as one under ns1a + protected static final TableIdentifier VIEW_NS1B_1 = TableIdentifier.of(NS1B, "view1"); + + // One view directly under ns2 + protected static final TableIdentifier VIEW_NS2_1 = TableIdentifier.of(NS2, "view1"); + + protected static final String VIEW_QUERY = "select * from ns1.layer1_table"; + + protected static final Schema SCHEMA = + new Schema( + required(3, "id", Types.IntegerType.get(), "unique ID 🤪"), + required(4, "data", Types.StringType.get())); + protected final PolarisAuthorizer polarisAuthorizer = + new PolarisAuthorizer( + new DefaultConfigurationStore( + Map.of( + PolarisConfiguration.ENFORCE_PRINCIPAL_CREDENTIAL_ROTATION_REQUIRED_CHECKING, + true))); + + protected BasePolarisCatalog baseCatalog; + protected PolarisAdminService adminService; + protected PolarisEntityManager entityManager; + protected PolarisBaseEntity catalogEntity; + protected PrincipalEntity principalEntity; + protected CallContext callContext; + protected AuthenticatedPolarisPrincipal authenticatedRoot; + + @BeforeEach + @SuppressWarnings("unchecked") + public void before() { + PolarisDiagnostics diagServices = new PolarisDefaultDiagServiceImpl(); + PolarisTreeMapStore backingStore = new PolarisTreeMapStore(diagServices); + InMemoryPolarisMetaStoreManagerFactory managerFactory = + new InMemoryPolarisMetaStoreManagerFactory(); + managerFactory.setStorageIntegrationProvider(new PolarisStorageIntegrationProviderImpl()); + RealmContext realmContext = () -> "realm"; + PolarisMetaStoreManager metaStoreManager = + managerFactory.getOrCreateMetaStoreManager(realmContext); + + Map configMap = new HashMap<>(); + configMap.put("ALLOW_SPECIFYING_FILE_IO_IMPL", true); + PolarisCallContext polarisContext = + new PolarisCallContext( + managerFactory.getOrCreateSessionSupplier(realmContext).get(), + diagServices, + new PolarisConfigurationStore() { + @Override + public @Nullable T getConfiguration(PolarisCallContext ctx, String configName) { + return (T) configMap.get(configName); + } + }, + Clock.systemDefaultZone()); + this.entityManager = + new PolarisEntityManager( + metaStoreManager, polarisContext::getMetaStore, new StorageCredentialCache()); + + callContext = + CallContext.of( + new RealmContext() { + @Override + public String getRealmIdentifier() { + return "test-realm"; + } + }, + polarisContext); + CallContext.setCurrentContext(callContext); + + PrincipalEntity rootEntity = + new PrincipalEntity( + PolarisEntity.of( + entityManager + .getMetaStoreManager() + .readEntityByName( + polarisContext, + null, + PolarisEntityType.PRINCIPAL, + PolarisEntitySubType.NULL_SUBTYPE, + "root") + .getEntity())); + + this.authenticatedRoot = new AuthenticatedPolarisPrincipal(rootEntity, Set.of()); + + this.adminService = + new PolarisAdminService(callContext, entityManager, authenticatedRoot, polarisAuthorizer); + + String storageLocation = "file:///tmp"; + FileStorageConfigInfo storageConfigModel = + FileStorageConfigInfo.builder() + .setStorageType(StorageConfigInfo.StorageTypeEnum.FILE) + .setAllowedLocations(List.of(storageLocation, "file:///tmp")) + .build(); + catalogEntity = + adminService.createCatalog( + new CatalogEntity.Builder() + .setName(CATALOG_NAME) + .setDefaultBaseLocation(storageLocation) + .setStorageConfigurationInfo(storageConfigModel, storageLocation) + .build()); + + initBaseCatalog(); + + PrincipalWithCredentials principal = + adminService.createPrincipal(new PrincipalEntity.Builder().setName(PRINCIPAL_NAME).build()); + principalEntity = + rotateAndRefreshPrincipal( + metaStoreManager, PRINCIPAL_NAME, principal.getCredentials(), polarisContext); + + // Pre-create the principal roles and catalog roles without any grants on securables, but + // assign both principal roles to the principal, then CATALOG_ROLE1 to PRINCIPAL_ROLE1, + // CATALOG_ROLE2 to PRINCIPAL_ROLE2, and CATALOG_ROLE_SHARED to both. + adminService.createPrincipalRole( + new PrincipalRoleEntity.Builder().setName(PRINCIPAL_ROLE1).build()); + adminService.createPrincipalRole( + new PrincipalRoleEntity.Builder().setName(PRINCIPAL_ROLE2).build()); + adminService.createCatalogRole( + CATALOG_NAME, new CatalogRoleEntity.Builder().setName(CATALOG_ROLE1).build()); + adminService.createCatalogRole( + CATALOG_NAME, new CatalogRoleEntity.Builder().setName(CATALOG_ROLE2).build()); + adminService.createCatalogRole( + CATALOG_NAME, new CatalogRoleEntity.Builder().setName(CATALOG_ROLE_SHARED).build()); + + adminService.assignPrincipalRole(PRINCIPAL_NAME, PRINCIPAL_ROLE1); + adminService.assignPrincipalRole(PRINCIPAL_NAME, PRINCIPAL_ROLE2); + + adminService.assignCatalogRoleToPrincipalRole(PRINCIPAL_ROLE1, CATALOG_NAME, CATALOG_ROLE1); + adminService.assignCatalogRoleToPrincipalRole(PRINCIPAL_ROLE2, CATALOG_NAME, CATALOG_ROLE2); + adminService.assignCatalogRoleToPrincipalRole( + PRINCIPAL_ROLE1, CATALOG_NAME, CATALOG_ROLE_SHARED); + adminService.assignCatalogRoleToPrincipalRole( + PRINCIPAL_ROLE2, CATALOG_NAME, CATALOG_ROLE_SHARED); + + // Do some shared setup with non-authz-aware baseCatalog. + baseCatalog.createNamespace(NS1); + baseCatalog.createNamespace(NS2); + baseCatalog.createNamespace(NS1A); + baseCatalog.createNamespace(NS1AA); + baseCatalog.createNamespace(NS1B); + + baseCatalog.buildTable(TABLE_NS1_1, SCHEMA).create(); + baseCatalog.buildTable(TABLE_NS1A_1, SCHEMA).create(); + baseCatalog.buildTable(TABLE_NS1A_2, SCHEMA).create(); + baseCatalog.buildTable(TABLE_NS1B_1, SCHEMA).create(); + baseCatalog.buildTable(TABLE_NS2_1, SCHEMA).create(); + + baseCatalog + .buildView(VIEW_NS1_1) + .withSchema(SCHEMA) + .withDefaultNamespace(NS1) + .withQuery("spark", VIEW_QUERY) + .create(); + baseCatalog + .buildView(VIEW_NS1A_1) + .withSchema(SCHEMA) + .withDefaultNamespace(NS1) + .withQuery("spark", VIEW_QUERY) + .create(); + baseCatalog + .buildView(VIEW_NS1A_2) + .withSchema(SCHEMA) + .withDefaultNamespace(NS1) + .withQuery("spark", VIEW_QUERY) + .create(); + baseCatalog + .buildView(VIEW_NS1B_1) + .withSchema(SCHEMA) + .withDefaultNamespace(NS1) + .withQuery("spark", VIEW_QUERY) + .create(); + baseCatalog + .buildView(VIEW_NS2_1) + .withSchema(SCHEMA) + .withDefaultNamespace(NS1) + .withQuery("spark", VIEW_QUERY) + .create(); + } + + @AfterEach + public void after() { + if (this.baseCatalog != null) { + try { + this.baseCatalog.close(); + this.baseCatalog = null; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } + + protected @NotNull PrincipalEntity rotateAndRefreshPrincipal( + PolarisMetaStoreManager metaStoreManager, + String principalName, + PrincipalWithCredentialsCredentials credentials, + PolarisCallContext polarisContext) { + PolarisMetaStoreManager.EntityResult lookupEntity = + metaStoreManager.readEntityByName( + callContext.getPolarisCallContext(), + null, + PolarisEntityType.PRINCIPAL, + PolarisEntitySubType.NULL_SUBTYPE, + principalName); + metaStoreManager.rotatePrincipalSecrets( + callContext.getPolarisCallContext(), + credentials.getClientId(), + lookupEntity.getEntity().getId(), + credentials.getClientSecret(), + false); + + return new PrincipalEntity( + PolarisEntity.of( + entityManager + .getMetaStoreManager() + .readEntityByName( + polarisContext, + null, + PolarisEntityType.PRINCIPAL, + PolarisEntitySubType.NULL_SUBTYPE, + principalName) + .getEntity())); + } + + /** + * This baseCatalog is used for setup rather than being the test target under a wrapper instance; + * we set up this baseCatalog with a PolarisPassthroughResolutionView to allow it to circumvent + * the "authorized" resolution set of entities used by wrapper instances, allowing it to resolve + * all entities in the underlying metaStoreManager at once. + */ + private void initBaseCatalog() { + if (this.baseCatalog != null) { + try { + this.baseCatalog.close(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + PolarisPassthroughResolutionView passthroughView = + new PolarisPassthroughResolutionView( + callContext, entityManager, authenticatedRoot, CATALOG_NAME); + this.baseCatalog = + new BasePolarisCatalog(entityManager, callContext, passthroughView, Mockito.mock()); + this.baseCatalog.initialize( + CATALOG_NAME, + ImmutableMap.of( + CatalogProperties.FILE_IO_IMPL, "org.apache.iceberg.inmemory.InMemoryFileIO")); + } + + public class TestPolarisCallContextCatalogFactory extends PolarisCallContextCatalogFactory { + public TestPolarisCallContextCatalogFactory() { + super( + new RealmEntityManagerFactory() { + @Override + public PolarisEntityManager getOrCreateEntityManager(RealmContext realmContext) { + return entityManager; + } + }, + Mockito.mock()); + } + + @Override + public Catalog createCallContextCatalog( + CallContext context, final PolarisResolutionManifest resolvedManifest) { + // This depends on the BasePolarisCatalog allowing calling initialize multiple times + // to override the previous config. + Catalog catalog = super.createCallContextCatalog(context, resolvedManifest); + catalog.initialize( + CATALOG_NAME, + ImmutableMap.of( + CatalogProperties.FILE_IO_IMPL, "org.apache.iceberg.inmemory.InMemoryFileIO")); + return catalog; + } + } + + /** + * Tests each "sufficient" privilege individually by invoking {@code grantAction} for each set of + * privileges, running the action being tested, revoking after each test set, and also ensuring + * that the request fails after each revocation. + * + * @param sufficientPrivileges each set of concurrent privileges expected to be sufficient + * together. + * @param action The operation being tested; could also be multiple operations that should all + * succeed with the sufficient privilege + * @param cleanupAction If non-null, additional action to run to "undo" a previous success action + * in case the action has side effects. Called before revoking the sufficient privilege; + * either the cleanup privileges must be latent, or the cleanup action could be run with + * PRINCIPAL_ROLE2 while runnint {@code action} with PRINCIPAL_ROLE1. + * @param principalName the name expected to appear in forbidden errors + * @param grantAction the grantPrivilege action to use for each test privilege that will apply the + * privilege to whatever context is used in the {@code action} + * @param revokeAction the revokePrivilege action to clean up after each granted test privilege + */ + protected void doTestSufficientPrivilegeSets( + List> sufficientPrivileges, + Runnable action, + Runnable cleanupAction, + String principalName, + Function grantAction, + Function revokeAction) { + for (Set privilegeSet : sufficientPrivileges) { + for (PolarisPrivilege privilege : privilegeSet) { + // Grant the single privilege at a catalog level to cascade to all objects. + Assertions.assertThat(grantAction.apply(privilege)).isTrue(); + } + + // Should run without issues. + try { + action.run(); + } catch (Throwable t) { + Assertions.fail( + String.format( + "Expected success with sufficientPrivileges '%s', got throwable instead.", + privilegeSet), + t); + } + if (cleanupAction != null) { + try { + cleanupAction.run(); + } catch (Throwable t) { + Assertions.fail( + String.format( + "Running cleanupAction with sufficientPrivileges '%s', got throwable.", + privilegeSet), + t); + } + } + + if (privilegeSet.size() > 1) { + // Knockout testing - Revoke single privileges and the same action should throw + // NotAuthorizedException. + for (PolarisPrivilege privilege : privilegeSet) { + Assertions.assertThat(revokeAction.apply(privilege)).isTrue(); + + try { + Assertions.assertThatThrownBy(() -> action.run()) + .isInstanceOf(ForbiddenException.class) + .hasMessageContaining(principalName) + .hasMessageContaining("is not authorized"); + } catch (Throwable t) { + Assertions.fail( + String.format( + "Expected failure after revoking sufficientPrivilege '%s' from set '%s'", + privilege, privilegeSet), + t); + } + + // Grant the single privilege at a catalog level to cascade to all objects. + Assertions.assertThat(grantAction.apply(privilege)).isTrue(); + } + } + + // Now remove all the privileges + for (PolarisPrivilege privilege : privilegeSet) { + Assertions.assertThat(revokeAction.apply(privilege)).isTrue(); + } + try { + Assertions.assertThatThrownBy(() -> action.run()) + .isInstanceOf(ForbiddenException.class) + .hasMessageContaining(principalName) + .hasMessageContaining("is not authorized"); + } catch (Throwable t) { + Assertions.fail( + String.format( + "Expected failure after revoking all sufficientPrivileges '%s'", privilegeSet), + t); + } + } + } + + /** + * Tests each "insufficient" privilege individually using CATALOG_ROLE1 by granting at the + * CATALOG_NAME level, ensuring the action fails, then revoking after each test case. + */ + protected void doTestInsufficientPrivileges( + List insufficientPrivileges, + String principalName, + Runnable action, + Function grantAction, + Function revokeAction) { + for (PolarisPrivilege privilege : insufficientPrivileges) { + // Grant the single privilege at a catalog level to cascade to all objects. + Assertions.assertThat(grantAction.apply(privilege)).isTrue(); + + // Should be insufficient + try { + Assertions.assertThatThrownBy(() -> action.run()) + .isInstanceOf(ForbiddenException.class) + .hasMessageContaining(principalName) + .hasMessageContaining("is not authorized"); + } catch (Throwable t) { + Assertions.fail( + String.format("Expected failure with insufficientPrivilege '%s'", privilege), t); + } + + // Revoking only matters in case there are some multi-privilege actions being tested with + // only granting individual privileges in isolation. + Assertions.assertThat(revokeAction.apply(privilege)).isTrue(); + } + } +} diff --git a/polaris-service/src/test/java/io/polaris/service/admin/PolarisServiceImplIntegrationTest.java b/polaris-service/src/test/java/io/polaris/service/admin/PolarisServiceImplIntegrationTest.java new file mode 100644 index 0000000000..0be6a9990a --- /dev/null +++ b/polaris-service/src/test/java/io/polaris/service/admin/PolarisServiceImplIntegrationTest.java @@ -0,0 +1,1770 @@ +package io.polaris.service.admin; + +import static io.dropwizard.jackson.Jackson.newObjectMapper; +import static io.polaris.service.context.DefaultContextResolver.REALM_PROPERTY_KEY; +import static org.assertj.core.api.Assertions.assertThat; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import io.dropwizard.testing.ConfigOverride; +import io.dropwizard.testing.ResourceHelpers; +import io.dropwizard.testing.junit5.DropwizardAppExtension; +import io.dropwizard.testing.junit5.DropwizardExtensionsSupport; +import io.polaris.core.admin.model.AwsStorageConfigInfo; +import io.polaris.core.admin.model.AzureStorageConfigInfo; +import io.polaris.core.admin.model.Catalog; +import io.polaris.core.admin.model.CatalogProperties; +import io.polaris.core.admin.model.CatalogRole; +import io.polaris.core.admin.model.CatalogRoles; +import io.polaris.core.admin.model.Catalogs; +import io.polaris.core.admin.model.CreateCatalogRequest; +import io.polaris.core.admin.model.CreateCatalogRoleRequest; +import io.polaris.core.admin.model.CreatePrincipalRequest; +import io.polaris.core.admin.model.CreatePrincipalRoleRequest; +import io.polaris.core.admin.model.ExternalCatalog; +import io.polaris.core.admin.model.FileStorageConfigInfo; +import io.polaris.core.admin.model.GrantCatalogRoleRequest; +import io.polaris.core.admin.model.PolarisCatalog; +import io.polaris.core.admin.model.Principal; +import io.polaris.core.admin.model.PrincipalRole; +import io.polaris.core.admin.model.PrincipalRoles; +import io.polaris.core.admin.model.PrincipalWithCredentials; +import io.polaris.core.admin.model.PrincipalWithCredentialsCredentials; +import io.polaris.core.admin.model.Principals; +import io.polaris.core.admin.model.StorageConfigInfo; +import io.polaris.core.admin.model.UpdateCatalogRequest; +import io.polaris.core.admin.model.UpdateCatalogRoleRequest; +import io.polaris.core.admin.model.UpdatePrincipalRequest; +import io.polaris.core.admin.model.UpdatePrincipalRoleRequest; +import io.polaris.core.entity.PolarisEntityConstants; +import io.polaris.service.PolarisApplication; +import io.polaris.service.auth.TokenUtils; +import io.polaris.service.config.PolarisApplicationConfig; +import io.polaris.service.test.PolarisConnectionExtension; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.client.Invocation; +import jakarta.ws.rs.core.Response; +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import org.apache.commons.lang3.RandomStringUtils; +import org.apache.iceberg.rest.responses.ErrorResponse; +import org.assertj.core.api.InstanceOfAssertFactories; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +@ExtendWith({DropwizardExtensionsSupport.class, PolarisConnectionExtension.class}) +public class PolarisServiceImplIntegrationTest { + private static final int MAX_IDENTIFIER_LENGTH = 256; + + // TODO: Add a test-only hook that fully clobbers all persistence state so we can have a fresh + // slate on every test case; otherwise, leftover state from one test from failures will interfere + // with other test cases. + private static final DropwizardAppExtension EXT = + new DropwizardAppExtension<>( + PolarisApplication.class, + ResourceHelpers.resourceFilePath("polaris-server-integrationtest.yml"), + ConfigOverride.config( + "server.applicationConnectors[0].port", + "0"), // Bind to random port to support parallelism + ConfigOverride.config("server.adminConnectors[0].port", "0"), + + // disallow FILE urls for the sake of tests below + ConfigOverride.config( + "featureConfiguration.SUPPORTED_CATALOG_STORAGE_TYPES", "S3,GCS,AZURE")); + private static String userToken; + private static String realm; + + @BeforeAll + public static void setup(PolarisConnectionExtension.PolarisToken adminToken) { + userToken = adminToken.token(); + realm = PolarisConnectionExtension.getTestRealm(PolarisServiceImplIntegrationTest.class); + } + + @AfterEach + public void tearDown() { + try (Response response = newRequest("http://localhost:%d/api/management/v1/catalogs").get()) { + response.readEntity(Catalogs.class).getCatalogs().stream() + .forEach( + catalog -> { + try (Response innerResponse = + newRequest( + "http://localhost:%d/api/management/v1/catalogs/" + catalog.getName()) + .delete()) {} + }); + } + try (Response response = newRequest("http://localhost:%d/api/management/v1/principals").get()) { + response.readEntity(Principals.class).getPrincipals().stream() + .filter( + principal -> + !principal.getName().equals(PolarisEntityConstants.getRootPrincipalName())) + .forEach( + principal -> { + try (Response innerResponse = + newRequest( + "http://localhost:%d/api/management/v1/principals/" + + principal.getName()) + .delete()) {} + }); + } + try (Response response = + newRequest("http://localhost:%d/api/management/v1/principal-roles").get()) { + response.readEntity(PrincipalRoles.class).getRoles().stream() + .filter( + principalRole -> + !principalRole + .getName() + .equals(PolarisEntityConstants.getNameOfPrincipalServiceAdminRole())) + .forEach( + principalRole -> { + try (Response innerResponse = + newRequest( + "http://localhost:%d/api/management/v1/principal-roles/" + + principalRole.getName()) + .delete()) {} + }); + } + } + + @Test + public void testCatalogSerializing() throws IOException { + CatalogProperties props = new CatalogProperties("s3://my-old-bucket/path/to/data"); + props.put("prop1", "propval"); + PolarisCatalog catalog = + PolarisCatalog.builder() + .setType(Catalog.TypeEnum.INTERNAL) + .setName("my_catalog") + .setProperties(props) + .setStorageConfigInfo( + AwsStorageConfigInfo.builder() + .setRoleArn("arn:aws:iam::123456789012:role/my-role") + .setExternalId("externalId") + .setUserArn("userArn") + .setStorageType(StorageConfigInfo.StorageTypeEnum.S3) + .setAllowedLocations(List.of("s3://my-old-bucket/path/to/data")) + .build()) + .build(); + + ObjectMapper mapper = new ObjectMapper(); + String json = mapper.writeValueAsString(catalog); + System.out.println(json); + Catalog deserialized = mapper.readValue(json, Catalog.class); + assertThat(deserialized).isInstanceOf(PolarisCatalog.class); + } + + @Test + public void testListCatalogs() { + try (Response response = newRequest("http://localhost:%d/api/management/v1/catalogs").get()) { + assertThat(response) + .returns(200, Response::getStatus) + .extracting(r -> r.readEntity(Catalogs.class)) + .returns( + List.of(), + l -> + l.getCatalogs().stream() + .filter(c -> !c.getName().equalsIgnoreCase("ROOT")) + .toList()); + } + } + + @Test + public void testListCatalogsUnauthorized() { + Principal principal = new Principal("a_new_user"); + String newToken = null; + try (Response response = + newRequest("http://localhost:%d/api/management/v1/principals") + .post(Entity.json(principal))) { + assertThat(response).returns(201, Response::getStatus); + PrincipalWithCredentials creds = response.readEntity(PrincipalWithCredentials.class); + newToken = + TokenUtils.getTokenFromSecrets( + EXT.client(), + EXT.getLocalPort(), + creds.getCredentials().getClientId(), + creds.getCredentials().getClientSecret(), + realm); + } + try (Response response = + newRequest("http://localhost:%d/api/management/v1/catalogs", "BEARER " + newToken).get()) { + assertThat(response).returns(403, Response::getStatus); + } + } + + @Test + public void testCreateCatalog() { + AwsStorageConfigInfo awsConfigModel = + AwsStorageConfigInfo.builder() + .setRoleArn("arn:aws:iam::123456789012:role/my-role") + .setExternalId("externalId") + .setUserArn("userArn") + .setStorageType(StorageConfigInfo.StorageTypeEnum.S3) + .setAllowedLocations(List.of("s3://my-old-bucket/path/to/data")) + .build(); + Catalog catalog = + PolarisCatalog.builder() + .setType(Catalog.TypeEnum.INTERNAL) + .setName("my-catalog") + .setProperties(new CatalogProperties("s3://my-bucket/path/to/data")) + .setStorageConfigInfo(awsConfigModel) + .build(); + try (Response response = + newRequest("http://localhost:%d/api/management/v1/catalogs") + .post( + Entity.json( + "{\"catalog\":{\"type\":\"INTERNAL\",\"name\":\"my-catalog\",\"properties\":{\"default-base-location\":\"s3://my-bucket/path/to/data\"},\"storageConfigInfo\":{\"storageType\":\"S3\",\"roleArn\":\"arn:aws:iam::123456789012:role/my-role\",\"externalId\":\"externalId\",\"userArn\":\"userArn\",\"allowedLocations\":[\"s3://my-old-bucket/path/to/data\"]}}}"))) { + assertThat(response).returns(Response.Status.CREATED.getStatusCode(), Response::getStatus); + } + + // 204 Successful delete + try (Response response = + newRequest("http://localhost:%d/api/management/v1/catalogs/my-catalog").delete()) { + assertThat(response).returns(204, Response::getStatus); + } + } + + @Test + public void testCreateCatalogWithInvalidName() { + AwsStorageConfigInfo awsConfigModel = + AwsStorageConfigInfo.builder() + .setRoleArn("arn:aws:iam::123456789012:role/my-role") + .setExternalId("externalId") + .setUserArn("userArn") + .setStorageType(StorageConfigInfo.StorageTypeEnum.S3) + .setAllowedLocations(List.of("s3://my-old-bucket/path/to/data")) + .build(); + + String goodName = RandomStringUtils.random(MAX_IDENTIFIER_LENGTH, true, true); + + ObjectMapper mapper = newObjectMapper(); + + Catalog catalog = + PolarisCatalog.builder() + .setType(Catalog.TypeEnum.INTERNAL) + .setName(goodName) + .setProperties(new CatalogProperties("s3://my-bucket/path/to/data")) + .setStorageConfigInfo(awsConfigModel) + .build(); + try (Response response = + newRequest("http://localhost:%d/api/management/v1/catalogs") + .post(Entity.json(mapper.writeValueAsString(catalog)))) { + assertThat(response).returns(Response.Status.CREATED.getStatusCode(), Response::getStatus); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + + String longInvalidName = RandomStringUtils.random(MAX_IDENTIFIER_LENGTH + 1, true, true); + List invalidCatalogNames = + Arrays.asList( + longInvalidName, + "", + "system$catalog1", + "SYSTEM$TestCatalog", + "System$test_catalog", + " SysTeM$ test catalog"); + + for (String invalidCatalogName : invalidCatalogNames) { + catalog = + PolarisCatalog.builder() + .setType(Catalog.TypeEnum.INTERNAL) + .setName(invalidCatalogName) + .setProperties(new CatalogProperties("s3://my-bucket/path/to/data")) + .setStorageConfigInfo(awsConfigModel) + .build(); + + try (Response response = + newRequest("http://localhost:%d/api/management/v1/catalogs") + .post(Entity.json(mapper.writeValueAsString(catalog)))) { + assertThat(response) + .returns(Response.Status.BAD_REQUEST.getStatusCode(), Response::getStatus); + assertThat(response.hasEntity()).isTrue(); + ErrorResponse errorResponse = response.readEntity(ErrorResponse.class); + assertThat(errorResponse.message()).contains("Invalid value:"); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + } + + @Test + public void testCreateCatalogWithNullBaseLocation() { + AwsStorageConfigInfo awsConfigModel = + AwsStorageConfigInfo.builder() + .setRoleArn("arn:aws:iam::123456789012:role/my-role") + .setExternalId("externalId") + .setUserArn("userArn") + .setStorageType(StorageConfigInfo.StorageTypeEnum.S3) + .setAllowedLocations(List.of("s3://my-old-bucket/path/to/data")) + .build(); + ObjectMapper mapper = new ObjectMapper(); + JsonNode storageConfig = mapper.valueToTree(awsConfigModel); + ObjectNode catalogNode = mapper.createObjectNode(); + catalogNode.set("storageConfigInfo", storageConfig); + catalogNode.put("name", "my-catalog"); + catalogNode.put("type", "INTERNAL"); + catalogNode.set("properties", mapper.createObjectNode()); + ObjectNode requestNode = mapper.createObjectNode(); + requestNode.set("catalog", catalogNode); + try (Response response = + newRequest("http://localhost:%d/api/management/v1/catalogs") + .post(Entity.json(requestNode))) { + assertThat(response) + .returns(Response.Status.BAD_REQUEST.getStatusCode(), Response::getStatus); + } + } + + @Test + public void testCreateCatalogWithoutProperties() { + AwsStorageConfigInfo awsConfigModel = + AwsStorageConfigInfo.builder() + .setRoleArn("arn:aws:iam::123456789012:role/my-role") + .setExternalId("externalId") + .setUserArn("userArn") + .setStorageType(StorageConfigInfo.StorageTypeEnum.S3) + .setAllowedLocations(List.of("s3://my-old-bucket/path/to/data")) + .build(); + ObjectMapper mapper = new ObjectMapper(); + JsonNode storageConfig = mapper.valueToTree(awsConfigModel); + ObjectNode catalogNode = mapper.createObjectNode(); + catalogNode.set("storageConfigInfo", storageConfig); + catalogNode.put("name", "my-catalog"); + catalogNode.put("type", "INTERNAL"); + ObjectNode requestNode = mapper.createObjectNode(); + requestNode.set("catalog", catalogNode); + + try (Response response = + newRequest("http://localhost:%d/api/management/v1/catalogs", "Bearer " + userToken) + .post(Entity.json(requestNode))) { + assertThat(response) + .returns(Response.Status.BAD_REQUEST.getStatusCode(), Response::getStatus); + ErrorResponse error = response.readEntity(ErrorResponse.class); + assertThat(error) + .isNotNull() + .returns( + "Invalid value: createCatalog.arg0.catalog.properties: must not be null", + ErrorResponse::message); + } + } + + @Test + public void testCreateCatalogWithoutStorageConfig() throws JsonProcessingException { + String catalogString = + "{\"catalog\": {\"type\":\"INTERNAL\",\"name\":\"my-catalog\",\"properties\":{\"default-base-location\":\"s3://my-bucket/path/to/data\"}}}"; + try (Response response = + newRequest("http://localhost:%d/api/management/v1/catalogs", "Bearer " + userToken) + .post(Entity.json(catalogString))) { + assertThat(response) + .returns(Response.Status.BAD_REQUEST.getStatusCode(), Response::getStatus); + ErrorResponse error = response.readEntity(ErrorResponse.class); + assertThat(error) + .isNotNull() + .returns( + "Invalid value: createCatalog.arg0.catalog.storageConfigInfo: must not be null", + ErrorResponse::message); + } + } + + @Test + public void testCreateCatalogWithUnparsableJson() throws JsonProcessingException { + String catalogString = "{\"catalog\": {{\"bad data}"; + try (Response response = + newRequest("http://localhost:%d/api/management/v1/catalogs", "Bearer " + userToken) + .post(Entity.json(catalogString))) { + assertThat(response) + .returns(Response.Status.BAD_REQUEST.getStatusCode(), Response::getStatus); + ErrorResponse error = response.readEntity(ErrorResponse.class); + assertThat(error) + .isNotNull() + .extracting(ErrorResponse::message) + .asString() + .startsWith("Invalid JSON: Unexpected character"); + } + } + + @Test + public void testCreateCatalogWithDisallowedStorageConfig() throws JsonProcessingException { + FileStorageConfigInfo fileStorage = + FileStorageConfigInfo.builder(StorageConfigInfo.StorageTypeEnum.FILE) + .setAllowedLocations(List.of("file://")) + .build(); + String catalogName = "my-external-catalog"; + Catalog catalog = + PolarisCatalog.builder() + .setType(Catalog.TypeEnum.INTERNAL) + .setName(catalogName) + .setProperties(new CatalogProperties("file:///tmp/path/to/data")) + .setStorageConfigInfo(fileStorage) + .build(); + try (Response response = + newRequest("http://localhost:%d/api/management/v1/catalogs", "Bearer " + userToken) + .post(Entity.json(new CreateCatalogRequest(catalog)))) { + assertThat(response) + .returns(Response.Status.BAD_REQUEST.getStatusCode(), Response::getStatus); + ErrorResponse error = response.readEntity(ErrorResponse.class); + assertThat(error) + .isNotNull() + .returns("Unsupported storage type: FILE", ErrorResponse::message); + } + } + + @Test + public void testUpdateCatalogWithDisallowedStorageConfig() throws JsonProcessingException { + AwsStorageConfigInfo awsConfigModel = + AwsStorageConfigInfo.builder() + .setRoleArn("arn:aws:iam::123456789012:role/my-role") + .setExternalId("externalId") + .setUserArn("userArn") + .setStorageType(StorageConfigInfo.StorageTypeEnum.S3) + .setAllowedLocations(List.of("s3://my-old-bucket/path/to/data")) + .build(); + String catalogName = "mycatalog"; + Catalog catalog = + PolarisCatalog.builder() + .setType(Catalog.TypeEnum.INTERNAL) + .setName(catalogName) + .setProperties(new CatalogProperties("s3://bucket/path/to/data")) + .setStorageConfigInfo(awsConfigModel) + .build(); + try (Response response = + newRequest("http://localhost:%d/api/management/v1/catalogs", "Bearer " + userToken) + .post(Entity.json(new CreateCatalogRequest(catalog)))) { + assertThat(response).returns(Response.Status.CREATED.getStatusCode(), Response::getStatus); + } + + // 200 successful GET after creation + Catalog fetchedCatalog = null; + try (Response response = + newRequest( + "http://localhost:%d/api/management/v1/catalogs/" + catalogName, + "Bearer " + userToken) + .get()) { + assertThat(response).returns(200, Response::getStatus); + fetchedCatalog = response.readEntity(Catalog.class); + + assertThat(fetchedCatalog.getName()).isEqualTo(catalogName); + assertThat(fetchedCatalog.getProperties().toMap()) + .isEqualTo(Map.of("default-base-location", "s3://bucket/path/to/data")); + assertThat(fetchedCatalog.getEntityVersion()).isGreaterThan(0); + } + + FileStorageConfigInfo fileStorage = + FileStorageConfigInfo.builder(StorageConfigInfo.StorageTypeEnum.FILE) + .setAllowedLocations(List.of("file://")) + .build(); + UpdateCatalogRequest updateRequest = + new UpdateCatalogRequest( + fetchedCatalog.getEntityVersion(), + Map.of("default-base-location", "file:///tmp/path/to/data/"), + fileStorage); + + // failure to update + try (Response response = + newRequest( + "http://localhost:%d/api/management/v1/catalogs/" + catalogName, + "Bearer " + userToken) + .put(Entity.json(updateRequest))) { + assertThat(response) + .returns(Response.Status.BAD_REQUEST.getStatusCode(), Response::getStatus); + ErrorResponse error = response.readEntity(ErrorResponse.class); + + assertThat(error).returns("Unsupported storage type: FILE", ErrorResponse::message); + } + } + + @Test + public void testCreateExternalCatalog() { + AwsStorageConfigInfo awsConfigModel = + AwsStorageConfigInfo.builder() + .setRoleArn("arn:aws:iam::123456789012:role/my-role") + .setExternalId("externalId") + .setUserArn("userArn") + .setStorageType(StorageConfigInfo.StorageTypeEnum.S3) + .setAllowedLocations(List.of("s3://my-old-bucket/path/to/data")) + .build(); + String catalogName = "my-external-catalog"; + String remoteUrl = "http://localhost:8080"; + Catalog catalog = + ExternalCatalog.builder() + .setType(Catalog.TypeEnum.EXTERNAL) + .setName(catalogName) + .setRemoteUrl(remoteUrl) + .setProperties(new CatalogProperties("s3://my-bucket/path/to/data")) + .setStorageConfigInfo(awsConfigModel) + .build(); + try (Response response = + newRequest("http://localhost:%d/api/management/v1/catalogs") + .post(Entity.json(new CreateCatalogRequest(catalog)))) { + assertThat(response).returns(Response.Status.CREATED.getStatusCode(), Response::getStatus); + } + + try (Response response = + newRequest("http://localhost:%d/api/management/v1/catalogs/" + catalogName).get()) { + assertThat(response).returns(Response.Status.OK.getStatusCode(), Response::getStatus); + Catalog fetchedCatalog = response.readEntity(Catalog.class); + assertThat(fetchedCatalog) + .isNotNull() + .isInstanceOf(ExternalCatalog.class) + .asInstanceOf(InstanceOfAssertFactories.type(ExternalCatalog.class)) + .returns(remoteUrl, ExternalCatalog::getRemoteUrl) + .extracting(ExternalCatalog::getStorageConfigInfo) + .isNotNull() + .isInstanceOf(AwsStorageConfigInfo.class) + .asInstanceOf(InstanceOfAssertFactories.type(AwsStorageConfigInfo.class)) + .returns("arn:aws:iam::123456789012:role/my-role", AwsStorageConfigInfo::getRoleArn); + } + + // 204 Successful delete + try (Response response = + newRequest("http://localhost:%d/api/management/v1/catalogs/" + catalogName).delete()) { + assertThat(response).returns(204, Response::getStatus); + } + } + + @Test + public void testCreateCatalogWithoutDefaultLocation() { + AwsStorageConfigInfo awsConfigModel = + AwsStorageConfigInfo.builder() + .setRoleArn("arn:aws:iam::123456789012:role/my-role") + .setExternalId("externalId") + .setUserArn("userArn") + .setStorageType(StorageConfigInfo.StorageTypeEnum.S3) + .setAllowedLocations(List.of("s3://my-old-bucket/path/to/data")) + .build(); + ObjectMapper mapper = new ObjectMapper(); + JsonNode storageConfig = mapper.valueToTree(awsConfigModel); + ObjectNode catalogNode = mapper.createObjectNode(); + catalogNode.set("storageConfigInfo", storageConfig); + catalogNode.put("name", "my-catalog"); + catalogNode.put("type", "INTERNAL"); + ObjectNode properties = mapper.createObjectNode(); + properties.set("default-base-location", mapper.nullNode()); + catalogNode.set("properties", properties); + ObjectNode requestNode = mapper.createObjectNode(); + requestNode.set("catalog", catalogNode); + + try (Response response = + newRequest("http://localhost:%d/api/management/v1/catalogs") + .post(Entity.json(requestNode))) { + assertThat(response) + .returns(Response.Status.BAD_REQUEST.getStatusCode(), Response::getStatus); + } + } + + @Test + public void serialization() throws JsonProcessingException { + CatalogProperties properties = new CatalogProperties("s3://my-bucket/path/to/data"); + ObjectMapper mapper = new ObjectMapper(); + CatalogProperties translated = mapper.convertValue(properties, CatalogProperties.class); + assertThat(translated.toMap()) + .containsEntry("default-base-location", "s3://my-bucket/path/to/data"); + } + + @Test + public void testCreateAndUpdateAzureCatalog() { + StorageConfigInfo storageConfig = + new AzureStorageConfigInfo("azure:tenantid:12345", StorageConfigInfo.StorageTypeEnum.AZURE); + Catalog catalog = + PolarisCatalog.builder() + .setType(Catalog.TypeEnum.INTERNAL) + .setName("myazurecatalog") + .setStorageConfigInfo(storageConfig) + .setProperties(new CatalogProperties("abfss://container1@acct1.dfs.core.windows.net/")) + .build(); + + // 200 Successful create + try (Response response = + newRequest("http://localhost:%d/api/management/v1/catalogs") + .post(Entity.json(new CreateCatalogRequest(catalog)))) { + assertThat(response).returns(Response.Status.CREATED.getStatusCode(), Response::getStatus); + } + + // 200 successful GET after creation + Catalog fetchedCatalog = null; + try (Response response = + newRequest("http://localhost:%d/api/management/v1/catalogs/myazurecatalog").get()) { + assertThat(response).returns(200, Response::getStatus); + fetchedCatalog = response.readEntity(Catalog.class); + + assertThat(fetchedCatalog.getName()).isEqualTo("myazurecatalog"); + assertThat(fetchedCatalog.getProperties().toMap()) + .isEqualTo( + Map.of("default-base-location", "abfss://container1@acct1.dfs.core.windows.net/")); + assertThat(fetchedCatalog.getEntityVersion()).isGreaterThan(0); + } + + StorageConfigInfo modifiedStorageConfig = + new AzureStorageConfigInfo("azure:tenantid:22222", StorageConfigInfo.StorageTypeEnum.AZURE); + UpdateCatalogRequest badUpdateRequest = + new UpdateCatalogRequest( + fetchedCatalog.getEntityVersion(), + Map.of("default-base-location", "abfss://newcontainer@acct1.dfs.core.windows.net/"), + modifiedStorageConfig); + try (Response response = + newRequest("http://localhost:%d/api/management/v1/catalogs/myazurecatalog") + .put(Entity.json(badUpdateRequest))) { + assertThat(response) + .returns(Response.Status.BAD_REQUEST.getStatusCode(), Response::getStatus); + ErrorResponse error = response.readEntity(ErrorResponse.class); + assertThat(error) + .isNotNull() + .extracting(ErrorResponse::message) + .asString() + .startsWith("Cannot modify"); + } + + UpdateCatalogRequest updateRequest = + new UpdateCatalogRequest( + fetchedCatalog.getEntityVersion(), + Map.of("default-base-location", "abfss://newcontainer@acct1.dfs.core.windows.net/"), + storageConfig); + + // 200 successful update + try (Response response = + newRequest("http://localhost:%d/api/management/v1/catalogs/myazurecatalog") + .put(Entity.json(updateRequest))) { + assertThat(response).returns(200, Response::getStatus); + fetchedCatalog = response.readEntity(Catalog.class); + + assertThat(fetchedCatalog.getProperties().toMap()) + .isEqualTo( + Map.of("default-base-location", "abfss://newcontainer@acct1.dfs.core.windows.net/")); + } + + // 204 Successful delete + try (Response response = + newRequest("http://localhost:%d/api/management/v1/catalogs/myazurecatalog").delete()) { + assertThat(response).returns(204, Response::getStatus); + } + } + + @Test + public void testCreateListUpdateAndDeleteCatalog() { + StorageConfigInfo storageConfig = + new AwsStorageConfigInfo( + "arn:aws:iam::123456789011:role/role1", StorageConfigInfo.StorageTypeEnum.S3); + Catalog catalog = + PolarisCatalog.builder() + .setType(Catalog.TypeEnum.INTERNAL) + .setName("mycatalog") + .setStorageConfigInfo(storageConfig) + .setProperties(new CatalogProperties("s3://bucket1/")) + .build(); + + try (Response response = + newRequest("http://localhost:%d/api/management/v1/catalogs") + .post(Entity.json(new CreateCatalogRequest(catalog)))) { + assertThat(response).returns(Response.Status.CREATED.getStatusCode(), Response::getStatus); + } + + // Second attempt to create the same entity should fail with CONFLICT. + try (Response response = + newRequest("http://localhost:%d/api/management/v1/catalogs") + .post(Entity.json(new CreateCatalogRequest(catalog)))) { + assertThat(response).returns(Response.Status.CONFLICT.getStatusCode(), Response::getStatus); + } + + // 200 successful GET after creation + Catalog fetchedCatalog = null; + try (Response response = + newRequest("http://localhost:%d/api/management/v1/catalogs/mycatalog").get()) { + assertThat(response).returns(200, Response::getStatus); + fetchedCatalog = response.readEntity(Catalog.class); + + assertThat(fetchedCatalog.getName()).isEqualTo("mycatalog"); + assertThat(fetchedCatalog.getProperties().toMap()) + .isEqualTo(Map.of("default-base-location", "s3://bucket1/")); + assertThat(fetchedCatalog.getEntityVersion()).isGreaterThan(0); + } + + // Should list the catalog. + try (Response response = newRequest("http://localhost:%d/api/management/v1/catalogs").get()) { + assertThat(response) + .returns(200, Response::getStatus) + .extracting(r -> r.readEntity(Catalogs.class)) + .extracting(Catalogs::getCatalogs) + .asInstanceOf(InstanceOfAssertFactories.list(Catalog.class)) + .filteredOn(cat -> !cat.getName().equalsIgnoreCase("ROOT")) + .satisfiesExactly(cat -> assertThat(cat).returns("mycatalog", Catalog::getName)); + } + + // Reject update of fields that can't be currently updated + StorageConfigInfo modifiedStorageConfig = + new AwsStorageConfigInfo( + "arn:aws:iam::123456789011:role/newrole", StorageConfigInfo.StorageTypeEnum.S3); + UpdateCatalogRequest badUpdateRequest = + new UpdateCatalogRequest( + fetchedCatalog.getEntityVersion(), + Map.of("default-base-location", "s3://newbucket/"), + modifiedStorageConfig); + try (Response response = + newRequest("http://localhost:%d/api/management/v1/catalogs/mycatalog") + .put(Entity.json(badUpdateRequest))) { + assertThat(response) + .returns(Response.Status.BAD_REQUEST.getStatusCode(), Response::getStatus); + ErrorResponse error = response.readEntity(ErrorResponse.class); + assertThat(error) + .isNotNull() + .extracting(ErrorResponse::message) + .asString() + .startsWith("Cannot modify"); + } + + UpdateCatalogRequest updateRequest = + new UpdateCatalogRequest( + fetchedCatalog.getEntityVersion(), + Map.of("default-base-location", "s3://newbucket/"), + storageConfig); + + // 200 successful update + try (Response response = + newRequest("http://localhost:%d/api/management/v1/catalogs/mycatalog") + .put(Entity.json(updateRequest))) { + assertThat(response).returns(200, Response::getStatus); + fetchedCatalog = response.readEntity(Catalog.class); + + assertThat(fetchedCatalog.getProperties().toMap()) + .isEqualTo(Map.of("default-base-location", "s3://newbucket/")); + } + + // 200 GET after update should show new properties + try (Response response = + newRequest("http://localhost:%d/api/management/v1/catalogs/mycatalog").get()) { + assertThat(response).returns(200, Response::getStatus); + fetchedCatalog = response.readEntity(Catalog.class); + + assertThat(fetchedCatalog.getProperties().toMap()) + .isEqualTo(Map.of("default-base-location", "s3://newbucket/")); + } + + // 204 Successful delete + try (Response response = + newRequest("http://localhost:%d/api/management/v1/catalogs/mycatalog").delete()) { + assertThat(response).returns(204, Response::getStatus); + } + + // NOT_FOUND after deletion + try (Response response = + newRequest("http://localhost:%d/api/management/v1/catalogs/mycatalog").get()) { + assertThat(response).returns(404, Response::getStatus); + } + + // Empty list + try (Response response = newRequest("http://localhost:%d/api/management/v1/catalogs").get()) { + assertThat(response) + .returns(200, Response::getStatus) + .extracting(r -> r.readEntity(Catalogs.class)) + .returns( + List.of(), + c -> + c.getCatalogs().stream() + .filter(cat -> !cat.getName().equalsIgnoreCase("ROOT")) + .toList()); + } + } + + private static Invocation.Builder newRequest(String url, String token) { + return EXT.client() + .target(String.format(url, EXT.getLocalPort())) + .request("application/json") + .header("Authorization", token) + .header(REALM_PROPERTY_KEY, realm); + } + + private static Invocation.Builder newRequest(String url) { + return newRequest(url, "Bearer " + userToken); + } + + @Test + public void testGetCatalogNotFound() { + // there's no catalog yet. Expect 404 + try (Response response = + newRequest("http://localhost:%d/api/management/v1/catalogs/mycatalog").get()) { + assertThat(response).returns(404, Response::getStatus); + } + } + + @Test + public void testGetCatalogInvalidName() { + String longInvalidName = RandomStringUtils.random(MAX_IDENTIFIER_LENGTH + 1, true, true); + List invalidCatalogNames = + Arrays.asList( + longInvalidName, + "system$catalog1", + "SYSTEM$TestCatalog", + "System$test_catalog", + " SysTeM$ test catalog"); + + for (String invalidCatalogName : invalidCatalogNames) { + // there's no catalog yet. Expect 404 + try (Response response = + newRequest("http://localhost:%d/api/management/v1/catalogs/" + invalidCatalogName) + .get()) { + assertThat(response) + .returns(Response.Status.BAD_REQUEST.getStatusCode(), Response::getStatus); + assertThat(response.hasEntity()).isTrue(); + ErrorResponse errorResponse = response.readEntity(ErrorResponse.class); + assertThat(errorResponse.message()).contains("Invalid value:"); + } + } + } + + @Test + public void testCatalogRoleInvalidName() { + Catalog catalog = + PolarisCatalog.builder() + .setType(Catalog.TypeEnum.INTERNAL) + .setName("mycatalog1") + .setProperties(new CatalogProperties("s3://required/base/location")) + .setStorageConfigInfo( + new AwsStorageConfigInfo( + "arn:aws:iam::012345678901:role/jdoe", StorageConfigInfo.StorageTypeEnum.S3)) + .build(); + try (Response response = + newRequest("http://localhost:%d/api/management/v1/catalogs") + .post(Entity.json(new CreateCatalogRequest(catalog)))) { + + assertThat(response).returns(Response.Status.CREATED.getStatusCode(), Response::getStatus); + } + + String longInvalidName = RandomStringUtils.random(MAX_IDENTIFIER_LENGTH + 1, true, true); + List invalidCatalogRoleNames = + Arrays.asList( + longInvalidName, + "system$catalog1", + "SYSTEM$TestCatalog", + "System$test_catalog", + " SysTeM$ test catalog"); + + for (String invalidCatalogRoleName : invalidCatalogRoleNames) { + try (Response response = + newRequest( + "http://localhost:%d/api/management/v1/catalogs/mycatalog1/catalog-roles/" + + invalidCatalogRoleName) + .get()) { + + assertThat(response) + .returns(Response.Status.BAD_REQUEST.getStatusCode(), Response::getStatus); + assertThat(response.hasEntity()).isTrue(); + ErrorResponse errorResponse = response.readEntity(ErrorResponse.class); + assertThat(errorResponse.message()).contains("Invalid value:"); + } + } + } + + @Test + public void testListPrincipalsUnauthorized() { + Principal principal = new Principal("new_admin"); + String newToken = null; + try (Response response = + newRequest("http://localhost:%d/api/management/v1/principals") + .post(Entity.json(principal))) { + assertThat(response).returns(201, Response::getStatus); + PrincipalWithCredentials creds = response.readEntity(PrincipalWithCredentials.class); + newToken = + TokenUtils.getTokenFromSecrets( + EXT.client(), + EXT.getLocalPort(), + creds.getCredentials().getClientId(), + creds.getCredentials().getClientSecret(), + realm); + } + try (Response response = + newRequest("http://localhost:%d/api/management/v1/principals", "Bearer " + newToken) + .get()) { + assertThat(response).returns(403, Response::getStatus); + } + } + + @Test + public void testCreatePrincipalAndRotateCredentials() { + Principal principal = + Principal.builder() + .setName("myprincipal") + .setProperties(Map.of("custom-tag", "foo")) + .build(); + + PrincipalWithCredentialsCredentials creds = null; + Principal returnedPrincipal = null; + try (Response response = + newRequest("http://localhost:%d/api/management/v1/principals") + .post(Entity.json(new CreatePrincipalRequest(principal, true)))) { + assertThat(response).returns(Response.Status.CREATED.getStatusCode(), Response::getStatus); + PrincipalWithCredentials parsed = response.readEntity(PrincipalWithCredentials.class); + creds = parsed.getCredentials(); + returnedPrincipal = parsed.getPrincipal(); + } + assertThat(creds.getClientId()).isEqualTo(returnedPrincipal.getClientId()); + + String oldClientId = creds.getClientId(); + String oldSecret = creds.getClientSecret(); + + // Now rotate the credentials. First, if we try to just use the adminToken to rotate the + // newly created principal's credentials, we should fail; rotateCredentials is only + // a "self" privilege that even admins can't inherit. + try (Response response = + newRequest("http://localhost:%d/api/management/v1/principals/myprincipal/rotate") + .post(Entity.json(""))) { + assertThat(response).returns(Response.Status.FORBIDDEN.getStatusCode(), Response::getStatus); + } + + // Get a fresh token associate with the principal itself. + String newPrincipalToken = + TokenUtils.getTokenFromSecrets( + EXT.client(), EXT.getLocalPort(), oldClientId, oldSecret, realm); + + // Any call should initially fail with error indicating that rotation is needed. + try (Response response = + newRequest( + "http://localhost:%d/api/management/v1/principals/myprincipal", + "Bearer " + newPrincipalToken) + .get()) { + assertThat(response).returns(Response.Status.FORBIDDEN.getStatusCode(), Response::getStatus); + ErrorResponse error = response.readEntity(ErrorResponse.class); + assertThat(error) + .isNotNull() + .extracting(ErrorResponse::message) + .asString() + .contains("PRINCIPAL_CREDENTIAL_ROTATION_REQUIRED_STATE"); + } + + // Now try to rotate using the principal's token. + try (Response response = + newRequest( + "http://localhost:%d/api/management/v1/principals/myprincipal/rotate", + "Bearer " + newPrincipalToken) + .post(Entity.json(""))) { + assertThat(response).returns(Response.Status.OK.getStatusCode(), Response::getStatus); + PrincipalWithCredentials parsed = response.readEntity(PrincipalWithCredentials.class); + creds = parsed.getCredentials(); + returnedPrincipal = parsed.getPrincipal(); + } + assertThat(creds.getClientId()).isEqualTo(returnedPrincipal.getClientId()); + + // ClientId shouldn't change + assertThat(creds.getClientId()).isEqualTo(oldClientId); + assertThat(creds.getClientSecret()).isNotEqualTo(oldSecret); + + // TODO: Test the validity of the old secret for getting tokens, here and then after a second + // rotation that makes the old secret fall off retention. + } + + @Test + public void testCreateListUpdateAndDeletePrincipal() { + Principal principal = + Principal.builder() + .setName("myprincipal") + .setProperties(Map.of("custom-tag", "foo")) + .build(); + try (Response response = + newRequest("http://localhost:%d/api/management/v1/principals") + .post(Entity.json(new CreatePrincipalRequest(principal, null)))) { + assertThat(response).returns(Response.Status.CREATED.getStatusCode(), Response::getStatus); + } + + // Second attempt to create the same entity should fail with CONFLICT. + try (Response response = + newRequest("http://localhost:%d/api/management/v1/principals") + .post(Entity.json(new CreatePrincipalRequest(principal, false)))) { + assertThat(response).returns(Response.Status.CONFLICT.getStatusCode(), Response::getStatus); + } + + // 200 successful GET after creation + Principal fetchedPrincipal = null; + try (Response response = + newRequest("http://localhost:%d/api/management/v1/principals/myprincipal").get()) { + assertThat(response).returns(200, Response::getStatus); + fetchedPrincipal = response.readEntity(Principal.class); + + assertThat(fetchedPrincipal.getName()).isEqualTo("myprincipal"); + assertThat(fetchedPrincipal.getProperties()).isEqualTo(Map.of("custom-tag", "foo")); + assertThat(fetchedPrincipal.getEntityVersion()).isGreaterThan(0); + } + + // Should list the principal. + try (Response response = newRequest("http://localhost:%d/api/management/v1/principals").get()) { + assertThat(response) + .returns(200, Response::getStatus) + .extracting(r -> r.readEntity(Principals.class)) + .extracting(Principals::getPrincipals) + .asInstanceOf(InstanceOfAssertFactories.list(Principal.class)) + .anySatisfy(pr -> assertThat(pr).returns("myprincipal", Principal::getName)); + } + + UpdatePrincipalRequest updateRequest = + new UpdatePrincipalRequest( + fetchedPrincipal.getEntityVersion(), Map.of("custom-tag", "updated")); + + // 200 successful update + try (Response response = + newRequest("http://localhost:%d/api/management/v1/principals/myprincipal") + .put(Entity.json(updateRequest))) { + assertThat(response).returns(200, Response::getStatus); + fetchedPrincipal = response.readEntity(Principal.class); + + assertThat(fetchedPrincipal.getProperties()).isEqualTo(Map.of("custom-tag", "updated")); + } + + // 200 GET after update should show new properties + try (Response response = + newRequest("http://localhost:%d/api/management/v1/principals/myprincipal").get()) { + assertThat(response).returns(200, Response::getStatus); + fetchedPrincipal = response.readEntity(Principal.class); + + assertThat(fetchedPrincipal.getProperties()).isEqualTo(Map.of("custom-tag", "updated")); + } + + // 204 Successful delete + try (Response response = + newRequest("http://localhost:%d/api/management/v1/principals/myprincipal").delete()) { + assertThat(response).returns(204, Response::getStatus); + } + + // NOT_FOUND after deletion + try (Response response = + newRequest("http://localhost:%d/api/management/v1/principals/myprincipal").get()) { + assertThat(response).returns(404, Response::getStatus); + } + + // Empty list + try (Response response = newRequest("http://localhost:%d/api/management/v1/principals").get()) { + assertThat(response) + .returns(200, Response::getStatus) + .extracting(r -> r.readEntity(Principals.class)) + .extracting(Principals::getPrincipals) + .asInstanceOf(InstanceOfAssertFactories.list(Principal.class)) + .noneSatisfy(pr -> assertThat(pr).returns("myprincipal", Principal::getName)); + } + } + + @Test + public void testCreatePrincipalWithInvalidName() { + String goodName = RandomStringUtils.random(MAX_IDENTIFIER_LENGTH, true, true); + Principal principal = + Principal.builder() + .setName(goodName) + .setProperties(Map.of("custom-tag", "good_principal")) + .build(); + try (Response response = + newRequest("http://localhost:%d/api/management/v1/principals") + .post(Entity.json(new CreatePrincipalRequest(principal, null)))) { + assertThat(response).returns(Response.Status.CREATED.getStatusCode(), Response::getStatus); + } + + String longInvalidName = RandomStringUtils.random(MAX_IDENTIFIER_LENGTH + 1, true, true); + List invalidPrincipalNames = + Arrays.asList( + longInvalidName, + "", + "system$principal1", + "SYSTEM$TestPrincipal", + "System$test_principal", + " SysTeM$ principal"); + + for (String invalidPrincipalName : invalidPrincipalNames) { + principal = + Principal.builder() + .setName(invalidPrincipalName) + .setProperties(Map.of("custom-tag", "bad_principal")) + .build(); + + try (Response response = + newRequest("http://localhost:%d/api/management/v1/principals") + .post(Entity.json(new CreatePrincipalRequest(principal, false)))) { + assertThat(response) + .returns(Response.Status.BAD_REQUEST.getStatusCode(), Response::getStatus); + assertThat(response.hasEntity()).isTrue(); + ErrorResponse errorResponse = response.readEntity(ErrorResponse.class); + assertThat(errorResponse.message()).contains("Invalid value:"); + } + } + } + + @Test + public void testGetPrincipalWithInvalidName() { + String longInvalidName = RandomStringUtils.random(MAX_IDENTIFIER_LENGTH + 1, true, true); + List invalidPrincipalNames = + Arrays.asList( + longInvalidName, + "system$principal1", + "SYSTEM$TestPrincipal", + "System$test_principal", + " SysTeM$ principal"); + + for (String invalidPrincipalName : invalidPrincipalNames) { + try (Response response = + newRequest("http://localhost:%d/api/management/v1/principals/" + invalidPrincipalName) + .get()) { + assertThat(response) + .returns(Response.Status.BAD_REQUEST.getStatusCode(), Response::getStatus); + assertThat(response.hasEntity()).isTrue(); + ErrorResponse errorResponse = response.readEntity(ErrorResponse.class); + assertThat(errorResponse.message()).contains("Invalid value:"); + } + } + } + + @Test + public void testCreateListUpdateAndDeletePrincipalRole() { + PrincipalRole principalRole = + new PrincipalRole("myprincipalrole", Map.of("custom-tag", "foo"), 0L, 0L, 1); + try (Response response = + newRequest("http://localhost:%d/api/management/v1/principal-roles") + .post(Entity.json(new CreatePrincipalRoleRequest(principalRole)))) { + assertThat(response).returns(Response.Status.CREATED.getStatusCode(), Response::getStatus); + } + + // Second attempt to create the same entity should fail with CONFLICT. + try (Response response = + newRequest("http://localhost:%d/api/management/v1/principal-roles") + .post(Entity.json(new CreatePrincipalRoleRequest(principalRole)))) { + + assertThat(response).returns(Response.Status.CONFLICT.getStatusCode(), Response::getStatus); + } + + // 200 successful GET after creation + PrincipalRole fetchedPrincipalRole = null; + try (Response response = + newRequest("http://localhost:%d/api/management/v1/principal-roles/myprincipalrole").get()) { + + assertThat(response).returns(200, Response::getStatus); + fetchedPrincipalRole = response.readEntity(PrincipalRole.class); + + assertThat(fetchedPrincipalRole.getName()).isEqualTo("myprincipalrole"); + assertThat(fetchedPrincipalRole.getProperties()).isEqualTo(Map.of("custom-tag", "foo")); + assertThat(fetchedPrincipalRole.getEntityVersion()).isGreaterThan(0); + } + + // Should list the principalRole. + try (Response response = + newRequest("http://localhost:%d/api/management/v1/principal-roles").get()) { + + assertThat(response) + .returns(200, Response::getStatus) + .extracting(r -> r.readEntity(PrincipalRoles.class)) + .extracting(PrincipalRoles::getRoles) + .asInstanceOf(InstanceOfAssertFactories.list(PrincipalRole.class)) + .anySatisfy(pr -> assertThat(pr).returns("myprincipalrole", PrincipalRole::getName)); + } + + UpdatePrincipalRoleRequest updateRequest = + new UpdatePrincipalRoleRequest( + fetchedPrincipalRole.getEntityVersion(), Map.of("custom-tag", "updated")); + + // 200 successful update + try (Response response = + newRequest("http://localhost:%d/api/management/v1/principal-roles/myprincipalrole") + .put(Entity.json(updateRequest))) { + assertThat(response).returns(200, Response::getStatus); + fetchedPrincipalRole = response.readEntity(PrincipalRole.class); + + assertThat(fetchedPrincipalRole.getProperties()).isEqualTo(Map.of("custom-tag", "updated")); + } + + // 200 GET after update should show new properties + try (Response response = + newRequest("http://localhost:%d/api/management/v1/principal-roles/myprincipalrole").get()) { + assertThat(response).returns(200, Response::getStatus); + fetchedPrincipalRole = response.readEntity(PrincipalRole.class); + + assertThat(fetchedPrincipalRole.getProperties()).isEqualTo(Map.of("custom-tag", "updated")); + } + + // 204 Successful delete + try (Response response = + newRequest("http://localhost:%d/api/management/v1/principal-roles/myprincipalrole") + .delete()) { + + assertThat(response).returns(204, Response::getStatus); + } + + // NOT_FOUND after deletion + try (Response response = + newRequest("http://localhost:%d/api/management/v1/principal-roles/myprincipalrole").get()) { + + assertThat(response).returns(404, Response::getStatus); + } + + // Empty list + try (Response response = + newRequest("http://localhost:%d/api/management/v1/principal-roles").get()) { + + assertThat(response) + .returns(200, Response::getStatus) + .extracting(r -> r.readEntity(PrincipalRoles.class)) + .extracting(PrincipalRoles::getRoles) + .asInstanceOf(InstanceOfAssertFactories.list(PrincipalRole.class)) + .noneSatisfy(pr -> assertThat(pr).returns("myprincipalrole", PrincipalRole::getName)); + } + } + + @Test + public void testCreatePrincipalRoleInvalidName() { + String goodName = RandomStringUtils.random(MAX_IDENTIFIER_LENGTH, true, true); + PrincipalRole principalRole = + new PrincipalRole(goodName, Map.of("custom-tag", "good_principal_role"), 0L, 0L, 1); + try (Response response = + newRequest("http://localhost:%d/api/management/v1/principal-roles") + .post(Entity.json(new CreatePrincipalRoleRequest(principalRole)))) { + assertThat(response).returns(Response.Status.CREATED.getStatusCode(), Response::getStatus); + } + + String longInvalidName = RandomStringUtils.random(MAX_IDENTIFIER_LENGTH + 1, true, true); + List invalidPrincipalRoleNames = + Arrays.asList( + longInvalidName, + "", + "system$principalrole1", + "SYSTEM$TestPrincipalRole", + "System$test_principal_role", + " SysTeM$ principal role"); + + for (String invalidPrincipalRoleName : invalidPrincipalRoleNames) { + principalRole = + new PrincipalRole( + invalidPrincipalRoleName, Map.of("custom-tag", "bad_principal_role"), 0L, 0L, 1); + + try (Response response = + newRequest("http://localhost:%d/api/management/v1/principal-roles") + .post(Entity.json(new CreatePrincipalRoleRequest(principalRole)))) { + assertThat(response) + .returns(Response.Status.BAD_REQUEST.getStatusCode(), Response::getStatus); + assertThat(response.hasEntity()).isTrue(); + ErrorResponse errorResponse = response.readEntity(ErrorResponse.class); + assertThat(errorResponse.message()).contains("Invalid value:"); + } + } + } + + @Test + public void testGetPrincipalRoleInvalidName() { + String longInvalidName = RandomStringUtils.random(MAX_IDENTIFIER_LENGTH + 1, true, true); + List invalidPrincipalRoleNames = + Arrays.asList( + longInvalidName, + "system$principalrole1", + "SYSTEM$TestPrincipalRole", + "System$test_principal_role", + " SysTeM$ principal role"); + + for (String invalidPrincipalRoleName : invalidPrincipalRoleNames) { + try (Response response = + newRequest( + "http://localhost:%d/api/management/v1/principal-roles/" + + invalidPrincipalRoleName) + .get()) { + assertThat(response) + .returns(Response.Status.BAD_REQUEST.getStatusCode(), Response::getStatus); + assertThat(response.hasEntity()).isTrue(); + ErrorResponse errorResponse = response.readEntity(ErrorResponse.class); + assertThat(errorResponse.message()).contains("Invalid value:"); + } + } + } + + @Test + public void testCreateListUpdateAndDeleteCatalogRole() { + Catalog catalog = + PolarisCatalog.builder() + .setType(Catalog.TypeEnum.INTERNAL) + .setName("mycatalog1") + .setProperties(new CatalogProperties("s3://required/base/location")) + .setStorageConfigInfo( + new AwsStorageConfigInfo( + "arn:aws:iam::012345678901:role/jdoe", StorageConfigInfo.StorageTypeEnum.S3)) + .build(); + try (Response response = + newRequest("http://localhost:%d/api/management/v1/catalogs") + .post(Entity.json(new CreateCatalogRequest(catalog)))) { + + assertThat(response).returns(Response.Status.CREATED.getStatusCode(), Response::getStatus); + } + + Catalog catalog2 = + PolarisCatalog.builder() + .setType(Catalog.TypeEnum.INTERNAL) + .setName("mycatalog2") + .setStorageConfigInfo( + new AwsStorageConfigInfo( + "arn:aws:iam::012345678901:role/jdoe", StorageConfigInfo.StorageTypeEnum.S3)) + .setProperties(new CatalogProperties("s3://required/base/location")) + .build(); + try (Response response = + newRequest("http://localhost:%d/api/management/v1/catalogs") + .post(Entity.json(new CreateCatalogRequest(catalog2)))) { + + assertThat(response).returns(Response.Status.CREATED.getStatusCode(), Response::getStatus); + } + + CatalogRole catalogRole = + new CatalogRole("mycatalogrole", Map.of("custom-tag", "foo"), 0L, 0L, 1); + try (Response response = + newRequest("http://localhost:%d/api/management/v1/catalogs/mycatalog1/catalog-roles") + .post(Entity.json(new CreateCatalogRoleRequest(catalogRole)))) { + + assertThat(response).returns(Response.Status.CREATED.getStatusCode(), Response::getStatus); + } + + // Second attempt to create the same entity should fail with CONFLICT. + try (Response response = + newRequest("http://localhost:%d/api/management/v1/catalogs/mycatalog1/catalog-roles") + .post(Entity.json(new CreateCatalogRoleRequest(catalogRole)))) { + + assertThat(response).returns(Response.Status.CONFLICT.getStatusCode(), Response::getStatus); + } + + // 200 successful GET after creation + CatalogRole fetchedCatalogRole = null; + try (Response response = + newRequest( + "http://localhost:%d/api/management/v1/catalogs/mycatalog1/catalog-roles/mycatalogrole") + .get()) { + + assertThat(response).returns(200, Response::getStatus); + fetchedCatalogRole = response.readEntity(CatalogRole.class); + + assertThat(fetchedCatalogRole.getName()).isEqualTo("mycatalogrole"); + assertThat(fetchedCatalogRole.getProperties()).isEqualTo(Map.of("custom-tag", "foo")); + assertThat(fetchedCatalogRole.getEntityVersion()).isGreaterThan(0); + } + + // Should list the catalogRole. + try (Response response = + newRequest("http://localhost:%d/api/management/v1/catalogs/mycatalog1/catalog-roles") + .get()) { + + assertThat(response) + .returns(200, Response::getStatus) + .extracting(r -> r.readEntity(CatalogRoles.class)) + .extracting(CatalogRoles::getRoles) + .asInstanceOf(InstanceOfAssertFactories.list(CatalogRole.class)) + .anySatisfy(cr -> assertThat(cr).returns("mycatalogrole", CatalogRole::getName)); + } + + // Empty list if listing in catalog2 + try (Response response = + newRequest("http://localhost:%d/api/management/v1/catalogs/mycatalog2/catalog-roles") + .get()) { + + assertThat(response) + .returns(200, Response::getStatus) + .extracting(r -> r.readEntity(CatalogRoles.class)) + .extracting(CatalogRoles::getRoles) + .asInstanceOf(InstanceOfAssertFactories.list(CatalogRole.class)) + .satisfiesExactly( + cr -> + assertThat(cr) + .returns( + PolarisEntityConstants.getNameOfCatalogAdminRole(), + CatalogRole::getName)); + } + + UpdateCatalogRoleRequest updateRequest = + new UpdateCatalogRoleRequest( + fetchedCatalogRole.getEntityVersion(), Map.of("custom-tag", "updated")); + + // 200 successful update + try (Response response = + newRequest( + "http://localhost:%d/api/management/v1/catalogs/mycatalog1/catalog-roles/mycatalogrole") + .put(Entity.json(updateRequest))) { + assertThat(response).returns(200, Response::getStatus); + fetchedCatalogRole = response.readEntity(CatalogRole.class); + + assertThat(fetchedCatalogRole.getProperties()).isEqualTo(Map.of("custom-tag", "updated")); + } + + // 200 GET after update should show new properties + try (Response response = + newRequest( + "http://localhost:%d/api/management/v1/catalogs/mycatalog1/catalog-roles/mycatalogrole") + .get()) { + assertThat(response).returns(200, Response::getStatus); + fetchedCatalogRole = response.readEntity(CatalogRole.class); + + assertThat(fetchedCatalogRole.getProperties()).isEqualTo(Map.of("custom-tag", "updated")); + } + + // 204 Successful delete + try (Response response = + newRequest( + "http://localhost:%d/api/management/v1/catalogs/mycatalog1/catalog-roles/mycatalogrole") + .delete()) { + + assertThat(response).returns(204, Response::getStatus); + } + + // NOT_FOUND after deletion + try (Response response = + newRequest( + "http://localhost:%d/api/management/v1/catalogs/mycatalog1/catalog-roles/mycatalogrole") + .get()) { + + assertThat(response).returns(404, Response::getStatus); + } + + // Empty list + try (Response response = + newRequest("http://localhost:%d/api/management/v1/catalogs/mycatalog1/catalog-roles") + .get()) { + + assertThat(response) + .returns(200, Response::getStatus) + .extracting(r -> r.readEntity(CatalogRoles.class)) + .extracting(CatalogRoles::getRoles) + .asInstanceOf(InstanceOfAssertFactories.list(CatalogRole.class)) + .noneSatisfy(cr -> assertThat(cr).returns("mycatalogrole", CatalogRole::getName)); + } + + // 204 Successful delete mycatalog + try (Response response = + newRequest("http://localhost:%d/api/management/v1/catalogs/mycatalog1").delete()) { + + assertThat(response).returns(204, Response::getStatus); + } + + // 204 Successful delete mycatalog2 + try (Response response = + newRequest("http://localhost:%d/api/management/v1/catalogs/mycatalog2").delete()) { + + assertThat(response).returns(204, Response::getStatus); + } + } + + @Test + public void testAssignListAndRevokePrincipalRoles() { + // Create two Principals + Principal principal1 = new Principal("myprincipal1"); + try (Response response = + newRequest("http://localhost:%d/api/management/v1/principals") + .post(Entity.json(new CreatePrincipalRequest(principal1, false)))) { + + assertThat(response).returns(Response.Status.CREATED.getStatusCode(), Response::getStatus); + } + + Principal principal2 = new Principal("myprincipal2"); + try (Response response = + newRequest("http://localhost:%d/api/management/v1/principals") + .post(Entity.json(new CreatePrincipalRequest(principal2, false)))) { + + assertThat(response).returns(Response.Status.CREATED.getStatusCode(), Response::getStatus); + } + + // One PrincipalRole + PrincipalRole principalRole = new PrincipalRole("myprincipalrole"); + try (Response response = + newRequest("http://localhost:%d/api/management/v1/principal-roles") + .post(Entity.json(new CreatePrincipalRoleRequest(principalRole)))) { + + assertThat(response).returns(Response.Status.CREATED.getStatusCode(), Response::getStatus); + } + + // Assign the role to myprincipal1 + try (Response response = + newRequest("http://localhost:%d/api/management/v1/principals/myprincipal1/principal-roles") + .put(Entity.json(principalRole))) { + + assertThat(response).returns(Response.Status.CREATED.getStatusCode(), Response::getStatus); + } + + // Should list myprincipalrole + try (Response response = + newRequest("http://localhost:%d/api/management/v1/principals/myprincipal1/principal-roles") + .get()) { + + assertThat(response) + .returns(200, Response::getStatus) + .extracting(r -> r.readEntity(PrincipalRoles.class)) + .extracting(PrincipalRoles::getRoles) + .asInstanceOf(InstanceOfAssertFactories.list(PrincipalRole.class)) + .hasSize(1) + .satisfiesExactly( + pr -> assertThat(pr).returns("myprincipalrole", PrincipalRole::getName)); + } + + // Should list myprincipal1 if listing assignees of myprincipalrole + try (Response response = + newRequest( + "http://localhost:%d/api/management/v1/principal-roles/myprincipalrole/principals") + .get()) { + + assertThat(response) + .returns(200, Response::getStatus) + .extracting(r -> r.readEntity(Principals.class)) + .extracting(Principals::getPrincipals) + .asInstanceOf(InstanceOfAssertFactories.list(Principal.class)) + .hasSize(1) + .satisfiesExactly(pr -> assertThat(pr).returns("myprincipal1", Principal::getName)); + } + + // Empty list if listing in principal2 + try (Response response = + newRequest("http://localhost:%d/api/management/v1/principals/myprincipal2/principal-roles") + .get()) { + + assertThat(response) + .returns(200, Response::getStatus) + .extracting(r -> r.readEntity(PrincipalRoles.class)) + .returns(List.of(), PrincipalRoles::getRoles); + } + + // 204 Successful revoke + try (Response response = + newRequest( + "http://localhost:%d/api/management/v1/principals/myprincipal1/principal-roles/myprincipalrole") + .delete()) { + + assertThat(response).returns(204, Response::getStatus); + } + + // Empty list + try (Response response = + newRequest("http://localhost:%d/api/management/v1/principals/myprincipal1/principal-roles") + .get()) { + + assertThat(response) + .returns(200, Response::getStatus) + .extracting(r -> r.readEntity(PrincipalRoles.class)) + .returns(List.of(), PrincipalRoles::getRoles); + } + try (Response response = + newRequest( + "http://localhost:%d/api/management/v1/principal-roles/myprincipalrole/principals") + .get()) { + + assertThat(response) + .returns(200, Response::getStatus) + .extracting(r -> r.readEntity(Principals.class)) + .returns(List.of(), Principals::getPrincipals); + } + + // 204 Successful delete myprincipal1 + try (Response response = + newRequest("http://localhost:%d/api/management/v1/principals/myprincipal1").delete()) { + + assertThat(response).returns(204, Response::getStatus); + } + + // 204 Successful delete myprincipal2 + try (Response response = + newRequest("http://localhost:%d/api/management/v1/principals/myprincipal2").delete()) { + + assertThat(response).returns(204, Response::getStatus); + } + + // 204 Successful delete myprincipalrole + try (Response response = + newRequest("http://localhost:%d/api/management/v1/principal-roles/myprincipalrole") + .delete()) { + + assertThat(response).returns(204, Response::getStatus); + } + } + + @Test + public void testAssignListAndRevokeCatalogRoles() { + // Create two PrincipalRoles + PrincipalRole principalRole1 = new PrincipalRole("mypr1"); + try (Response response = + newRequest("http://localhost:%d/api/management/v1/principal-roles") + .post(Entity.json(new CreatePrincipalRoleRequest(principalRole1)))) { + + assertThat(response).returns(Response.Status.CREATED.getStatusCode(), Response::getStatus); + } + + PrincipalRole principalRole2 = new PrincipalRole("mypr2"); + try (Response response = + newRequest("http://localhost:%d/api/management/v1/principal-roles") + .post(Entity.json(new CreatePrincipalRoleRequest(principalRole2)))) { + + assertThat(response).returns(Response.Status.CREATED.getStatusCode(), Response::getStatus); + } + + // One CatalogRole + Catalog catalog = + PolarisCatalog.builder() + .setType(Catalog.TypeEnum.INTERNAL) + .setName("mycatalog") + .setStorageConfigInfo( + new AwsStorageConfigInfo( + "arn:aws:iam::012345678901:role/jdoe", StorageConfigInfo.StorageTypeEnum.S3)) + .setProperties(new CatalogProperties("s3://bucket1/")) + .build(); + try (Response response = + newRequest("http://localhost:%d/api/management/v1/catalogs") + .post(Entity.json(new CreateCatalogRequest(catalog)))) { + + assertThat(response).returns(Response.Status.CREATED.getStatusCode(), Response::getStatus); + } + + CatalogRole catalogRole = new CatalogRole("mycr"); + try (Response response = + newRequest("http://localhost:%d/api/management/v1/catalogs/mycatalog/catalog-roles") + .post(Entity.json(new CreateCatalogRoleRequest(catalogRole)))) { + + assertThat(response).returns(Response.Status.CREATED.getStatusCode(), Response::getStatus); + } + + // Create another one in a different catalog. + Catalog otherCatalog = + PolarisCatalog.builder() + .setType(Catalog.TypeEnum.INTERNAL) + .setName("othercatalog") + .setProperties(new CatalogProperties("s3://path/to/data")) + .setStorageConfigInfo( + new AwsStorageConfigInfo( + "arn:aws:iam::012345678901:role/jdoe", StorageConfigInfo.StorageTypeEnum.S3)) + .build(); + try (Response response = + newRequest("http://localhost:%d/api/management/v1/catalogs") + .post(Entity.json(new CreateCatalogRequest(otherCatalog)))) { + + assertThat(response).returns(Response.Status.CREATED.getStatusCode(), Response::getStatus); + } + + CatalogRole otherCatalogRole = new CatalogRole("myothercr"); + try (Response response = + newRequest("http://localhost:%d/api/management/v1/catalogs/othercatalog/catalog-roles") + .post(Entity.json(new CreateCatalogRoleRequest(otherCatalogRole)))) { + + assertThat(response).returns(Response.Status.CREATED.getStatusCode(), Response::getStatus); + } + + // Assign both the roles to mypr1 + try (Response response = + newRequest( + "http://localhost:%d/api/management/v1/principal-roles/mypr1/catalog-roles/mycatalog") + .put(Entity.json(new GrantCatalogRoleRequest(catalogRole)))) { + + assertThat(response).returns(Response.Status.CREATED.getStatusCode(), Response::getStatus); + } + try (Response response = + newRequest( + "http://localhost:%d/api/management/v1/principal-roles/mypr1/catalog-roles/othercatalog") + .put(Entity.json(new GrantCatalogRoleRequest(otherCatalogRole)))) { + + assertThat(response).returns(Response.Status.CREATED.getStatusCode(), Response::getStatus); + } + + // Should list only mycr + try (Response response = + newRequest( + "http://localhost:%d/api/management/v1/principal-roles/mypr1/catalog-roles/mycatalog") + .get()) { + + assertThat(response) + .returns(200, Response::getStatus) + .extracting(r -> r.readEntity(CatalogRoles.class)) + .extracting(CatalogRoles::getRoles) + .asInstanceOf(InstanceOfAssertFactories.list(CatalogRole.class)) + .hasSize(1) + .satisfiesExactly(cr -> assertThat(cr).returns("mycr", CatalogRole::getName)); + } + + // Should list mypr1 if listing assignees of mycr + try (Response response = + newRequest( + "http://localhost:%d/api/management/v1/catalogs/mycatalog/catalog-roles/mycr/principal-roles") + .get()) { + + assertThat(response) + .returns(200, Response::getStatus) + .extracting(r -> r.readEntity(PrincipalRoles.class)) + .extracting(PrincipalRoles::getRoles) + .asInstanceOf(InstanceOfAssertFactories.list(PrincipalRole.class)) + .hasSize(1) + .satisfiesExactly(pr -> assertThat(pr).returns("mypr1", PrincipalRole::getName)); + } + + // Empty list if listing in principalRole2 + try (Response response = + newRequest( + "http://localhost:%d/api/management/v1/principal-roles/mypr2/catalog-roles/mycatalog") + .get()) { + + assertThat(response) + .returns(200, Response::getStatus) + .extracting(r -> r.readEntity(CatalogRoles.class)) + .returns(List.of(), CatalogRoles::getRoles); + } + + // 204 Successful revoke + try (Response response = + newRequest( + "http://localhost:%d/api/management/v1/principal-roles/mypr1/catalog-roles/mycatalog/mycr") + .delete()) { + + assertThat(response).returns(204, Response::getStatus); + } + + // Empty list + try (Response response = + newRequest( + "http://localhost:%d/api/management/v1/principal-roles/mypr1/catalog-roles/mycatalog") + .get()) { + + assertThat(response) + .returns(200, Response::getStatus) + .extracting(r -> r.readEntity(CatalogRoles.class)) + .returns(List.of(), CatalogRoles::getRoles); + } + try (Response response = + newRequest( + "http://localhost:%d/api/management/v1/catalogs/mycatalog/catalog-roles/mycr/principal-roles") + .get()) { + + assertThat(response) + .returns(200, Response::getStatus) + .extracting(r -> r.readEntity(PrincipalRoles.class)) + .returns(List.of(), PrincipalRoles::getRoles); + } + + // 204 Successful delete mypr1 + try (Response response = + newRequest("http://localhost:%d/api/management/v1/principal-roles/mypr1").delete()) { + + assertThat(response).returns(204, Response::getStatus); + } + + // 204 Successful delete mypr2 + try (Response response = + newRequest("http://localhost:%d/api/management/v1/principal-roles/mypr2").delete()) { + + assertThat(response).returns(204, Response::getStatus); + } + + // 204 Successful delete mycr + try (Response response = + newRequest("http://localhost:%d/api/management/v1/catalogs/mycatalog/catalog-roles/mycr") + .delete()) { + + assertThat(response).returns(204, Response::getStatus); + } + + // 204 Successful delete mycatalog + try (Response response = + newRequest("http://localhost:%d/api/management/v1/catalogs/mycatalog").delete()) { + + assertThat(response).returns(204, Response::getStatus); + } + + // 204 Successful delete myothercr + try (Response response = + newRequest( + "http://localhost:%d/api/management/v1/catalogs/othercatalog/catalog-roles/myothercr") + .delete()) { + + assertThat(response).returns(204, Response::getStatus); + } + + // 204 Successful delete othercatalog + try (Response response = + newRequest("http://localhost:%d/api/management/v1/catalogs/othercatalog").delete()) { + + assertThat(response).returns(204, Response::getStatus); + } + } +} diff --git a/polaris-service/src/test/java/io/polaris/service/auth/JWTRSAKeyPairTest.java b/polaris-service/src/test/java/io/polaris/service/auth/JWTRSAKeyPairTest.java new file mode 100644 index 0000000000..1cf8927537 --- /dev/null +++ b/polaris-service/src/test/java/io/polaris/service/auth/JWTRSAKeyPairTest.java @@ -0,0 +1,144 @@ +package io.polaris.service.auth; + +import static org.assertj.core.api.Fail.fail; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import com.auth0.jwt.JWT; +import com.auth0.jwt.JWTVerifier; +import com.auth0.jwt.algorithms.Algorithm; +import com.auth0.jwt.interfaces.DecodedJWT; +import io.polaris.core.PolarisCallContext; +import io.polaris.core.context.CallContext; +import io.polaris.core.context.RealmContext; +import io.polaris.core.entity.PolarisBaseEntity; +import io.polaris.core.entity.PolarisEntitySubType; +import io.polaris.core.entity.PolarisEntityType; +import io.polaris.core.entity.PolarisPrincipalSecrets; +import io.polaris.core.persistence.PolarisEntityManager; +import io.polaris.core.persistence.PolarisMetaStoreManager; +import io.polaris.core.storage.cache.StorageCredentialCache; +import io.polaris.service.config.DefaultConfigurationStore; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +public class JWTRSAKeyPairTest { + + private void writePemToTmpFile(String privateFileLocation, String publicFileLocation) + throws Exception { + new File(privateFileLocation).delete(); + new File(publicFileLocation).delete(); + KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); + kpg.initialize(2048); + KeyPair kp = kpg.generateKeyPair(); + try (BufferedWriter writer = new BufferedWriter(new FileWriter(privateFileLocation, true))) { + writer.write("-----BEGIN PRIVATE KEY-----"); // pragma: allowlist secret + writer.newLine(); + writer.write(Base64.getMimeEncoder().encodeToString(kp.getPrivate().getEncoded())); + writer.newLine(); + writer.write("-----END PRIVATE KEY-----"); + writer.newLine(); + } + try (BufferedWriter writer = new BufferedWriter(new FileWriter(publicFileLocation, true))) { + writer.write("-----BEGIN PUBLIC KEY-----"); + writer.newLine(); + writer.write(Base64.getMimeEncoder().encodeToString(kp.getPublic().getEncoded())); + writer.newLine(); + writer.write("-----END PUBLIC KEY-----"); + writer.newLine(); + } + } + + public CallContext getTestCallContext(PolarisCallContext polarisCallContext) { + return CallContext.setCurrentContext( + new CallContext() { + @Override + public RealmContext getRealmContext() { + return null; + } + + @Override + public PolarisCallContext getPolarisCallContext() { + return polarisCallContext; + } + + @Override + public Map contextVariables() { + return Map.of("token", "me"); + } + }); + } + + @Test + public void testSuccessfulTokenGeneration() throws Exception { + String privateFileLocation = "/tmp/test-private.pem"; + String publicFileLocation = "/tmp/test-public.pem"; + writePemToTmpFile(privateFileLocation, publicFileLocation); + + final String clientId = "test-client-id"; + final String scope = "PRINCIPAL_ROLE:TEST"; + + Map config = new HashMap<>(); + + config.put("LOCAL_PRIVATE_KEY_LOCATION_KEY", privateFileLocation); + config.put("LOCAL_PUBLIC_LOCATION_KEY", publicFileLocation); + + DefaultConfigurationStore store = new DefaultConfigurationStore(config); + PolarisCallContext polarisCallContext = new PolarisCallContext(null, null, store, null); + CallContext.setCurrentContext(getTestCallContext(polarisCallContext)); + PolarisMetaStoreManager metastoreManager = Mockito.mock(PolarisMetaStoreManager.class); + String mainSecret = "client-secret"; + PolarisPrincipalSecrets principalSecrets = + new PolarisPrincipalSecrets(1L, clientId, mainSecret, "otherSecret"); + PolarisEntityManager entityManager = + new PolarisEntityManager(metastoreManager, Mockito::mock, new StorageCredentialCache()); + Mockito.when(metastoreManager.loadPrincipalSecrets(polarisCallContext, clientId)) + .thenReturn(new PolarisMetaStoreManager.PrincipalSecretsResult(principalSecrets)); + PolarisBaseEntity principal = + new PolarisBaseEntity( + 0L, + 1L, + PolarisEntityType.PRINCIPAL, + PolarisEntitySubType.NULL_SUBTYPE, + 0L, + "principal"); + Mockito.when(metastoreManager.loadEntity(polarisCallContext, 0L, 1L)) + .thenReturn(new PolarisMetaStoreManager.EntityResult(principal)); + TokenBroker tokenBroker = new JWTRSAKeyPair(entityManager, 420); + TokenResponse token = null; + try { + token = + tokenBroker.generateFromClientSecrets( + clientId, mainSecret, TokenRequestValidator.CLIENT_CREDENTIALS, scope); + } catch (Exception e) { + fail("Unexpected exception: " + e); + } + assertNotNull(token); + assertEquals(420, token.getExpiresIn()); + + LocalRSAKeyProvider provider = new LocalRSAKeyProvider(); + assertNotNull(provider.getPrivateKey()); + assertNotNull(provider.getPublicKey()); + JWTVerifier verifier = + JWT.require( + Algorithm.RSA256( + (RSAPublicKey) provider.getPublicKey(), + (RSAPrivateKey) provider.getPrivateKey())) + .withIssuer("polaris") + .build(); + DecodedJWT decodedJWT = verifier.verify(token.getAccessToken()); + assertNotNull(decodedJWT); + assertEquals(decodedJWT.getClaim("scope").asString(), "PRINCIPAL_ROLE:TEST"); + assertEquals(decodedJWT.getClaim("client_id").asString(), "test-client-id"); + } +} diff --git a/polaris-service/src/test/java/io/polaris/service/auth/JWTSymmetricKeyGeneratorTest.java b/polaris-service/src/test/java/io/polaris/service/auth/JWTSymmetricKeyGeneratorTest.java new file mode 100644 index 0000000000..6176228fca --- /dev/null +++ b/polaris-service/src/test/java/io/polaris/service/auth/JWTSymmetricKeyGeneratorTest.java @@ -0,0 +1,79 @@ +package io.polaris.service.auth; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import com.auth0.jwt.JWT; +import com.auth0.jwt.JWTVerifier; +import com.auth0.jwt.algorithms.Algorithm; +import com.auth0.jwt.interfaces.DecodedJWT; +import io.polaris.core.PolarisCallContext; +import io.polaris.core.context.CallContext; +import io.polaris.core.context.RealmContext; +import io.polaris.core.entity.PolarisBaseEntity; +import io.polaris.core.entity.PolarisEntitySubType; +import io.polaris.core.entity.PolarisEntityType; +import io.polaris.core.entity.PolarisPrincipalSecrets; +import io.polaris.core.persistence.PolarisEntityManager; +import io.polaris.core.persistence.PolarisMetaStoreManager; +import io.polaris.core.storage.cache.StorageCredentialCache; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +public class JWTSymmetricKeyGeneratorTest { + + /** Sanity test to verify that we can generate a token */ + @Test + public void testJWTSymmetricKeyGenerator() { + PolarisCallContext polarisCallContext = new PolarisCallContext(null, null, null, null); + CallContext.setCurrentContext( + new CallContext() { + @Override + public RealmContext getRealmContext() { + return () -> "realm"; + } + + @Override + public PolarisCallContext getPolarisCallContext() { + return polarisCallContext; + } + + @Override + public Map contextVariables() { + return Map.of(); + } + }); + PolarisMetaStoreManager metastoreManager = Mockito.mock(PolarisMetaStoreManager.class); + String mainSecret = "test_secret"; + String clientId = "test_client_id"; + PolarisPrincipalSecrets principalSecrets = + new PolarisPrincipalSecrets(1L, clientId, mainSecret, "otherSecret"); + PolarisEntityManager entityManager = + new PolarisEntityManager(metastoreManager, Mockito::mock, new StorageCredentialCache()); + Mockito.when(metastoreManager.loadPrincipalSecrets(polarisCallContext, clientId)) + .thenReturn(new PolarisMetaStoreManager.PrincipalSecretsResult(principalSecrets)); + PolarisBaseEntity principal = + new PolarisBaseEntity( + 0L, + 1L, + PolarisEntityType.PRINCIPAL, + PolarisEntitySubType.NULL_SUBTYPE, + 0L, + "principal"); + Mockito.when(metastoreManager.loadEntity(polarisCallContext, 0L, 1L)) + .thenReturn(new PolarisMetaStoreManager.EntityResult(principal)); + TokenBroker generator = new JWTSymmetricKeyBroker(entityManager, 666, () -> "polaris"); + TokenResponse token = + generator.generateFromClientSecrets( + clientId, mainSecret, TokenRequestValidator.CLIENT_CREDENTIALS, "PRINCIPAL_ROLE:TEST"); + assertNotNull(token); + + JWTVerifier verifier = JWT.require(Algorithm.HMAC256("polaris")).withIssuer("polaris").build(); + DecodedJWT decodedJWT = verifier.verify(token.getAccessToken()); + assertNotNull(decodedJWT); + assertEquals(666, token.getExpiresIn()); + assertEquals(decodedJWT.getClaim("scope").asString(), "PRINCIPAL_ROLE:TEST"); + assertEquals(decodedJWT.getClaim("client_id").asString(), clientId); + } +} diff --git a/polaris-service/src/test/java/io/polaris/service/auth/TokenRequestValidatorTest.java b/polaris-service/src/test/java/io/polaris/service/auth/TokenRequestValidatorTest.java new file mode 100644 index 0000000000..37217a40d7 --- /dev/null +++ b/polaris-service/src/test/java/io/polaris/service/auth/TokenRequestValidatorTest.java @@ -0,0 +1,73 @@ +package io.polaris.service.auth; + +import java.util.Arrays; +import java.util.Optional; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class TokenRequestValidatorTest { + @Test + public void testValidateForClientCredentialsFlowNullClientId() { + Assertions.assertEquals( + OAuthTokenErrorResponse.Error.invalid_client, + new TokenRequestValidator() + .validateForClientCredentialsFlow(null, "notnull", "notnull", "nontnull") + .get()); + Assertions.assertEquals( + OAuthTokenErrorResponse.Error.invalid_client, + new TokenRequestValidator() + .validateForClientCredentialsFlow("", "notnull", "notnull", "nonnull") + .get()); + } + + @Test + public void testValidateForClientCredentialsFlowNullClientSecret() { + Assertions.assertEquals( + OAuthTokenErrorResponse.Error.invalid_client, + new TokenRequestValidator() + .validateForClientCredentialsFlow("client-id", null, "notnull", "nontnull") + .get()); + Assertions.assertEquals( + OAuthTokenErrorResponse.Error.invalid_client, + new TokenRequestValidator() + .validateForClientCredentialsFlow("client-id", "", "notnull", "notnull") + .get()); + } + + @Test + public void testValidateForClientCredentialsFlowInvalidGrantType() { + Assertions.assertEquals( + OAuthTokenErrorResponse.Error.invalid_grant, + new TokenRequestValidator() + .validateForClientCredentialsFlow( + "client-id", "client-secret", "not-client-credentials", "notnull") + .get()); + Assertions.assertEquals( + OAuthTokenErrorResponse.Error.invalid_grant, + new TokenRequestValidator() + .validateForClientCredentialsFlow("client-id", "client-secret", "grant", "notnull") + .get()); + } + + @Test + public void testValidateForClientCredentialsFlowInvalidScope() { + for (String scope : + Arrays.asList("null", "", ",", "ALL", "PRINCIPAL_ROLE:", "PRINCIPAL_ROLE")) { + Assertions.assertEquals( + OAuthTokenErrorResponse.Error.invalid_scope, + new TokenRequestValidator() + .validateForClientCredentialsFlow( + "client-id", "client-secret", "client_credentials", scope) + .get()); + } + } + + @Test + public void testValidateForClientCredentialsFlowAllValid() { + Assertions.assertEquals( + Optional.empty(), + new TokenRequestValidator() + .validateForClientCredentialsFlow( + "client-id", "client-secret", "client_credentials", "PRINCIPAL_ROLE:ALL")); + } +} diff --git a/polaris-service/src/test/java/io/polaris/service/auth/TokenUtils.java b/polaris-service/src/test/java/io/polaris/service/auth/TokenUtils.java new file mode 100644 index 0000000000..bab323aca6 --- /dev/null +++ b/polaris-service/src/test/java/io/polaris/service/auth/TokenUtils.java @@ -0,0 +1,53 @@ +package io.polaris.service.auth; + +import static io.polaris.service.context.DefaultContextResolver.REALM_PROPERTY_KEY; +import static org.assertj.core.api.Assertions.assertThat; + +import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.client.Invocation; +import jakarta.ws.rs.core.MultivaluedHashMap; +import jakarta.ws.rs.core.Response; +import java.util.Map; +import org.apache.iceberg.rest.responses.OAuthTokenResponse; + +public class TokenUtils { + + /** Get token against default realm */ + public static String getTokenFromSecrets( + Client client, int port, String clientId, String clientSecret) { + return getTokenFromSecrets(client, port, clientId, clientSecret, null); + } + + /** Get token against specified realm */ + public static String getTokenFromSecrets( + Client client, int port, String clientId, String clientSecret, String realm) { + String token; + + Invocation.Builder builder = + client + .target(String.format("http://localhost:%d/api/catalog/v1/oauth/tokens", port)) + .request("application/json"); + if (realm != null) { + builder = builder.header(REALM_PROPERTY_KEY, realm); + } + + try (Response response = + builder.post( + Entity.form( + new MultivaluedHashMap<>( + Map.of( + "grant_type", + "client_credentials", + "scope", + "PRINCIPAL_ROLE:ALL", + "client_id", + clientId, + "client_secret", + clientSecret))))) { + assertThat(response).returns(200, Response::getStatus); + token = response.readEntity(OAuthTokenResponse.class).token(); + } + return token; + } +} diff --git a/polaris-service/src/test/java/io/polaris/service/catalog/BasePolarisCatalogTest.java b/polaris-service/src/test/java/io/polaris/service/catalog/BasePolarisCatalogTest.java new file mode 100644 index 0000000000..9207ba50de --- /dev/null +++ b/polaris-service/src/test/java/io/polaris/service/catalog/BasePolarisCatalogTest.java @@ -0,0 +1,954 @@ +package io.polaris.service.catalog; + +import static org.apache.iceberg.types.Types.NestedField.required; +import static org.mockito.ArgumentMatchers.isA; +import static org.mockito.Mockito.when; + +import com.google.common.collect.ImmutableMap; +import io.micrometer.core.instrument.MeterRegistry; +import io.polaris.core.PolarisCallContext; +import io.polaris.core.PolarisConfigurationStore; +import io.polaris.core.PolarisDefaultDiagServiceImpl; +import io.polaris.core.PolarisDiagnostics; +import io.polaris.core.admin.model.AwsStorageConfigInfo; +import io.polaris.core.admin.model.StorageConfigInfo; +import io.polaris.core.auth.AuthenticatedPolarisPrincipal; +import io.polaris.core.auth.PolarisAuthorizer; +import io.polaris.core.context.CallContext; +import io.polaris.core.context.RealmContext; +import io.polaris.core.entity.CatalogEntity; +import io.polaris.core.entity.PolarisBaseEntity; +import io.polaris.core.entity.PolarisEntity; +import io.polaris.core.entity.PolarisEntitySubType; +import io.polaris.core.entity.PolarisEntityType; +import io.polaris.core.entity.PrincipalEntity; +import io.polaris.core.entity.TaskEntity; +import io.polaris.core.persistence.MetaStoreManagerFactory; +import io.polaris.core.persistence.PolarisEntityManager; +import io.polaris.core.persistence.PolarisMetaStoreManager; +import io.polaris.core.persistence.PolarisMetaStoreSession; +import io.polaris.core.storage.PolarisCredentialProperty; +import io.polaris.core.storage.PolarisStorageActions; +import io.polaris.core.storage.PolarisStorageIntegration; +import io.polaris.core.storage.PolarisStorageIntegrationProvider; +import io.polaris.core.storage.aws.AwsCredentialsStorageIntegration; +import io.polaris.core.storage.aws.AwsStorageConfigurationInfo; +import io.polaris.core.storage.cache.StorageCredentialCache; +import io.polaris.service.admin.PolarisAdminService; +import io.polaris.service.persistence.InMemoryPolarisMetaStoreManagerFactory; +import io.polaris.service.task.TaskExecutor; +import io.polaris.service.task.TaskFileIOSupplier; +import io.polaris.service.types.NotificationRequest; +import io.polaris.service.types.NotificationType; +import io.polaris.service.types.TableUpdateNotification; +import java.io.IOException; +import java.time.Clock; +import java.util.Arrays; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import org.apache.commons.lang3.NotImplementedException; +import org.apache.iceberg.BaseTable; +import org.apache.iceberg.CatalogProperties; +import org.apache.iceberg.PartitionSpec; +import org.apache.iceberg.Schema; +import org.apache.iceberg.Table; +import org.apache.iceberg.TableMetadata; +import org.apache.iceberg.TableMetadataParser; +import org.apache.iceberg.catalog.CatalogTests; +import org.apache.iceberg.catalog.Namespace; +import org.apache.iceberg.catalog.SupportsNamespaces; +import org.apache.iceberg.catalog.TableIdentifier; +import org.apache.iceberg.exceptions.ForbiddenException; +import org.apache.iceberg.exceptions.NoSuchNamespaceException; +import org.apache.iceberg.inmemory.InMemoryFileIO; +import org.apache.iceberg.io.FileIO; +import org.apache.iceberg.types.Types; +import org.assertj.core.api.AbstractBooleanAssert; +import org.assertj.core.api.Assertions; +import org.jetbrains.annotations.Nullable; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import software.amazon.awssdk.services.sts.StsClient; +import software.amazon.awssdk.services.sts.model.AssumeRoleRequest; +import software.amazon.awssdk.services.sts.model.AssumeRoleResponse; +import software.amazon.awssdk.services.sts.model.Credentials; + +public class BasePolarisCatalogTest extends CatalogTests { + protected static final Namespace NS = Namespace.of("newdb"); + protected static final TableIdentifier TABLE = TableIdentifier.of(NS, "table"); + protected static final Schema SCHEMA = + new Schema( + required(3, "id", Types.IntegerType.get(), "unique ID 🤪"), + required(4, "data", Types.StringType.get())); + public static final String CATALOG_NAME = "polaris-catalog"; + public static final String TEST_ACCESS_KEY = "test_access_key"; + public static final String SECRET_ACCESS_KEY = "secret_access_key"; + public static final String SESSION_TOKEN = "session_token"; + + private BasePolarisCatalog catalog; + private AwsStorageConfigInfo storageConfigModel; + private StsClient stsClient; + private PolarisMetaStoreManager metaStoreManager; + private PolarisCallContext polarisContext; + private PolarisAdminService adminService; + private PolarisEntityManager entityManager; + private AuthenticatedPolarisPrincipal authenticatedRoot; + + @BeforeEach + @SuppressWarnings("unchecked") + public void before() { + PolarisDiagnostics diagServices = new PolarisDefaultDiagServiceImpl(); + RealmContext realmContext = () -> "realm"; + PolarisStorageIntegrationProvider storageIntegrationProvider = Mockito.mock(); + InMemoryPolarisMetaStoreManagerFactory managerFactory = + new InMemoryPolarisMetaStoreManagerFactory(); + managerFactory.setStorageIntegrationProvider(storageIntegrationProvider); + metaStoreManager = managerFactory.getOrCreateMetaStoreManager(realmContext); + Map configMap = new HashMap<>(); + configMap.put("ALLOW_SPECIFYING_FILE_IO_IMPL", true); + polarisContext = + new PolarisCallContext( + managerFactory.getOrCreateSessionSupplier(realmContext).get(), + diagServices, + new PolarisConfigurationStore() { + @Override + public @Nullable T getConfiguration(PolarisCallContext ctx, String configName) { + return (T) configMap.get(configName); + } + }, + Clock.systemDefaultZone()); + entityManager = + new PolarisEntityManager( + metaStoreManager, polarisContext::getMetaStore, new StorageCredentialCache()); + + CallContext callContext = CallContext.of(realmContext, polarisContext); + CallContext.setCurrentContext(callContext); + + PrincipalEntity rootEntity = + new PrincipalEntity( + PolarisEntity.of( + entityManager + .getMetaStoreManager() + .readEntityByName( + polarisContext, + null, + PolarisEntityType.PRINCIPAL, + PolarisEntitySubType.NULL_SUBTYPE, + "root") + .getEntity())); + + authenticatedRoot = new AuthenticatedPolarisPrincipal(rootEntity, Set.of()); + + adminService = + new PolarisAdminService( + callContext, + entityManager, + authenticatedRoot, + new PolarisAuthorizer(new PolarisConfigurationStore() {})); + String storageLocation = "s3://my-bucket/path/to/data"; + storageConfigModel = + AwsStorageConfigInfo.builder() + .setRoleArn("arn:aws:iam::012345678901:role/jdoe") + .setExternalId("externalId") + .setUserArn("aws::a:user:arn") + .setStorageType(StorageConfigInfo.StorageTypeEnum.S3) + .setAllowedLocations(List.of(storageLocation, "s3://externally-owned-bucket")) + .build(); + PolarisEntity catalogEntity = + adminService.createCatalog( + new CatalogEntity.Builder() + .setName(CATALOG_NAME) + .setDefaultBaseLocation(storageLocation) + .setReplaceNewLocationPrefixWithCatalogDefault("file:") + .setStorageConfigurationInfo(storageConfigModel, storageLocation) + .build()); + + PolarisPassthroughResolutionView passthroughView = + new PolarisPassthroughResolutionView( + callContext, entityManager, authenticatedRoot, CATALOG_NAME); + TaskExecutor taskExecutor = Mockito.mock(); + this.catalog = + new BasePolarisCatalog(entityManager, callContext, passthroughView, taskExecutor); + this.catalog.initialize( + CATALOG_NAME, + ImmutableMap.of( + CatalogProperties.FILE_IO_IMPL, "org.apache.iceberg.inmemory.InMemoryFileIO")); + stsClient = Mockito.mock(StsClient.class); + when(stsClient.assumeRole(isA(AssumeRoleRequest.class))) + .thenReturn( + AssumeRoleResponse.builder() + .credentials( + Credentials.builder() + .accessKeyId(TEST_ACCESS_KEY) + .secretAccessKey(SECRET_ACCESS_KEY) + .sessionToken(SESSION_TOKEN) + .build()) + .build()); + PolarisStorageIntegration storageIntegration = + new AwsCredentialsStorageIntegration(stsClient); + when(storageIntegrationProvider.getStorageIntegrationForConfig( + isA(AwsStorageConfigurationInfo.class))) + .thenReturn((PolarisStorageIntegration) storageIntegration); + } + + @AfterEach + public void after() throws IOException { + catalog().close(); + } + + @Override + protected BasePolarisCatalog catalog() { + return catalog; + } + + @Override + protected boolean requiresNamespaceCreate() { + return true; + } + + @Override + protected boolean supportsNestedNamespaces() { + return true; + } + + @Override + protected boolean overridesRequestedLocation() { + return true; + } + + protected boolean supportsNotifications() { + return true; + } + + @Test + public void testRenameTableMissingDestinationNamespace() { + Assumptions.assumeTrue( + requiresNamespaceCreate(), + "Only applicable if namespaces must be created before adding children"); + + BasePolarisCatalog catalog = catalog(); + catalog.createNamespace(NS); + + Assertions.assertThat(catalog.tableExists(TABLE)) + .as("Source table should not exist before create") + .isFalse(); + + catalog.buildTable(TABLE, SCHEMA).create(); + Assertions.assertThat(catalog.tableExists(TABLE)) + .as("Table should exist after create") + .isTrue(); + + Namespace newNamespace = Namespace.of("nonexistent_namespace"); + TableIdentifier renamedTable = TableIdentifier.of(newNamespace, "table_renamed"); + + Assertions.assertThat(catalog.namespaceExists(newNamespace)) + .as("Destination namespace should not exist before rename") + .isFalse(); + + Assertions.assertThat(catalog.tableExists(renamedTable)) + .as("Destination table should not exist before rename") + .isFalse(); + + Assertions.assertThatThrownBy(() -> catalog.renameTable(TABLE, renamedTable)) + .isInstanceOf(NoSuchNamespaceException.class) + .hasMessageContaining("Namespace does not exist"); + + Assertions.assertThat(catalog.namespaceExists(newNamespace)) + .as("Destination namespace should not exist after failed rename") + .isFalse(); + + Assertions.assertThat(catalog.tableExists(renamedTable)) + .as("Table should not exist after failed rename") + .isFalse(); + } + + @Test + public void testCreateNestedNamespaceUnderMissingParent() { + Assumptions.assumeTrue( + requiresNamespaceCreate(), + "Only applicable if namespaces must be created before adding children"); + Assumptions.assumeTrue( + supportsNestedNamespaces(), "Only applicable if nested namespaces are supoprted"); + + BasePolarisCatalog catalog = catalog(); + + Namespace child1 = Namespace.of("parent", "child1"); + + Assertions.assertThatThrownBy(() -> catalog.createNamespace(child1)) + .isInstanceOf(NoSuchNamespaceException.class) + .hasMessageContaining("Parent"); + } + + @Test + public void testUpdateNotificationWhenTableAndNamespacesDontExist() { + Assumptions.assumeTrue( + requiresNamespaceCreate(), + "Only applicable if namespaces must be created before adding children"); + Assumptions.assumeTrue( + supportsNestedNamespaces(), "Only applicable if nested namespaces are supported"); + Assumptions.assumeTrue( + supportsNotifications(), "Only applicable if notifications are supported"); + + final String tableLocation = "s3://externally-owned-bucket/table/"; + final String tableMetadataLocation = tableLocation + "metadata/v1.metadata.json"; + BasePolarisCatalog catalog = catalog(); + + Namespace namespace = Namespace.of("parent", "child1"); + TableIdentifier table = TableIdentifier.of(namespace, "table"); + + NotificationRequest request = new NotificationRequest(); + request.setNotificationType(NotificationType.UPDATE); + TableUpdateNotification update = new TableUpdateNotification(); + update.setMetadataLocation(tableMetadataLocation); + update.setTableName(table.name()); + update.setTableUuid(UUID.randomUUID().toString()); + update.setTimestamp(230950845L); + request.setPayload(update); + + InMemoryFileIO fileIO = (InMemoryFileIO) catalog.getIo(); + + fileIO.addFile( + tableMetadataLocation, + TableMetadataParser.toJson(createSampleTableMetadata(tableLocation)).getBytes()); + + Assertions.assertThat(catalog.sendNotification(table, request)) + .as("Notification should be sent successfully") + .isTrue(); + Assertions.assertThat(catalog.namespaceExists(namespace)) + .as("Intermediate namespaces should be created") + .isTrue(); + Assertions.assertThat(catalog.tableExists(table)) + .as("Table should be created on receiving notification") + .isTrue(); + } + + @Test + public void testUpdateNotificationCreateTableInDisallowedLocation() { + Assumptions.assumeTrue( + requiresNamespaceCreate(), + "Only applicable if namespaces must be created before adding children"); + Assumptions.assumeTrue( + supportsNestedNamespaces(), "Only applicable if nested namespaces are supported"); + Assumptions.assumeTrue( + supportsNotifications(), "Only applicable if notifications are supported"); + + // The location of the metadata JSON file specified in the create will be forbidden. + final String tableLocation = "s3://forbidden-table-location/table/"; + final String tableMetadataLocation = tableLocation + "metadata/v1.metadata.json"; + BasePolarisCatalog catalog = catalog(); + + Namespace namespace = Namespace.of("parent", "child1"); + TableIdentifier table = TableIdentifier.of(namespace, "table"); + + NotificationRequest request = new NotificationRequest(); + request.setNotificationType(NotificationType.UPDATE); + TableUpdateNotification update = new TableUpdateNotification(); + update.setMetadataLocation(tableMetadataLocation); + update.setTableName(table.name()); + update.setTableUuid(UUID.randomUUID().toString()); + update.setTimestamp(230950845L); + request.setPayload(update); + + InMemoryFileIO fileIO = (InMemoryFileIO) catalog.getIo(); + + fileIO.addFile( + tableMetadataLocation, + TableMetadataParser.toJson(createSampleTableMetadata(tableLocation)).getBytes()); + + Assertions.assertThatThrownBy(() -> catalog.sendNotification(table, request)) + .isInstanceOf(ForbiddenException.class) + .hasMessageContaining("Invalid location"); + } + + @Test + public void testUpdateNotificationCreateTableWithLocalFilePrefix() { + Assumptions.assumeTrue( + requiresNamespaceCreate(), + "Only applicable if namespaces must be created before adding children"); + Assumptions.assumeTrue( + supportsNestedNamespaces(), "Only applicable if nested namespaces are supported"); + Assumptions.assumeTrue( + supportsNotifications(), "Only applicable if notifications are supported"); + + // The location of the metadata JSON file specified in the create will be forbidden. + final String metadataLocation = "file:///etc/metadata.json/../passwd"; + String catalogWithoutStorage = "catalogWithoutStorage"; + PolarisEntity catalogEntity = + adminService.createCatalog( + new CatalogEntity.Builder().setName(catalogWithoutStorage).build()); + + CallContext callContext = CallContext.getCurrentContext(); + PolarisPassthroughResolutionView passthroughView = + new PolarisPassthroughResolutionView( + callContext, entityManager, authenticatedRoot, catalogWithoutStorage); + TaskExecutor taskExecutor = Mockito.mock(); + BasePolarisCatalog catalog = + new BasePolarisCatalog(entityManager, callContext, passthroughView, taskExecutor); + catalog.initialize( + catalogWithoutStorage, + ImmutableMap.of( + CatalogProperties.FILE_IO_IMPL, "org.apache.iceberg.inmemory.InMemoryFileIO")); + + Namespace namespace = Namespace.of("parent", "child1"); + TableIdentifier table = TableIdentifier.of(namespace, "table"); + + NotificationRequest request = new NotificationRequest(); + request.setNotificationType(NotificationType.UPDATE); + TableUpdateNotification update = new TableUpdateNotification(); + update.setMetadataLocation(metadataLocation); + update.setTableName(table.name()); + update.setTableUuid(UUID.randomUUID().toString()); + update.setTimestamp(230950845L); + request.setPayload(update); + + InMemoryFileIO fileIO = (InMemoryFileIO) catalog.getIo(); + + fileIO.addFile( + metadataLocation, + TableMetadataParser.toJson(createSampleTableMetadata(metadataLocation)).getBytes()); + + Assertions.assertThatThrownBy(() -> catalog.sendNotification(table, request)) + .isInstanceOf(ForbiddenException.class) + .hasMessageContaining("Invalid location"); + } + + @Test + public void testUpdateNotificationCreateTableWithHttpPrefix() { + Assumptions.assumeTrue( + requiresNamespaceCreate(), + "Only applicable if namespaces must be created before adding children"); + Assumptions.assumeTrue( + supportsNestedNamespaces(), "Only applicable if nested namespaces are supported"); + Assumptions.assumeTrue( + supportsNotifications(), "Only applicable if notifications are supported"); + + String catalogName = "catalogForMaliciousDomain"; + PolarisEntity catalogEntity = + adminService.createCatalog(new CatalogEntity.Builder().setName(catalogName).build()); + + CallContext callContext = CallContext.getCurrentContext(); + PolarisPassthroughResolutionView passthroughView = + new PolarisPassthroughResolutionView( + callContext, entityManager, authenticatedRoot, catalogName); + TaskExecutor taskExecutor = Mockito.mock(); + BasePolarisCatalog catalog = + new BasePolarisCatalog(entityManager, callContext, passthroughView, taskExecutor); + catalog.initialize( + catalogName, + ImmutableMap.of( + CatalogProperties.FILE_IO_IMPL, "org.apache.iceberg.inmemory.InMemoryFileIO")); + + Namespace namespace = Namespace.of("parent", "child1"); + TableIdentifier table = TableIdentifier.of(namespace, "table"); + + InMemoryFileIO fileIO = (InMemoryFileIO) catalog.getIo(); + + // The location of the metadata JSON file specified in the create will be forbidden. + final String metadataLocation = "http://maliciousdomain.com/metadata.json"; + NotificationRequest request = new NotificationRequest(); + request.setNotificationType(NotificationType.UPDATE); + TableUpdateNotification update = new TableUpdateNotification(); + update.setMetadataLocation(metadataLocation); + update.setTableName(table.name()); + update.setTableUuid(UUID.randomUUID().toString()); + update.setTimestamp(230950845L); + request.setPayload(update); + + fileIO.addFile( + metadataLocation, + TableMetadataParser.toJson(createSampleTableMetadata(metadataLocation)).getBytes()); + + Assertions.assertThatThrownBy(() -> catalog.sendNotification(table, request)) + .isInstanceOf(ForbiddenException.class) + .hasMessageContaining("Invalid location"); + + // It also fails if we try to use https + final String httpsMetadataLocation = "https://maliciousdomain.com/metadata.json"; + NotificationRequest newRequest = new NotificationRequest(); + newRequest.setNotificationType(NotificationType.UPDATE); + newRequest.setPayload( + new TableUpdateNotification( + table.name(), 230950845L, UUID.randomUUID().toString(), httpsMetadataLocation, null)); + + fileIO.addFile( + httpsMetadataLocation, + TableMetadataParser.toJson(createSampleTableMetadata(metadataLocation)).getBytes()); + + Assertions.assertThatThrownBy(() -> catalog.sendNotification(table, newRequest)) + .isInstanceOf(ForbiddenException.class) + .hasMessageContaining("Invalid location"); + } + + @Test + public void testUpdateNotificationWhenNamespacesExist() { + Assumptions.assumeTrue( + requiresNamespaceCreate(), + "Only applicable if namespaces must be created before adding children"); + Assumptions.assumeTrue( + supportsNestedNamespaces(), "Only applicable if nested namespaces are supported"); + Assumptions.assumeTrue( + supportsNotifications(), "Only applicable if notifications are supported"); + + final String tableLocation = "s3://externally-owned-bucket/table/"; + final String tableMetadataLocation = tableLocation + "metadata/v1.metadata.json"; + BasePolarisCatalog catalog = catalog(); + + Namespace namespace = Namespace.of("parent", "child1"); + + createNonExistingNamespaces(namespace); + + TableIdentifier table = TableIdentifier.of(namespace, "table"); + + NotificationRequest request = new NotificationRequest(); + request.setNotificationType(NotificationType.UPDATE); + TableUpdateNotification update = new TableUpdateNotification(); + update.setMetadataLocation(tableMetadataLocation); + update.setTableName(table.name()); + update.setTableUuid(UUID.randomUUID().toString()); + update.setTimestamp(230950845L); + request.setPayload(update); + + InMemoryFileIO fileIO = (InMemoryFileIO) catalog.getIo(); + + fileIO.addFile( + tableMetadataLocation, + TableMetadataParser.toJson(createSampleTableMetadata(tableLocation)).getBytes()); + + Assertions.assertThat(catalog.sendNotification(table, request)) + .as("Notification should be sent successfully") + .isTrue(); + Assertions.assertThat(catalog.namespaceExists(namespace)) + .as("Intermediate namespaces should be created") + .isTrue(); + Assertions.assertThat(catalog.tableExists(table)) + .as("Table should be created on receiving notification") + .isTrue(); + } + + @Test + public void testUpdateNotificationWhenTableExists() { + Assumptions.assumeTrue( + requiresNamespaceCreate(), + "Only applicable if namespaces must be created before adding children"); + Assumptions.assumeTrue( + supportsNestedNamespaces(), "Only applicable if nested namespaces are supported"); + Assumptions.assumeTrue( + supportsNotifications(), "Only applicable if notifications are supported"); + + final String tableLocation = "s3://externally-owned-bucket/table/"; + final String tableMetadataLocation = tableLocation + "metadata/v1.metadata.json"; + BasePolarisCatalog catalog = catalog(); + + Namespace namespace = Namespace.of("parent", "child1"); + + createNonExistingNamespaces(namespace); + + TableIdentifier table = TableIdentifier.of(namespace, "table"); + + catalog.createTable( + table, + new Schema( + Types.NestedField.required(1, "intType", Types.IntegerType.get()), + Types.NestedField.required(2, "stringType", Types.StringType.get()))); + + NotificationRequest request = new NotificationRequest(); + request.setNotificationType(NotificationType.UPDATE); + TableUpdateNotification update = new TableUpdateNotification(); + update.setMetadataLocation(tableMetadataLocation); + update.setTableName(table.name()); + update.setTableUuid(UUID.randomUUID().toString()); + update.setTimestamp(230950845L); + request.setPayload(update); + + InMemoryFileIO fileIO = (InMemoryFileIO) catalog.getIo(); + + fileIO.addFile( + tableMetadataLocation, + TableMetadataParser.toJson(createSampleTableMetadata(tableLocation)).getBytes()); + + Assertions.assertThat(catalog.sendNotification(table, request)) + .as("Notification should be sent successfully") + .isTrue(); + Assertions.assertThat(catalog.namespaceExists(namespace)) + .as("Intermediate namespaces should be created") + .isTrue(); + Assertions.assertThat(catalog.tableExists(table)) + .as("Table should be created on receiving notification") + .isTrue(); + } + + @Test + public void testUpdateNotificationWhenTableExistsInDisallowedLocation() { + Assumptions.assumeTrue( + requiresNamespaceCreate(), + "Only applicable if namespaces must be created before adding children"); + Assumptions.assumeTrue( + supportsNestedNamespaces(), "Only applicable if nested namespaces are supported"); + Assumptions.assumeTrue( + supportsNotifications(), "Only applicable if notifications are supported"); + + // The location of the metadata JSON file specified in the update will be forbidden. + final String tableLocation = "s3://forbidden-table-location/table/"; + final String tableMetadataLocation = tableLocation + "metadata/v1.metadata.json"; + BasePolarisCatalog catalog = catalog(); + + Namespace namespace = Namespace.of("parent", "child1"); + + createNonExistingNamespaces(namespace); + + TableIdentifier table = TableIdentifier.of(namespace, "table"); + + catalog.createTable( + table, + new Schema( + Types.NestedField.required(1, "intType", Types.IntegerType.get()), + Types.NestedField.required(2, "stringType", Types.StringType.get()))); + + NotificationRequest request = new NotificationRequest(); + request.setNotificationType(NotificationType.UPDATE); + TableUpdateNotification update = new TableUpdateNotification(); + update.setMetadataLocation(tableMetadataLocation); + update.setTableName(table.name()); + update.setTableUuid(UUID.randomUUID().toString()); + update.setTimestamp(230950845L); + request.setPayload(update); + + InMemoryFileIO fileIO = (InMemoryFileIO) catalog.getIo(); + + fileIO.addFile( + tableMetadataLocation, + TableMetadataParser.toJson(createSampleTableMetadata(tableLocation)).getBytes()); + + Assertions.assertThatThrownBy(() -> catalog.sendNotification(table, request)) + .isInstanceOf(ForbiddenException.class) + .hasMessageContaining("Invalid location"); + } + + @Test + public void testUpdateNotificationWhenTableExistsFileSpecifiesDisallowedLocation() { + Assumptions.assumeTrue( + requiresNamespaceCreate(), + "Only applicable if namespaces must be created before adding children"); + Assumptions.assumeTrue( + supportsNestedNamespaces(), "Only applicable if nested namespaces are supported"); + Assumptions.assumeTrue( + supportsNotifications(), "Only applicable if notifications are supported"); + + final String tableLocation = "s3://externally-owned-bucket/table/"; + final String tableMetadataLocation = tableLocation + "metadata/v1.metadata.json"; + BasePolarisCatalog catalog = catalog(); + + Namespace namespace = Namespace.of("parent", "child1"); + + createNonExistingNamespaces(namespace); + + TableIdentifier table = TableIdentifier.of(namespace, "table"); + + catalog.createTable( + table, + new Schema( + Types.NestedField.required(1, "intType", Types.IntegerType.get()), + Types.NestedField.required(2, "stringType", Types.StringType.get()))); + + NotificationRequest request = new NotificationRequest(); + request.setNotificationType(NotificationType.UPDATE); + TableUpdateNotification update = new TableUpdateNotification(); + update.setMetadataLocation(tableMetadataLocation); + update.setTableName(table.name()); + update.setTableUuid(UUID.randomUUID().toString()); + update.setTimestamp(230950845L); + request.setPayload(update); + + InMemoryFileIO fileIO = (InMemoryFileIO) catalog.getIo(); + + // Though the metadata JSON file itself is in an allowed location, make it internally specify + // a forbidden table location. + TableMetadata forbiddenMetadata = + createSampleTableMetadata("s3://forbidden-table-location/table/"); + fileIO.addFile(tableMetadataLocation, TableMetadataParser.toJson(forbiddenMetadata).getBytes()); + + // TODO: Once we prefetch or prevalidate json contents, we should perform location validation + // proactively and this sendNotification should throw. For now, if we only validate the path + // later when trying to read it, we'll wait until something tries to get a credential to error + // out. + Assertions.assertThat(catalog.sendNotification(table, request)) + .as("Notification should be sent successfully") + .isTrue(); + Assertions.assertThatThrownBy( + () -> + catalog.getCredentialConfig( + table, forbiddenMetadata, Set.of(PolarisStorageActions.READ))) + .isInstanceOf(ForbiddenException.class) + .hasMessageContaining("Invalid location"); + } + + @Test + public void testDropNotificationWhenTableAndNamespacesDontExist() { + Assumptions.assumeTrue( + requiresNamespaceCreate(), + "Only applicable if namespaces must be created before adding children"); + Assumptions.assumeTrue( + supportsNestedNamespaces(), "Only applicable if nested namespaces are supported"); + Assumptions.assumeTrue( + supportsNotifications(), "Only applicable if notifications are supported"); + + final String tableLocation = "s3://externally-owned-bucket/table/"; + final String tableMetadataLocation = tableLocation + "metadata/v1.metadata.json"; + BasePolarisCatalog catalog = catalog(); + + Namespace namespace = Namespace.of("parent", "child1"); + TableIdentifier table = TableIdentifier.of(namespace, "table"); + + NotificationRequest request = new NotificationRequest(); + request.setNotificationType(NotificationType.DROP); + TableUpdateNotification update = new TableUpdateNotification(); + update.setMetadataLocation(tableMetadataLocation); + update.setTableName(table.name()); + update.setTableUuid(UUID.randomUUID().toString()); + update.setTimestamp(230950845L); + request.setPayload(update); + + Assertions.assertThat(catalog.sendNotification(table, request)) + .as("Notification should fail since the target table doesn't exist") + .isFalse(); + Assertions.assertThat(catalog.namespaceExists(namespace)) + .as("Intermediate namespaces should not be created") + .isFalse(); + Assertions.assertThat(catalog.tableExists(table)).as("Table should not exist").isFalse(); + } + + @Test + public void testDropNotificationWhenNamespacesExist() { + Assumptions.assumeTrue( + requiresNamespaceCreate(), + "Only applicable if namespaces must be created before adding children"); + Assumptions.assumeTrue( + supportsNestedNamespaces(), "Only applicable if nested namespaces are supported"); + Assumptions.assumeTrue( + supportsNotifications(), "Only applicable if notifications are supported"); + + final String tableLocation = "s3://externally-owned-bucket/table/"; + final String tableMetadataLocation = tableLocation + "metadata/v1.metadata.json"; + BasePolarisCatalog catalog = catalog(); + + Namespace namespace = Namespace.of("parent", "child1"); + + createNonExistingNamespaces(namespace); + + TableIdentifier table = TableIdentifier.of(namespace, "table"); + + NotificationRequest request = new NotificationRequest(); + request.setNotificationType(NotificationType.DROP); + TableUpdateNotification update = new TableUpdateNotification(); + update.setMetadataLocation(tableMetadataLocation); + update.setTableName(table.name()); + update.setTableUuid(UUID.randomUUID().toString()); + update.setTimestamp(230950845L); + request.setPayload(update); + + InMemoryFileIO fileIO = (InMemoryFileIO) catalog.getIo(); + + fileIO.addFile( + tableMetadataLocation, + TableMetadataParser.toJson(createSampleTableMetadata(tableLocation)).getBytes()); + + Assertions.assertThat(catalog.sendNotification(table, request)) + .as("Notification should fail since table doesn't exist") + .isFalse(); + Assertions.assertThat(catalog.namespaceExists(namespace)) + .as("Intermediate namespaces should exist") + .isTrue(); + Assertions.assertThat(catalog.tableExists(table)) + .as("Table should not be created on receiving notification") + .isFalse(); + } + + @Test + public void testDropNotificationWhenTableExists() { + Assumptions.assumeTrue( + requiresNamespaceCreate(), + "Only applicable if namespaces must be created before adding children"); + Assumptions.assumeTrue( + supportsNestedNamespaces(), "Only applicable if nested namespaces are supported"); + Assumptions.assumeTrue( + supportsNotifications(), "Only applicable if notifications are supported"); + + final String tableLocation = "s3://externally-owned-bucket/table/"; + final String tableMetadataLocation = tableLocation + "metadata/v1.metadata.json"; + BasePolarisCatalog catalog = catalog(); + + Namespace namespace = Namespace.of("parent", "child1"); + + createNonExistingNamespaces(namespace); + + TableIdentifier table = TableIdentifier.of(namespace, "table"); + + catalog.createTable( + table, + new Schema( + Types.NestedField.required(1, "intType", Types.IntegerType.get()), + Types.NestedField.required(2, "stringType", Types.StringType.get()))); + + NotificationRequest request = new NotificationRequest(); + request.setNotificationType(NotificationType.DROP); + TableUpdateNotification update = new TableUpdateNotification(); + update.setMetadataLocation(tableMetadataLocation); + update.setTableName(table.name()); + update.setTableUuid(UUID.randomUUID().toString()); + update.setTimestamp(230950845L); + request.setPayload(update); + + InMemoryFileIO fileIO = (InMemoryFileIO) catalog.getIo(); + + fileIO.addFile( + tableMetadataLocation, + TableMetadataParser.toJson(createSampleTableMetadata(tableLocation)).getBytes()); + + Assertions.assertThat(catalog.sendNotification(table, request)) + .as("Notification should be sent successfully") + .isTrue(); + Assertions.assertThat(catalog.namespaceExists(namespace)) + .as("Intermediate namespaces should already exist") + .isTrue(); + Assertions.assertThat(catalog.tableExists(table)) + .as("Table should be dropped on receiving notification") + .isFalse(); + } + + @Test + public void testDropTableWithPurge() { + if (this.requiresNamespaceCreate()) { + ((SupportsNamespaces) catalog).createNamespace(NS); + } + + Assertions.assertThatPredicate(catalog::tableExists) + .as("Table should not exist before create") + .rejects(TABLE); + + Table table = catalog.buildTable(TABLE, SCHEMA).create(); + Assertions.assertThatPredicate(catalog::tableExists) + .as("Table should exist after create") + .accepts(TABLE); + Assertions.assertThat(table).isInstanceOf(BaseTable.class); + TableMetadata tableMetadata = ((BaseTable) table).operations().current(); + + boolean dropped = catalog.dropTable(TABLE, true); + ((AbstractBooleanAssert) + Assertions.assertThat(dropped).as("Should drop a table that does exist", new Object[0])) + .isTrue(); + Assertions.assertThatPredicate(catalog::tableExists) + .as("Table should not exist after drop") + .rejects(TABLE); + List tasks = + metaStoreManager.loadTasks(polarisContext, "testExecutor", 1).getEntities(); + Assertions.assertThat(tasks).hasSize(1); + TaskEntity taskEntity = TaskEntity.of(tasks.get(0)); + EnumMap credentials = + metaStoreManager + .getSubscopedCredsForEntity( + polarisContext, + 0, + taskEntity.getId(), + true, + Set.of(tableMetadata.location()), + Set.of(tableMetadata.location())) + .getCredentials(); + Assertions.assertThat(credentials) + .isNotNull() + .isNotEmpty() + .containsEntry(PolarisCredentialProperty.AWS_KEY_ID, TEST_ACCESS_KEY) + .containsEntry(PolarisCredentialProperty.AWS_SECRET_KEY, SECRET_ACCESS_KEY) + .containsEntry(PolarisCredentialProperty.AWS_TOKEN, SESSION_TOKEN); + FileIO fileIO = + new TaskFileIOSupplier( + new MetaStoreManagerFactory() { + @Override + public PolarisMetaStoreManager getOrCreateMetaStoreManager( + RealmContext realmContext) { + return metaStoreManager; + } + + @Override + public Supplier getOrCreateSessionSupplier( + RealmContext realmContext) { + return () -> polarisContext.getMetaStore(); + } + + @Override + public StorageCredentialCache getOrCreateStorageCredentialCache( + RealmContext realmContext) { + return new StorageCredentialCache(); + } + + @Override + public void setMetricRegistry(MeterRegistry metricRegistry) {} + + @Override + public Map + bootstrapRealms(List realms) { + throw new NotImplementedException("Bootstrapping realms is not supported"); + } + + @Override + public void setStorageIntegrationProvider( + PolarisStorageIntegrationProvider storageIntegrationProvider) {} + }) + .apply(taskEntity); + Assertions.assertThat(fileIO).isNotNull().isInstanceOf(InMemoryFileIO.class); + } + + private TableMetadata createSampleTableMetadata(String tableLocation) { + Schema schema = + new Schema( + Types.NestedField.required(1, "intType", Types.IntegerType.get()), + Types.NestedField.required(2, "stringType", Types.StringType.get())); + PartitionSpec partitionSpec = + PartitionSpec.builderFor(schema).identity("intType").withSpecId(1000).build(); + + return TableMetadata.newTableMetadata( + schema, partitionSpec, tableLocation, ImmutableMap.of()); + } + + private void createNonExistingNamespaces(Namespace namespace) { + // Pre-create namespaces if they don't exist + for (int i = 1; i <= namespace.length(); i++) { + Namespace nsLevel = + Namespace.of( + Arrays.stream(namespace.levels()) + .limit(i) + .collect(Collectors.toList()) + .toArray(String[]::new)); + if (!catalog.namespaceExists(nsLevel)) { + catalog.createNamespace(nsLevel); + } + } + } + + @Test + public void testRetriableException() { + RuntimeException s3Exception = new RuntimeException("Access Denied"); + RuntimeException azureBlobStorageException = + new RuntimeException( + "This request is not authorized to perform this operation using this permission"); + RuntimeException gcsException = new RuntimeException("Forbidden"); + RuntimeException otherException = new RuntimeException(new IOException("Connection reset")); + Assertions.assertThat(BasePolarisCatalog.SHOULD_RETRY_REFRESH_PREDICATE.test(s3Exception)) + .isFalse(); + Assertions.assertThat( + BasePolarisCatalog.SHOULD_RETRY_REFRESH_PREDICATE.test(azureBlobStorageException)) + .isFalse(); + Assertions.assertThat(BasePolarisCatalog.SHOULD_RETRY_REFRESH_PREDICATE.test(gcsException)) + .isFalse(); + Assertions.assertThat(BasePolarisCatalog.SHOULD_RETRY_REFRESH_PREDICATE.test(otherException)) + .isTrue(); + } +} diff --git a/polaris-service/src/test/java/io/polaris/service/catalog/BasePolarisCatalogViewTest.java b/polaris-service/src/test/java/io/polaris/service/catalog/BasePolarisCatalogViewTest.java new file mode 100644 index 0000000000..0680995d5d --- /dev/null +++ b/polaris-service/src/test/java/io/polaris/service/catalog/BasePolarisCatalogViewTest.java @@ -0,0 +1,129 @@ +package io.polaris.service.catalog; + +import com.google.common.collect.ImmutableMap; +import io.polaris.core.PolarisCallContext; +import io.polaris.core.PolarisConfigurationStore; +import io.polaris.core.PolarisDefaultDiagServiceImpl; +import io.polaris.core.PolarisDiagnostics; +import io.polaris.core.admin.model.FileStorageConfigInfo; +import io.polaris.core.admin.model.StorageConfigInfo; +import io.polaris.core.auth.AuthenticatedPolarisPrincipal; +import io.polaris.core.auth.PolarisAuthorizer; +import io.polaris.core.context.CallContext; +import io.polaris.core.context.RealmContext; +import io.polaris.core.entity.CatalogEntity; +import io.polaris.core.entity.PolarisEntity; +import io.polaris.core.entity.PolarisEntitySubType; +import io.polaris.core.entity.PolarisEntityType; +import io.polaris.core.entity.PrincipalEntity; +import io.polaris.core.persistence.PolarisEntityManager; +import io.polaris.core.persistence.PolarisMetaStoreManager; +import io.polaris.core.storage.cache.StorageCredentialCache; +import io.polaris.service.admin.PolarisAdminService; +import io.polaris.service.persistence.InMemoryPolarisMetaStoreManagerFactory; +import io.polaris.service.storage.PolarisStorageIntegrationProviderImpl; +import java.time.Clock; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.apache.iceberg.CatalogProperties; +import org.apache.iceberg.catalog.Catalog; +import org.apache.iceberg.view.ViewCatalogTests; +import org.jetbrains.annotations.Nullable; +import org.junit.jupiter.api.BeforeEach; +import org.mockito.Mockito; + +public class BasePolarisCatalogViewTest extends ViewCatalogTests { + public static final String CATALOG_NAME = "polaris-catalog"; + private BasePolarisCatalog catalog; + + @BeforeEach + @SuppressWarnings("unchecked") + public void before() { + PolarisDiagnostics diagServices = new PolarisDefaultDiagServiceImpl(); + RealmContext realmContext = () -> "realm"; + InMemoryPolarisMetaStoreManagerFactory managerFactory = + new InMemoryPolarisMetaStoreManagerFactory(); + managerFactory.setStorageIntegrationProvider(new PolarisStorageIntegrationProviderImpl()); + PolarisMetaStoreManager metaStoreManager = + managerFactory.getOrCreateMetaStoreManager(realmContext); + Map configMap = new HashMap<>(); + configMap.put("ALLOW_WILDCARD_LOCATION", true); + configMap.put("ALLOW_SPECIFYING_FILE_IO_IMPL", true); + PolarisCallContext polarisContext = + new PolarisCallContext( + managerFactory.getOrCreateSessionSupplier(realmContext).get(), + diagServices, + new PolarisConfigurationStore() { + @Override + public @Nullable T getConfiguration(PolarisCallContext ctx, String configName) { + return (T) configMap.get(configName); + } + }, + Clock.systemDefaultZone()); + + PolarisEntityManager entityManager = + new PolarisEntityManager( + metaStoreManager, polarisContext::getMetaStore, new StorageCredentialCache()); + + CallContext callContext = CallContext.of(null, polarisContext); + CallContext.setCurrentContext(callContext); + + PrincipalEntity rootEntity = + new PrincipalEntity( + PolarisEntity.of( + entityManager + .getMetaStoreManager() + .readEntityByName( + polarisContext, + null, + PolarisEntityType.PRINCIPAL, + PolarisEntitySubType.NULL_SUBTYPE, + "root") + .getEntity())); + AuthenticatedPolarisPrincipal authenticatedRoot = + new AuthenticatedPolarisPrincipal(rootEntity, Set.of()); + + PolarisAdminService adminService = + new PolarisAdminService( + callContext, + entityManager, + authenticatedRoot, + new PolarisAuthorizer(new PolarisConfigurationStore() {})); + PolarisEntity catalogEntity = + adminService.createCatalog( + new CatalogEntity.Builder() + .setName(CATALOG_NAME) + .setStorageConfigurationInfo( + new FileStorageConfigInfo( + StorageConfigInfo.StorageTypeEnum.FILE, List.of("file://", "/", "*")), + "file://tmp") + .build()); + + PolarisPassthroughResolutionView passthroughView = + new PolarisPassthroughResolutionView( + callContext, entityManager, authenticatedRoot, CATALOG_NAME); + this.catalog = + new BasePolarisCatalog(entityManager, callContext, passthroughView, Mockito.mock()); + this.catalog.initialize( + CATALOG_NAME, + ImmutableMap.of( + CatalogProperties.FILE_IO_IMPL, "org.apache.iceberg.inmemory.InMemoryFileIO")); + } + + @Override + protected BasePolarisCatalog catalog() { + return catalog; + } + + @Override + protected Catalog tableCatalog() { + return catalog; + } + + @Override + protected boolean requiresNamespaceCreate() { + return true; + } +} diff --git a/polaris-service/src/test/java/io/polaris/service/catalog/PolarisCatalogHandlerWrapperAuthzTest.java b/polaris-service/src/test/java/io/polaris/service/catalog/PolarisCatalogHandlerWrapperAuthzTest.java new file mode 100644 index 0000000000..2b45e11205 --- /dev/null +++ b/polaris-service/src/test/java/io/polaris/service/catalog/PolarisCatalogHandlerWrapperAuthzTest.java @@ -0,0 +1,1671 @@ +package io.polaris.service.catalog; + +import com.google.common.collect.ImmutableMap; +import io.polaris.core.admin.model.FileStorageConfigInfo; +import io.polaris.core.admin.model.PrincipalWithCredentialsCredentials; +import io.polaris.core.admin.model.StorageConfigInfo; +import io.polaris.core.auth.AuthenticatedPolarisPrincipal; +import io.polaris.core.context.CallContext; +import io.polaris.core.context.RealmContext; +import io.polaris.core.entity.CatalogEntity; +import io.polaris.core.entity.CatalogRoleEntity; +import io.polaris.core.entity.PolarisPrivilege; +import io.polaris.core.entity.PrincipalEntity; +import io.polaris.core.persistence.PolarisEntityManager; +import io.polaris.core.persistence.PolarisMetaStoreManager; +import io.polaris.core.persistence.resolver.PolarisResolutionManifest; +import io.polaris.service.admin.PolarisAuthzTestBase; +import io.polaris.service.config.RealmEntityManagerFactory; +import io.polaris.service.context.PolarisCallContextCatalogFactory; +import io.polaris.service.types.NotificationRequest; +import io.polaris.service.types.NotificationType; +import io.polaris.service.types.TableUpdateNotification; +import java.time.Instant; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import org.apache.iceberg.CatalogProperties; +import org.apache.iceberg.catalog.Catalog; +import org.apache.iceberg.catalog.Namespace; +import org.apache.iceberg.catalog.TableIdentifier; +import org.apache.iceberg.exceptions.ForbiddenException; +import org.apache.iceberg.rest.requests.CommitTransactionRequest; +import org.apache.iceberg.rest.requests.CreateNamespaceRequest; +import org.apache.iceberg.rest.requests.CreateTableRequest; +import org.apache.iceberg.rest.requests.CreateViewRequest; +import org.apache.iceberg.rest.requests.ImmutableCreateViewRequest; +import org.apache.iceberg.rest.requests.RegisterTableRequest; +import org.apache.iceberg.rest.requests.RenameTableRequest; +import org.apache.iceberg.rest.requests.UpdateNamespacePropertiesRequest; +import org.apache.iceberg.rest.requests.UpdateTableRequest; +import org.apache.iceberg.view.ImmutableSQLViewRepresentation; +import org.apache.iceberg.view.ImmutableViewVersion; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +public class PolarisCatalogHandlerWrapperAuthzTest extends PolarisAuthzTestBase { + private PolarisCatalogHandlerWrapper newWrapper() { + return newWrapper(Set.of()); + } + + private PolarisCatalogHandlerWrapper newWrapper(Set activatedPrincipalRoles) { + return newWrapper( + activatedPrincipalRoles, CATALOG_NAME, new TestPolarisCallContextCatalogFactory()); + } + + private PolarisCatalogHandlerWrapper newWrapper( + Set activatedPrincipalRoles, + String catalogName, + PolarisCallContextCatalogFactory factory) { + final AuthenticatedPolarisPrincipal authenticatedPrincipal = + new AuthenticatedPolarisPrincipal(principalEntity, activatedPrincipalRoles); + return new PolarisCatalogHandlerWrapper( + callContext, + entityManager, + authenticatedPrincipal, + factory, + catalogName, + polarisAuthorizer); + } + + /** + * Tests each "sufficient" privilege individually using CATALOG_ROLE1 by granting at the + * CATALOG_NAME level, revoking after each test, and also ensuring that the request fails after + * revocation. + * + * @param sufficientPrivileges List of privileges that should be sufficient each in isolation for + * {@code action} to succeed. + * @param action The operation being tested; could also be multiple operations that should all + * succeed with the sufficient privilege + * @param cleanupAction If non-null, additional action to run to "undo" a previous success action + * in case the action has side effects. Called before revoking the sufficient privilege; + * either the cleanup privileges must be latent, or the cleanup action could be run with + * PRINCIPAL_ROLE2 while runnint {@code action} with PRINCIPAL_ROLE1. + */ + private void doTestSufficientPrivileges( + List sufficientPrivileges, Runnable action, Runnable cleanupAction) { + doTestSufficientPrivilegeSets( + sufficientPrivileges.stream().map(priv -> Set.of(priv)).toList(), + action, + cleanupAction, + PRINCIPAL_NAME); + } + + /** + * @param sufficientPrivileges each set of concurrent privileges expected to be sufficient + * together. + * @param action + * @param cleanupAction + * @param principalName + */ + private void doTestSufficientPrivilegeSets( + List> sufficientPrivileges, + Runnable action, + Runnable cleanupAction, + String principalName) { + doTestSufficientPrivilegeSets( + sufficientPrivileges, action, cleanupAction, principalName, CATALOG_NAME); + } + + /** + * @param sufficientPrivileges each set of concurrent privileges expected to be sufficient + * together. + * @param action + * @param cleanupAction + * @param principalName + * @param catalogName + */ + private void doTestSufficientPrivilegeSets( + List> sufficientPrivileges, + Runnable action, + Runnable cleanupAction, + String principalName, + String catalogName) { + doTestSufficientPrivilegeSets( + sufficientPrivileges, + action, + cleanupAction, + principalName, + (privilege) -> + adminService.grantPrivilegeOnCatalogToRole(catalogName, CATALOG_ROLE1, privilege), + (privilege) -> + adminService.revokePrivilegeOnCatalogFromRole(catalogName, CATALOG_ROLE1, privilege)); + } + + private void doTestInsufficientPrivileges( + List insufficientPrivileges, Runnable action) { + doTestInsufficientPrivileges(insufficientPrivileges, PRINCIPAL_NAME, action); + } + + /** + * Tests each "insufficient" privilege individually using CATALOG_ROLE1 by granting at the + * CATALOG_NAME level, ensuring the action fails, then revoking after each test case. + */ + private void doTestInsufficientPrivileges( + List insufficientPrivileges, String principalName, Runnable action) { + doTestInsufficientPrivileges( + insufficientPrivileges, + principalName, + action, + (privilege) -> + adminService.grantPrivilegeOnCatalogToRole(CATALOG_NAME, CATALOG_ROLE1, privilege), + (privilege) -> + adminService.revokePrivilegeOnCatalogFromRole(CATALOG_NAME, CATALOG_ROLE1, privilege)); + } + + @Test + public void testListNamespacesAllSufficientPrivileges() { + doTestSufficientPrivileges( + List.of( + PolarisPrivilege.NAMESPACE_LIST, + PolarisPrivilege.NAMESPACE_READ_PROPERTIES, + PolarisPrivilege.NAMESPACE_WRITE_PROPERTIES, + PolarisPrivilege.NAMESPACE_CREATE, + PolarisPrivilege.NAMESPACE_FULL_METADATA, + PolarisPrivilege.CATALOG_MANAGE_CONTENT), + () -> newWrapper().listNamespaces(Namespace.of()), + null /* cleanupAction */); + } + + @Test + public void testListNamespacesInsufficientPermissions() { + doTestInsufficientPrivileges( + List.of( + PolarisPrivilege.TABLE_FULL_METADATA, + PolarisPrivilege.VIEW_FULL_METADATA, + PolarisPrivilege.NAMESPACE_DROP), + () -> newWrapper().listNamespaces(Namespace.of())); + } + + @Test + public void testInsufficientPermissionsPriorToSecretRotation() { + String principalName = "all_the_powers"; + PolarisMetaStoreManager.CreatePrincipalResult newPrincipal = + entityManager + .getMetaStoreManager() + .createPrincipal( + callContext.getPolarisCallContext(), + new PrincipalEntity.Builder() + .setName(principalName) + .setCreateTimestamp(Instant.now().toEpochMilli()) + .setCredentialRotationRequiredState() + .build()); + adminService.assignPrincipalRole(principalName, PRINCIPAL_ROLE1); + adminService.assignPrincipalRole(principalName, PRINCIPAL_ROLE2); + + final AuthenticatedPolarisPrincipal authenticatedPrincipal = + new AuthenticatedPolarisPrincipal( + PrincipalEntity.of(newPrincipal.getPrincipal()), Set.of()); + PolarisCatalogHandlerWrapper wrapper = + new PolarisCatalogHandlerWrapper( + callContext, + entityManager, + authenticatedPrincipal, + new TestPolarisCallContextCatalogFactory(), + CATALOG_NAME, + polarisAuthorizer); + + // a variety of actions are all disallowed because the principal's credentials must be rotated + doTestInsufficientPrivileges( + List.of(PolarisPrivilege.values()), + principalName, + () -> wrapper.listNamespaces(Namespace.of())); + Namespace ns3 = Namespace.of("ns3"); + doTestInsufficientPrivileges( + List.of(PolarisPrivilege.values()), + principalName, + () -> wrapper.createNamespace(CreateNamespaceRequest.builder().withNamespace(ns3).build())); + doTestInsufficientPrivileges( + List.of(PolarisPrivilege.values()), principalName, () -> wrapper.listTables(NS1)); + PrincipalWithCredentialsCredentials credentials = + new PrincipalWithCredentialsCredentials( + newPrincipal.getPrincipalSecrets().getPrincipalClientId(), + newPrincipal.getPrincipalSecrets().getMainSecret()); + PrincipalEntity refreshPrincipal = + rotateAndRefreshPrincipal( + entityManager.getMetaStoreManager(), + principalName, + credentials, + callContext.getPolarisCallContext()); + final AuthenticatedPolarisPrincipal authenticatedPrincipal1 = + new AuthenticatedPolarisPrincipal(PrincipalEntity.of(refreshPrincipal), Set.of()); + PolarisCatalogHandlerWrapper refreshedWrapper = + new PolarisCatalogHandlerWrapper( + callContext, + entityManager, + authenticatedPrincipal1, + new TestPolarisCallContextCatalogFactory(), + CATALOG_NAME, + polarisAuthorizer); + + doTestSufficientPrivilegeSets( + List.of(Set.of(PolarisPrivilege.NAMESPACE_LIST)), + () -> refreshedWrapper.listNamespaces(Namespace.of()), + null, + principalName); + doTestSufficientPrivilegeSets( + List.of(Set.of(PolarisPrivilege.NAMESPACE_CREATE)), + () -> + refreshedWrapper.createNamespace( + CreateNamespaceRequest.builder().withNamespace(ns3).build()), + null, + principalName); + doTestSufficientPrivilegeSets( + List.of(Set.of(PolarisPrivilege.TABLE_LIST)), + () -> refreshedWrapper.listTables(ns3), + null, + principalName); + } + + @Test + public void testListNamespacesCatalogLevelWithPrincipalRoleActivation() { + // Grant catalog-level privilege to CATALOG_ROLE1 + Assertions.assertThat( + adminService.grantPrivilegeOnCatalogToRole( + CATALOG_NAME, CATALOG_ROLE1, PolarisPrivilege.NAMESPACE_LIST)) + .isTrue(); + Assertions.assertThat(newWrapper().listNamespaces(Namespace.of()).namespaces()) + .containsAll(List.of(NS1, NS2)); + + // Just activating PRINCIPAL_ROLE1 should also work. + Assertions.assertThat( + newWrapper(Set.of(PRINCIPAL_ROLE1)).listNamespaces(Namespace.of()).namespaces()) + .containsAll(List.of(NS1, NS2)); + + // If we only activate PRINCIPAL_ROLE2 it won't have the privilege. + Assertions.assertThatThrownBy( + () -> newWrapper(Set.of(PRINCIPAL_ROLE2)).listNamespaces(Namespace.of())) + .isInstanceOf(ForbiddenException.class) + .hasMessageContaining("is not authorized"); + + // If we revoke, then it should fail again even with all principal roles activated. + Assertions.assertThat( + adminService.revokePrivilegeOnCatalogFromRole( + CATALOG_NAME, CATALOG_ROLE1, PolarisPrivilege.NAMESPACE_LIST)) + .isTrue(); + Assertions.assertThatThrownBy(() -> newWrapper().listNamespaces(Namespace.of())) + .isInstanceOf(ForbiddenException.class); + } + + @Test + public void testListNamespacesChildOnly() { + // Grant only NS1-level privilege to CATALOG_ROLE1 + Assertions.assertThat( + adminService.grantPrivilegeOnNamespaceToRole( + CATALOG_NAME, CATALOG_ROLE1, NS1, PolarisPrivilege.NAMESPACE_LIST)) + .isTrue(); + + // Listing directly on NS1 succeeds + Assertions.assertThat(newWrapper().listNamespaces(NS1).namespaces()) + .containsAll(List.of(NS1A, NS1B)); + + // Root listing fails + Assertions.assertThatThrownBy(() -> newWrapper().listNamespaces(Namespace.of())) + .isInstanceOf(ForbiddenException.class); + + // NS2 listing fails + Assertions.assertThatThrownBy(() -> newWrapper().listNamespaces(Namespace.of())) + .isInstanceOf(ForbiddenException.class); + + // Listing on a child of NS1 succeeds + Assertions.assertThat(newWrapper().listNamespaces(NS1A).namespaces()) + .containsAll(List.of(NS1AA)); + } + + @Test + public void testCreateNamespaceAllSufficientPrivileges() { + Assertions.assertThat( + adminService.grantPrivilegeOnCatalogToRole( + CATALOG_NAME, CATALOG_ROLE2, PolarisPrivilege.NAMESPACE_DROP)) + .isTrue(); + + // Use PRINCIPAL_ROLE1 for privilege-testing, PRINCIPAL_ROLE2 for cleanup. + doTestSufficientPrivileges( + List.of( + PolarisPrivilege.NAMESPACE_CREATE, + PolarisPrivilege.NAMESPACE_FULL_METADATA, + PolarisPrivilege.CATALOG_MANAGE_CONTENT), + () -> { + newWrapper(Set.of(PRINCIPAL_ROLE1)) + .createNamespace( + CreateNamespaceRequest.builder().withNamespace(Namespace.of("newns")).build()); + newWrapper(Set.of(PRINCIPAL_ROLE1)) + .createNamespace( + CreateNamespaceRequest.builder() + .withNamespace(Namespace.of("ns1", "ns1a", "newns")) + .build()); + }, + () -> { + newWrapper(Set.of(PRINCIPAL_ROLE2)).dropNamespace(Namespace.of("newns")); + newWrapper(Set.of(PRINCIPAL_ROLE2)).dropNamespace(Namespace.of("ns1", "ns1a", "newns")); + }); + } + + @Test + public void testCreateNamespacesInsufficientPermissions() { + doTestInsufficientPrivileges( + List.of( + PolarisPrivilege.TABLE_FULL_METADATA, + PolarisPrivilege.VIEW_FULL_METADATA, + PolarisPrivilege.NAMESPACE_DROP, + PolarisPrivilege.NAMESPACE_READ_PROPERTIES, + PolarisPrivilege.NAMESPACE_WRITE_PROPERTIES, + PolarisPrivilege.NAMESPACE_LIST), + () -> + newWrapper() + .createNamespace( + CreateNamespaceRequest.builder().withNamespace(Namespace.of("newns")).build())); + } + + @Test + public void testLoadNamespaceMetadataSufficientPrivileges() { + doTestSufficientPrivileges( + List.of( + PolarisPrivilege.NAMESPACE_READ_PROPERTIES, + PolarisPrivilege.NAMESPACE_WRITE_PROPERTIES, + PolarisPrivilege.NAMESPACE_FULL_METADATA, + PolarisPrivilege.CATALOG_MANAGE_CONTENT), + () -> newWrapper().loadNamespaceMetadata(NS1A), + null /* cleanupAction */); + } + + @Test + public void testLoadNamespaceMetadataInsufficientPermissions() { + doTestInsufficientPrivileges( + List.of( + PolarisPrivilege.TABLE_FULL_METADATA, + PolarisPrivilege.VIEW_FULL_METADATA, + PolarisPrivilege.NAMESPACE_CREATE, + PolarisPrivilege.NAMESPACE_LIST, + PolarisPrivilege.NAMESPACE_DROP), + () -> newWrapper().loadNamespaceMetadata(NS1A)); + } + + @Test + public void testNamespaceExistsAllSufficientPrivileges() { + // TODO: If we change the behavior of existence-check to return 404 on unauthorized, + // the overall test structure will need to change (other tests catching ForbiddenException + // need to still have catalog-level "REFERENCE" equivalent privileges, and the exists() + // tests need to expect 404 instead). + doTestSufficientPrivileges( + List.of( + PolarisPrivilege.NAMESPACE_LIST, + PolarisPrivilege.NAMESPACE_READ_PROPERTIES, + PolarisPrivilege.NAMESPACE_WRITE_PROPERTIES, + PolarisPrivilege.NAMESPACE_CREATE, + PolarisPrivilege.NAMESPACE_FULL_METADATA, + PolarisPrivilege.CATALOG_MANAGE_CONTENT), + () -> newWrapper().namespaceExists(NS1A), + null /* cleanupAction */); + } + + @Test + public void testNamespaceExistsInsufficientPermissions() { + doTestInsufficientPrivileges( + List.of( + PolarisPrivilege.TABLE_FULL_METADATA, + PolarisPrivilege.VIEW_FULL_METADATA, + PolarisPrivilege.NAMESPACE_DROP), + () -> newWrapper().namespaceExists(NS1A)); + } + + @Test + public void testDropNamespaceSufficientPrivileges() { + Assertions.assertThat( + adminService.grantPrivilegeOnCatalogToRole( + CATALOG_NAME, CATALOG_ROLE2, PolarisPrivilege.NAMESPACE_CREATE)) + .isTrue(); + + // Use PRINCIPAL_ROLE1 for privilege-testing, PRINCIPAL_ROLE2 for cleanup. + doTestSufficientPrivileges( + List.of( + PolarisPrivilege.NAMESPACE_DROP, + PolarisPrivilege.NAMESPACE_FULL_METADATA, + PolarisPrivilege.CATALOG_MANAGE_CONTENT), + () -> { + newWrapper(Set.of(PRINCIPAL_ROLE1)).dropNamespace(NS1AA); + }, + () -> { + newWrapper(Set.of(PRINCIPAL_ROLE2)) + .createNamespace(CreateNamespaceRequest.builder().withNamespace(NS1AA).build()); + }); + } + + @Test + public void testDropNamespaceInsufficientPermissions() { + doTestInsufficientPrivileges( + List.of( + PolarisPrivilege.TABLE_FULL_METADATA, + PolarisPrivilege.VIEW_FULL_METADATA, + PolarisPrivilege.NAMESPACE_CREATE, + PolarisPrivilege.NAMESPACE_LIST, + PolarisPrivilege.NAMESPACE_READ_PROPERTIES, + PolarisPrivilege.NAMESPACE_WRITE_PROPERTIES), + () -> newWrapper().dropNamespace(NS1AA)); + } + + @Test + public void testUpdateNamespacePropertiesAllSufficientPrivileges() { + doTestSufficientPrivileges( + List.of( + PolarisPrivilege.NAMESPACE_WRITE_PROPERTIES, + PolarisPrivilege.NAMESPACE_FULL_METADATA, + PolarisPrivilege.CATALOG_MANAGE_CONTENT), + () -> { + newWrapper() + .updateNamespaceProperties( + NS1A, UpdateNamespacePropertiesRequest.builder().update("foo", "bar").build()); + newWrapper() + .updateNamespaceProperties( + NS1A, UpdateNamespacePropertiesRequest.builder().remove("foo").build()); + }, + null /* cleanupAction */); + } + + @Test + public void testUpdateNamespacePropertiesInsufficientPermissions() { + doTestInsufficientPrivileges( + List.of( + PolarisPrivilege.TABLE_FULL_METADATA, + PolarisPrivilege.VIEW_FULL_METADATA, + PolarisPrivilege.NAMESPACE_LIST, + PolarisPrivilege.NAMESPACE_READ_PROPERTIES, + PolarisPrivilege.NAMESPACE_CREATE, + PolarisPrivilege.NAMESPACE_DROP), + () -> + newWrapper() + .updateNamespaceProperties( + NS1A, UpdateNamespacePropertiesRequest.builder().update("foo", "bar").build())); + } + + @Test + public void testListTablesAllSufficientPrivileges() { + doTestSufficientPrivileges( + List.of( + PolarisPrivilege.TABLE_LIST, + PolarisPrivilege.TABLE_READ_PROPERTIES, + PolarisPrivilege.TABLE_WRITE_PROPERTIES, + PolarisPrivilege.TABLE_READ_DATA, + PolarisPrivilege.TABLE_WRITE_DATA, + PolarisPrivilege.TABLE_CREATE, + PolarisPrivilege.TABLE_FULL_METADATA, + PolarisPrivilege.CATALOG_MANAGE_CONTENT), + () -> newWrapper().listTables(NS1A), + null /* cleanupAction */); + } + + @Test + public void testListTablesInsufficientPermissions() { + doTestInsufficientPrivileges( + List.of( + PolarisPrivilege.NAMESPACE_FULL_METADATA, + PolarisPrivilege.VIEW_FULL_METADATA, + PolarisPrivilege.TABLE_DROP), + () -> newWrapper().listTables(NS1A)); + } + + @Test + public void testCreateTableDirectAllSufficientPrivileges() { + Assertions.assertThat( + adminService.grantPrivilegeOnCatalogToRole( + CATALOG_NAME, CATALOG_ROLE2, PolarisPrivilege.TABLE_DROP)) + .isTrue(); + Assertions.assertThat( + adminService.grantPrivilegeOnCatalogToRole( + CATALOG_NAME, CATALOG_ROLE2, PolarisPrivilege.TABLE_WRITE_DATA)) + .isTrue(); + + final TableIdentifier newtable = TableIdentifier.of(NS2, "newtable"); + final CreateTableRequest createRequest = + CreateTableRequest.builder().withName("newtable").withSchema(SCHEMA).build(); + + // Use PRINCIPAL_ROLE1 for privilege-testing, PRINCIPAL_ROLE2 for cleanup. + doTestSufficientPrivileges( + List.of( + PolarisPrivilege.TABLE_CREATE, + PolarisPrivilege.TABLE_FULL_METADATA, + PolarisPrivilege.CATALOG_MANAGE_CONTENT), + () -> { + newWrapper(Set.of(PRINCIPAL_ROLE1)).createTableDirect(NS2, createRequest); + }, + () -> { + newWrapper(Set.of(PRINCIPAL_ROLE2)).dropTableWithPurge(newtable); + }); + } + + @Test + public void testCreateTableDirectInsufficientPermissions() { + final TableIdentifier newtable = TableIdentifier.of(NS2, "newtable"); + final CreateTableRequest createRequest = + CreateTableRequest.builder().withName("newtable").withSchema(SCHEMA).build(); + + doTestInsufficientPrivileges( + List.of( + PolarisPrivilege.NAMESPACE_FULL_METADATA, + PolarisPrivilege.VIEW_FULL_METADATA, + PolarisPrivilege.TABLE_DROP, + PolarisPrivilege.TABLE_READ_PROPERTIES, + PolarisPrivilege.TABLE_WRITE_PROPERTIES, + PolarisPrivilege.TABLE_READ_DATA, + PolarisPrivilege.TABLE_WRITE_DATA, + PolarisPrivilege.TABLE_LIST), + () -> { + newWrapper(Set.of(PRINCIPAL_ROLE1)).createTableDirect(NS2, createRequest); + }); + } + + @Test + public void testCreateTableStagedAllSufficientPrivileges() { + Assertions.assertThat( + adminService.grantPrivilegeOnCatalogToRole( + CATALOG_NAME, CATALOG_ROLE2, PolarisPrivilege.TABLE_DROP)) + .isTrue(); + + final CreateTableRequest createStagedRequest = + CreateTableRequest.builder() + .withName("stagetable") + .withSchema(SCHEMA) + .stageCreate() + .build(); + + // Use PRINCIPAL_ROLE1 for privilege-testing, PRINCIPAL_ROLE2 for cleanup. + doTestSufficientPrivileges( + List.of( + PolarisPrivilege.TABLE_CREATE, + PolarisPrivilege.TABLE_FULL_METADATA, + PolarisPrivilege.CATALOG_MANAGE_CONTENT), + () -> { + newWrapper(Set.of(PRINCIPAL_ROLE1)).createTableStaged(NS2, createStagedRequest); + }, + // createTableStaged doesn't actually commit any metadata + null); + } + + @Test + public void testCreateTableStagedInsufficientPermissions() { + final CreateTableRequest createStagedRequest = + CreateTableRequest.builder() + .withName("stagetable") + .withSchema(SCHEMA) + .stageCreate() + .build(); + + doTestInsufficientPrivileges( + List.of( + PolarisPrivilege.NAMESPACE_FULL_METADATA, + PolarisPrivilege.VIEW_FULL_METADATA, + PolarisPrivilege.TABLE_DROP, + PolarisPrivilege.TABLE_READ_PROPERTIES, + PolarisPrivilege.TABLE_WRITE_PROPERTIES, + PolarisPrivilege.TABLE_READ_DATA, + PolarisPrivilege.TABLE_WRITE_DATA, + PolarisPrivilege.TABLE_LIST), + () -> { + newWrapper(Set.of(PRINCIPAL_ROLE1)).createTableStaged(NS2, createStagedRequest); + }); + } + + @Test + public void testCreateTableStagedWithWriteDelegationAllSufficientPrivileges() { + Assertions.assertThat( + adminService.grantPrivilegeOnCatalogToRole( + CATALOG_NAME, CATALOG_ROLE2, PolarisPrivilege.TABLE_DROP)) + .isTrue(); + + final CreateTableRequest createStagedWithWriteDelegationRequest = + CreateTableRequest.builder() + .withName("stagetable") + .withSchema(SCHEMA) + .stageCreate() + .build(); + + doTestSufficientPrivilegeSets( + List.of( + Set.of(PolarisPrivilege.TABLE_CREATE, PolarisPrivilege.TABLE_WRITE_DATA), + Set.of(PolarisPrivilege.CATALOG_MANAGE_CONTENT)), + () -> { + newWrapper(Set.of(PRINCIPAL_ROLE1)) + .createTableStagedWithWriteDelegation( + NS2, createStagedWithWriteDelegationRequest, "vended-credentials"); + }, + // createTableStagedWithWriteDelegation doesn't actually commit any metadata + null, + PRINCIPAL_NAME); + } + + @Test + public void testCreateTableStagedWithWriteDelegationInsufficientPermissions() { + final CreateTableRequest createStagedWithWriteDelegationRequest = + CreateTableRequest.builder() + .withName("stagetable") + .withSchema(SCHEMA) + .stageCreate() + .build(); + + doTestInsufficientPrivileges( + List.of( + PolarisPrivilege.NAMESPACE_FULL_METADATA, + PolarisPrivilege.VIEW_FULL_METADATA, + PolarisPrivilege.TABLE_DROP, + PolarisPrivilege.TABLE_CREATE, // TABLE_CREATE itself is insufficient for delegation + PolarisPrivilege.TABLE_READ_PROPERTIES, + PolarisPrivilege.TABLE_WRITE_PROPERTIES, + PolarisPrivilege.TABLE_READ_DATA, + PolarisPrivilege.TABLE_WRITE_DATA, + PolarisPrivilege.TABLE_LIST), + () -> { + newWrapper(Set.of(PRINCIPAL_ROLE1)) + .createTableStagedWithWriteDelegation( + NS2, createStagedWithWriteDelegationRequest, "vended-credentials"); + }); + } + + @Test + public void testRegisterTableAllSufficientPrivileges() { + Assertions.assertThat( + adminService.grantPrivilegeOnCatalogToRole( + CATALOG_NAME, CATALOG_ROLE2, PolarisPrivilege.TABLE_DROP)) + .isTrue(); + Assertions.assertThat( + adminService.grantPrivilegeOnCatalogToRole( + CATALOG_NAME, CATALOG_ROLE2, PolarisPrivilege.TABLE_READ_PROPERTIES)) + .isTrue(); + + // To get a handy metadata file we can use one from another table. + final String metadataLocation = newWrapper().loadTable(TABLE_NS1_1, "all").metadataLocation(); + + final TableIdentifier newtable = TableIdentifier.of(NS2, "newtable"); + final RegisterTableRequest registerRequest = + new RegisterTableRequest() { + @Override + public String name() { + return "newtable"; + } + + @Override + public String metadataLocation() { + return metadataLocation; + } + }; + + // Use PRINCIPAL_ROLE1 for privilege-testing, PRINCIPAL_ROLE2 for cleanup. + doTestSufficientPrivileges( + List.of( + PolarisPrivilege.TABLE_CREATE, + PolarisPrivilege.TABLE_FULL_METADATA, + PolarisPrivilege.CATALOG_MANAGE_CONTENT), + () -> { + newWrapper(Set.of(PRINCIPAL_ROLE1)).registerTable(NS2, registerRequest); + }, + () -> { + newWrapper(Set.of(PRINCIPAL_ROLE2)).dropTableWithoutPurge(newtable); + }); + } + + @Test + public void testRegisterTableInsufficientPermissions() { + Assertions.assertThat( + adminService.grantPrivilegeOnCatalogToRole( + CATALOG_NAME, CATALOG_ROLE2, PolarisPrivilege.TABLE_READ_PROPERTIES)) + .isTrue(); + + // To get a handy metadata file we can use one from another table. + final String metadataLocation = newWrapper().loadTable(TABLE_NS1_1, "all").metadataLocation(); + + final TableIdentifier newtable = TableIdentifier.of(NS2, "newtable"); + final RegisterTableRequest registerRequest = + new RegisterTableRequest() { + @Override + public String name() { + return "newtable"; + } + + @Override + public String metadataLocation() { + return metadataLocation; + } + }; + + doTestInsufficientPrivileges( + List.of( + PolarisPrivilege.NAMESPACE_FULL_METADATA, + PolarisPrivilege.VIEW_FULL_METADATA, + PolarisPrivilege.TABLE_DROP, + PolarisPrivilege.TABLE_READ_PROPERTIES, + PolarisPrivilege.TABLE_WRITE_PROPERTIES, + PolarisPrivilege.TABLE_READ_DATA, + PolarisPrivilege.TABLE_WRITE_DATA, + PolarisPrivilege.TABLE_LIST), + () -> { + newWrapper(Set.of(PRINCIPAL_ROLE1)).registerTable(NS2, registerRequest); + }); + } + + @Test + public void testLoadTableSufficientPrivileges() { + doTestSufficientPrivileges( + List.of( + PolarisPrivilege.TABLE_READ_PROPERTIES, + PolarisPrivilege.TABLE_WRITE_PROPERTIES, + PolarisPrivilege.TABLE_READ_DATA, + PolarisPrivilege.TABLE_WRITE_DATA, + PolarisPrivilege.TABLE_FULL_METADATA, + PolarisPrivilege.CATALOG_MANAGE_CONTENT), + () -> newWrapper().loadTable(TABLE_NS1A_2, "all"), + null /* cleanupAction */); + } + + @Test + public void testLoadTableInsufficientPermissions() { + doTestInsufficientPrivileges( + List.of( + PolarisPrivilege.NAMESPACE_FULL_METADATA, + PolarisPrivilege.VIEW_FULL_METADATA, + PolarisPrivilege.TABLE_CREATE, + PolarisPrivilege.TABLE_LIST, + PolarisPrivilege.TABLE_DROP), + () -> newWrapper().loadTable(TABLE_NS1A_2, "all")); + } + + @Test + public void testLoadTableWithReadAccessDelegationSufficientPrivileges() { + doTestSufficientPrivileges( + List.of( + PolarisPrivilege.TABLE_READ_DATA, + PolarisPrivilege.TABLE_WRITE_DATA, + PolarisPrivilege.CATALOG_MANAGE_CONTENT), + () -> newWrapper().loadTableWithAccessDelegation(TABLE_NS1A_2, "vended-credentials", "all"), + null /* cleanupAction */); + } + + @Test + public void testLoadTableWithReadAccessDelegationInsufficientPermissions() { + doTestInsufficientPrivileges( + List.of( + PolarisPrivilege.NAMESPACE_FULL_METADATA, + PolarisPrivilege.VIEW_FULL_METADATA, + PolarisPrivilege.TABLE_FULL_METADATA, + PolarisPrivilege.TABLE_READ_PROPERTIES, + PolarisPrivilege.TABLE_WRITE_PROPERTIES, + PolarisPrivilege.TABLE_CREATE, + PolarisPrivilege.TABLE_LIST, + PolarisPrivilege.TABLE_DROP), + () -> + newWrapper().loadTableWithAccessDelegation(TABLE_NS1A_2, "vended-credentials", "all")); + } + + @Test + public void testLoadTableWithWriteAccessDelegationSufficientPrivileges() { + doTestSufficientPrivileges( + List.of( + // TODO: Once we give different creds for read/write privilege, move this + // TABLE_READ_DATA into a special-case test; with only TABLE_READ_DATA we'd expet + // to receive a read-only credential. + PolarisPrivilege.TABLE_READ_DATA, + PolarisPrivilege.TABLE_WRITE_DATA, + PolarisPrivilege.CATALOG_MANAGE_CONTENT), + () -> newWrapper().loadTableWithAccessDelegation(TABLE_NS1A_2, "vended-credentials", "all"), + null /* cleanupAction */); + } + + @Test + public void testLoadTableWithWriteAccessDelegationInsufficientPermissions() { + doTestInsufficientPrivileges( + List.of( + PolarisPrivilege.NAMESPACE_FULL_METADATA, + PolarisPrivilege.VIEW_FULL_METADATA, + PolarisPrivilege.TABLE_FULL_METADATA, + PolarisPrivilege.TABLE_READ_PROPERTIES, + PolarisPrivilege.TABLE_WRITE_PROPERTIES, + PolarisPrivilege.TABLE_CREATE, + PolarisPrivilege.TABLE_LIST, + PolarisPrivilege.TABLE_DROP), + () -> + newWrapper().loadTableWithAccessDelegation(TABLE_NS1A_2, "vended-credentials", "all")); + } + + @Test + public void testUpdateTableSufficientPrivileges() { + doTestSufficientPrivileges( + List.of( + PolarisPrivilege.TABLE_WRITE_PROPERTIES, + PolarisPrivilege.TABLE_WRITE_DATA, + PolarisPrivilege.TABLE_FULL_METADATA, + PolarisPrivilege.CATALOG_MANAGE_CONTENT), + () -> newWrapper().updateTable(TABLE_NS1A_2, new UpdateTableRequest()), + null /* cleanupAction */); + } + + @Test + public void testUpdateTableInsufficientPermissions() { + doTestInsufficientPrivileges( + List.of( + PolarisPrivilege.NAMESPACE_FULL_METADATA, + PolarisPrivilege.VIEW_FULL_METADATA, + PolarisPrivilege.TABLE_READ_PROPERTIES, + PolarisPrivilege.TABLE_READ_DATA, + PolarisPrivilege.TABLE_CREATE, + PolarisPrivilege.TABLE_LIST, + PolarisPrivilege.TABLE_DROP), + () -> newWrapper().updateTable(TABLE_NS1A_2, new UpdateTableRequest())); + } + + @Test + public void testUpdateTableForStagedCreateSufficientPrivileges() { + // Note: This is kind of cheating by only leaning on the PolarisCatalogHandlerWrapper level + // of differentiation between updateForStageCreate vs regular update so that we don't need + // to actually set up the staged create but still test the privileges. If the underlying + // behavior diverges, we need to change this test to actually start with a stageCreate. + doTestSufficientPrivileges( + List.of( + PolarisPrivilege.TABLE_CREATE, + PolarisPrivilege.TABLE_FULL_METADATA, + PolarisPrivilege.CATALOG_MANAGE_CONTENT), + () -> newWrapper().updateTableForStagedCreate(TABLE_NS1A_2, new UpdateTableRequest()), + null /* cleanupAction */); + } + + @Test + public void testUpdateTableForStagedCreateInsufficientPermissions() { + doTestInsufficientPrivileges( + List.of( + PolarisPrivilege.NAMESPACE_FULL_METADATA, + PolarisPrivilege.VIEW_FULL_METADATA, + PolarisPrivilege.TABLE_DROP, + PolarisPrivilege.TABLE_READ_PROPERTIES, + PolarisPrivilege.TABLE_WRITE_PROPERTIES, + PolarisPrivilege.TABLE_READ_DATA, + PolarisPrivilege.TABLE_WRITE_DATA, + PolarisPrivilege.TABLE_LIST), + () -> newWrapper().updateTableForStagedCreate(TABLE_NS1A_2, new UpdateTableRequest())); + } + + @Test + public void testDropTableWithoutPurgeAllSufficientPrivileges() { + Assertions.assertThat( + adminService.grantPrivilegeOnCatalogToRole( + CATALOG_NAME, CATALOG_ROLE2, PolarisPrivilege.TABLE_CREATE)) + .isTrue(); + + final CreateTableRequest createRequest = + CreateTableRequest.builder().withName(TABLE_NS1_1.name()).withSchema(SCHEMA).build(); + + // Use PRINCIPAL_ROLE1 for privilege-testing, PRINCIPAL_ROLE2 for cleanup. + doTestSufficientPrivileges( + List.of( + PolarisPrivilege.TABLE_DROP, + PolarisPrivilege.TABLE_FULL_METADATA, + PolarisPrivilege.CATALOG_MANAGE_CONTENT), + () -> { + newWrapper(Set.of(PRINCIPAL_ROLE1)).dropTableWithoutPurge(TABLE_NS1_1); + }, + () -> { + newWrapper(Set.of(PRINCIPAL_ROLE2)) + .createTableDirect(TABLE_NS1_1.namespace(), createRequest); + }); + } + + @Test + public void testDropTableWithoutPurgeInsufficientPermissions() { + doTestInsufficientPrivileges( + List.of( + PolarisPrivilege.NAMESPACE_FULL_METADATA, + PolarisPrivilege.VIEW_FULL_METADATA, + PolarisPrivilege.TABLE_CREATE, + PolarisPrivilege.TABLE_READ_PROPERTIES, + PolarisPrivilege.TABLE_WRITE_PROPERTIES, + PolarisPrivilege.TABLE_READ_DATA, + PolarisPrivilege.TABLE_WRITE_DATA, + PolarisPrivilege.TABLE_LIST), + () -> { + newWrapper(Set.of(PRINCIPAL_ROLE1)).dropTableWithoutPurge(TABLE_NS1_1); + }); + } + + @Test + public void testDropTableWithPurgeAllSufficientPrivileges() { + Assertions.assertThat( + adminService.grantPrivilegeOnCatalogToRole( + CATALOG_NAME, CATALOG_ROLE2, PolarisPrivilege.TABLE_CREATE)) + .isTrue(); + + final CreateTableRequest createRequest = + CreateTableRequest.builder().withName(TABLE_NS1_1.name()).withSchema(SCHEMA).build(); + + // Use PRINCIPAL_ROLE1 for privilege-testing, PRINCIPAL_ROLE2 for cleanup. + doTestSufficientPrivilegeSets( + List.of( + Set.of(PolarisPrivilege.TABLE_WRITE_DATA, PolarisPrivilege.TABLE_FULL_METADATA), + Set.of(PolarisPrivilege.TABLE_WRITE_DATA, PolarisPrivilege.TABLE_DROP), + Set.of(PolarisPrivilege.CATALOG_MANAGE_CONTENT)), + () -> { + newWrapper(Set.of(PRINCIPAL_ROLE1)).dropTableWithPurge(TABLE_NS1_1); + }, + () -> { + newWrapper(Set.of(PRINCIPAL_ROLE2)) + .createTableDirect(TABLE_NS1_1.namespace(), createRequest); + }, + PRINCIPAL_NAME); + } + + @Test + public void testDropTableWithPurgeInsufficientPermissions() { + doTestInsufficientPrivileges( + List.of( + PolarisPrivilege.NAMESPACE_FULL_METADATA, + PolarisPrivilege.VIEW_FULL_METADATA, + PolarisPrivilege.TABLE_FULL_METADATA, + PolarisPrivilege.TABLE_DROP, // TABLE_DROP itself is insufficient for purge + PolarisPrivilege.TABLE_CREATE, + PolarisPrivilege.TABLE_READ_PROPERTIES, + PolarisPrivilege.TABLE_WRITE_PROPERTIES, + PolarisPrivilege.TABLE_READ_DATA, + PolarisPrivilege.TABLE_WRITE_DATA, + PolarisPrivilege.TABLE_LIST), + () -> { + newWrapper(Set.of(PRINCIPAL_ROLE1)).dropTableWithPurge(TABLE_NS1_1); + }); + } + + @Test + public void testTableExistsSufficientPrivileges() { + doTestSufficientPrivileges( + List.of( + PolarisPrivilege.TABLE_LIST, + PolarisPrivilege.TABLE_READ_PROPERTIES, + PolarisPrivilege.TABLE_WRITE_PROPERTIES, + PolarisPrivilege.TABLE_READ_DATA, + PolarisPrivilege.TABLE_WRITE_DATA, + PolarisPrivilege.TABLE_CREATE, + PolarisPrivilege.TABLE_FULL_METADATA, + PolarisPrivilege.CATALOG_MANAGE_CONTENT), + () -> newWrapper().tableExists(TABLE_NS1A_2), + null /* cleanupAction */); + } + + @Test + public void testTableExistsInsufficientPermissions() { + doTestInsufficientPrivileges( + List.of( + PolarisPrivilege.NAMESPACE_FULL_METADATA, + PolarisPrivilege.VIEW_FULL_METADATA, + PolarisPrivilege.TABLE_DROP), + () -> newWrapper().tableExists(TABLE_NS1A_2)); + } + + @Test + public void testRenameTableAllSufficientPrivileges() { + final TableIdentifier srcTable = TABLE_NS1_1; + final TableIdentifier dstTable = TableIdentifier.of(NS1AA, "newtable"); + final RenameTableRequest rename1 = + RenameTableRequest.builder().withSource(srcTable).withDestination(dstTable).build(); + final RenameTableRequest rename2 = + RenameTableRequest.builder().withSource(dstTable).withDestination(srcTable).build(); + + doTestSufficientPrivilegeSets( + List.of( + Set.of(PolarisPrivilege.TABLE_FULL_METADATA), + Set.of(PolarisPrivilege.TABLE_CREATE, PolarisPrivilege.TABLE_DROP), + Set.of(PolarisPrivilege.CATALOG_MANAGE_CONTENT)), + () -> { + newWrapper(Set.of(PRINCIPAL_ROLE1)).renameTable(rename1); + }, + () -> { + newWrapper(Set.of(PRINCIPAL_ROLE1)).renameTable(rename2); + }, + PRINCIPAL_NAME); + } + + @Test + public void testRenameTableInsufficientPermissions() { + final TableIdentifier srcTable = TABLE_NS1_1; + final TableIdentifier dstTable = TableIdentifier.of(NS1AA, "newtable"); + final RenameTableRequest rename1 = + RenameTableRequest.builder().withSource(srcTable).withDestination(dstTable).build(); + + doTestInsufficientPrivileges( + List.of( + PolarisPrivilege.NAMESPACE_FULL_METADATA, + PolarisPrivilege.VIEW_FULL_METADATA, + PolarisPrivilege.TABLE_DROP, + PolarisPrivilege.TABLE_CREATE, + PolarisPrivilege.TABLE_READ_PROPERTIES, + PolarisPrivilege.TABLE_WRITE_PROPERTIES, + PolarisPrivilege.TABLE_READ_DATA, + PolarisPrivilege.TABLE_WRITE_DATA, + PolarisPrivilege.TABLE_LIST), + () -> { + newWrapper(Set.of(PRINCIPAL_ROLE1)).renameTable(rename1); + }); + } + + @Test + public void testRenameTablePrivilegesOnWrongSourceOrDestination() { + final TableIdentifier srcTable = TABLE_NS2_1; + final TableIdentifier dstTable = TableIdentifier.of(NS1AA, "newtable"); + final RenameTableRequest rename1 = + RenameTableRequest.builder().withSource(srcTable).withDestination(dstTable).build(); + final RenameTableRequest rename2 = + RenameTableRequest.builder().withSource(dstTable).withDestination(srcTable).build(); + + // Minimum privileges should succeed -- drop on src, create on dst parent. + Assertions.assertThat( + adminService.grantPrivilegeOnTableToRole( + CATALOG_NAME, CATALOG_ROLE1, srcTable, PolarisPrivilege.TABLE_DROP)) + .isTrue(); + Assertions.assertThat( + adminService.grantPrivilegeOnNamespaceToRole( + CATALOG_NAME, CATALOG_ROLE1, dstTable.namespace(), PolarisPrivilege.TABLE_CREATE)) + .isTrue(); + + // Initial rename should succeed + newWrapper().renameTable(rename1); + + // Inverse operation should fail + Assertions.assertThatThrownBy(() -> newWrapper().renameTable(rename2)) + .isInstanceOf(ForbiddenException.class); + + // Now grant TABLE_DROP on dst + Assertions.assertThat( + adminService.grantPrivilegeOnTableToRole( + CATALOG_NAME, CATALOG_ROLE1, dstTable, PolarisPrivilege.TABLE_DROP)) + .isTrue(); + + // Still not enough without TABLE_CREATE at source + Assertions.assertThatThrownBy(() -> newWrapper().renameTable(rename2)) + .isInstanceOf(ForbiddenException.class); + + // Even grant CATALOG_MANAGE_CONTENT under all of NS1 + Assertions.assertThat( + adminService.grantPrivilegeOnNamespaceToRole( + CATALOG_NAME, CATALOG_ROLE1, NS1, PolarisPrivilege.CATALOG_MANAGE_CONTENT)) + .isTrue(); + + // Still not enough to rename back to src since src was NS2. + Assertions.assertThatThrownBy(() -> newWrapper().renameTable(rename2)) + .isInstanceOf(ForbiddenException.class); + + // Finally, grant TABLE_CREATE on NS2 and it should succeed to rename back to src. + Assertions.assertThat( + adminService.grantPrivilegeOnNamespaceToRole( + CATALOG_NAME, CATALOG_ROLE1, NS2, PolarisPrivilege.TABLE_CREATE)) + .isTrue(); + newWrapper().renameTable(rename2); + } + + @Test + public void testCommitTransactionSufficientPrivileges() { + CommitTransactionRequest req = + new CommitTransactionRequest( + List.of( + UpdateTableRequest.create(TABLE_NS1_1, List.of(), List.of()), + UpdateTableRequest.create(TABLE_NS1A_1, List.of(), List.of()), + UpdateTableRequest.create(TABLE_NS1B_1, List.of(), List.of()), + UpdateTableRequest.create(TABLE_NS2_1, List.of(), List.of()))); + + doTestSufficientPrivilegeSets( + List.of( + Set.of(PolarisPrivilege.CATALOG_MANAGE_CONTENT), + Set.of(PolarisPrivilege.TABLE_FULL_METADATA), + Set.of(PolarisPrivilege.TABLE_CREATE, PolarisPrivilege.TABLE_WRITE_DATA), + Set.of(PolarisPrivilege.TABLE_CREATE, PolarisPrivilege.TABLE_WRITE_PROPERTIES)), + () -> newWrapper().commitTransaction(req), + null, + PRINCIPAL_NAME /* cleanupAction */); + } + + @Test + public void testCommitTransactionInsufficientPermissions() { + CommitTransactionRequest req = + new CommitTransactionRequest( + List.of( + UpdateTableRequest.create(TABLE_NS1_1, List.of(), List.of()), + UpdateTableRequest.create(TABLE_NS1A_1, List.of(), List.of()), + UpdateTableRequest.create(TABLE_NS1B_1, List.of(), List.of()), + UpdateTableRequest.create(TABLE_NS2_1, List.of(), List.of()))); + + doTestInsufficientPrivileges( + List.of( + PolarisPrivilege.NAMESPACE_FULL_METADATA, + PolarisPrivilege.VIEW_FULL_METADATA, + PolarisPrivilege.TABLE_READ_PROPERTIES, + PolarisPrivilege.TABLE_WRITE_PROPERTIES, + PolarisPrivilege.TABLE_READ_DATA, + PolarisPrivilege.TABLE_WRITE_DATA, + PolarisPrivilege.TABLE_CREATE, + PolarisPrivilege.TABLE_LIST, + PolarisPrivilege.TABLE_DROP), + () -> newWrapper().commitTransaction(req)); + } + + @Test + public void testCommitTransactionMixedPermissions() { + CommitTransactionRequest req = + new CommitTransactionRequest( + List.of( + UpdateTableRequest.create(TABLE_NS1_1, List.of(), List.of()), + UpdateTableRequest.create(TABLE_NS1A_1, List.of(), List.of()), + UpdateTableRequest.create(TABLE_NS1B_1, List.of(), List.of()), + UpdateTableRequest.create(TABLE_NS2_1, List.of(), List.of()))); + + // Grant TABLE_CREATE for all of NS1 + Assertions.assertThat( + adminService.grantPrivilegeOnNamespaceToRole( + CATALOG_NAME, CATALOG_ROLE1, NS1, PolarisPrivilege.TABLE_CREATE)) + .isTrue(); + Assertions.assertThatThrownBy(() -> newWrapper().commitTransaction(req)) + .isInstanceOf(ForbiddenException.class); + + // Grant TABLE_FULL_METADATA directly on TABLE_NS1_1 + Assertions.assertThat( + adminService.grantPrivilegeOnTableToRole( + CATALOG_NAME, CATALOG_ROLE1, TABLE_NS1_1, PolarisPrivilege.TABLE_FULL_METADATA)) + .isTrue(); + Assertions.assertThatThrownBy(() -> newWrapper().commitTransaction(req)) + .isInstanceOf(ForbiddenException.class); + + // Grant TABLE_WRITE_PROPERTIES on NS1A namespace + Assertions.assertThat( + adminService.grantPrivilegeOnNamespaceToRole( + CATALOG_NAME, CATALOG_ROLE1, NS1A, PolarisPrivilege.TABLE_WRITE_PROPERTIES)) + .isTrue(); + Assertions.assertThatThrownBy(() -> newWrapper().commitTransaction(req)) + .isInstanceOf(ForbiddenException.class); + + // Grant TABLE_WRITE_DATA directly on TABLE_NS1B_1 + Assertions.assertThat( + adminService.grantPrivilegeOnTableToRole( + CATALOG_NAME, CATALOG_ROLE1, TABLE_NS1B_1, PolarisPrivilege.TABLE_WRITE_DATA)) + .isTrue(); + Assertions.assertThatThrownBy(() -> newWrapper().commitTransaction(req)) + .isInstanceOf(ForbiddenException.class); + + // Grant TABLE_WRITE_PROPERTIES directly on TABLE_NS2_1 + Assertions.assertThat( + adminService.grantPrivilegeOnTableToRole( + CATALOG_NAME, CATALOG_ROLE1, TABLE_NS2_1, PolarisPrivilege.TABLE_WRITE_PROPERTIES)) + .isTrue(); + Assertions.assertThatThrownBy(() -> newWrapper().commitTransaction(req)) + .isInstanceOf(ForbiddenException.class); + + // Also grant TABLE_CREATE directly on TABLE_NS2_1 + // TODO: If we end up having fine-grained differentiation between updateForStagedCreate + // and update, then this one should only be TABLE_CREATE on the *parent* of this last table + // and the table shouldn't exist. + Assertions.assertThat( + adminService.grantPrivilegeOnTableToRole( + CATALOG_NAME, CATALOG_ROLE1, TABLE_NS2_1, PolarisPrivilege.TABLE_CREATE)) + .isTrue(); + newWrapper().commitTransaction(req); + } + + @Test + public void testListViewsAllSufficientPrivileges() { + doTestSufficientPrivileges( + List.of( + PolarisPrivilege.VIEW_LIST, + PolarisPrivilege.VIEW_READ_PROPERTIES, + PolarisPrivilege.VIEW_WRITE_PROPERTIES, + PolarisPrivilege.VIEW_CREATE, + PolarisPrivilege.VIEW_FULL_METADATA, + PolarisPrivilege.CATALOG_MANAGE_CONTENT), + () -> newWrapper().listViews(NS1A), + null /* cleanupAction */); + } + + @Test + public void testListViewsInsufficientPermissions() { + doTestInsufficientPrivileges( + List.of( + PolarisPrivilege.TABLE_FULL_METADATA, + PolarisPrivilege.NAMESPACE_FULL_METADATA, + PolarisPrivilege.VIEW_DROP), + () -> newWrapper().listViews(NS1A)); + } + + @Test + public void testCreateViewAllSufficientPrivileges() { + Assertions.assertThat( + adminService.grantPrivilegeOnCatalogToRole( + CATALOG_NAME, CATALOG_ROLE2, PolarisPrivilege.VIEW_DROP)) + .isTrue(); + + final TableIdentifier newview = TableIdentifier.of(NS2, "newview"); + final CreateViewRequest createRequest = + ImmutableCreateViewRequest.builder() + .name("newview") + .schema(SCHEMA) + .viewVersion( + ImmutableViewVersion.builder() + .versionId(1) + .timestampMillis(System.currentTimeMillis()) + .schemaId(1) + .defaultNamespace(NS1) + .addRepresentations( + ImmutableSQLViewRepresentation.builder() + .sql(VIEW_QUERY) + .dialect("spark") + .build()) + .build()) + .build(); + + // Use PRINCIPAL_ROLE1 for privilege-testing, PRINCIPAL_ROLE2 for cleanup. + doTestSufficientPrivileges( + List.of( + PolarisPrivilege.VIEW_CREATE, + PolarisPrivilege.VIEW_FULL_METADATA, + PolarisPrivilege.CATALOG_MANAGE_CONTENT), + () -> { + newWrapper(Set.of(PRINCIPAL_ROLE1)).createView(NS2, createRequest); + }, + () -> { + newWrapper(Set.of(PRINCIPAL_ROLE2)).dropView(newview); + }); + } + + @Test + public void testCreateViewInsufficientPermissions() { + final TableIdentifier newview = TableIdentifier.of(NS2, "newview"); + + final CreateViewRequest createRequest = + ImmutableCreateViewRequest.builder() + .name("newview") + .schema(SCHEMA) + .viewVersion( + ImmutableViewVersion.builder() + .versionId(1) + .timestampMillis(System.currentTimeMillis()) + .schemaId(1) + .defaultNamespace(NS1) + .addRepresentations( + ImmutableSQLViewRepresentation.builder() + .sql(VIEW_QUERY) + .dialect("spark") + .build()) + .build()) + .build(); + + doTestInsufficientPrivileges( + List.of( + PolarisPrivilege.NAMESPACE_FULL_METADATA, + PolarisPrivilege.TABLE_FULL_METADATA, + PolarisPrivilege.VIEW_DROP, + PolarisPrivilege.VIEW_READ_PROPERTIES, + PolarisPrivilege.VIEW_WRITE_PROPERTIES, + PolarisPrivilege.VIEW_LIST), + () -> { + newWrapper(Set.of(PRINCIPAL_ROLE1)).createView(NS2, createRequest); + }); + } + + @Test + public void testLoadViewSufficientPrivileges() { + doTestSufficientPrivileges( + List.of( + PolarisPrivilege.VIEW_READ_PROPERTIES, + PolarisPrivilege.VIEW_WRITE_PROPERTIES, + PolarisPrivilege.VIEW_FULL_METADATA, + PolarisPrivilege.CATALOG_MANAGE_CONTENT), + () -> newWrapper().loadView(VIEW_NS1A_2), + null /* cleanupAction */); + } + + @Test + public void testLoadViewInsufficientPermissions() { + doTestInsufficientPrivileges( + List.of( + PolarisPrivilege.NAMESPACE_FULL_METADATA, + PolarisPrivilege.TABLE_FULL_METADATA, + PolarisPrivilege.VIEW_CREATE, + PolarisPrivilege.VIEW_LIST, + PolarisPrivilege.VIEW_DROP), + () -> newWrapper().loadView(VIEW_NS1A_2)); + } + + @Test + public void testUpdateViewSufficientPrivileges() { + doTestSufficientPrivileges( + List.of( + PolarisPrivilege.VIEW_WRITE_PROPERTIES, + PolarisPrivilege.VIEW_FULL_METADATA, + PolarisPrivilege.CATALOG_MANAGE_CONTENT), + () -> newWrapper().replaceView(VIEW_NS1A_2, new UpdateTableRequest()), + null /* cleanupAction */); + } + + @Test + public void testUpdateViewInsufficientPermissions() { + doTestInsufficientPrivileges( + List.of( + PolarisPrivilege.NAMESPACE_FULL_METADATA, + PolarisPrivilege.TABLE_FULL_METADATA, + PolarisPrivilege.VIEW_READ_PROPERTIES, + PolarisPrivilege.VIEW_CREATE, + PolarisPrivilege.VIEW_LIST, + PolarisPrivilege.VIEW_DROP), + () -> newWrapper().replaceView(VIEW_NS1A_2, new UpdateTableRequest())); + } + + @Test + public void testDropViewAllSufficientPrivileges() { + Assertions.assertThat( + adminService.grantPrivilegeOnCatalogToRole( + CATALOG_NAME, CATALOG_ROLE2, PolarisPrivilege.VIEW_CREATE)) + .isTrue(); + + final CreateViewRequest createRequest = + ImmutableCreateViewRequest.builder() + .name(VIEW_NS1_1.name()) + .schema(SCHEMA) + .viewVersion( + ImmutableViewVersion.builder() + .versionId(1) + .timestampMillis(System.currentTimeMillis()) + .schemaId(1) + .defaultNamespace(NS1) + .addRepresentations( + ImmutableSQLViewRepresentation.builder() + .sql(VIEW_QUERY) + .dialect("spark") + .build()) + .build()) + .build(); + + // Use PRINCIPAL_ROLE1 for privilege-testing, PRINCIPAL_ROLE2 for cleanup. + doTestSufficientPrivileges( + List.of( + PolarisPrivilege.VIEW_DROP, + PolarisPrivilege.VIEW_FULL_METADATA, + PolarisPrivilege.CATALOG_MANAGE_CONTENT), + () -> { + newWrapper(Set.of(PRINCIPAL_ROLE1)).dropView(VIEW_NS1_1); + }, + () -> { + newWrapper(Set.of(PRINCIPAL_ROLE2)).createView(VIEW_NS1_1.namespace(), createRequest); + }); + } + + @Test + public void testDropViewInsufficientPermissions() { + doTestInsufficientPrivileges( + List.of( + PolarisPrivilege.NAMESPACE_FULL_METADATA, + PolarisPrivilege.TABLE_FULL_METADATA, + PolarisPrivilege.VIEW_CREATE, + PolarisPrivilege.VIEW_READ_PROPERTIES, + PolarisPrivilege.VIEW_WRITE_PROPERTIES, + PolarisPrivilege.VIEW_LIST), + () -> { + newWrapper(Set.of(PRINCIPAL_ROLE1)).dropView(VIEW_NS1_1); + }); + } + + @Test + public void testViewExistsSufficientPrivileges() { + doTestSufficientPrivileges( + List.of( + PolarisPrivilege.VIEW_LIST, + PolarisPrivilege.VIEW_READ_PROPERTIES, + PolarisPrivilege.VIEW_WRITE_PROPERTIES, + PolarisPrivilege.VIEW_CREATE, + PolarisPrivilege.VIEW_FULL_METADATA, + PolarisPrivilege.CATALOG_MANAGE_CONTENT), + () -> newWrapper().viewExists(VIEW_NS1A_2), + null /* cleanupAction */); + } + + @Test + public void testViewExistsInsufficientPermissions() { + doTestInsufficientPrivileges( + List.of( + PolarisPrivilege.NAMESPACE_FULL_METADATA, + PolarisPrivilege.TABLE_FULL_METADATA, + PolarisPrivilege.VIEW_DROP), + () -> newWrapper().viewExists(VIEW_NS1A_2)); + } + + @Test + public void testRenameViewAllSufficientPrivileges() { + final TableIdentifier srcView = VIEW_NS1_1; + final TableIdentifier dstView = TableIdentifier.of(NS1AA, "newview"); + final RenameTableRequest rename1 = + RenameTableRequest.builder().withSource(srcView).withDestination(dstView).build(); + final RenameTableRequest rename2 = + RenameTableRequest.builder().withSource(dstView).withDestination(srcView).build(); + + doTestSufficientPrivilegeSets( + List.of( + Set.of(PolarisPrivilege.VIEW_FULL_METADATA), + Set.of(PolarisPrivilege.CATALOG_MANAGE_CONTENT), + Set.of(PolarisPrivilege.VIEW_DROP, PolarisPrivilege.VIEW_CREATE)), + () -> { + newWrapper(Set.of(PRINCIPAL_ROLE1)).renameView(rename1); + }, + () -> { + newWrapper(Set.of(PRINCIPAL_ROLE1)).renameView(rename2); + }, + PRINCIPAL_NAME); + } + + @Test + public void testRenameViewInsufficientPermissions() { + final TableIdentifier srcView = VIEW_NS1_1; + final TableIdentifier dstView = TableIdentifier.of(NS1AA, "newview"); + final RenameTableRequest rename1 = + RenameTableRequest.builder().withSource(srcView).withDestination(dstView).build(); + + doTestInsufficientPrivileges( + List.of( + PolarisPrivilege.NAMESPACE_FULL_METADATA, + PolarisPrivilege.TABLE_FULL_METADATA, + PolarisPrivilege.VIEW_DROP, + PolarisPrivilege.VIEW_CREATE, + PolarisPrivilege.VIEW_READ_PROPERTIES, + PolarisPrivilege.VIEW_WRITE_PROPERTIES, + PolarisPrivilege.VIEW_LIST), + () -> { + newWrapper(Set.of(PRINCIPAL_ROLE1)).renameView(rename1); + }); + } + + @Test + public void testRenameViewPrivilegesOnWrongSourceOrDestination() { + final TableIdentifier srcView = VIEW_NS2_1; + final TableIdentifier dstView = TableIdentifier.of(NS1AA, "newview"); + final RenameTableRequest rename1 = + RenameTableRequest.builder().withSource(srcView).withDestination(dstView).build(); + final RenameTableRequest rename2 = + RenameTableRequest.builder().withSource(dstView).withDestination(srcView).build(); + + // Minimum privileges should succeed -- drop on src, create on dst parent. + Assertions.assertThat( + adminService.grantPrivilegeOnViewToRole( + CATALOG_NAME, CATALOG_ROLE1, srcView, PolarisPrivilege.VIEW_DROP)) + .isTrue(); + Assertions.assertThat( + adminService.grantPrivilegeOnNamespaceToRole( + CATALOG_NAME, CATALOG_ROLE1, dstView.namespace(), PolarisPrivilege.VIEW_CREATE)) + .isTrue(); + + // Initial rename should succeed + newWrapper().renameView(rename1); + + // Inverse operation should fail + Assertions.assertThatThrownBy(() -> newWrapper().renameView(rename2)) + .isInstanceOf(ForbiddenException.class); + + // Now grant VIEW_DROP on dst + Assertions.assertThat( + adminService.grantPrivilegeOnViewToRole( + CATALOG_NAME, CATALOG_ROLE1, dstView, PolarisPrivilege.VIEW_DROP)) + .isTrue(); + + // Still not enough without VIEW_CREATE at source + Assertions.assertThatThrownBy(() -> newWrapper().renameView(rename2)) + .isInstanceOf(ForbiddenException.class); + + // Even grant CATALOG_MANAGE_CONTENT under all of NS1 + Assertions.assertThat( + adminService.grantPrivilegeOnNamespaceToRole( + CATALOG_NAME, CATALOG_ROLE1, NS1, PolarisPrivilege.CATALOG_MANAGE_CONTENT)) + .isTrue(); + + // Still not enough to rename back to src since src was NS2. + Assertions.assertThatThrownBy(() -> newWrapper().renameView(rename2)) + .isInstanceOf(ForbiddenException.class); + + // Finally, grant VIEW_CREATE on NS2 and it should succeed to rename back to src. + Assertions.assertThat( + adminService.grantPrivilegeOnNamespaceToRole( + CATALOG_NAME, CATALOG_ROLE1, NS2, PolarisPrivilege.VIEW_CREATE)) + .isTrue(); + newWrapper().renameView(rename2); + } + + @Test + public void testSendNotificationSufficientPrivileges() { + String externalCatalog = "externalCatalog"; + + String storageLocation = "file:///tmp"; + FileStorageConfigInfo storageConfigModel = + FileStorageConfigInfo.builder() + .setStorageType(StorageConfigInfo.StorageTypeEnum.FILE) + .setAllowedLocations(List.of(storageLocation, "file:///tmp")) + .build(); + adminService.createCatalog( + new CatalogEntity.Builder() + .setName(externalCatalog) + .setDefaultBaseLocation(storageLocation) + .setStorageConfigurationInfo(storageConfigModel, storageLocation) + .setCatalogType("EXTERNAL") + .build()); + adminService.createCatalogRole( + externalCatalog, new CatalogRoleEntity.Builder().setName(CATALOG_ROLE1).build()); + adminService.createCatalogRole( + externalCatalog, new CatalogRoleEntity.Builder().setName(CATALOG_ROLE2).build()); + + adminService.assignPrincipalRole(PRINCIPAL_NAME, PRINCIPAL_ROLE1); + adminService.assignCatalogRoleToPrincipalRole(PRINCIPAL_ROLE1, externalCatalog, CATALOG_ROLE1); + adminService.assignCatalogRoleToPrincipalRole(PRINCIPAL_ROLE2, externalCatalog, CATALOG_ROLE2); + Assertions.assertThat( + adminService.grantPrivilegeOnCatalogToRole( + externalCatalog, CATALOG_ROLE2, PolarisPrivilege.TABLE_DROP)) + .isTrue(); + Assertions.assertThat( + adminService.grantPrivilegeOnCatalogToRole( + externalCatalog, CATALOG_ROLE2, PolarisPrivilege.NAMESPACE_DROP)) + .isTrue(); + + Namespace namespace = Namespace.of("extns1", "extns2"); + TableIdentifier table = TableIdentifier.of(namespace, "tbl1"); + + String tableUuid = UUID.randomUUID().toString(); + + NotificationRequest request = new NotificationRequest(); + request.setNotificationType(NotificationType.CREATE); + TableUpdateNotification update = new TableUpdateNotification(); + update.setMetadataLocation("file:///tmp/bucket/table/metadata/v1.metadata.json"); + update.setTableName(table.name()); + update.setTableUuid(tableUuid); + update.setTimestamp(230950845L); + request.setPayload(update); + + NotificationRequest request2 = new NotificationRequest(); + request2.setNotificationType(NotificationType.UPDATE); + TableUpdateNotification update2 = new TableUpdateNotification(); + update2.setMetadataLocation("file:///tmp/bucket/table/metadata/v2.metadata.json"); + update2.setTableName(table.name()); + update2.setTableUuid(tableUuid); + update2.setTimestamp(330950845L); + request2.setPayload(update2); + + NotificationRequest request3 = new NotificationRequest(); + request3.setNotificationType(NotificationType.DROP); + TableUpdateNotification update3 = new TableUpdateNotification(); + update3.setTableName(table.name()); + update3.setTableUuid(tableUuid); + update3.setTimestamp(430950845L); + request3.setPayload(update3); + + PolarisCallContextCatalogFactory factory = + new PolarisCallContextCatalogFactory( + new RealmEntityManagerFactory() { + @Override + public PolarisEntityManager getOrCreateEntityManager(RealmContext realmContext) { + return entityManager; + } + }, + Mockito.mock()) { + @Override + public Catalog createCallContextCatalog( + CallContext context, PolarisResolutionManifest resolvedManifest) { + Catalog catalog = super.createCallContextCatalog(context, resolvedManifest); + catalog.initialize( + externalCatalog, + ImmutableMap.of( + CatalogProperties.FILE_IO_IMPL, "org.apache.iceberg.inmemory.InMemoryFileIO")); + return catalog; + } + }; + doTestSufficientPrivilegeSets( + List.of( + Set.of(PolarisPrivilege.CATALOG_MANAGE_CONTENT), + Set.of(PolarisPrivilege.TABLE_FULL_METADATA, PolarisPrivilege.NAMESPACE_FULL_METADATA), + Set.of( + PolarisPrivilege.TABLE_FULL_METADATA, + PolarisPrivilege.NAMESPACE_CREATE, + PolarisPrivilege.NAMESPACE_DROP), + Set.of( + PolarisPrivilege.TABLE_CREATE, + PolarisPrivilege.TABLE_DROP, + PolarisPrivilege.TABLE_WRITE_PROPERTIES, + PolarisPrivilege.NAMESPACE_FULL_METADATA), + Set.of( + PolarisPrivilege.TABLE_CREATE, + PolarisPrivilege.TABLE_DROP, + PolarisPrivilege.TABLE_WRITE_PROPERTIES, + PolarisPrivilege.NAMESPACE_CREATE, + PolarisPrivilege.NAMESPACE_DROP)), + () -> { + newWrapper(Set.of(PRINCIPAL_ROLE1), externalCatalog, factory) + .sendNotification(table, request); + newWrapper(Set.of(PRINCIPAL_ROLE1), externalCatalog, factory) + .sendNotification(table, request2); + newWrapper(Set.of(PRINCIPAL_ROLE1), externalCatalog, factory) + .sendNotification(table, request3); + }, + () -> { + newWrapper(Set.of(PRINCIPAL_ROLE2), externalCatalog, factory) + .dropNamespace(Namespace.of("extns1", "extns2")); + newWrapper(Set.of(PRINCIPAL_ROLE2), externalCatalog, factory) + .dropNamespace(Namespace.of("extns1")); + }, + PRINCIPAL_NAME, + externalCatalog); + } + + @Test + public void testSendNotificationInsufficientPermissions() { + Namespace namespace = Namespace.of("ns1", "ns2"); + TableIdentifier table = TableIdentifier.of(namespace, "tbl1"); + + NotificationRequest request = new NotificationRequest(); + request.setNotificationType(NotificationType.UPDATE); + TableUpdateNotification update = new TableUpdateNotification(); + update.setMetadataLocation("file:///tmp/bucket/table/metadata/v1.metadata.json"); + update.setTableName(table.name()); + update.setTableUuid(UUID.randomUUID().toString()); + update.setTimestamp(230950845L); + request.setPayload(update); + + doTestInsufficientPrivileges( + List.of( + PolarisPrivilege.NAMESPACE_FULL_METADATA, + PolarisPrivilege.TABLE_FULL_METADATA, + PolarisPrivilege.VIEW_FULL_METADATA), + () -> { + newWrapper(Set.of(PRINCIPAL_ROLE1)).sendNotification(table, request); + }); + } +} diff --git a/polaris-service/src/test/java/io/polaris/service/catalog/PolarisPassthroughResolutionView.java b/polaris-service/src/test/java/io/polaris/service/catalog/PolarisPassthroughResolutionView.java new file mode 100644 index 0000000000..aa52bbfa28 --- /dev/null +++ b/polaris-service/src/test/java/io/polaris/service/catalog/PolarisPassthroughResolutionView.java @@ -0,0 +1,129 @@ +package io.polaris.service.catalog; + +import io.polaris.core.auth.AuthenticatedPolarisPrincipal; +import io.polaris.core.catalog.PolarisCatalogHelpers; +import io.polaris.core.context.CallContext; +import io.polaris.core.entity.PolarisEntitySubType; +import io.polaris.core.entity.PolarisEntityType; +import io.polaris.core.persistence.PolarisEntityManager; +import io.polaris.core.persistence.PolarisResolvedPathWrapper; +import io.polaris.core.persistence.resolver.PolarisResolutionManifest; +import io.polaris.core.persistence.resolver.PolarisResolutionManifestCatalogView; +import io.polaris.core.persistence.resolver.ResolverPath; +import java.util.Arrays; +import org.apache.iceberg.catalog.Namespace; +import org.apache.iceberg.catalog.TableIdentifier; + +/** + * For test purposes or for elevated-privilege scenarios where entity resolution is allowed to + * directly access a PolarisEntityManager/PolarisMetaStoreManager without being part of an + * authorization-gated PolarisResolutionManifest, this class delegates entity resolution directly to + * new single-use PolarisResolutionManifests for each desired resolved path without defining a fixed + * set of resolved entities that need to be checked against authorizable operations. + */ +public class PolarisPassthroughResolutionView implements PolarisResolutionManifestCatalogView { + private final PolarisEntityManager entityManager; + private final CallContext callContext; + private final AuthenticatedPolarisPrincipal authenticatedPrincipal; + private final String catalogName; + + public PolarisPassthroughResolutionView( + CallContext callContext, + PolarisEntityManager entityManager, + AuthenticatedPolarisPrincipal authenticatedPrincipal, + String catalogName) { + this.entityManager = entityManager; + this.callContext = callContext; + this.authenticatedPrincipal = authenticatedPrincipal; + this.catalogName = catalogName; + } + + @Override + public PolarisResolvedPathWrapper getResolvedReferenceCatalogEntity() { + PolarisResolutionManifest manifest = + entityManager.prepareResolutionManifest(callContext, authenticatedPrincipal, catalogName); + manifest.resolveAll(); + return manifest.getResolvedReferenceCatalogEntity(); + } + + @Override + public PolarisResolvedPathWrapper getResolvedPath(Object key) { + PolarisResolutionManifest manifest = + entityManager.prepareResolutionManifest(callContext, authenticatedPrincipal, catalogName); + + if (key instanceof Namespace) { + Namespace namespace = (Namespace) key; + manifest.addPath( + new ResolverPath(Arrays.asList(namespace.levels()), PolarisEntityType.NAMESPACE), + namespace); + manifest.resolveAll(); + return manifest.getResolvedPath(namespace); + } else { + throw new IllegalStateException( + String.format( + "Trying to getResolvedPath(key) for %s with class %s", key, key.getClass())); + } + } + + @Override + public PolarisResolvedPathWrapper getResolvedPath(Object key, PolarisEntitySubType subType) { + PolarisResolutionManifest manifest = + entityManager.prepareResolutionManifest(callContext, authenticatedPrincipal, catalogName); + + if (key instanceof TableIdentifier) { + TableIdentifier identifier = (TableIdentifier) key; + manifest.addPath( + new ResolverPath( + PolarisCatalogHelpers.tableIdentifierToList(identifier), + PolarisEntityType.TABLE_LIKE), + identifier); + manifest.resolveAll(); + return manifest.getResolvedPath(identifier, subType); + } else { + throw new IllegalStateException( + String.format( + "Trying to getResolvedPath(key, subType) for %s with class %s and subType %s", + key, key.getClass(), subType)); + } + } + + @Override + public PolarisResolvedPathWrapper getPassthroughResolvedPath(Object key) { + PolarisResolutionManifest manifest = + entityManager.prepareResolutionManifest(callContext, authenticatedPrincipal, catalogName); + + if (key instanceof Namespace) { + Namespace namespace = (Namespace) key; + manifest.addPassthroughPath( + new ResolverPath(Arrays.asList(namespace.levels()), PolarisEntityType.NAMESPACE), + namespace); + return manifest.getPassthroughResolvedPath(namespace); + } else { + throw new IllegalStateException( + String.format( + "Trying to getResolvedPath(key) for %s with class %s", key, key.getClass())); + } + } + + @Override + public PolarisResolvedPathWrapper getPassthroughResolvedPath( + Object key, PolarisEntitySubType subType) { + PolarisResolutionManifest manifest = + entityManager.prepareResolutionManifest(callContext, authenticatedPrincipal, catalogName); + + if (key instanceof TableIdentifier) { + TableIdentifier identifier = (TableIdentifier) key; + manifest.addPassthroughPath( + new ResolverPath( + PolarisCatalogHelpers.tableIdentifierToList(identifier), + PolarisEntityType.TABLE_LIKE), + identifier); + return manifest.getPassthroughResolvedPath(identifier, subType); + } else { + throw new IllegalStateException( + String.format( + "Trying to getResolvedPath(key, subType) for %s with class %s and subType %s", + key, key.getClass(), subType)); + } + } +} diff --git a/polaris-service/src/test/java/io/polaris/service/catalog/PolarisRestCatalogIntegrationTest.java b/polaris-service/src/test/java/io/polaris/service/catalog/PolarisRestCatalogIntegrationTest.java new file mode 100644 index 0000000000..3d47331a59 --- /dev/null +++ b/polaris-service/src/test/java/io/polaris/service/catalog/PolarisRestCatalogIntegrationTest.java @@ -0,0 +1,577 @@ +package io.polaris.service.catalog; + +import static io.polaris.service.context.DefaultContextResolver.REALM_PROPERTY_KEY; +import static org.apache.iceberg.types.Types.NestedField.required; +import static org.assertj.core.api.Assertions.assertThat; + +import com.google.common.collect.ImmutableMap; +import io.dropwizard.testing.ConfigOverride; +import io.dropwizard.testing.ResourceHelpers; +import io.dropwizard.testing.junit5.DropwizardAppExtension; +import io.dropwizard.testing.junit5.DropwizardExtensionsSupport; +import io.polaris.core.admin.model.AwsStorageConfigInfo; +import io.polaris.core.admin.model.Catalog; +import io.polaris.core.admin.model.CatalogGrant; +import io.polaris.core.admin.model.CatalogPrivilege; +import io.polaris.core.admin.model.CatalogRole; +import io.polaris.core.admin.model.FileStorageConfigInfo; +import io.polaris.core.admin.model.GrantResource; +import io.polaris.core.admin.model.GrantResources; +import io.polaris.core.admin.model.NamespaceGrant; +import io.polaris.core.admin.model.NamespacePrivilege; +import io.polaris.core.admin.model.PolarisCatalog; +import io.polaris.core.admin.model.StorageConfigInfo; +import io.polaris.core.admin.model.TableGrant; +import io.polaris.core.admin.model.TablePrivilege; +import io.polaris.core.admin.model.ViewGrant; +import io.polaris.core.admin.model.ViewPrivilege; +import io.polaris.core.entity.CatalogEntity; +import io.polaris.service.PolarisApplication; +import io.polaris.service.auth.BasePolarisAuthenticator; +import io.polaris.service.auth.TokenUtils; +import io.polaris.service.config.PolarisApplicationConfig; +import io.polaris.service.test.PolarisConnectionExtension; +import io.polaris.service.test.PolarisConnectionExtension.PolarisToken; +import io.polaris.service.test.SnowmanCredentialsExtension; +import io.polaris.service.test.SnowmanCredentialsExtension.SnowmanCredentials; +import io.polaris.service.types.NotificationRequest; +import io.polaris.service.types.NotificationType; +import io.polaris.service.types.TableUpdateNotification; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.core.Response; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import org.apache.iceberg.CatalogProperties; +import org.apache.iceberg.Schema; +import org.apache.iceberg.catalog.CatalogTests; +import org.apache.iceberg.catalog.Namespace; +import org.apache.iceberg.catalog.SessionCatalog; +import org.apache.iceberg.catalog.TableIdentifier; +import org.apache.iceberg.rest.HTTPClient; +import org.apache.iceberg.rest.RESTCatalog; +import org.apache.iceberg.rest.auth.OAuth2Properties; +import org.apache.iceberg.rest.responses.ErrorResponse; +import org.apache.iceberg.types.Types; +import org.assertj.core.api.InstanceOfAssertFactories; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; +import org.junit.jupiter.api.extension.ExtendWith; + +/** + * Import the full core Iceberg catalog tests by hitting the REST service via the RESTCatalog + * client. + */ +@ExtendWith({ + DropwizardExtensionsSupport.class, + PolarisConnectionExtension.class, + SnowmanCredentialsExtension.class +}) +public class PolarisRestCatalogIntegrationTest extends CatalogTests { + private static final String TEST_ROLE_ARN = + Optional.ofNullable(System.getenv("INTEGRATION_TEST_ROLE_ARN")) + .orElse("arn:aws:iam::123456789012:role/my-role"); + private static final String S3_BUCKET_BASE = + Optional.ofNullable(System.getenv("INTEGRATION_TEST_S3_PATH")) + .orElse("file:///tmp/buckets/my-bucket"); + private static DropwizardAppExtension EXT = + new DropwizardAppExtension<>( + PolarisApplication.class, + ResourceHelpers.resourceFilePath("polaris-server-integrationtest.yml"), + ConfigOverride.config( + "server.applicationConnectors[0].port", + "0"), // Bind to random port to support parallelism + ConfigOverride.config( + "server.adminConnectors[0].port", "0")); // Bind to random port to support parallelism + + protected static final Schema SCHEMA = new Schema(required(4, "data", Types.StringType.get())); + protected static final String VIEW_QUERY = "select * from ns1.layer1_table"; + + private RESTCatalog restCatalog; + private String currentCatalogName; + private String userToken; + private static String realm; + + @BeforeAll + public static void setup() throws IOException { + realm = PolarisConnectionExtension.getTestRealm(PolarisRestCatalogIntegrationTest.class); + + Path testDir = Path.of("build/test_data/iceberg/" + realm); + if (Files.exists(testDir)) { + if (Files.isDirectory(testDir)) { + Files.walk(testDir) + .sorted(Comparator.reverseOrder()) + .forEach( + path -> { + try { + Files.delete(path); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + + } else { + Files.delete(testDir); + } + } + Files.createDirectories(testDir); + } + + @BeforeEach + public void before( + TestInfo testInfo, PolarisToken adminToken, SnowmanCredentials snowmanCredentials) { + userToken = + TokenUtils.getTokenFromSecrets( + EXT.client(), + EXT.getLocalPort(), + snowmanCredentials.clientId(), + snowmanCredentials.clientSecret(), + realm); + testInfo + .getTestMethod() + .ifPresent( + method -> { + currentCatalogName = method.getName(); + AwsStorageConfigInfo awsConfigModel = + AwsStorageConfigInfo.builder() + .setRoleArn(TEST_ROLE_ARN) + .setExternalId("externalId") + .setUserArn("a:user:arn") + .setStorageType(StorageConfigInfo.StorageTypeEnum.S3) + .setAllowedLocations(List.of("s3://my-old-bucket/path/to/data")) + .build(); + Catalog catalog = + PolarisCatalog.builder() + .setType(Catalog.TypeEnum.INTERNAL) + .setName(currentCatalogName) + .setProperties( + io.polaris.core.admin.model.CatalogProperties.builder( + S3_BUCKET_BASE + "/" + System.getenv("USER") + "/path/to/data") + .addProperty( + CatalogEntity + .REPLACE_NEW_LOCATION_PREFIX_WITH_CATALOG_DEFAULT_KEY, + "file:") + .build()) + .setStorageConfigInfo( + S3_BUCKET_BASE.startsWith("file:/") + ? new FileStorageConfigInfo( + StorageConfigInfo.StorageTypeEnum.FILE, List.of("file://")) + : awsConfigModel) + .build(); + try (Response response = + EXT.client() + .target( + String.format( + "http://localhost:%d/api/management/v1/catalogs", EXT.getLocalPort())) + .request("application/json") + .header("Authorization", "Bearer " + adminToken.token()) + .header(REALM_PROPERTY_KEY, realm) + .post(Entity.json(catalog))) { + assertThat(response) + .returns(Response.Status.CREATED.getStatusCode(), Response::getStatus); + } + + // Create a new CatalogRole that has CATALOG_MANAGE_CONTENT and CATALOG_MANAGE_ACCESS + CatalogRole newRole = new CatalogRole("custom-admin"); + try (Response response = + EXT.client() + .target( + String.format( + "http://localhost:%d/api/management/v1/catalogs/%s/catalog-roles", + EXT.getLocalPort(), currentCatalogName)) + .request("application/json") + .header("Authorization", "Bearer " + adminToken.token()) + .header(REALM_PROPERTY_KEY, realm) + .post(Entity.json(newRole))) { + assertThat(response) + .returns(Response.Status.CREATED.getStatusCode(), Response::getStatus); + } + CatalogGrant grantResource = + new CatalogGrant( + CatalogPrivilege.CATALOG_MANAGE_CONTENT, GrantResource.TypeEnum.CATALOG); + try (Response response = + EXT.client() + .target( + String.format( + "http://localhost:%d/api/management/v1/catalogs/%s/catalog-roles/custom-admin/grants", + EXT.getLocalPort(), currentCatalogName)) + .request("application/json") + .header("Authorization", "Bearer " + adminToken.token()) + .header(REALM_PROPERTY_KEY, realm) + .put(Entity.json(grantResource))) { + assertThat(response) + .returns(Response.Status.CREATED.getStatusCode(), Response::getStatus); + } + CatalogGrant grantAccessResource = + new CatalogGrant( + CatalogPrivilege.CATALOG_MANAGE_ACCESS, GrantResource.TypeEnum.CATALOG); + try (Response response = + EXT.client() + .target( + String.format( + "http://localhost:%d/api/management/v1/catalogs/%s/catalog-roles/custom-admin/grants", + EXT.getLocalPort(), currentCatalogName)) + .request("application/json") + .header("Authorization", "Bearer " + adminToken.token()) + .header(REALM_PROPERTY_KEY, realm) + .put(Entity.json(grantAccessResource))) { + assertThat(response) + .returns(Response.Status.CREATED.getStatusCode(), Response::getStatus); + } + + // Assign this new CatalogRole to the service_admin PrincipalRole + try (Response response = + EXT.client() + .target( + String.format( + "http://localhost:%d/api/management/v1/catalogs/%s/catalog-roles/custom-admin", + EXT.getLocalPort(), currentCatalogName)) + .request("application/json") + .header("Authorization", "Bearer " + adminToken.token()) + .header(REALM_PROPERTY_KEY, realm) + .get()) { + assertThat(response) + .returns(Response.Status.OK.getStatusCode(), Response::getStatus); + CatalogRole catalogRole = response.readEntity(CatalogRole.class); + try (Response assignResponse = + EXT.client() + .target( + String.format( + "http://localhost:%d/api/management/v1/principal-roles/catalog-admin/catalog-roles/%s", + EXT.getLocalPort(), currentCatalogName)) + .request("application/json") + .header("Authorization", "Bearer " + adminToken.token()) + .header(REALM_PROPERTY_KEY, realm) + .put(Entity.json(catalogRole))) { + assertThat(assignResponse) + .returns(Response.Status.CREATED.getStatusCode(), Response::getStatus); + } + } + + SessionCatalog.SessionContext context = SessionCatalog.SessionContext.createEmpty(); + this.restCatalog = + new RESTCatalog( + context, + (config) -> + HTTPClient.builder(config) + .uri(config.get(CatalogProperties.URI)) + .build()); + this.restCatalog.initialize( + "polaris", + ImmutableMap.of( + CatalogProperties.URI, + "http://localhost:" + EXT.getLocalPort() + "/api/catalog", + OAuth2Properties.CREDENTIAL, + snowmanCredentials.clientId() + ":" + snowmanCredentials.clientSecret(), + OAuth2Properties.SCOPE, + BasePolarisAuthenticator.PRINCIPAL_ROLE_ALL, + CatalogProperties.FILE_IO_IMPL, + "org.apache.iceberg.inmemory.InMemoryFileIO", + "warehouse", + currentCatalogName, + "header." + REALM_PROPERTY_KEY, + realm)); + }); + } + + @Override + protected RESTCatalog catalog() { + return restCatalog; + } + + @Override + protected boolean requiresNamespaceCreate() { + return true; + } + + @Override + protected boolean supportsNestedNamespaces() { + return true; + } + + @Override + protected boolean supportsServerSideRetry() { + return true; + } + + @Override + protected boolean overridesRequestedLocation() { + return true; + } + + private void createCatalogRole(String catalogRoleName) { + CatalogRole catalogRole = new CatalogRole(catalogRoleName); + try (Response response = + EXT.client() + .target( + String.format( + "http://localhost:%d/api/management/v1/catalogs/%s/catalog-roles", + EXT.getLocalPort(), currentCatalogName)) + .request("application/json") + .header("Authorization", "Bearer " + userToken) + .header(REALM_PROPERTY_KEY, realm) + .post(Entity.json(catalogRole))) { + assertThat(response).returns(Response.Status.CREATED.getStatusCode(), Response::getStatus); + } + } + + private void addGrant(String catalogRoleName, GrantResource grant) { + try (Response response = + EXT.client() + .target( + String.format( + "http://localhost:%d/api/management/v1/catalogs/%s/catalog-roles/%s/grants", + EXT.getLocalPort(), currentCatalogName, catalogRoleName)) + .request("application/json") + .header("Authorization", "Bearer " + userToken) + .header(REALM_PROPERTY_KEY, realm) + .put(Entity.json(grant))) { + assertThat(response).returns(Response.Status.CREATED.getStatusCode(), Response::getStatus); + } + } + + @Test + public void testListGrantsOnCatalogObjectsToCatalogRoles() { + restCatalog.createNamespace(Namespace.of("ns1")); + restCatalog.createNamespace(Namespace.of("ns1", "ns1a")); + restCatalog.createNamespace(Namespace.of("ns2")); + + restCatalog.buildTable(TableIdentifier.of(Namespace.of("ns1"), "tbl1"), SCHEMA).create(); + restCatalog + .buildTable(TableIdentifier.of(Namespace.of("ns1", "ns1a"), "tbl1"), SCHEMA) + .create(); + restCatalog.buildTable(TableIdentifier.of(Namespace.of("ns2"), "tbl2"), SCHEMA).create(); + + restCatalog + .buildView(TableIdentifier.of(Namespace.of("ns1"), "view1")) + .withSchema(SCHEMA) + .withDefaultNamespace(Namespace.of("ns1")) + .withQuery("spark", VIEW_QUERY) + .create(); + restCatalog + .buildView(TableIdentifier.of(Namespace.of("ns1", "ns1a"), "view1")) + .withSchema(SCHEMA) + .withDefaultNamespace(Namespace.of("ns1")) + .withQuery("spark", VIEW_QUERY) + .create(); + restCatalog + .buildView(TableIdentifier.of(Namespace.of("ns2"), "view2")) + .withSchema(SCHEMA) + .withDefaultNamespace(Namespace.of("ns1")) + .withQuery("spark", VIEW_QUERY) + .create(); + + CatalogGrant catalogGrant1 = + new CatalogGrant(CatalogPrivilege.CATALOG_MANAGE_CONTENT, GrantResource.TypeEnum.CATALOG); + + CatalogGrant catalogGrant2 = + new CatalogGrant(CatalogPrivilege.NAMESPACE_FULL_METADATA, GrantResource.TypeEnum.CATALOG); + + CatalogGrant catalogGrant3 = + new CatalogGrant(CatalogPrivilege.VIEW_FULL_METADATA, GrantResource.TypeEnum.CATALOG); + + NamespaceGrant namespaceGrant1 = + new NamespaceGrant( + List.of("ns1"), + NamespacePrivilege.NAMESPACE_FULL_METADATA, + GrantResource.TypeEnum.NAMESPACE); + + NamespaceGrant namespaceGrant2 = + new NamespaceGrant( + List.of("ns1", "ns1a"), + NamespacePrivilege.TABLE_CREATE, + GrantResource.TypeEnum.NAMESPACE); + + NamespaceGrant namespaceGrant3 = + new NamespaceGrant( + List.of("ns2"), + NamespacePrivilege.VIEW_READ_PROPERTIES, + GrantResource.TypeEnum.NAMESPACE); + + TableGrant tableGrant1 = + new TableGrant( + List.of("ns1"), + "tbl1", + TablePrivilege.TABLE_FULL_METADATA, + GrantResource.TypeEnum.TABLE); + + TableGrant tableGrant2 = + new TableGrant( + List.of("ns1", "ns1a"), + "tbl1", + TablePrivilege.TABLE_READ_DATA, + GrantResource.TypeEnum.TABLE); + + TableGrant tableGrant3 = + new TableGrant( + List.of("ns2"), "tbl2", TablePrivilege.TABLE_WRITE_DATA, GrantResource.TypeEnum.TABLE); + + ViewGrant viewGrant1 = + new ViewGrant( + List.of("ns1"), "view1", ViewPrivilege.VIEW_FULL_METADATA, GrantResource.TypeEnum.VIEW); + + ViewGrant viewGrant2 = + new ViewGrant( + List.of("ns1", "ns1a"), + "view1", + ViewPrivilege.VIEW_READ_PROPERTIES, + GrantResource.TypeEnum.VIEW); + + ViewGrant viewGrant3 = + new ViewGrant( + List.of("ns2"), + "view2", + ViewPrivilege.VIEW_WRITE_PROPERTIES, + GrantResource.TypeEnum.VIEW); + + createCatalogRole("catalogrole1"); + createCatalogRole("catalogrole2"); + + List role1Grants = + List.of( + catalogGrant1, + catalogGrant2, + namespaceGrant1, + namespaceGrant2, + tableGrant1, + tableGrant2, + viewGrant1, + viewGrant2); + role1Grants.stream().forEach(grant -> addGrant("catalogrole1", grant)); + List role2Grants = + List.of( + catalogGrant1, + catalogGrant3, + namespaceGrant1, + namespaceGrant3, + tableGrant1, + tableGrant3, + viewGrant1, + viewGrant3); + role2Grants.stream().forEach(grant -> addGrant("catalogrole2", grant)); + + // List grants for catalogrole1 + try (Response response = + EXT.client() + .target( + String.format( + "http://localhost:%d/api/management/v1/catalogs/%s/catalog-roles/%s/grants", + EXT.getLocalPort(), currentCatalogName, "catalogrole1")) + .request("application/json") + .header("Authorization", "Bearer " + userToken) + .header(REALM_PROPERTY_KEY, realm) + .get()) { + assertThat(response) + .returns(200, Response::getStatus) + .extracting(r -> r.readEntity(GrantResources.class)) + .extracting(GrantResources::getGrants) + .asInstanceOf(InstanceOfAssertFactories.list(GrantResource.class)) + .containsExactlyInAnyOrder(role1Grants.toArray(new GrantResource[0])); + } + + // List grants for catalogrole2 + try (Response response = + EXT.client() + .target( + String.format( + "http://localhost:%d/api/management/v1/catalogs/%s/catalog-roles/%s/grants", + EXT.getLocalPort(), currentCatalogName, "catalogrole2")) + .request("application/json") + .header("Authorization", "Bearer " + userToken) + .header(REALM_PROPERTY_KEY, realm) + .get()) { + assertThat(response) + .returns(200, Response::getStatus) + .extracting(r -> r.readEntity(GrantResources.class)) + .extracting(GrantResources::getGrants) + .asInstanceOf(InstanceOfAssertFactories.list(GrantResource.class)) + .containsExactlyInAnyOrder(role2Grants.toArray(new GrantResource[0])); + } + } + + @Test + public void testListGrantsAfterRename() { + restCatalog.createNamespace(Namespace.of("ns1")); + restCatalog.createNamespace(Namespace.of("ns1", "ns1a")); + restCatalog.createNamespace(Namespace.of("ns2")); + + restCatalog + .buildTable(TableIdentifier.of(Namespace.of("ns1", "ns1a"), "tbl1"), SCHEMA) + .create(); + + TableGrant tableGrant1 = + new TableGrant( + List.of("ns1", "ns1a"), + "tbl1", + TablePrivilege.TABLE_FULL_METADATA, + GrantResource.TypeEnum.TABLE); + + createCatalogRole("catalogrole1"); + addGrant("catalogrole1", tableGrant1); + + // Grants will follow the table through the rename + restCatalog.renameTable( + TableIdentifier.of(Namespace.of("ns1", "ns1a"), "tbl1"), + TableIdentifier.of(Namespace.of("ns2"), "newtable")); + + TableGrant expectedGrant = + new TableGrant( + List.of("ns2"), + "newtable", + TablePrivilege.TABLE_FULL_METADATA, + GrantResource.TypeEnum.TABLE); + + try (Response response = + EXT.client() + .target( + String.format( + "http://localhost:%d/api/management/v1/catalogs/%s/catalog-roles/%s/grants", + EXT.getLocalPort(), currentCatalogName, "catalogrole1")) + .request("application/json") + .header("Authorization", "Bearer " + userToken) + .header(REALM_PROPERTY_KEY, realm) + .get()) { + assertThat(response) + .returns(200, Response::getStatus) + .extracting(r -> r.readEntity(GrantResources.class)) + .extracting(GrantResources::getGrants) + .asInstanceOf(InstanceOfAssertFactories.list(GrantResource.class)) + .containsExactly(expectedGrant); + } + } + + @Test + public void testSendNotificationInternalCatalog() { + NotificationRequest notification = new NotificationRequest(); + notification.setNotificationType(NotificationType.CREATE); + notification.setPayload( + new TableUpdateNotification( + "tbl1", + System.currentTimeMillis(), + UUID.randomUUID().toString(), + "s3://my-bucket/path/to/metadata.json", + null)); + restCatalog.createNamespace(Namespace.of("ns1")); + try (Response response = + EXT.client() + .target( + String.format( + "http://localhost:%d/api/catalog/v1/%s/namespaces/ns1/tables/tbl1/notifications", + EXT.getLocalPort(), currentCatalogName)) + .request("application/json") + .header("Authorization", "Bearer " + userToken) + .header(REALM_PROPERTY_KEY, realm) + .post(Entity.json(notification))) { + assertThat(response) + .returns(Response.Status.BAD_REQUEST.getStatusCode(), Response::getStatus) + .extracting(r -> r.readEntity(ErrorResponse.class)) + .returns("Cannot update internal catalog via notifications", ErrorResponse::message); + } + } +} diff --git a/polaris-service/src/test/java/io/polaris/service/catalog/PolarisRestCatalogViewIntegrationTest.java b/polaris-service/src/test/java/io/polaris/service/catalog/PolarisRestCatalogViewIntegrationTest.java new file mode 100644 index 0000000000..1366aa7405 --- /dev/null +++ b/polaris-service/src/test/java/io/polaris/service/catalog/PolarisRestCatalogViewIntegrationTest.java @@ -0,0 +1,273 @@ +package io.polaris.service.catalog; + +import static io.polaris.service.context.DefaultContextResolver.REALM_PROPERTY_KEY; +import static org.assertj.core.api.Assertions.assertThat; + +import com.google.common.collect.ImmutableMap; +import io.dropwizard.testing.ConfigOverride; +import io.dropwizard.testing.ResourceHelpers; +import io.dropwizard.testing.junit5.DropwizardAppExtension; +import io.dropwizard.testing.junit5.DropwizardExtensionsSupport; +import io.polaris.core.admin.model.AwsStorageConfigInfo; +import io.polaris.core.admin.model.Catalog; +import io.polaris.core.admin.model.CatalogGrant; +import io.polaris.core.admin.model.CatalogPrivilege; +import io.polaris.core.admin.model.CatalogRole; +import io.polaris.core.admin.model.FileStorageConfigInfo; +import io.polaris.core.admin.model.GrantResource; +import io.polaris.core.admin.model.PolarisCatalog; +import io.polaris.core.admin.model.StorageConfigInfo; +import io.polaris.core.entity.CatalogEntity; +import io.polaris.service.PolarisApplication; +import io.polaris.service.auth.BasePolarisAuthenticator; +import io.polaris.service.config.PolarisApplicationConfig; +import io.polaris.service.test.PolarisConnectionExtension; +import io.polaris.service.test.PolarisConnectionExtension.PolarisToken; +import io.polaris.service.test.SnowmanCredentialsExtension; +import io.polaris.service.test.SnowmanCredentialsExtension.SnowmanCredentials; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.core.Response; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; +import org.apache.iceberg.CatalogProperties; +import org.apache.iceberg.catalog.SessionCatalog; +import org.apache.iceberg.rest.HTTPClient; +import org.apache.iceberg.rest.RESTCatalog; +import org.apache.iceberg.rest.auth.OAuth2Properties; +import org.apache.iceberg.view.ViewCatalogTests; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.TestInfo; +import org.junit.jupiter.api.extension.ExtendWith; + +/** + * Import the full core Iceberg catalog tests by hitting the REST service via the RESTCatalog + * client. + */ +@ExtendWith({ + DropwizardExtensionsSupport.class, + PolarisConnectionExtension.class, + SnowmanCredentialsExtension.class +}) +public class PolarisRestCatalogViewIntegrationTest extends ViewCatalogTests { + public static final String TEST_ROLE_ARN = + Optional.ofNullable(System.getenv("INTEGRATION_TEST_ROLE_ARN")) + .orElse("arn:aws:iam::123456789012:role/my-role"); + public static final String S3_BUCKET_BASE = + Optional.ofNullable(System.getenv("INTEGRATION_TEST_S3_PATH")) + .orElse("file:///tmp/buckets/my-bucket"); + private static DropwizardAppExtension EXT = + new DropwizardAppExtension<>( + PolarisApplication.class, + ResourceHelpers.resourceFilePath("polaris-server-integrationtest.yml"), + ConfigOverride.config( + "server.applicationConnectors[0].port", + "0"), // Bind to random port to support parallelism + ConfigOverride.config( + "server.adminConnectors[0].port", "0")); // Bind to random port to support parallelism + + private RESTCatalog restCatalog; + private static String realm; + + @BeforeAll + public static void setup() throws IOException { + realm = PolarisConnectionExtension.getTestRealm(PolarisRestCatalogViewIntegrationTest.class); + + Path testDir = Path.of("build/test_data/iceberg/" + realm); + if (Files.exists(testDir)) { + if (Files.isDirectory(testDir)) { + Files.walk(testDir) + .sorted(Comparator.reverseOrder()) + .forEach( + path -> { + try { + Files.delete(path); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + + } else { + Files.delete(testDir); + } + } + Files.createDirectories(testDir); + } + + @BeforeEach + public void before( + TestInfo testInfo, PolarisToken adminToken, SnowmanCredentials snowmanCredentials) { + String userToken = adminToken.token(); + testInfo + .getTestMethod() + .ifPresent( + method -> { + String catalogName = method.getName(); + try (Response response = + EXT.client() + .target( + String.format( + "http://localhost:%d/api/management/v1/catalogs/%s", + EXT.getLocalPort(), catalogName)) + .request("application/json") + .header("Authorization", "Bearer " + userToken) + .header(REALM_PROPERTY_KEY, realm) + .get()) { + if (response.getStatus() == Response.Status.OK.getStatusCode()) { + // Already exists! Must be in a parameterized test. + // Quick hack to get a unique catalogName. + // TODO: Have a while-loop instead with consecutive incrementing suffixes. + catalogName = catalogName + System.currentTimeMillis(); + } + } + + AwsStorageConfigInfo awsConfigModel = + AwsStorageConfigInfo.builder() + .setRoleArn(TEST_ROLE_ARN) + .setExternalId("externalId") + .setUserArn("userArn") + .setStorageType(StorageConfigInfo.StorageTypeEnum.S3) + .setAllowedLocations(List.of("s3://my-old-bucket/path/to/data")) + .build(); + io.polaris.core.admin.model.CatalogProperties props = + new io.polaris.core.admin.model.CatalogProperties( + S3_BUCKET_BASE + "/" + System.getenv("USER") + "/path/to/data"); + props.put( + CatalogEntity.REPLACE_NEW_LOCATION_PREFIX_WITH_CATALOG_DEFAULT_KEY, "file:"); + Catalog catalog = + PolarisCatalog.builder() + .setType(Catalog.TypeEnum.INTERNAL) + .setName(catalogName) + .setProperties(props) + .setStorageConfigInfo( + S3_BUCKET_BASE.startsWith("file:") + ? new FileStorageConfigInfo( + StorageConfigInfo.StorageTypeEnum.FILE, List.of("file://")) + : awsConfigModel) + .build(); + try (Response response = + EXT.client() + .target( + String.format( + "http://localhost:%d/api/management/v1/catalogs", EXT.getLocalPort())) + .request("application/json") + .header("Authorization", "Bearer " + userToken) + .header(REALM_PROPERTY_KEY, realm) + .post(Entity.json(catalog))) { + assertThat(response) + .returns(Response.Status.CREATED.getStatusCode(), Response::getStatus); + } + CatalogRole newRole = new CatalogRole("admin"); + try (Response response = + EXT.client() + .target( + String.format( + "http://localhost:%d/api/management/v1/catalogs/%s/catalog-roles", + EXT.getLocalPort(), catalogName)) + .request("application/json") + .header("Authorization", "Bearer " + userToken) + .header(REALM_PROPERTY_KEY, realm) + .post(Entity.json(newRole))) { + assertThat(response) + .returns(Response.Status.CREATED.getStatusCode(), Response::getStatus); + } + CatalogGrant grantResource = + new CatalogGrant( + CatalogPrivilege.CATALOG_MANAGE_CONTENT, GrantResource.TypeEnum.CATALOG); + try (Response response = + EXT.client() + .target( + String.format( + "http://localhost:%d/api/management/v1/catalogs/%s/catalog-roles/admin/grants", + EXT.getLocalPort(), catalogName)) + .request("application/json") + .header("Authorization", "Bearer " + userToken) + .header(REALM_PROPERTY_KEY, realm) + .put(Entity.json(grantResource))) { + assertThat(response) + .returns(Response.Status.CREATED.getStatusCode(), Response::getStatus); + } + + try (Response response = + EXT.client() + .target( + String.format( + "http://localhost:%d/api/management/v1/catalogs/%s/catalog-roles/admin", + EXT.getLocalPort(), catalogName)) + .request("application/json") + .header("Authorization", "Bearer " + userToken) + .header(REALM_PROPERTY_KEY, realm) + .get()) { + assertThat(response) + .returns(Response.Status.OK.getStatusCode(), Response::getStatus); + CatalogRole catalogRole = response.readEntity(CatalogRole.class); + try (Response assignResponse = + EXT.client() + .target( + String.format( + "http://localhost:%d/api/management/v1/principal-roles/catalog-admin/catalog-roles/%s", + EXT.getLocalPort(), catalogName)) + .request("application/json") + .header("Authorization", "Bearer " + userToken) + .header(REALM_PROPERTY_KEY, realm) + .put(Entity.json(catalogRole))) { + assertThat(response) + .returns(Response.Status.OK.getStatusCode(), Response::getStatus); + } + } + + SessionCatalog.SessionContext context = SessionCatalog.SessionContext.createEmpty(); + this.restCatalog = + new RESTCatalog( + context, + (config) -> + HTTPClient.builder(config) + .uri(config.get(CatalogProperties.URI)) + .build()); + this.restCatalog.initialize( + "polaris", + ImmutableMap.of( + CatalogProperties.URI, + "http://localhost:" + EXT.getLocalPort() + "/api/catalog", + OAuth2Properties.CREDENTIAL, + snowmanCredentials.clientId() + ":" + snowmanCredentials.clientSecret(), + OAuth2Properties.SCOPE, + BasePolarisAuthenticator.PRINCIPAL_ROLE_ALL, + CatalogProperties.FILE_IO_IMPL, + "org.apache.iceberg.inmemory.InMemoryFileIO", + "warehouse", + catalogName, + "header." + REALM_PROPERTY_KEY, + realm)); + }); + } + + @Override + protected RESTCatalog catalog() { + return restCatalog; + } + + @Override + protected org.apache.iceberg.catalog.Catalog tableCatalog() { + return restCatalog; + } + + @Override + protected boolean requiresNamespaceCreate() { + return true; + } + + @Override + protected boolean supportsServerSideRetry() { + return true; + } + + @Override + protected boolean overridesRequestedLocation() { + return true; + } +} diff --git a/polaris-service/src/test/java/io/polaris/service/catalog/PolarisSparkIntegrationTest.java b/polaris-service/src/test/java/io/polaris/service/catalog/PolarisSparkIntegrationTest.java new file mode 100644 index 0000000000..1106f7bd5b --- /dev/null +++ b/polaris-service/src/test/java/io/polaris/service/catalog/PolarisSparkIntegrationTest.java @@ -0,0 +1,341 @@ +package io.polaris.service.catalog; + +import static io.polaris.service.context.DefaultContextResolver.REALM_PROPERTY_KEY; +import static org.assertj.core.api.Assertions.assertThat; + +import com.adobe.testing.s3mock.testcontainers.S3MockContainer; +import io.dropwizard.testing.ConfigOverride; +import io.dropwizard.testing.ResourceHelpers; +import io.dropwizard.testing.junit5.DropwizardAppExtension; +import io.dropwizard.testing.junit5.DropwizardExtensionsSupport; +import io.polaris.core.admin.model.AwsStorageConfigInfo; +import io.polaris.core.admin.model.Catalog; +import io.polaris.core.admin.model.CatalogProperties; +import io.polaris.core.admin.model.ExternalCatalog; +import io.polaris.core.admin.model.PolarisCatalog; +import io.polaris.core.admin.model.StorageConfigInfo; +import io.polaris.service.PolarisApplication; +import io.polaris.service.config.PolarisApplicationConfig; +import io.polaris.service.test.PolarisConnectionExtension; +import io.polaris.service.types.NotificationRequest; +import io.polaris.service.types.NotificationType; +import io.polaris.service.types.TableUpdateNotification; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.core.Response; +import java.time.Instant; +import java.util.List; +import java.util.Map; +import org.apache.iceberg.rest.requests.ImmutableRegisterTableRequest; +import org.apache.iceberg.rest.responses.LoadTableResponse; +import org.apache.spark.sql.Row; +import org.apache.spark.sql.SparkSession; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.slf4j.LoggerFactory; + +@ExtendWith({DropwizardExtensionsSupport.class, PolarisConnectionExtension.class}) +public class PolarisSparkIntegrationTest { + private static final DropwizardAppExtension EXT = + new DropwizardAppExtension<>( + PolarisApplication.class, + ResourceHelpers.resourceFilePath("polaris-server-integrationtest.yml"), + ConfigOverride.config( + "server.applicationConnectors[0].port", + "0"), // Bind to random port to support parallelism + ConfigOverride.config( + "server.adminConnectors[0].port", "0")); // Bind to random port to support parallelism + + public static final String CATALOG_NAME = "mycatalog"; + public static final String EXTERNAL_CATALOG_NAME = "external_catalog"; + private static S3MockContainer s3Container = + new S3MockContainer("3.9.1").withInitialBuckets("my-bucket,my-old-bucket"); + private static PolarisConnectionExtension.PolarisToken polarisToken; + private static SparkSession spark; + private static String realm; + + @BeforeAll + public static void setup(PolarisConnectionExtension.PolarisToken polarisToken) { + s3Container.start(); + PolarisSparkIntegrationTest.polarisToken = polarisToken; + realm = PolarisConnectionExtension.getTestRealm(PolarisSparkIntegrationTest.class); + } + + @AfterAll + public static void cleanup() { + s3Container.stop(); + } + + @BeforeEach + public void before() { + AwsStorageConfigInfo awsConfigModel = + AwsStorageConfigInfo.builder() + .setRoleArn("arn:aws:iam::123456789012:role/my-role") + .setExternalId("externalId") + .setUserArn("userArn") + .setStorageType(StorageConfigInfo.StorageTypeEnum.S3) + .setAllowedLocations(List.of("s3://my-old-bucket/path/to/data")) + .build(); + CatalogProperties props = new CatalogProperties("s3://my-bucket/path/to/data"); + props.putAll( + Map.of( + "table-default.s3.endpoint", + s3Container.getHttpEndpoint(), + "table-default.s3.path-style-access", + "true", + "table-default.s3.access-key-id", + "foo", + "table-default.s3.secret-access-key", + "bar", + "s3.endpoint", + s3Container.getHttpEndpoint(), + "s3.path-style-access", + "true", + "s3.access-key-id", + "foo", + "s3.secret-access-key", + "bar")); + Catalog catalog = + PolarisCatalog.builder() + .setType(Catalog.TypeEnum.INTERNAL) + .setName(CATALOG_NAME) + .setProperties(props) + .setStorageConfigInfo(awsConfigModel) + .build(); + + try (Response response = + EXT.client() + .target( + String.format("http://localhost:%d/api/management/v1/catalogs", EXT.getLocalPort())) + .request("application/json") + .header("Authorization", "BEARER " + polarisToken.token()) + .header(REALM_PROPERTY_KEY, realm) + .post(Entity.json(catalog))) { + assertThat(response).returns(Response.Status.CREATED.getStatusCode(), Response::getStatus); + } + + CatalogProperties externalProps = new CatalogProperties("s3://my-bucket/path/to/data"); + externalProps.putAll( + Map.of( + "s3.endpoint", + s3Container.getHttpEndpoint(), + "s3.path-style-access", + "true", + "s3.access-key-id", + "foo", + "s3.secret-access-key", + "bar")); + Catalog externalCatalog = + ExternalCatalog.builder() + .setType(Catalog.TypeEnum.EXTERNAL) + .setName(EXTERNAL_CATALOG_NAME) + .setProperties(externalProps) + .setStorageConfigInfo(awsConfigModel) + .setRemoteUrl("http://dummy_url") + .build(); + try (Response response = + EXT.client() + .target( + String.format("http://localhost:%d/api/management/v1/catalogs", EXT.getLocalPort())) + .request("application/json") + .header("Authorization", "BEARER " + polarisToken.token()) + .header(REALM_PROPERTY_KEY, realm) + .post(Entity.json(externalCatalog))) { + assertThat(response).returns(Response.Status.CREATED.getStatusCode(), Response::getStatus); + } + SparkSession.Builder sessionBuilder = + SparkSession.builder() + .master("local[1]") + .config("spark.hadoop.fs.s3.impl", "org.apache.hadoop.fs.s3a.S3AFileSystem") + .config( + "spark.hadoop.fs.s3.aws.credentials.provider", + "org.apache.hadoop.fs.s3.TemporaryAWSCredentialsProvider") + .config("spark.hadoop.fs.s3.access.key", "foo") + .config("spark.hadoop.fs.s3.secret.key", "bar") + .config( + "spark.sql.extensions", + "org.apache.iceberg.spark.extensions.IcebergSparkSessionExtensions") + .config("spark.ui.showConsoleProgress", false) + .config("spark.ui.enabled", "false"); + spark = + withCatalog(withCatalog(sessionBuilder, CATALOG_NAME), EXTERNAL_CATALOG_NAME).getOrCreate(); + + spark.sql("USE " + CATALOG_NAME); + } + + private SparkSession.Builder withCatalog(SparkSession.Builder builder, String catalogName) { + return builder + .config( + String.format("spark.sql.catalog.%s", catalogName), + "org.apache.iceberg.spark.SparkCatalog") + .config(String.format("spark.sql.catalog.%s.type", catalogName), "rest") + .config( + String.format("spark.sql.catalog.%s.uri", catalogName), + "http://localhost:" + EXT.getLocalPort() + "/api/catalog") + .config(String.format("spark.sql.catalog.%s.warehouse", catalogName), catalogName) + .config(String.format("spark.sql.catalog.%s.scope", catalogName), "PRINCIPAL_ROLE:ALL") + .config(String.format("spark.sql.catalog.%s.header.realm", catalogName), realm) + .config(String.format("spark.sql.catalog.%s.token", catalogName), polarisToken.token()) + .config(String.format("spark.sql.catalog.%s.s3.access-key-id", catalogName), "fakekey") + .config( + String.format("spark.sql.catalog.%s.s3.secret-access-key", catalogName), "fakesecret") + .config(String.format("spark.sql.catalog.%s.s3.region", catalogName), "us-west-2"); + } + + @AfterEach + public void after() { + cleanupCatalog(CATALOG_NAME); + cleanupCatalog(EXTERNAL_CATALOG_NAME); + try { + SparkSession.clearDefaultSession(); + SparkSession.clearActiveSession(); + spark.close(); + } catch (Exception e) { + LoggerFactory.getLogger(getClass()).error("Unable to close spark session", e); + } + } + + private void cleanupCatalog(String catalogName) { + spark.sql("USE " + catalogName); + List namespaces = spark.sql("SHOW NAMESPACES").collectAsList(); + for (Row namespace : namespaces) { + List tables = spark.sql("SHOW TABLES IN " + namespace.getString(0)).collectAsList(); + for (Row table : tables) { + spark.sql("DROP TABLE " + namespace.getString(0) + "." + table.getString(1)); + } + spark.sql("DROP NAMESPACE " + namespace.getString(0)); + } + try (Response response = + EXT.client() + .target( + String.format( + "http://localhost:%d/api/management/v1/catalogs/" + catalogName, + EXT.getLocalPort())) + .request("application/json") + .header("Authorization", "BEARER " + polarisToken.token()) + .header(REALM_PROPERTY_KEY, realm) + .delete()) { + assertThat(response).returns(Response.Status.NO_CONTENT.getStatusCode(), Response::getStatus); + } + } + + @Test + public void testCreateTable() { + long namespaceCount = spark.sql("SHOW NAMESPACES").count(); + assertThat(namespaceCount).isEqualTo(0L); + + spark.sql("CREATE NAMESPACE ns1"); + spark.sql("USE ns1"); + spark.sql("CREATE TABLE tb1 (col1 integer, col2 string)"); + spark.sql("INSERT INTO tb1 VALUES (1, 'a'), (2, 'b'), (3, 'c')"); + long recordCount = spark.sql("SELECT * FROM tb1").count(); + assertThat(recordCount).isEqualTo(3); + } + + @Test + public void testCreateAndUpdateExternalTable() { + long namespaceCount = spark.sql("SHOW NAMESPACES").count(); + assertThat(namespaceCount).isEqualTo(0L); + + spark.sql("CREATE NAMESPACE ns1"); + spark.sql("USE ns1"); + spark.sql("CREATE TABLE tb1 (col1 integer, col2 string)"); + spark.sql("INSERT INTO tb1 VALUES (1, 'a'), (2, 'b'), (3, 'c')"); + long recordCount = spark.sql("SELECT * FROM tb1").count(); + assertThat(recordCount).isEqualTo(3); + + spark.sql("USE " + EXTERNAL_CATALOG_NAME); + List existingNamespaces = spark.sql("SHOW NAMESPACES").collectAsList(); + assertThat(existingNamespaces).isEmpty(); + + spark.sql("CREATE NAMESPACE externalns1"); + spark.sql("USE externalns1"); + List existingTables = spark.sql("SHOW TABLES").collectAsList(); + assertThat(existingTables).isEmpty(); + + LoadTableResponse tableResponse = loadTable(CATALOG_NAME, "ns1", "tb1"); + try (Response registerResponse = + EXT.client() + .target( + String.format( + "http://localhost:%d/api/catalog/v1/" + + EXTERNAL_CATALOG_NAME + + "/namespaces/externalns1/register", + EXT.getLocalPort())) + .request("application/json") + .header("Authorization", "BEARER " + polarisToken.token()) + .header(REALM_PROPERTY_KEY, realm) + .post( + Entity.json( + ImmutableRegisterTableRequest.builder() + .name("mytb1") + .metadataLocation(tableResponse.metadataLocation()) + .build()))) { + assertThat(registerResponse).returns(Response.Status.OK.getStatusCode(), Response::getStatus); + } + + long tableCount = spark.sql("SHOW TABLES").count(); + assertThat(tableCount).isEqualTo(1); + List tables = spark.sql("SHOW TABLES").collectAsList(); + assertThat(tables).hasSize(1).extracting(row -> row.getString(1)).containsExactly("mytb1"); + long rowCount = spark.sql("SELECT * FROM mytb1").count(); + assertThat(rowCount).isEqualTo(3); + try { + spark.sql("INSERT INTO mytb1 VALUES (20, 'new_text')"); + Assertions.fail("Expected exception when inserting into external table"); + } catch (Exception e) { + LoggerFactory.getLogger(getClass()).info("Expected exception", e); + // expected exception + } + + spark.sql("INSERT INTO " + CATALOG_NAME + ".ns1.tb1 VALUES (20, 'new_text')"); + tableResponse = loadTable(CATALOG_NAME, "ns1", "tb1"); + TableUpdateNotification updateNotification = + new TableUpdateNotification( + "mytb1", + Instant.now().toEpochMilli(), + tableResponse.tableMetadata().uuid(), + tableResponse.metadataLocation(), + tableResponse.tableMetadata()); + NotificationRequest notificationRequest = new NotificationRequest(); + notificationRequest.setPayload(updateNotification); + notificationRequest.setNotificationType(NotificationType.UPDATE); + try (Response notifyResponse = + EXT.client() + .target( + String.format( + "http://localhost:%d/api/catalog/v1/%s/namespaces/externalns1/tables/mytb1/notifications", + EXT.getLocalPort(), EXTERNAL_CATALOG_NAME)) + .request("application/json") + .header("Authorization", "BEARER " + polarisToken.token()) + .header(REALM_PROPERTY_KEY, realm) + .post(Entity.json(notificationRequest))) { + assertThat(notifyResponse) + .returns(Response.Status.NO_CONTENT.getStatusCode(), Response::getStatus); + } + // refresh the table so it queries for the latest metadata.json + spark.sql("REFRESH TABLE mytb1"); + rowCount = spark.sql("SELECT * FROM mytb1").count(); + assertThat(rowCount).isEqualTo(4); + } + + private LoadTableResponse loadTable(String catalog, String namespace, String table) { + try (Response response = + EXT.client() + .target( + String.format( + "http://localhost:%d/api/catalog/v1/%s/namespaces/%s/tables/%s", + EXT.getLocalPort(), catalog, namespace, table)) + .request("application/json") + .header("Authorization", "BEARER " + polarisToken.token()) + .header(REALM_PROPERTY_KEY, realm) + .get()) { + assertThat(response).returns(Response.Status.OK.getStatusCode(), Response::getStatus); + return response.readEntity(LoadTableResponse.class); + } + } +} diff --git a/polaris-service/src/test/java/io/polaris/service/entity/CatalogEntityTest.java b/polaris-service/src/test/java/io/polaris/service/entity/CatalogEntityTest.java new file mode 100644 index 0000000000..536d4f803e --- /dev/null +++ b/polaris-service/src/test/java/io/polaris/service/entity/CatalogEntityTest.java @@ -0,0 +1,217 @@ +package io.polaris.service.entity; + +import io.polaris.core.admin.model.AwsStorageConfigInfo; +import io.polaris.core.admin.model.AzureStorageConfigInfo; +import io.polaris.core.admin.model.Catalog; +import io.polaris.core.admin.model.CatalogProperties; +import io.polaris.core.admin.model.GcpStorageConfigInfo; +import io.polaris.core.admin.model.PolarisCatalog; +import io.polaris.core.admin.model.StorageConfigInfo; +import io.polaris.core.entity.CatalogEntity; +import java.util.List; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +public class CatalogEntityTest { + + @Test + public void testInvalidAllowedLocationPrefix() { + String storageLocation = "unsupportPrefix://mybucket/path"; + AwsStorageConfigInfo awsStorageConfigModel = + AwsStorageConfigInfo.builder() + .setRoleArn("arn:aws:iam::012345678901:role/jdoe") + .setExternalId("externalId") + .setUserArn("aws::a:user:arn") + .setStorageType(StorageConfigInfo.StorageTypeEnum.S3) + .setAllowedLocations(List.of(storageLocation, "s3://externally-owned-bucket")) + .build(); + CatalogProperties prop = new CatalogProperties(storageLocation); + Catalog awsCatalog = + PolarisCatalog.builder() + .setType(Catalog.TypeEnum.INTERNAL) + .setName("name") + .setProperties(prop) + .setStorageConfigInfo(awsStorageConfigModel) + .build(); + Exception ex = + Assertions.assertThrows( + IllegalArgumentException.class, () -> CatalogEntity.fromCatalog(awsCatalog)); + Assertions.assertTrue( + ex.getMessage() + .contains( + "Location prefix not allowed: 'unsupportPrefix://mybucket/path', expected prefix: 's3://'")); + + // Invaliad azure prefix + AzureStorageConfigInfo azureStorageConfigModel = + AzureStorageConfigInfo.builder() + .setAllowedLocations( + List.of(storageLocation, "abfs://container@storageaccount.blob.windows.net/path")) + .setStorageType(StorageConfigInfo.StorageTypeEnum.AZURE) + .setTenantId("tenantId") + .build(); + Catalog azureCatalog = + PolarisCatalog.builder() + .setType(Catalog.TypeEnum.INTERNAL) + .setName("name") + .setProperties( + new CatalogProperties("abfs://container@storageaccount.blob.windows.net/path")) + .setStorageConfigInfo(azureStorageConfigModel) + .build(); + Exception ex2 = + Assertions.assertThrows( + IllegalArgumentException.class, () -> CatalogEntity.fromCatalog(azureCatalog)); + Assertions.assertTrue( + ex2.getMessage() + .contains("Invalid azure adls location uri unsupportPrefix://mybucket/path")); + + // invalid gcp prefix + GcpStorageConfigInfo gcpStorageConfigModel = + GcpStorageConfigInfo.builder() + .setStorageType(StorageConfigInfo.StorageTypeEnum.GCS) + .setAllowedLocations(List.of(storageLocation, "gs://externally-owned-bucket")) + .build(); + Catalog gcpCatalog = + PolarisCatalog.builder() + .setType(Catalog.TypeEnum.INTERNAL) + .setName("name") + .setProperties(new CatalogProperties("gs://externally-owned-bucket")) + .setStorageConfigInfo(gcpStorageConfigModel) + .build(); + Exception ex3 = + Assertions.assertThrows( + IllegalArgumentException.class, () -> CatalogEntity.fromCatalog(gcpCatalog)); + Assertions.assertTrue( + ex3.getMessage() + .contains( + "Location prefix not allowed: 'unsupportPrefix://mybucket/path', expected prefix: 'gs://'")); + } + + @Test + public void testExceedMaxAllowedLocations() { + String storageLocation = "s3://mybucket/path/"; + AwsStorageConfigInfo awsStorageConfigModel = + AwsStorageConfigInfo.builder() + .setRoleArn("arn:aws:iam::012345678901:role/jdoe") + .setExternalId("externalId") + .setUserArn("aws::a:user:arn") + .setStorageType(StorageConfigInfo.StorageTypeEnum.S3) + .setAllowedLocations( + List.of( + storageLocation + "1/", + storageLocation + "2/", + storageLocation + "3/", + storageLocation + "4/", + storageLocation + "5/", + storageLocation + "6/")) + .build(); + CatalogProperties prop = new CatalogProperties(storageLocation); + Catalog awsCatalog = + PolarisCatalog.builder() + .setType(Catalog.TypeEnum.INTERNAL) + .setName("name") + .setProperties(prop) + .setStorageConfigInfo(awsStorageConfigModel) + .build(); + Exception ex = + Assertions.assertThrows( + IllegalArgumentException.class, () -> CatalogEntity.fromCatalog(awsCatalog)); + Assertions.assertTrue(ex.getMessage().contains("Number of allowed locations exceeds 5")); + } + + @Test + public void testValidAllowedLocationPrefix() { + String basedLocation = "s3://externally-owned-bucket"; + AwsStorageConfigInfo awsStorageConfigModel = + AwsStorageConfigInfo.builder() + .setRoleArn("arn:aws:iam::012345678901:role/jdoe") + .setExternalId("externalId") + .setUserArn("aws::a:user:arn") + .setStorageType(StorageConfigInfo.StorageTypeEnum.S3) + .setAllowedLocations(List.of(basedLocation)) + .build(); + + CatalogProperties prop = new CatalogProperties(basedLocation); + Catalog awsCatalog = + PolarisCatalog.builder() + .setType(Catalog.TypeEnum.INTERNAL) + .setName("name") + .setProperties(prop) + .setStorageConfigInfo(awsStorageConfigModel) + .build(); + Assertions.assertDoesNotThrow(() -> CatalogEntity.fromCatalog(awsCatalog)); + + basedLocation = "abfs://container@storageaccount.blob.windows.net/path"; + prop.put(CatalogEntity.DEFAULT_BASE_LOCATION_KEY, basedLocation); + AzureStorageConfigInfo azureStorageConfigModel = + AzureStorageConfigInfo.builder() + .setAllowedLocations(List.of(basedLocation)) + .setStorageType(StorageConfigInfo.StorageTypeEnum.AZURE) + .setTenantId("tenantId") + .build(); + Catalog azureCatalog = + PolarisCatalog.builder() + .setType(Catalog.TypeEnum.INTERNAL) + .setName("name") + .setProperties(new CatalogProperties(basedLocation)) + .setStorageConfigInfo(azureStorageConfigModel) + .build(); + Assertions.assertDoesNotThrow(() -> CatalogEntity.fromCatalog(azureCatalog)); + + basedLocation = "gs://externally-owned-bucket"; + prop.put(CatalogEntity.DEFAULT_BASE_LOCATION_KEY, basedLocation); + GcpStorageConfigInfo gcpStorageConfigModel = + GcpStorageConfigInfo.builder() + .setStorageType(StorageConfigInfo.StorageTypeEnum.GCS) + .setAllowedLocations(List.of(basedLocation)) + .build(); + Catalog gcpCatalog = + PolarisCatalog.builder() + .setType(Catalog.TypeEnum.INTERNAL) + .setName("name") + .setProperties(new CatalogProperties(basedLocation)) + .setStorageConfigInfo(gcpStorageConfigModel) + .build(); + Assertions.assertDoesNotThrow(() -> CatalogEntity.fromCatalog(gcpCatalog)); + } + + @ParameterizedTest + @ValueSource(strings = {"", "arn:aws:iam::0123456:role/jdoe", "aws-cn", "aws-us-gov"}) + public void testInvalidArn(String roleArn) { + String basedLocation = "s3://externally-owned-bucket"; + AwsStorageConfigInfo awsStorageConfigModel = + AwsStorageConfigInfo.builder() + .setRoleArn(roleArn) + .setExternalId("externalId") + .setStorageType(StorageConfigInfo.StorageTypeEnum.S3) + .setAllowedLocations(List.of(basedLocation)) + .build(); + + CatalogProperties prop = new CatalogProperties(basedLocation); + Catalog awsCatalog = + PolarisCatalog.builder() + .setType(Catalog.TypeEnum.INTERNAL) + .setName("name") + .setProperties(prop) + .setStorageConfigInfo(awsStorageConfigModel) + .build(); + Exception ex = + Assertions.assertThrows( + IllegalArgumentException.class, () -> CatalogEntity.fromCatalog(awsCatalog)); + String expectedMessage = ""; + switch (roleArn) { + case "": + expectedMessage = "ARN cannot be null or empty"; + break; + case "aws-cn": + case "aws-us-gov": + expectedMessage = "AWS China or Gov Cloud are temporarily not supported"; + break; + default: + expectedMessage = "Invalid role ARN format"; + } + ; + Assertions.assertEquals(ex.getMessage(), expectedMessage); + } +} diff --git a/polaris-service/src/test/java/io/polaris/service/task/ManifestFileCleanupTaskHandlerTest.java b/polaris-service/src/test/java/io/polaris/service/task/ManifestFileCleanupTaskHandlerTest.java new file mode 100644 index 0000000000..5204f332e2 --- /dev/null +++ b/polaris-service/src/test/java/io/polaris/service/task/ManifestFileCleanupTaskHandlerTest.java @@ -0,0 +1,213 @@ +package io.polaris.service.task; + +import static org.assertj.core.api.Assertions.assertThatPredicate; + +import io.polaris.core.PolarisCallContext; +import io.polaris.core.PolarisDefaultDiagServiceImpl; +import io.polaris.core.context.CallContext; +import io.polaris.core.context.RealmContext; +import io.polaris.core.entity.AsyncTaskType; +import io.polaris.core.entity.TaskEntity; +import io.polaris.service.persistence.InMemoryPolarisMetaStoreManagerFactory; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicInteger; +import org.apache.commons.codec.binary.Base64; +import org.apache.iceberg.ManifestFile; +import org.apache.iceberg.ManifestFiles; +import org.apache.iceberg.catalog.Namespace; +import org.apache.iceberg.catalog.TableIdentifier; +import org.apache.iceberg.inmemory.InMemoryFileIO; +import org.apache.iceberg.io.FileIO; +import org.apache.iceberg.io.OutputFile; +import org.apache.iceberg.io.PositionOutputStream; +import org.junit.jupiter.api.Test; + +class ManifestFileCleanupTaskHandlerTest { + + @Test + public void testCleanupFileNotExists() throws IOException { + InMemoryPolarisMetaStoreManagerFactory metaStoreManagerFactory = + new InMemoryPolarisMetaStoreManagerFactory(); + RealmContext realmContext = () -> "realmName"; + PolarisCallContext polarisCallContext = + new PolarisCallContext( + metaStoreManagerFactory.getOrCreateSessionSupplier(realmContext).get(), + new PolarisDefaultDiagServiceImpl()); + try (CallContext callCtx = CallContext.of(realmContext, polarisCallContext)) { + CallContext.setCurrentContext(callCtx); + FileIO fileIO = new InMemoryFileIO(); + TableIdentifier tableIdentifier = + TableIdentifier.of(Namespace.of("db1", "schema1"), "table1"); + ManifestFileCleanupTaskHandler handler = + new ManifestFileCleanupTaskHandler((task) -> fileIO, Executors.newSingleThreadExecutor()); + ManifestFile manifestFile = + TaskTestUtils.manifestFile( + fileIO, "manifest1.avro", 1L, "dataFile1.parquet", "dataFile2.parquet"); + fileIO.deleteFile(manifestFile.path()); + TaskEntity task = + new TaskEntity.Builder() + .withTaskType(AsyncTaskType.FILE_CLEANUP) + .withData( + new ManifestFileCleanupTaskHandler.ManifestCleanupTask( + tableIdentifier, + Base64.encodeBase64String(ManifestFiles.encode(manifestFile)))) + .setName(UUID.randomUUID().toString()) + .build(); + assertThatPredicate(handler::canHandleTask).accepts(task); + assertThatPredicate(handler::handleTask).accepts(task); + } + } + + @Test + public void testCleanupFileManifestExistsDataFilesDontExist() throws IOException { + InMemoryPolarisMetaStoreManagerFactory metaStoreManagerFactory = + new InMemoryPolarisMetaStoreManagerFactory(); + RealmContext realmContext = () -> "realmName"; + PolarisCallContext polarisCallContext = + new PolarisCallContext( + metaStoreManagerFactory.getOrCreateSessionSupplier(realmContext).get(), + new PolarisDefaultDiagServiceImpl()); + try (CallContext callCtx = CallContext.of(realmContext, polarisCallContext)) { + CallContext.setCurrentContext(callCtx); + FileIO fileIO = new InMemoryFileIO(); + TableIdentifier tableIdentifier = + TableIdentifier.of(Namespace.of("db1", "schema1"), "table1"); + ManifestFileCleanupTaskHandler handler = + new ManifestFileCleanupTaskHandler((task) -> fileIO, Executors.newSingleThreadExecutor()); + ManifestFile manifestFile = + TaskTestUtils.manifestFile( + fileIO, "manifest1.avro", 100L, "dataFile1.parquet", "dataFile2.parquet"); + TaskEntity task = + new TaskEntity.Builder() + .withTaskType(AsyncTaskType.FILE_CLEANUP) + .withData( + new ManifestFileCleanupTaskHandler.ManifestCleanupTask( + tableIdentifier, + Base64.encodeBase64String(ManifestFiles.encode(manifestFile)))) + .setName(UUID.randomUUID().toString()) + .build(); + assertThatPredicate(handler::canHandleTask).accepts(task); + assertThatPredicate(handler::handleTask).accepts(task); + } + } + + @Test + public void testCleanupFiles() throws IOException { + InMemoryPolarisMetaStoreManagerFactory metaStoreManagerFactory = + new InMemoryPolarisMetaStoreManagerFactory(); + RealmContext realmContext = () -> "realmName"; + PolarisCallContext polarisCallContext = + new PolarisCallContext( + metaStoreManagerFactory.getOrCreateSessionSupplier(realmContext).get(), + new PolarisDefaultDiagServiceImpl()); + try (CallContext callCtx = CallContext.of(realmContext, polarisCallContext)) { + CallContext.setCurrentContext(callCtx); + FileIO fileIO = + new InMemoryFileIO() { + @Override + public void close() { + // no-op + } + }; + TableIdentifier tableIdentifier = + TableIdentifier.of(Namespace.of("db1", "schema1"), "table1"); + ManifestFileCleanupTaskHandler handler = + new ManifestFileCleanupTaskHandler((task) -> fileIO, Executors.newSingleThreadExecutor()); + String dataFile1Path = "dataFile1.parquet"; + OutputFile dataFile1 = fileIO.newOutputFile(dataFile1Path); + PositionOutputStream out1 = dataFile1.createOrOverwrite(); + out1.write("the data".getBytes()); + out1.close(); + String dataFile2Path = "dataFile2.parquet"; + OutputFile dataFile2 = fileIO.newOutputFile(dataFile2Path); + PositionOutputStream out2 = dataFile2.createOrOverwrite(); + out2.write("the data".getBytes()); + out2.close(); + ManifestFile manifestFile = + TaskTestUtils.manifestFile(fileIO, "manifest1.avro", 100L, dataFile1Path, dataFile2Path); + TaskEntity task = + new TaskEntity.Builder() + .withTaskType(AsyncTaskType.FILE_CLEANUP) + .withData( + new ManifestFileCleanupTaskHandler.ManifestCleanupTask( + tableIdentifier, + Base64.encodeBase64String(ManifestFiles.encode(manifestFile)))) + .setName(UUID.randomUUID().toString()) + .build(); + assertThatPredicate(handler::canHandleTask).accepts(task); + assertThatPredicate(handler::handleTask).accepts(task); + assertThatPredicate((String f) -> TaskUtils.exists(f, fileIO)).rejects(dataFile1Path); + assertThatPredicate((String f) -> TaskUtils.exists(f, fileIO)).rejects(dataFile2Path); + } + } + + @Test + public void testCleanupFilesWithRetries() throws IOException { + InMemoryPolarisMetaStoreManagerFactory metaStoreManagerFactory = + new InMemoryPolarisMetaStoreManagerFactory(); + RealmContext realmContext = () -> "realmName"; + PolarisCallContext polarisCallContext = + new PolarisCallContext( + metaStoreManagerFactory.getOrCreateSessionSupplier(realmContext).get(), + new PolarisDefaultDiagServiceImpl()); + try (CallContext callCtx = CallContext.of(realmContext, polarisCallContext)) { + CallContext.setCurrentContext(callCtx); + Map retryCounter = new HashMap<>(); + FileIO fileIO = + new InMemoryFileIO() { + @Override + public void close() { + // no-op + } + + @Override + public void deleteFile(String location) { + int attempts = + retryCounter + .computeIfAbsent(location, k -> new AtomicInteger(0)) + .incrementAndGet(); + if (attempts < 3) { + throw new RuntimeException("I'm failing to test retries"); + } else { + // succeed on the third attempt + super.deleteFile(location); + } + } + }; + + TableIdentifier tableIdentifier = + TableIdentifier.of(Namespace.of("db1", "schema1"), "table1"); + ManifestFileCleanupTaskHandler handler = + new ManifestFileCleanupTaskHandler((task) -> fileIO, Executors.newSingleThreadExecutor()); + String dataFile1Path = "dataFile1.parquet"; + OutputFile dataFile1 = fileIO.newOutputFile(dataFile1Path); + PositionOutputStream out1 = dataFile1.createOrOverwrite(); + out1.write("the data".getBytes()); + out1.close(); + String dataFile2Path = "dataFile2.parquet"; + OutputFile dataFile2 = fileIO.newOutputFile(dataFile2Path); + PositionOutputStream out2 = dataFile2.createOrOverwrite(); + out2.write("the data".getBytes()); + out2.close(); + ManifestFile manifestFile = + TaskTestUtils.manifestFile(fileIO, "manifest1.avro", 100L, dataFile1Path, dataFile2Path); + TaskEntity task = + new TaskEntity.Builder() + .withTaskType(AsyncTaskType.FILE_CLEANUP) + .withData( + new ManifestFileCleanupTaskHandler.ManifestCleanupTask( + tableIdentifier, + Base64.encodeBase64String(ManifestFiles.encode(manifestFile)))) + .setName(UUID.randomUUID().toString()) + .build(); + assertThatPredicate(handler::canHandleTask).accepts(task); + assertThatPredicate(handler::handleTask).accepts(task); + assertThatPredicate((String f) -> TaskUtils.exists(f, fileIO)).rejects(dataFile1Path); + assertThatPredicate((String f) -> TaskUtils.exists(f, fileIO)).rejects(dataFile2Path); + } + } +} diff --git a/polaris-service/src/test/java/io/polaris/service/task/TableCleanupTaskHandlerTest.java b/polaris-service/src/test/java/io/polaris/service/task/TableCleanupTaskHandlerTest.java new file mode 100644 index 0000000000..91032fdf9f --- /dev/null +++ b/polaris-service/src/test/java/io/polaris/service/task/TableCleanupTaskHandlerTest.java @@ -0,0 +1,352 @@ +package io.polaris.service.task; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.polaris.core.PolarisCallContext; +import io.polaris.core.PolarisDefaultDiagServiceImpl; +import io.polaris.core.context.CallContext; +import io.polaris.core.context.RealmContext; +import io.polaris.core.entity.AsyncTaskType; +import io.polaris.core.entity.PolarisBaseEntity; +import io.polaris.core.entity.PolarisEntityType; +import io.polaris.core.entity.TableLikeEntity; +import io.polaris.core.entity.TaskEntity; +import io.polaris.service.persistence.InMemoryPolarisMetaStoreManagerFactory; +import java.io.IOException; +import java.util.List; +import org.apache.commons.codec.binary.Base64; +import org.apache.iceberg.ManifestFile; +import org.apache.iceberg.ManifestFiles; +import org.apache.iceberg.Snapshot; +import org.apache.iceberg.catalog.Namespace; +import org.apache.iceberg.catalog.TableIdentifier; +import org.apache.iceberg.inmemory.InMemoryFileIO; +import org.apache.iceberg.io.FileIO; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.slf4j.LoggerFactory; + +class TableCleanupTaskHandlerTest { + + @Test + public void testTableCleanup() throws IOException { + InMemoryPolarisMetaStoreManagerFactory metaStoreManagerFactory = + new InMemoryPolarisMetaStoreManagerFactory(); + RealmContext realmContext = () -> "realmName"; + PolarisCallContext polarisCallContext = + new PolarisCallContext( + metaStoreManagerFactory.getOrCreateSessionSupplier(realmContext).get(), + new PolarisDefaultDiagServiceImpl()); + try (CallContext callCtx = CallContext.of(realmContext, polarisCallContext)) { + CallContext.setCurrentContext(callCtx); + FileIO fileIO = new InMemoryFileIO(); + TableIdentifier tableIdentifier = + TableIdentifier.of(Namespace.of("db1", "schema1"), "table1"); + TableCleanupTaskHandler handler = + new TableCleanupTaskHandler(Mockito.mock(), metaStoreManagerFactory, (task) -> fileIO); + long snapshotId = 100L; + ManifestFile manifestFile = + TaskTestUtils.manifestFile( + fileIO, "manifest1.avro", snapshotId, "dataFile1.parquet", "dataFile2.parquet"); + TestSnapshot snapshot = + TaskTestUtils.newSnapshot(fileIO, "manifestList.avro", 1, snapshotId, 99L, manifestFile); + String metadataFile = "v1-49494949.metadata.json"; + TaskTestUtils.writeTableMetadata(fileIO, metadataFile, snapshot); + + TaskEntity task = + new TaskEntity.Builder() + .setName("cleanup_" + tableIdentifier.toString()) + .withTaskType(AsyncTaskType.ENTITY_CLEANUP_SCHEDULER) + .withData( + new TableLikeEntity.Builder(tableIdentifier, metadataFile) + .setName("table1") + .setCatalogId(1) + .setCreateTimestamp(100) + .build()) + .build(); + Assertions.assertThatPredicate(handler::canHandleTask).accepts(task); + + CallContext.setCurrentContext(CallContext.of(realmContext, polarisCallContext)); + handler.handleTask(task); + + assertThat( + metaStoreManagerFactory + .getOrCreateMetaStoreManager(realmContext) + .loadTasks(polarisCallContext, "test", 1) + .getEntities()) + .hasSize(1) + .satisfiesExactly( + taskEntity -> + assertThat(taskEntity) + .returns(PolarisEntityType.TASK.getCode(), PolarisBaseEntity::getTypeCode) + .extracting(entity -> TaskEntity.of(entity)) + .returns(AsyncTaskType.FILE_CLEANUP, TaskEntity::getTaskType) + .returns( + new ManifestFileCleanupTaskHandler.ManifestCleanupTask( + tableIdentifier, + Base64.encodeBase64String(ManifestFiles.encode(manifestFile))), + entity -> + entity.readData( + ManifestFileCleanupTaskHandler.ManifestCleanupTask.class))); + } + } + + @Test + public void testTableCleanupHandlesAlreadyDeletedMetadata() throws IOException { + InMemoryPolarisMetaStoreManagerFactory metaStoreManagerFactory = + new InMemoryPolarisMetaStoreManagerFactory(); + RealmContext realmContext = () -> "realmName"; + PolarisCallContext polarisCallContext = + new PolarisCallContext( + metaStoreManagerFactory.getOrCreateSessionSupplier(realmContext).get(), + new PolarisDefaultDiagServiceImpl()); + try (CallContext callCtx = CallContext.of(realmContext, polarisCallContext)) { + CallContext.setCurrentContext(callCtx); + FileIO fileIO = + new InMemoryFileIO() { + @Override + public void close() { + // no-op + } + }; + TableIdentifier tableIdentifier = + TableIdentifier.of(Namespace.of("db1", "schema1"), "table1"); + TableCleanupTaskHandler handler = + new TableCleanupTaskHandler(Mockito.mock(), metaStoreManagerFactory, (task) -> fileIO); + long snapshotId = 100L; + ManifestFile manifestFile = + TaskTestUtils.manifestFile( + fileIO, "manifest1.avro", snapshotId, "dataFile1.parquet", "dataFile2.parquet"); + TestSnapshot snapshot = + TaskTestUtils.newSnapshot(fileIO, "manifestList.avro", 1, snapshotId, 99L, manifestFile); + String metadataFile = "v1-49494949.metadata.json"; + TaskTestUtils.writeTableMetadata(fileIO, metadataFile, snapshot); + + TableLikeEntity tableLikeEntity = + new TableLikeEntity.Builder(tableIdentifier, metadataFile) + .setName("table1") + .setCatalogId(1) + .setCreateTimestamp(100) + .build(); + TaskEntity task = + new TaskEntity.Builder() + .setName("cleanup_" + tableIdentifier.toString()) + .withTaskType(AsyncTaskType.ENTITY_CLEANUP_SCHEDULER) + .withData(tableLikeEntity) + .build(); + Assertions.assertThatPredicate(handler::canHandleTask).accepts(task); + + CallContext.setCurrentContext(CallContext.of(realmContext, polarisCallContext)); + + // handle the same task twice + // the first one should successfully delete the metadata + List results = List.of(handler.handleTask(task), handler.handleTask(task)); + assertThat(results).containsExactly(true, true); + + // both tasks successfully executed, but only one should queue subtasks + assertThat( + metaStoreManagerFactory + .getOrCreateMetaStoreManager(realmContext) + .loadTasks(polarisCallContext, "test", 5) + .getEntities()) + .hasSize(1); + } + } + + @Test + public void testTableCleanupDuplicatesTasksIfFileStillExists() throws IOException { + InMemoryPolarisMetaStoreManagerFactory metaStoreManagerFactory = + new InMemoryPolarisMetaStoreManagerFactory(); + RealmContext realmContext = () -> "realmName"; + PolarisCallContext polarisCallContext = + new PolarisCallContext( + metaStoreManagerFactory.getOrCreateSessionSupplier(realmContext).get(), + new PolarisDefaultDiagServiceImpl()); + try (CallContext callCtx = CallContext.of(realmContext, polarisCallContext)) { + CallContext.setCurrentContext(callCtx); + FileIO fileIO = + new InMemoryFileIO() { + @Override + public void deleteFile(String location) { + LoggerFactory.getLogger(TableCleanupTaskHandler.class) + .info( + "Not deleting file at location {} to simulate concurrent tasks runs", + location); + // don't do anything + } + + @Override + public void close() { + // no-op + } + }; + TableIdentifier tableIdentifier = + TableIdentifier.of(Namespace.of("db1", "schema1"), "table1"); + TableCleanupTaskHandler handler = + new TableCleanupTaskHandler(Mockito.mock(), metaStoreManagerFactory, (task) -> fileIO); + long snapshotId = 100L; + ManifestFile manifestFile = + TaskTestUtils.manifestFile( + fileIO, "manifest1.avro", snapshotId, "dataFile1.parquet", "dataFile2.parquet"); + TestSnapshot snapshot = + TaskTestUtils.newSnapshot(fileIO, "manifestList.avro", 1, snapshotId, 99L, manifestFile); + String metadataFile = "v1-49494949.metadata.json"; + TaskTestUtils.writeTableMetadata(fileIO, metadataFile, snapshot); + + TaskEntity task = + new TaskEntity.Builder() + .setName("cleanup_" + tableIdentifier.toString()) + .withTaskType(AsyncTaskType.ENTITY_CLEANUP_SCHEDULER) + .withData( + new TableLikeEntity.Builder(tableIdentifier, metadataFile) + .setName("table1") + .setCatalogId(1) + .setCreateTimestamp(100) + .build()) + .build(); + Assertions.assertThatPredicate(handler::canHandleTask).accepts(task); + + CallContext.setCurrentContext(CallContext.of(realmContext, polarisCallContext)); + + // handle the same task twice + // the first one should successfully delete the metadata + List results = List.of(handler.handleTask(task), handler.handleTask(task)); + assertThat(results).containsExactly(true, true); + + // both tasks successfully executed, but only one should queue subtasks + assertThat( + metaStoreManagerFactory + .getOrCreateMetaStoreManager(realmContext) + .loadTasks(polarisCallContext, "test", 5) + .getEntities()) + .hasSize(2) + .satisfiesExactly( + taskEntity -> + assertThat(taskEntity) + .returns(PolarisEntityType.TASK.getCode(), PolarisBaseEntity::getTypeCode) + .extracting(entity -> TaskEntity.of(entity)) + .returns(AsyncTaskType.FILE_CLEANUP, TaskEntity::getTaskType) + .returns( + new ManifestFileCleanupTaskHandler.ManifestCleanupTask( + tableIdentifier, + Base64.encodeBase64String(ManifestFiles.encode(manifestFile))), + entity -> + entity.readData( + ManifestFileCleanupTaskHandler.ManifestCleanupTask.class)), + taskEntity -> + assertThat(taskEntity) + .returns(PolarisEntityType.TASK.getCode(), PolarisBaseEntity::getTypeCode) + .extracting(entity -> TaskEntity.of(entity)) + .returns(AsyncTaskType.FILE_CLEANUP, TaskEntity::getTaskType) + .returns( + new ManifestFileCleanupTaskHandler.ManifestCleanupTask( + tableIdentifier, + Base64.encodeBase64String(ManifestFiles.encode(manifestFile))), + entity -> + entity.readData( + ManifestFileCleanupTaskHandler.ManifestCleanupTask.class))); + } + } + + @Test + public void testTableCleanupMultipleSnapshots() throws IOException { + InMemoryPolarisMetaStoreManagerFactory metaStoreManagerFactory = + new InMemoryPolarisMetaStoreManagerFactory(); + RealmContext realmContext = () -> "realmName"; + PolarisCallContext polarisCallContext = + new PolarisCallContext( + metaStoreManagerFactory.getOrCreateSessionSupplier(realmContext).get(), + new PolarisDefaultDiagServiceImpl()); + try (CallContext callCtx = CallContext.of(realmContext, polarisCallContext)) { + CallContext.setCurrentContext(callCtx); + FileIO fileIO = new InMemoryFileIO(); + TableIdentifier tableIdentifier = + TableIdentifier.of(Namespace.of("db1", "schema1"), "table1"); + TableCleanupTaskHandler handler = + new TableCleanupTaskHandler(Mockito.mock(), metaStoreManagerFactory, (task) -> fileIO); + long snapshotId1 = 100L; + ManifestFile manifestFile1 = + TaskTestUtils.manifestFile( + fileIO, "manifest1.avro", snapshotId1, "dataFile1.parquet", "dataFile2.parquet"); + ManifestFile manifestFile2 = + TaskTestUtils.manifestFile( + fileIO, "manifest2.avro", snapshotId1, "dataFile3.parquet", "dataFile4.parquet"); + Snapshot snapshot = + TaskTestUtils.newSnapshot( + fileIO, "manifestList.avro", 1, snapshotId1, 99L, manifestFile1, manifestFile2); + ManifestFile manifestFile3 = + TaskTestUtils.manifestFile( + fileIO, "manifest3.avro", snapshot.snapshotId() + 1, "dataFile5.parquet"); + Snapshot snapshot2 = + TaskTestUtils.newSnapshot( + fileIO, + "manifestList2.avro", + snapshot.sequenceNumber() + 1, + snapshot.snapshotId() + 1, + snapshot.snapshotId(), + manifestFile1, + manifestFile3); // exclude manifest2 from the new snapshot + String metadataFile = "v1-295495059.metadata.json"; + TaskTestUtils.writeTableMetadata(fileIO, metadataFile, snapshot, snapshot2); + + TaskEntity task = + new TaskEntity.Builder() + .withTaskType(AsyncTaskType.ENTITY_CLEANUP_SCHEDULER) + .withData( + new TableLikeEntity.Builder(tableIdentifier, metadataFile) + .setName("table1") + .setCatalogId(1) + .setCreateTimestamp(100) + .build()) + .build(); + Assertions.assertThatPredicate(handler::canHandleTask).accepts(task); + + CallContext.setCurrentContext(CallContext.of(realmContext, polarisCallContext)); + handler.handleTask(task); + + assertThat( + metaStoreManagerFactory + .getOrCreateMetaStoreManager(realmContext) + .loadTasks(polarisCallContext, "test", 5) + .getEntities()) + // all three manifests should be present, even though one is excluded from the latest + // snapshot + .hasSize(3) + .satisfiesExactlyInAnyOrder( + taskEntity -> + assertThat(taskEntity) + .returns(PolarisEntityType.TASK.getCode(), PolarisBaseEntity::getTypeCode) + .extracting(entity -> TaskEntity.of(entity)) + .returns( + new ManifestFileCleanupTaskHandler.ManifestCleanupTask( + tableIdentifier, + Base64.encodeBase64String(ManifestFiles.encode(manifestFile1))), + entity -> + entity.readData( + ManifestFileCleanupTaskHandler.ManifestCleanupTask.class)), + taskEntity -> + assertThat(taskEntity) + .returns(PolarisEntityType.TASK.getCode(), PolarisBaseEntity::getTypeCode) + .extracting(entity -> TaskEntity.of(entity)) + .returns( + new ManifestFileCleanupTaskHandler.ManifestCleanupTask( + tableIdentifier, + Base64.encodeBase64String(ManifestFiles.encode(manifestFile2))), + entity -> + entity.readData( + ManifestFileCleanupTaskHandler.ManifestCleanupTask.class)), + taskEntity -> + assertThat(taskEntity) + .returns(PolarisEntityType.TASK.getCode(), PolarisBaseEntity::getTypeCode) + .extracting(entity -> TaskEntity.of(entity)) + .returns( + new ManifestFileCleanupTaskHandler.ManifestCleanupTask( + tableIdentifier, + Base64.encodeBase64String(ManifestFiles.encode(manifestFile3))), + entity -> + entity.readData( + ManifestFileCleanupTaskHandler.ManifestCleanupTask.class))); + } + } +} diff --git a/polaris-service/src/test/java/io/polaris/service/task/TaskTestUtils.java b/polaris-service/src/test/java/io/polaris/service/task/TaskTestUtils.java new file mode 100644 index 0000000000..44abb0c0ac --- /dev/null +++ b/polaris-service/src/test/java/io/polaris/service/task/TaskTestUtils.java @@ -0,0 +1,88 @@ +package io.polaris.service.task; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; +import org.apache.iceberg.DataFile; +import org.apache.iceberg.DataFiles; +import org.apache.iceberg.FileFormat; +import org.apache.iceberg.ManifestFile; +import org.apache.iceberg.ManifestFiles; +import org.apache.iceberg.ManifestWriter; +import org.apache.iceberg.PartitionSpec; +import org.apache.iceberg.Schema; +import org.apache.iceberg.Snapshot; +import org.apache.iceberg.SortOrder; +import org.apache.iceberg.TableMetadata; +import org.apache.iceberg.TableMetadataParser; +import org.apache.iceberg.avro.Avro; +import org.apache.iceberg.io.FileAppender; +import org.apache.iceberg.io.FileIO; +import org.apache.iceberg.io.PositionOutputStream; +import org.apache.iceberg.types.Types; +import org.jetbrains.annotations.NotNull; + +public class TaskTestUtils { + static ManifestFile manifestFile( + FileIO fileIO, String manifestFilePath, long snapshotId, String... dataFiles) + throws IOException { + ManifestWriter writer = + ManifestFiles.write( + 2, PartitionSpec.unpartitioned(), fileIO.newOutputFile(manifestFilePath), snapshotId); + for (String dataFile : dataFiles) { + writer.add( + new DataFiles.Builder(PartitionSpec.unpartitioned()) + .withFileSizeInBytes(100L) + .withFormat(FileFormat.PARQUET) + .withPath(dataFile) + .withRecordCount(10) + .build()); + } + writer.close(); + return writer.toManifestFile(); + } + + static void writeTableMetadata(FileIO fileIO, String metadataFile, Snapshot... snapshots) + throws IOException { + TableMetadata.Builder tmBuidler = + TableMetadata.buildFromEmpty() + .setLocation("path/to/table") + .addSchema( + new Schema( + List.of(Types.NestedField.of(1, false, "field1", Types.StringType.get()))), + 1) + .addSortOrder(SortOrder.unsorted()) + .assignUUID(UUID.randomUUID().toString()) + .addPartitionSpec(PartitionSpec.unpartitioned()); + for (Snapshot snapshot : snapshots) { + tmBuidler.addSnapshot(snapshot); + } + TableMetadata tableMetadata = tmBuidler.build(); + PositionOutputStream out = fileIO.newOutputFile(metadataFile).createOrOverwrite(); + out.write(TableMetadataParser.toJson(tableMetadata).getBytes(StandardCharsets.UTF_8)); + out.close(); + } + + static @NotNull TestSnapshot newSnapshot( + FileIO fileIO, + String manifestListLocation, + long sequenceNumber, + long snapshotId, + long parentSnapshot, + ManifestFile... manifestFiles) + throws IOException { + FileAppender manifestListWriter = + Avro.write(fileIO.newOutputFile(manifestListLocation)) + .schema(ManifestFile.schema()) + .named("manifest_file") + .overwrite() + .build(); + manifestListWriter.addAll(Arrays.asList(manifestFiles)); + manifestListWriter.close(); + TestSnapshot snapshot = + new TestSnapshot(sequenceNumber, snapshotId, parentSnapshot, 1L, manifestListLocation); + return snapshot; + } +} diff --git a/polaris-service/src/test/java/io/polaris/service/task/TestSnapshot.java b/polaris-service/src/test/java/io/polaris/service/task/TestSnapshot.java new file mode 100644 index 0000000000..a3f2ddcea3 --- /dev/null +++ b/polaris-service/src/test/java/io/polaris/service/task/TestSnapshot.java @@ -0,0 +1,115 @@ +package io.polaris.service.task; + +import com.google.common.collect.Lists; +import java.io.IOException; +import java.util.List; +import java.util.Map; +import org.apache.iceberg.DataFile; +import org.apache.iceberg.GenericManifestFile; +import org.apache.iceberg.GenericPartitionFieldSummary; +import org.apache.iceberg.ManifestContent; +import org.apache.iceberg.ManifestFile; +import org.apache.iceberg.Snapshot; +import org.apache.iceberg.avro.Avro; +import org.apache.iceberg.exceptions.RuntimeIOException; +import org.apache.iceberg.io.CloseableIterable; +import org.apache.iceberg.io.FileIO; + +final class TestSnapshot implements Snapshot { + private long sequenceNumber; + private long snapshotId; + private long parentSnapshot; + private long timestampMillis; + private String manifestListLocation; + + public TestSnapshot( + long sequenceNumber, + long snapshotId, + long parentSnapshot, + long timestampMillis, + String manifestListLocation) { + this.sequenceNumber = sequenceNumber; + this.snapshotId = snapshotId; + this.parentSnapshot = parentSnapshot; + this.timestampMillis = timestampMillis; + this.manifestListLocation = manifestListLocation; + } + + @Override + public long sequenceNumber() { + return sequenceNumber; + } + + @Override + public long snapshotId() { + return snapshotId; + } + + @Override + public Long parentId() { + return parentSnapshot; + } + + @Override + public long timestampMillis() { + return timestampMillis; + } + + @Override + public List allManifests(FileIO io) { + try (CloseableIterable files = + Avro.read(io.newInputFile(manifestListLocation)) + .rename("manifest_file", GenericManifestFile.class.getName()) + .rename("partitions", GenericPartitionFieldSummary.class.getName()) + .rename("r508", GenericPartitionFieldSummary.class.getName()) + .classLoader(GenericManifestFile.class.getClassLoader()) + .project(ManifestFile.schema()) + .reuseContainers(false) + .build()) { + + return Lists.newLinkedList(files); + + } catch (IOException e) { + throw new RuntimeIOException(e, "Cannot read manifest list file: %s", manifestListLocation); + } + } + + @Override + public List dataManifests(FileIO io) { + return allManifests(io).stream() + .filter(mf -> mf.content().equals(ManifestContent.DATA)) + .toList(); + } + + @Override + public List deleteManifests(FileIO io) { + return allManifests(io).stream() + .filter(mf -> mf.content().equals(ManifestContent.DELETES)) + .toList(); + } + + @Override + public String operation() { + return "op"; + } + + @Override + public Map summary() { + return Map.of(); + } + + @Override + public Iterable addedDataFiles(FileIO io) { + return null; + } + + @Override + public Iterable removedDataFiles(FileIO io) { + return null; + } + + @Override + public String manifestListLocation() { + return manifestListLocation; + } +} diff --git a/polaris-service/src/test/java/io/polaris/service/test/PolarisConnectionExtension.java b/polaris-service/src/test/java/io/polaris/service/test/PolarisConnectionExtension.java new file mode 100644 index 0000000000..683f7694bd --- /dev/null +++ b/polaris-service/src/test/java/io/polaris/service/test/PolarisConnectionExtension.java @@ -0,0 +1,233 @@ +package io.polaris.service.test; + +import static io.polaris.service.context.DefaultContextResolver.REALM_PROPERTY_KEY; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.dropwizard.testing.junit5.DropwizardAppExtension; +import io.polaris.core.PolarisDiagnostics; +import io.polaris.core.context.CallContext; +import io.polaris.core.context.RealmContext; +import io.polaris.core.entity.PolarisEntityConstants; +import io.polaris.core.entity.PolarisEntitySubType; +import io.polaris.core.entity.PolarisEntityType; +import io.polaris.core.entity.PolarisGrantRecord; +import io.polaris.core.entity.PolarisPrincipalSecrets; +import io.polaris.core.persistence.LocalPolarisMetaStoreManagerFactory; +import io.polaris.core.persistence.MetaStoreManagerFactory; +import io.polaris.core.persistence.PolarisMetaStoreManager; +import io.polaris.core.storage.PolarisCredentialProperty; +import io.polaris.core.storage.PolarisStorageActions; +import io.polaris.core.storage.PolarisStorageConfigurationInfo; +import io.polaris.core.storage.PolarisStorageIntegration; +import io.polaris.core.storage.PolarisStorageIntegrationProvider; +import io.polaris.service.auth.TokenUtils; +import io.polaris.service.config.PolarisApplicationConfig; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.EnumMap; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.junit.jupiter.api.extension.BeforeAllCallback; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ExtensionContext.Namespace; +import org.junit.jupiter.api.extension.ParameterContext; +import org.junit.jupiter.api.extension.ParameterResolutionException; +import org.junit.jupiter.api.extension.ParameterResolver; +import org.junit.platform.commons.util.ReflectionUtils; +import org.mockito.Mockito; +import org.slf4j.LoggerFactory; +import software.amazon.awssdk.services.sts.StsClient; +import software.amazon.awssdk.services.sts.model.AssumeRoleRequest; +import software.amazon.awssdk.services.sts.model.AssumeRoleResponse; +import software.amazon.awssdk.services.sts.model.Credentials; + +public class PolarisConnectionExtension implements BeforeAllCallback, ParameterResolver { + + public static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private MetaStoreManagerFactory metaStoreManagerFactory; + private DropwizardAppExtension dropwizardAppExtension; + + public record PolarisToken(String token) {} + + private static PolarisPrincipalSecrets adminSecrets; + private static String realm; + + @Override + public void beforeAll(ExtensionContext extensionContext) throws Exception { + dropwizardAppExtension = findDropwizardExtension(extensionContext); + if (dropwizardAppExtension == null) { + return; + } + + // Generate unique realm using test name for each test since the tests can run in parallel + realm = getTestRealm(extensionContext.getRequiredTestClass()); + extensionContext + .getStore(Namespace.create(extensionContext.getRequiredTestClass())) + .put(REALM_PROPERTY_KEY, realm); + + try { + PolarisApplicationConfig config = + (PolarisApplicationConfig) dropwizardAppExtension.getConfiguration(); + metaStoreManagerFactory = config.getMetaStoreManagerFactory(); + + if (metaStoreManagerFactory instanceof LocalPolarisMetaStoreManagerFactory msmf) { + StsClient mockSts = Mockito.mock(StsClient.class); + Mockito.when(mockSts.assumeRole(Mockito.isA(AssumeRoleRequest.class))) + .thenReturn( + AssumeRoleResponse.builder() + .credentials( + Credentials.builder() + .accessKeyId("theaccesskey") + .secretAccessKey("thesecretkey") + .sessionToken("thesessiontoken") + .build()) + .build()); + msmf.setStorageIntegrationProvider( + new PolarisStorageIntegrationProvider() { + @Override + public @Nullable + PolarisStorageIntegration getStorageIntegrationForConfig( + PolarisStorageConfigurationInfo polarisStorageConfigurationInfo) { + return new PolarisStorageIntegration("testIntegration") { + @Override + public EnumMap getSubscopedCreds( + @NotNull PolarisDiagnostics diagnostics, + @NotNull T storageConfig, + boolean allowListOperation, + @NotNull Set allowedReadLocations, + @NotNull Set allowedWriteLocations) { + return new EnumMap<>(PolarisCredentialProperty.class); + } + + @Override + public EnumMap + descPolarisStorageConfiguration( + @NotNull PolarisStorageConfigurationInfo storageConfigInfo) { + return new EnumMap<>(PolarisStorageConfigurationInfo.DescribeProperty.class); + } + + @Override + public @NotNull Map> + validateAccessToLocations( + @NotNull T storageConfig, + @NotNull Set actions, + @NotNull Set locations) { + return Map.of(); + } + }; + } + }); + } + + RealmContext realmContext = + config + .getRealmContextResolver() + .resolveRealmContext( + "http://localhost", "GET", "/", Map.of(), Map.of(REALM_PROPERTY_KEY, realm)); + CallContext ctx = + config + .getCallContextResolver() + .resolveCallContext(realmContext, "GET", "/", Map.of(), Map.of()); + CallContext.setCurrentContext(ctx); + PolarisMetaStoreManager metaStoreManager = + metaStoreManagerFactory.getOrCreateMetaStoreManager(ctx.getRealmContext()); + PolarisMetaStoreManager.EntityResult principal = + metaStoreManager.readEntityByName( + ctx.getPolarisCallContext(), + null, + PolarisEntityType.PRINCIPAL, + PolarisEntitySubType.NULL_SUBTYPE, + PolarisEntityConstants.getRootPrincipalName()); + + Map propertiesMap = readInternalProperties(principal); + adminSecrets = + metaStoreManager + .loadPrincipalSecrets(ctx.getPolarisCallContext(), propertiesMap.get("client_id")) + .getPrincipalSecrets(); + } finally { + CallContext.unsetCurrentContext(); + } + } + + public static String getTestRealm(Class testClassName) { + return testClassName.getName().replace('.', '_'); + } + + static PolarisPrincipalSecrets getAdminSecrets() { + return adminSecrets; + } + + public static @Nullable DropwizardAppExtension findDropwizardExtension( + ExtensionContext extensionContext) throws IllegalAccessException { + Field dropwizardExtensionField = + findAnnotatedFields(extensionContext.getRequiredTestClass(), true); + if (dropwizardExtensionField == null) { + LoggerFactory.getLogger(PolarisGrantRecord.class) + .warn( + "Unable to find dropwizard extension field in test class " + + extensionContext.getRequiredTestClass()); + return null; + } + DropwizardAppExtension appExtension = + (DropwizardAppExtension) ReflectionUtils.makeAccessible(dropwizardExtensionField).get(null); + return appExtension; + } + + @Override + public boolean supportsParameter( + ParameterContext parameterContext, ExtensionContext extensionContext) + throws ParameterResolutionException { + return parameterContext.getParameter().getType().equals(PolarisToken.class) + || parameterContext.getParameter().getType().equals(PolarisPrincipalSecrets.class); + } + + @Override + public Object resolveParameter( + ParameterContext parameterContext, ExtensionContext extensionContext) + throws ParameterResolutionException { + if (parameterContext.getParameter().getType().equals(PolarisToken.class)) { + String token = + TokenUtils.getTokenFromSecrets( + dropwizardAppExtension.client(), + dropwizardAppExtension.getLocalPort(), + adminSecrets.getPrincipalClientId(), + adminSecrets.getMainSecret(), + realm); + return new PolarisToken(token); + } else { + return metaStoreManagerFactory; + } + } + + private static Map readInternalProperties( + PolarisMetaStoreManager.EntityResult principal) { + try { + return OBJECT_MAPPER.readValue( + principal.getEntity().getInternalProperties(), + new TypeReference>() {}); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + private static Field findAnnotatedFields(Class testClass, boolean isStaticMember) { + final Optional set = + Arrays.stream(testClass.getDeclaredFields()) + .filter(m -> isStaticMember == Modifier.isStatic(m.getModifiers())) + .filter(m -> DropwizardAppExtension.class.isAssignableFrom(m.getType())) + .findFirst(); + if (set.isPresent()) { + return set.get(); + } + if (!testClass.getSuperclass().equals(Object.class)) { + return findAnnotatedFields(testClass.getSuperclass(), isStaticMember); + } + return null; + } +} diff --git a/polaris-service/src/test/java/io/polaris/service/test/SnowmanCredentialsExtension.java b/polaris-service/src/test/java/io/polaris/service/test/SnowmanCredentialsExtension.java new file mode 100644 index 0000000000..3137f7d117 --- /dev/null +++ b/polaris-service/src/test/java/io/polaris/service/test/SnowmanCredentialsExtension.java @@ -0,0 +1,209 @@ +package io.polaris.service.test; + +import static io.polaris.service.context.DefaultContextResolver.REALM_PROPERTY_KEY; +import static org.assertj.core.api.Assertions.assertThat; + +import io.dropwizard.testing.junit5.DropwizardAppExtension; +import io.polaris.core.admin.model.GrantPrincipalRoleRequest; +import io.polaris.core.admin.model.Principal; +import io.polaris.core.admin.model.PrincipalRole; +import io.polaris.core.admin.model.PrincipalWithCredentials; +import io.polaris.core.entity.PolarisPrincipalSecrets; +import io.polaris.service.auth.TokenUtils; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.junit.jupiter.api.extension.AfterAllCallback; +import org.junit.jupiter.api.extension.BeforeAllCallback; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ExtensionContext.Namespace; +import org.junit.jupiter.api.extension.ParameterContext; +import org.junit.jupiter.api.extension.ParameterResolutionException; +import org.junit.jupiter.api.extension.ParameterResolver; +import org.slf4j.LoggerFactory; + +public class SnowmanCredentialsExtension + implements BeforeAllCallback, AfterAllCallback, ParameterResolver { + + private SnowmanCredentials snowmanCredentials; + + public record SnowmanCredentials(String clientId, String clientSecret) {} + + @Override + public void beforeAll(ExtensionContext extensionContext) throws Exception { + PolarisPrincipalSecrets adminSecrets = PolarisConnectionExtension.getAdminSecrets(); + String realm = + extensionContext + .getStore(Namespace.create(extensionContext.getRequiredTestClass())) + .get(REALM_PROPERTY_KEY, String.class); + + if (adminSecrets == null) { + LoggerFactory.getLogger(SnowmanCredentialsExtension.class) + .atError() + .log( + "No admin secrets configured - you must also configure your test with PolarisConnectionExtension"); + return; + } + DropwizardAppExtension dropwizard = + PolarisConnectionExtension.findDropwizardExtension(extensionContext); + if (dropwizard == null) { + return; + } + String userToken = + TokenUtils.getTokenFromSecrets( + dropwizard.client(), + dropwizard.getLocalPort(), + adminSecrets.getPrincipalClientId(), + adminSecrets.getMainSecret(), + realm); + + PrincipalRole principalRole = new PrincipalRole("catalog-admin"); + try (Response createPrResponse = + dropwizard + .client() + .target( + String.format( + "http://localhost:%d/api/management/v1/principal-roles", + dropwizard.getLocalPort())) + .request("application/json") + .header("Authorization", "Bearer " + userToken) + .header(REALM_PROPERTY_KEY, realm) + .post(Entity.json(principalRole))) { + assertThat(createPrResponse) + .returns(Response.Status.CREATED.getStatusCode(), Response::getStatus); + } + + Principal principal = new Principal("snowman"); + + try (Response createPResponse = + dropwizard + .client() + .target( + String.format( + "http://localhost:%d/api/management/v1/principals", dropwizard.getLocalPort())) + .request("application/json") + .header("Authorization", "Bearer " + userToken) // how is token getting used? + .header(REALM_PROPERTY_KEY, realm) + .post(Entity.json(principal))) { + assertThat(createPResponse) + .returns(Response.Status.CREATED.getStatusCode(), Response::getStatus); + PrincipalWithCredentials snowmanWithCredentials = + createPResponse.readEntity(PrincipalWithCredentials.class); + try (Response rotateResp = + dropwizard + .client() + .target( + String.format( + "http://localhost:%d/api/management/v1/principals/%s/rotate", + dropwizard.getLocalPort(), "snowman")) + .request(MediaType.APPLICATION_JSON) + .header( + "Authorization", + "Bearer " + + TokenUtils.getTokenFromSecrets( + dropwizard.client(), + dropwizard.getLocalPort(), + snowmanWithCredentials.getCredentials().getClientId(), + snowmanWithCredentials.getCredentials().getClientSecret(), + realm)) + .header(REALM_PROPERTY_KEY, realm) + .post(Entity.json(snowmanWithCredentials))) { + + assertThat(rotateResp).returns(200, Response::getStatus); + + // Use the rotated credentials. + snowmanWithCredentials = rotateResp.readEntity(PrincipalWithCredentials.class); + } + snowmanCredentials = + new SnowmanCredentials( + snowmanWithCredentials.getCredentials().getClientId(), + snowmanWithCredentials.getCredentials().getClientSecret()); + } + try (Response assignPrResponse = + dropwizard + .client() + .target( + String.format( + "http://localhost:%d/api/management/v1/principals/snowman/principal-roles", + dropwizard.getLocalPort())) + .request("application/json") + .header("Authorization", "Bearer " + userToken) // how is token getting used? + .header(REALM_PROPERTY_KEY, realm) + .put(Entity.json(new GrantPrincipalRoleRequest(principalRole)))) { + assertThat(assignPrResponse) + .returns(Response.Status.CREATED.getStatusCode(), Response::getStatus); + } + } + + @Override + public void afterAll(ExtensionContext extensionContext) throws Exception { + PolarisPrincipalSecrets adminSecrets = PolarisConnectionExtension.getAdminSecrets(); + String realm = + extensionContext + .getStore(Namespace.create(extensionContext.getRequiredTestClass())) + .get(REALM_PROPERTY_KEY, String.class); + + if (adminSecrets == null) { + LoggerFactory.getLogger(SnowmanCredentialsExtension.class) + .atError() + .log( + "No admin secrets configured - you must also configure your test with PolarisConnectionExtension"); + return; + } + DropwizardAppExtension dropwizard = + PolarisConnectionExtension.findDropwizardExtension(extensionContext); + if (dropwizard == null) { + return; + } + String userToken = + TokenUtils.getTokenFromSecrets( + dropwizard.client(), + dropwizard.getLocalPort(), + adminSecrets.getPrincipalClientId(), + adminSecrets.getMainSecret(), + realm); + + try (Response deletePrResponse = + dropwizard + .client() + .target( + String.format( + "http://localhost:%d/api/management/v1/principal-roles/%s", + dropwizard.getLocalPort(), "catalog-admin")) + .request("application/json") + .header("Authorization", "Bearer " + userToken) + .header(REALM_PROPERTY_KEY, realm) + .delete()) {} + + try (Response deleteResponse = + dropwizard + .client() + .target( + String.format( + "http://localhost:%d/api/management/v1/principals/%s", + dropwizard.getLocalPort(), "snowman")) + .request("application/json") + .header("Authorization", "Bearer " + userToken) + .header(REALM_PROPERTY_KEY, realm) + .delete()) {} + } + + // FIXME - this would be better done with a Credentials-specific annotation processor so + // tests could declare which credentials they want (e.g., @TestCredentials("root") ) + // For now, snowman comes from here and root comes from PolarisConnectionExtension + + @Override + public boolean supportsParameter( + ParameterContext parameterContext, ExtensionContext extensionContext) + throws ParameterResolutionException { + + return parameterContext.getParameter().getType() == SnowmanCredentials.class; + } + + @Override + public Object resolveParameter( + ParameterContext parameterContext, ExtensionContext extensionContext) + throws ParameterResolutionException { + return snowmanCredentials; + } +} diff --git a/polaris-service/src/test/resources/META-INF/persistence.xml b/polaris-service/src/test/resources/META-INF/persistence.xml new file mode 100644 index 0000000000..d6a1c5aa4e --- /dev/null +++ b/polaris-service/src/test/resources/META-INF/persistence.xml @@ -0,0 +1,27 @@ + + + + + + org.eclipse.persistence.jpa.PersistenceProvider + io.polaris.core.persistence.models.ModelEntity + io.polaris.core.persistence.models.ModelEntityActive + io.polaris.core.persistence.models.ModelEntityChangeTracking + io.polaris.core.persistence.models.ModelEntityDropped + io.polaris.core.persistence.models.ModelGrantRecord + io.polaris.core.persistence.models.ModelPrincipalSecrets + io.polaris.core.persistence.models.ModelSequenceId + NONE + + + + + + + + + + + \ No newline at end of file diff --git a/polaris-service/src/test/resources/META-INF/services/io.polaris.service.auth.DiscoverableAuthenticator b/polaris-service/src/test/resources/META-INF/services/io.polaris.service.auth.DiscoverableAuthenticator new file mode 100644 index 0000000000..c34535714a --- /dev/null +++ b/polaris-service/src/test/resources/META-INF/services/io.polaris.service.auth.DiscoverableAuthenticator @@ -0,0 +1 @@ +io.polaris.service.auth.TestInlineBearerTokenPolarisAuthenticator \ No newline at end of file diff --git a/polaris-service/src/test/resources/polaris-server-integrationtest.yml b/polaris-service/src/test/resources/polaris-server-integrationtest.yml new file mode 100644 index 0000000000..64146096df --- /dev/null +++ b/polaris-service/src/test/resources/polaris-server-integrationtest.yml @@ -0,0 +1,150 @@ +server: + # Maximum number of threads. + maxThreads: 200 + + # Minimum number of thread to keep alive. + minThreads: 10 + applicationConnectors: + # HTTP-specific options. + - type: http + + # The port on which the HTTP server listens for service requests. + port: 8181 + + adminConnectors: + - type: http + port: 8182 + + # The hostname of the interface to which the HTTP server socket wil be found. If omitted, the + # socket will listen on all interfaces. + #bindHost: localhost + + # ssl: + # keyStore: ./example.keystore + # keyStorePassword: example + # + # keyStoreType: JKS # (optional, JKS is default) + + # HTTP request log settings + requestLog: + appenders: + # Settings for logging to stdout. + - type: console + + # Settings for logging to a file. + - type: file + + # The file to which statements will be logged. + currentLogFilename: ./logs/request.log + + # When the log file rolls over, the file will be archived to requests-2012-03-15.log.gz, + # requests.log will be truncated, and new statements written to it. + archivedLogFilenamePattern: ./logs/requests-%d.log.gz + + # The maximum number of log files to archive. + archivedFileCount: 14 + + # Enable archiving if the request log entries go to the their own file + archive: true + +# Either 'jdbc' or 'polaris'; specifies the underlying delegate catalog +baseCatalogType: "polaris" + +featureConfiguration: + ENFORCE_PRINCIPAL_CREDENTIAL_ROTATION_REQUIRED_CHECKING: true + DISABLE_TOKEN_GENERATION_FOR_USER_PRINCIPALS: true + ALLOW_WILDCARD_LOCATION: true + ALLOW_SPECIFYING_FILE_IO_IMPL: true + SUPPORTED_CATALOG_STORAGE_TYPES: + - FILE + - S3 + - GCS + - AZURE + +sqlLiteCatalogDirs: + default-realm: ./build/test_data/iceberg + +metaStoreManager: + type: in-memory +# type: remote +# url: http://sdp-devvm-mcollado:8080 + +oauth2: + type: default + tokenBroker: + type: symmetric-key + secret: polaris +# type: snowflake +# clientId: ${GS_POLARIS_SERVICE_CLIENT_ID} +# clientSecret: ${GS_POLARIS_SERVICE_CLIENT_SECRET} +# clientSecret2: ${GS_POLARIS_SERVICE_CLIENT_SECRET2} + +authenticator: + class: io.polaris.service.auth.DefaultPolarisAuthenticator + tokenBroker: + type: symmetric-key + secret: polaris + + +callContextResolver: + type: default +# type: snowflake +# account: ${SNOWFLAKE_ACCOUNT:-SNOWFLAKE} +# scheme: ${GS_SCHEME:-http} +# host: ${GS_HOST:-localhost} +# port: ${GS_PORT:-8080} + +realmContextResolver: + type: default +# type: snowflake +# account: ${SNOWFLAKE_ACCOUNT:-SNOWFLAKE} +# scheme: ${GS_SCHEME:-http} +# host: ${GS_HOST:-localhost} +# port: ${GS_PORT:-8080} + +defaultRealm: SNOWFLAKE + +cors: + allowed-origins: + - snowflake.com + + # Logging settings. +logging: + + # The default level of all loggers. Can be OFF, ERROR, WARN, INFO, DEBUG, TRACE, or ALL. + level: INFO + + # Logger-specific levels. + loggers: + io.polaris: DEBUG + + appenders: + + - type: console + # If true, write log statements to stdout. + # enabled: true + # Do not display log statements below this threshold to stdout. + threshold: ALL + # Custom Logback PatternLayout with threadname. + logFormat: "%-5p [%d{ISO8601} - %-6r] [%t] [%X{aid}%X{sid}%X{tid}%X{wid}%X{oid}%X{srv}%X{job}%X{rid}] %c{30}: %m %kvp%n%ex" + + # Settings for logging to a file. + - type: file + # If true, write log statements to a file. + # enabled: true + # Do not write log statements below this threshold to the file. + threshold: ALL + # Custom Logback PatternLayout with threadname. + logFormat: "%-5p [%d{ISO8601} - %-6r] [%t] [%X{aid}%X{sid}%X{tid}%X{wid}%X{oid}%X{srv}%X{job}%X{rid}] %c: %m %kvp%n%ex" + + # when using json logging, you must use a format like this, else the + # mdc section of the json log will be incorrect + # logFormat: "%-5p [%d{ISO8601} - %-6r] [%t] [%X] %c: %m%n%ex" + + # The file to which statements will be logged. + currentLogFilename: ./logs/iceberg-rest.log + # When the log file rolls over, the file will be archived to snowflake-2012-03-15.log.gz, + # snowflake.log will be truncated, and new statements written to it. + archivedLogFilenamePattern: ./logs/iceberg-rest-%d.log.gz + # The maximum number of log files to archive. + archivedFileCount: 14 diff --git a/regtests/.dockerignore b/regtests/.dockerignore new file mode 100644 index 0000000000..fbc79dce91 --- /dev/null +++ b/regtests/.dockerignore @@ -0,0 +1,5 @@ +.python-version +.pytest_cache +t_pyspark/src/spark-warehouse +t_pyspark/src/.pytest_cache +polaris/polaris.management.egg-info \ No newline at end of file diff --git a/regtests/.gitignore b/regtests/.gitignore new file mode 100644 index 0000000000..46a767f646 --- /dev/null +++ b/regtests/.gitignore @@ -0,0 +1,4 @@ +.venv +t_pyspark/src/__pycache__ +.pytest_cache +.python-version diff --git a/regtests/Dockerfile b/regtests/Dockerfile new file mode 100644 index 0000000000..270315235b --- /dev/null +++ b/regtests/Dockerfile @@ -0,0 +1,28 @@ +FROM apache/spark:3.5.1-python3 +ARG POLARIS_HOST=polaris +ENV POLARIS_HOST=$POLARIS_HOST +ENV SPARK_HOME=/opt/spark + +USER root +RUN apt update +RUN apt-get install -y diffutils wget curl python3.8-venv +RUN mkdir -p /home/spark && \ + chown -R spark /home/spark && \ + mkdir -p /tmp/polaris-regtests && \ + chown -R spark /tmp/polaris-regtests +RUN mkdir /opt/spark/conf && chmod -R 777 /opt/spark/conf + +USER spark +ENV PYTHONPATH="${SPARK_HOME}/python/:${SPARK_HOME}/python/lib/py4j-0.10.9.7-src.zip:$PYTHONPATH" + +# Copy and run setup.sh separately so that test sources can change, but the setup script run is still cached +WORKDIR /home/spark/regtests +COPY ./setup.sh /home/spark/regtests/setup.sh +COPY ./pyspark-setup.sh /home/spark/regtests/pyspark-setup.sh +COPY ./client/python /home/spark/regtests/client/python + +RUN ./setup.sh + +COPY --chown=spark . /home/spark/regtests + +CMD ["./run.sh"] \ No newline at end of file diff --git a/regtests/README.md b/regtests/README.md new file mode 100644 index 0000000000..783501e16d --- /dev/null +++ b/regtests/README.md @@ -0,0 +1,134 @@ +# End-to-end regression tests + +## Run Tests With Docker Compose + +Tests can be run with docker-compose by executing + +```bash +docker compose up --build --exit-code-from regtest +``` + +This is the flow used in CI and should be done locally before pushing to github to ensure that no environmental +factors contribute to the outcome of the tests. + +## Run all tests + +Polaris REST server must be running on localhost:8181 before running tests. + +Running test harness will automatically run idempotent setup script. + +``` +./run.sh +``` + +## Run in VERBOSE mode with test stdout printing to console + +``` +VERBOSE=1 ./run.sh t_spark_sql/src/spark_sql_basic.sh +``` + +## Run with Cloud resources +Several tests require access to cloud resources, such as S3 or GCS. To run these tests, you must export the appropriate +environment variables prior to running the tests. Each cloud can be enabled independently. +Create a .env file that contains the following variables: + +``` +# AWS variables +AWS_TEST_ENABLED=true +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= +AWS_STORAGE_BUCKET= +AWS_ROLE_ARN= +AWS_TEST_BASE=s3:/// + +# GCP variables +GCS_TEST_ENABLED=true +GCS_TEST_BASE=gs:// +GOOGLE_APPLICATION_CREDENTIALS=/tmp/credentials/ + +# Azure variables +AZURE_TEST_ENABLED=true +AZURE_TENANT_ID= +AZURE_DFS_TEST_BASE=abfss://@.dfs.core.windows.net/ +AZURE_BLOB_TEST_BASE=abfss://@.blob.core.windows.net/ +``` +`GOOGLE_APPLICATION_CREDENTIALS` must be mounted to the container volumes. Copy your credentials file +into the `credentials` folder. Then specify the name of the file in your .env file - do not change the +path, as `/tmp/credentials` is the folder on the container where the credentials file will be mounted. + +## Setup without running tests + +Setup is idempotent. + +``` +./setup.sh +``` + +## Experiment with failed test + +``` +rm t_hello_world/ref/hello_world.sh.ref +./run.sh +``` + +``` +Tue Apr 23 06:32:23 UTC 2024: Running all tests +Tue Apr 23 06:32:23 UTC 2024: Starting test t_hello_world:hello_world.sh +Tue Apr 23 06:32:23 UTC 2024: Test run concluded for t_hello_world:hello_world.sh +Tue Apr 23 06:32:23 UTC 2024: Test FAILED: t_hello_world:hello_world.sh +Tue Apr 23 06:32:23 UTC 2024: To compare and fix diffs: /tmp/polaris-regtests/t_hello_world/hello_world.sh.fixdiffs.sh +Tue Apr 23 06:32:23 UTC 2024: Starting test t_spark_sql:spark_sql_basic.sh +Tue Apr 23 06:32:32 UTC 2024: Test run concluded for t_spark_sql:spark_sql_basic.sh +Tue Apr 23 06:32:32 UTC 2024: Test SUCCEEDED: t_spark_sql:spark_sql_basic.sh +``` + +Simply run the specified fixdiffs file to run `meld` and fix the ref file. + +``` +/tmp/polaris-regtests/t_hello_world/hello_world.sh.fixdiffs.sh +``` + +## Run a spark-sql interactive shell + +With in-memory standalong Polaris running: + +``` +./run_spark_sql.sh +``` + +## Python Tests + +Python tests are based on `pytest`. They rely on a python Polaris client, which is generated from the openapi spec. +The client can be generated using two commands: + +```bash +# generate the management api client +$ docker run --rm \ + -v ${PWD}:/local openapitools/openapi-generator-cli generate \ + -i /local/spec/polaris-management-service.yml \ + -g python \ + -o /local/regtests/client/python --additional-properties=packageName=polaris.management --additional-properties=apiNamePrefix=polaris + +# generate the iceberg rest client +$ docker run --rm \ + -v ${PWD}:/local openapitools/openapi-generator-cli generate \ + -i /local/spec/rest-catalog-open-api.yaml \ + -g python \ + -o /local/regtests/client/python --additional-properties=packageName=polaris.catalog --additional-properties=apiNameSuffix="" --additional-properties=apiNamePrefix=Iceberg +``` + +Tests rely on Python 3.8 or higher. `pyenv` can be used to install a current version and mapped to the local directory +by using + +```bash +pyenv install 3.8 +pyenv local 3.8 +``` + +Once you've done that, you can run `setup.sh` to generate a python virtual environment (installed at `~/polaris-venv`) +and download all of the test dependencies into it. From here, `run.sh` will be able to execute any pytest present. + +To debug, setup IntelliJ to point at your virtual environment to find your test dependencies +(see https://www.jetbrains.com/help/idea/configuring-python-sdk.html). Then run the test in your IDE. + +The above is handled automatically when running reg tests from the docker image. \ No newline at end of file diff --git a/regtests/client/python/.github/workflows/python.yml b/regtests/client/python/.github/workflows/python.yml new file mode 100644 index 0000000000..f5a230e07b --- /dev/null +++ b/regtests/client/python/.github/workflows/python.yml @@ -0,0 +1,38 @@ +# NOTE: This file is auto generated by OpenAPI Generator. +# URL: https://openapi-generator.tech +# +# ref: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python + +name: polaris.management Python package + +on: [push, pull_request] + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] + + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install flake8 pytest + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + if [ -f test-requirements.txt ]; then pip install -r test-requirements.txt; fi + - name: Lint with flake8 + run: | + # stop the build if there are Python syntax errors or undefined names + flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide + flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + - name: Test with pytest + run: | + pytest diff --git a/regtests/client/python/.gitignore b/regtests/client/python/.gitignore new file mode 100644 index 0000000000..43995bd42f --- /dev/null +++ b/regtests/client/python/.gitignore @@ -0,0 +1,66 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover +.hypothesis/ +venv/ +.venv/ +.python-version +.pytest_cache + +# Translations +*.mo +*.pot + +# Django stuff: +*.log + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +#Ipython Notebook +.ipynb_checkpoints diff --git a/regtests/client/python/.gitlab-ci.yml b/regtests/client/python/.gitlab-ci.yml new file mode 100644 index 0000000000..3a2ed010b8 --- /dev/null +++ b/regtests/client/python/.gitlab-ci.yml @@ -0,0 +1,31 @@ +# NOTE: This file is auto generated by OpenAPI Generator. +# URL: https://openapi-generator.tech +# +# ref: https://docs.gitlab.com/ee/ci/README.html +# ref: https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Python.gitlab-ci.yml + +stages: + - test + +.pytest: + stage: test + script: + - pip install -r requirements.txt + - pip install -r test-requirements.txt + - pytest --cov=polaris.catalog + +pytest-3.7: + extends: .pytest + image: python:3.7-alpine +pytest-3.8: + extends: .pytest + image: python:3.8-alpine +pytest-3.9: + extends: .pytest + image: python:3.9-alpine +pytest-3.10: + extends: .pytest + image: python:3.10-alpine +pytest-3.11: + extends: .pytest + image: python:3.11-alpine diff --git a/regtests/client/python/.openapi-generator-ignore b/regtests/client/python/.openapi-generator-ignore new file mode 100644 index 0000000000..7484ee590a --- /dev/null +++ b/regtests/client/python/.openapi-generator-ignore @@ -0,0 +1,23 @@ +# OpenAPI Generator Ignore +# Generated by openapi-generator https://github.com/openapitools/openapi-generator + +# Use this file to prevent files from being overwritten by the generator. +# The patterns follow closely to .gitignore or .dockerignore. + +# As an example, the C# client generator defines ApiClient.cs. +# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: +#ApiClient.cs + +# You can match any string of characters against a directory, file or extension with a single asterisk (*): +#foo/*/qux +# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux + +# You can recursively match patterns against a directory, file or extension with a double asterisk (**): +#foo/**/qux +# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux + +# You can also negate patterns with an exclamation (!). +# For example, you can ignore all files in a docs folder with the file extension .md: +#docs/*.md +# Then explicitly reverse the ignore rule for a single file: +#!docs/README.md diff --git a/regtests/client/python/.openapi-generator/FILES b/regtests/client/python/.openapi-generator/FILES new file mode 100644 index 0000000000..4bf280b4c6 --- /dev/null +++ b/regtests/client/python/.openapi-generator/FILES @@ -0,0 +1,249 @@ +.github/workflows/python.yml +.gitignore +.gitlab-ci.yml +.travis.yml +README.md +docs/AddPartitionSpecUpdate.md +docs/AddSchemaUpdate.md +docs/AddSnapshotUpdate.md +docs/AddSortOrderUpdate.md +docs/AddViewVersionUpdate.md +docs/AndOrExpression.md +docs/AssertCreate.md +docs/AssertCurrentSchemaId.md +docs/AssertDefaultSortOrderId.md +docs/AssertDefaultSpecId.md +docs/AssertLastAssignedFieldId.md +docs/AssertLastAssignedPartitionId.md +docs/AssertRefSnapshotId.md +docs/AssertTableUUID.md +docs/AssertViewUUID.md +docs/AssignUUIDUpdate.md +docs/BaseUpdate.md +docs/BlobMetadata.md +docs/CatalogConfig.md +docs/CommitReport.md +docs/CommitTableRequest.md +docs/CommitTableResponse.md +docs/CommitTransactionRequest.md +docs/CommitViewRequest.md +docs/ContentFile.md +docs/CountMap.md +docs/CounterResult.md +docs/CreateNamespaceRequest.md +docs/CreateNamespaceResponse.md +docs/CreateTableRequest.md +docs/CreateViewRequest.md +docs/DataFile.md +docs/EqualityDeleteFile.md +docs/ErrorModel.md +docs/Expression.md +docs/FileFormat.md +docs/GetNamespaceResponse.md +docs/IcebergCatalogAPI.md +docs/IcebergConfigurationAPI.md +docs/IcebergErrorResponse.md +docs/IcebergOAuth2API.md +docs/ListNamespacesResponse.md +docs/ListTablesResponse.md +docs/ListType.md +docs/LiteralExpression.md +docs/LoadTableResult.md +docs/LoadViewResult.md +docs/MapType.md +docs/MetadataLogInner.md +docs/MetricResult.md +docs/ModelSchema.md +docs/NotExpression.md +docs/NotificationRequest.md +docs/NotificationType.md +docs/NullOrder.md +docs/OAuthError.md +docs/OAuthTokenResponse.md +docs/PartitionField.md +docs/PartitionSpec.md +docs/PartitionStatisticsFile.md +docs/PositionDeleteFile.md +docs/PrimitiveTypeValue.md +docs/RegisterTableRequest.md +docs/RemovePartitionStatisticsUpdate.md +docs/RemovePropertiesUpdate.md +docs/RemoveSnapshotRefUpdate.md +docs/RemoveSnapshotsUpdate.md +docs/RemoveStatisticsUpdate.md +docs/RenameTableRequest.md +docs/ReportMetricsRequest.md +docs/SQLViewRepresentation.md +docs/ScanReport.md +docs/SetCurrentSchemaUpdate.md +docs/SetCurrentViewVersionUpdate.md +docs/SetDefaultSortOrderUpdate.md +docs/SetDefaultSpecUpdate.md +docs/SetExpression.md +docs/SetLocationUpdate.md +docs/SetPartitionStatisticsUpdate.md +docs/SetPropertiesUpdate.md +docs/SetSnapshotRefUpdate.md +docs/SetStatisticsUpdate.md +docs/Snapshot.md +docs/SnapshotLogInner.md +docs/SnapshotReference.md +docs/SnapshotSummary.md +docs/SortDirection.md +docs/SortField.md +docs/SortOrder.md +docs/StatisticsFile.md +docs/StructField.md +docs/StructType.md +docs/TableIdentifier.md +docs/TableMetadata.md +docs/TableRequirement.md +docs/TableUpdate.md +docs/TableUpdateNotification.md +docs/Term.md +docs/TimerResult.md +docs/TokenType.md +docs/TransformTerm.md +docs/Type.md +docs/UnaryExpression.md +docs/UpdateNamespacePropertiesRequest.md +docs/UpdateNamespacePropertiesResponse.md +docs/UpgradeFormatVersionUpdate.md +docs/ValueMap.md +docs/ViewHistoryEntry.md +docs/ViewMetadata.md +docs/ViewRepresentation.md +docs/ViewRequirement.md +docs/ViewUpdate.md +docs/ViewVersion.md +git_push.sh +polaris/__init__.py +polaris/catalog/__init__.py +polaris/catalog/api/__init__.py +polaris/catalog/api/iceberg_catalog_api.py +polaris/catalog/api/iceberg_configuration_api.py +polaris/catalog/api/iceberg_o_auth2_api.py +polaris/catalog/api_client.py +polaris/catalog/api_response.py +polaris/catalog/configuration.py +polaris/catalog/exceptions.py +polaris/catalog/models/__init__.py +polaris/catalog/models/add_partition_spec_update.py +polaris/catalog/models/add_schema_update.py +polaris/catalog/models/add_snapshot_update.py +polaris/catalog/models/add_sort_order_update.py +polaris/catalog/models/add_view_version_update.py +polaris/catalog/models/and_or_expression.py +polaris/catalog/models/assert_create.py +polaris/catalog/models/assert_current_schema_id.py +polaris/catalog/models/assert_default_sort_order_id.py +polaris/catalog/models/assert_default_spec_id.py +polaris/catalog/models/assert_last_assigned_field_id.py +polaris/catalog/models/assert_last_assigned_partition_id.py +polaris/catalog/models/assert_ref_snapshot_id.py +polaris/catalog/models/assert_table_uuid.py +polaris/catalog/models/assert_view_uuid.py +polaris/catalog/models/assign_uuid_update.py +polaris/catalog/models/base_update.py +polaris/catalog/models/blob_metadata.py +polaris/catalog/models/catalog_config.py +polaris/catalog/models/commit_report.py +polaris/catalog/models/commit_table_request.py +polaris/catalog/models/commit_table_response.py +polaris/catalog/models/commit_transaction_request.py +polaris/catalog/models/commit_view_request.py +polaris/catalog/models/content_file.py +polaris/catalog/models/count_map.py +polaris/catalog/models/counter_result.py +polaris/catalog/models/create_namespace_request.py +polaris/catalog/models/create_namespace_response.py +polaris/catalog/models/create_table_request.py +polaris/catalog/models/create_view_request.py +polaris/catalog/models/data_file.py +polaris/catalog/models/equality_delete_file.py +polaris/catalog/models/error_model.py +polaris/catalog/models/expression.py +polaris/catalog/models/file_format.py +polaris/catalog/models/get_namespace_response.py +polaris/catalog/models/iceberg_error_response.py +polaris/catalog/models/list_namespaces_response.py +polaris/catalog/models/list_tables_response.py +polaris/catalog/models/list_type.py +polaris/catalog/models/literal_expression.py +polaris/catalog/models/load_table_result.py +polaris/catalog/models/load_view_result.py +polaris/catalog/models/map_type.py +polaris/catalog/models/metadata_log_inner.py +polaris/catalog/models/metric_result.py +polaris/catalog/models/model_schema.py +polaris/catalog/models/not_expression.py +polaris/catalog/models/notification_request.py +polaris/catalog/models/notification_type.py +polaris/catalog/models/null_order.py +polaris/catalog/models/o_auth_error.py +polaris/catalog/models/o_auth_token_response.py +polaris/catalog/models/partition_field.py +polaris/catalog/models/partition_spec.py +polaris/catalog/models/partition_statistics_file.py +polaris/catalog/models/position_delete_file.py +polaris/catalog/models/primitive_type_value.py +polaris/catalog/models/register_table_request.py +polaris/catalog/models/remove_partition_statistics_update.py +polaris/catalog/models/remove_properties_update.py +polaris/catalog/models/remove_snapshot_ref_update.py +polaris/catalog/models/remove_snapshots_update.py +polaris/catalog/models/remove_statistics_update.py +polaris/catalog/models/rename_table_request.py +polaris/catalog/models/report_metrics_request.py +polaris/catalog/models/scan_report.py +polaris/catalog/models/set_current_schema_update.py +polaris/catalog/models/set_current_view_version_update.py +polaris/catalog/models/set_default_sort_order_update.py +polaris/catalog/models/set_default_spec_update.py +polaris/catalog/models/set_expression.py +polaris/catalog/models/set_location_update.py +polaris/catalog/models/set_partition_statistics_update.py +polaris/catalog/models/set_properties_update.py +polaris/catalog/models/set_snapshot_ref_update.py +polaris/catalog/models/set_statistics_update.py +polaris/catalog/models/snapshot.py +polaris/catalog/models/snapshot_log_inner.py +polaris/catalog/models/snapshot_reference.py +polaris/catalog/models/snapshot_summary.py +polaris/catalog/models/sort_direction.py +polaris/catalog/models/sort_field.py +polaris/catalog/models/sort_order.py +polaris/catalog/models/sql_view_representation.py +polaris/catalog/models/statistics_file.py +polaris/catalog/models/struct_field.py +polaris/catalog/models/struct_type.py +polaris/catalog/models/table_identifier.py +polaris/catalog/models/table_metadata.py +polaris/catalog/models/table_requirement.py +polaris/catalog/models/table_update.py +polaris/catalog/models/table_update_notification.py +polaris/catalog/models/term.py +polaris/catalog/models/timer_result.py +polaris/catalog/models/token_type.py +polaris/catalog/models/transform_term.py +polaris/catalog/models/type.py +polaris/catalog/models/unary_expression.py +polaris/catalog/models/update_namespace_properties_request.py +polaris/catalog/models/update_namespace_properties_response.py +polaris/catalog/models/upgrade_format_version_update.py +polaris/catalog/models/value_map.py +polaris/catalog/models/view_history_entry.py +polaris/catalog/models/view_metadata.py +polaris/catalog/models/view_representation.py +polaris/catalog/models/view_requirement.py +polaris/catalog/models/view_update.py +polaris/catalog/models/view_version.py +polaris/catalog/py.typed +polaris/catalog/rest.py +pyproject.toml +requirements.txt +setup.cfg +setup.py +test-requirements.txt +test/__init__.py +tox.ini diff --git a/regtests/client/python/.openapi-generator/VERSION b/regtests/client/python/.openapi-generator/VERSION new file mode 100644 index 0000000000..6116b14d2c --- /dev/null +++ b/regtests/client/python/.openapi-generator/VERSION @@ -0,0 +1 @@ +7.8.0-SNAPSHOT diff --git a/regtests/client/python/.travis.yml b/regtests/client/python/.travis.yml new file mode 100644 index 0000000000..dabde49c17 --- /dev/null +++ b/regtests/client/python/.travis.yml @@ -0,0 +1,17 @@ +# ref: https://docs.travis-ci.com/user/languages/python +language: python +python: + - "3.7" + - "3.8" + - "3.9" + - "3.10" + - "3.11" + # uncomment the following if needed + #- "3.11-dev" # 3.11 development branch + #- "nightly" # nightly build +# command to install dependencies +install: + - "pip install -r requirements.txt" + - "pip install -r test-requirements.txt" +# command to run tests +script: pytest --cov=polaris.catalog diff --git a/regtests/client/python/README.md b/regtests/client/python/README.md new file mode 100644 index 0000000000..553569f4d7 --- /dev/null +++ b/regtests/client/python/README.md @@ -0,0 +1,264 @@ +# polaris.catalog +Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + +This Python package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project: + +- API version: 0.0.1 +- Package version: 1.0.0 +- Generator version: 7.8.0-SNAPSHOT +- Build package: org.openapitools.codegen.languages.PythonClientCodegen + +## Requirements. + +Python 3.7+ + +## Installation & Usage +### pip install + +If the python package is hosted on a repository, you can install directly using: + +```sh +pip install git+https://github.com/GIT_USER_ID/GIT_REPO_ID.git +``` +(you may need to run `pip` with root permission: `sudo pip install git+https://github.com/GIT_USER_ID/GIT_REPO_ID.git`) + +Then import the package: +```python +import polaris.catalog +``` + +### Setuptools + +Install via [Setuptools](http://pypi.python.org/pypi/setuptools). + +```sh +python setup.py install --user +``` +(or `sudo python setup.py install` to install the package for all users) + +Then import the package: +```python +import polaris.catalog +``` + +### Tests + +Execute `pytest` to run the tests. + +## Getting Started + +Please follow the [installation procedure](#installation--usage) and then run the following: + +```python + +import polaris.catalog +from polaris.catalog.rest import ApiException +from pprint import pprint + +# Defining the host is optional and defaults to https://localhost +# See configuration.py for a list of all supported configuration parameters. +configuration = polaris.catalog.Configuration( + host = "https://localhost" +) + +# The client must configure the authentication and authorization parameters +# in accordance with the API server security policy. +# Examples for each auth method are provided below, use the example that +# satisfies your auth use case. + +configuration.access_token = os.environ["ACCESS_TOKEN"] + +# Configure Bearer authorization: BearerAuth +configuration = polaris.catalog.Configuration( + access_token = os.environ["BEARER_TOKEN"] +) + + +# Enter a context with an instance of the API client +with polaris.catalog.ApiClient(configuration) as api_client: + # Create an instance of the API class + api_instance = polaris.catalog.IcebergCatalogAPI(api_client) + prefix = 'prefix_example' # str | An optional prefix in the path + commit_transaction_request = polaris.catalog.CommitTransactionRequest() # CommitTransactionRequest | Commit updates to multiple tables in an atomic operation A commit for a single table consists of a table identifier with requirements and updates. Requirements are assertions that will be validated before attempting to make and commit changes. For example, `assert-ref-snapshot-id` will check that a named ref's snapshot ID has a certain value. Updates are changes to make to table metadata. For example, after asserting that the current main ref is at the expected snapshot, a commit may add a new child snapshot and set the ref to the new snapshot id. + + try: + # Commit updates to multiple tables in an atomic operation + api_instance.commit_transaction(prefix, commit_transaction_request) + except ApiException as e: + print("Exception when calling IcebergCatalogAPI->commit_transaction: %s\n" % e) + +``` + +## Documentation for API Endpoints + +All URIs are relative to *https://localhost* + +Class | Method | HTTP request | Description +------------ | ------------- | ------------- | ------------- +*IcebergCatalogAPI* | [**commit_transaction**](docs/IcebergCatalogAPI.md#commit_transaction) | **POST** /v1/{prefix}/transactions/commit | Commit updates to multiple tables in an atomic operation +*IcebergCatalogAPI* | [**create_namespace**](docs/IcebergCatalogAPI.md#create_namespace) | **POST** /v1/{prefix}/namespaces | Create a namespace +*IcebergCatalogAPI* | [**create_table**](docs/IcebergCatalogAPI.md#create_table) | **POST** /v1/{prefix}/namespaces/{namespace}/tables | Create a table in the given namespace +*IcebergCatalogAPI* | [**create_view**](docs/IcebergCatalogAPI.md#create_view) | **POST** /v1/{prefix}/namespaces/{namespace}/views | Create a view in the given namespace +*IcebergCatalogAPI* | [**drop_namespace**](docs/IcebergCatalogAPI.md#drop_namespace) | **DELETE** /v1/{prefix}/namespaces/{namespace} | Drop a namespace from the catalog. Namespace must be empty. +*IcebergCatalogAPI* | [**drop_table**](docs/IcebergCatalogAPI.md#drop_table) | **DELETE** /v1/{prefix}/namespaces/{namespace}/tables/{table} | Drop a table from the catalog +*IcebergCatalogAPI* | [**drop_view**](docs/IcebergCatalogAPI.md#drop_view) | **DELETE** /v1/{prefix}/namespaces/{namespace}/views/{view} | Drop a view from the catalog +*IcebergCatalogAPI* | [**list_namespaces**](docs/IcebergCatalogAPI.md#list_namespaces) | **GET** /v1/{prefix}/namespaces | List namespaces, optionally providing a parent namespace to list underneath +*IcebergCatalogAPI* | [**list_tables**](docs/IcebergCatalogAPI.md#list_tables) | **GET** /v1/{prefix}/namespaces/{namespace}/tables | List all table identifiers underneath a given namespace +*IcebergCatalogAPI* | [**list_views**](docs/IcebergCatalogAPI.md#list_views) | **GET** /v1/{prefix}/namespaces/{namespace}/views | List all view identifiers underneath a given namespace +*IcebergCatalogAPI* | [**load_namespace_metadata**](docs/IcebergCatalogAPI.md#load_namespace_metadata) | **GET** /v1/{prefix}/namespaces/{namespace} | Load the metadata properties for a namespace +*IcebergCatalogAPI* | [**load_table**](docs/IcebergCatalogAPI.md#load_table) | **GET** /v1/{prefix}/namespaces/{namespace}/tables/{table} | Load a table from the catalog +*IcebergCatalogAPI* | [**load_view**](docs/IcebergCatalogAPI.md#load_view) | **GET** /v1/{prefix}/namespaces/{namespace}/views/{view} | Load a view from the catalog +*IcebergCatalogAPI* | [**namespace_exists**](docs/IcebergCatalogAPI.md#namespace_exists) | **HEAD** /v1/{prefix}/namespaces/{namespace} | Check if a namespace exists +*IcebergCatalogAPI* | [**register_table**](docs/IcebergCatalogAPI.md#register_table) | **POST** /v1/{prefix}/namespaces/{namespace}/register | Register a table in the given namespace using given metadata file location +*IcebergCatalogAPI* | [**rename_table**](docs/IcebergCatalogAPI.md#rename_table) | **POST** /v1/{prefix}/tables/rename | Rename a table from its current name to a new name +*IcebergCatalogAPI* | [**rename_view**](docs/IcebergCatalogAPI.md#rename_view) | **POST** /v1/{prefix}/views/rename | Rename a view from its current name to a new name +*IcebergCatalogAPI* | [**replace_view**](docs/IcebergCatalogAPI.md#replace_view) | **POST** /v1/{prefix}/namespaces/{namespace}/views/{view} | Replace a view +*IcebergCatalogAPI* | [**report_metrics**](docs/IcebergCatalogAPI.md#report_metrics) | **POST** /v1/{prefix}/namespaces/{namespace}/tables/{table}/metrics | Send a metrics report to this endpoint to be processed by the backend +*IcebergCatalogAPI* | [**send_notification**](docs/IcebergCatalogAPI.md#send_notification) | **POST** /v1/{prefix}/namespaces/{namespace}/tables/{table}/notifications | Sends a notification to the table +*IcebergCatalogAPI* | [**table_exists**](docs/IcebergCatalogAPI.md#table_exists) | **HEAD** /v1/{prefix}/namespaces/{namespace}/tables/{table} | Check if a table exists +*IcebergCatalogAPI* | [**update_properties**](docs/IcebergCatalogAPI.md#update_properties) | **POST** /v1/{prefix}/namespaces/{namespace}/properties | Set or remove properties on a namespace +*IcebergCatalogAPI* | [**update_table**](docs/IcebergCatalogAPI.md#update_table) | **POST** /v1/{prefix}/namespaces/{namespace}/tables/{table} | Commit updates to a table +*IcebergCatalogAPI* | [**view_exists**](docs/IcebergCatalogAPI.md#view_exists) | **HEAD** /v1/{prefix}/namespaces/{namespace}/views/{view} | Check if a view exists +*IcebergConfigurationAPI* | [**get_config**](docs/IcebergConfigurationAPI.md#get_config) | **GET** /v1/config | List all catalog configuration settings +*IcebergOAuth2API* | [**get_token**](docs/IcebergOAuth2API.md#get_token) | **POST** /v1/oauth/tokens | Get a token using an OAuth2 flow + + +## Documentation For Models + + - [AddPartitionSpecUpdate](docs/AddPartitionSpecUpdate.md) + - [AddSchemaUpdate](docs/AddSchemaUpdate.md) + - [AddSnapshotUpdate](docs/AddSnapshotUpdate.md) + - [AddSortOrderUpdate](docs/AddSortOrderUpdate.md) + - [AddViewVersionUpdate](docs/AddViewVersionUpdate.md) + - [AndOrExpression](docs/AndOrExpression.md) + - [AssertCreate](docs/AssertCreate.md) + - [AssertCurrentSchemaId](docs/AssertCurrentSchemaId.md) + - [AssertDefaultSortOrderId](docs/AssertDefaultSortOrderId.md) + - [AssertDefaultSpecId](docs/AssertDefaultSpecId.md) + - [AssertLastAssignedFieldId](docs/AssertLastAssignedFieldId.md) + - [AssertLastAssignedPartitionId](docs/AssertLastAssignedPartitionId.md) + - [AssertRefSnapshotId](docs/AssertRefSnapshotId.md) + - [AssertTableUUID](docs/AssertTableUUID.md) + - [AssertViewUUID](docs/AssertViewUUID.md) + - [AssignUUIDUpdate](docs/AssignUUIDUpdate.md) + - [BaseUpdate](docs/BaseUpdate.md) + - [BlobMetadata](docs/BlobMetadata.md) + - [CatalogConfig](docs/CatalogConfig.md) + - [CommitReport](docs/CommitReport.md) + - [CommitTableRequest](docs/CommitTableRequest.md) + - [CommitTableResponse](docs/CommitTableResponse.md) + - [CommitTransactionRequest](docs/CommitTransactionRequest.md) + - [CommitViewRequest](docs/CommitViewRequest.md) + - [ContentFile](docs/ContentFile.md) + - [CountMap](docs/CountMap.md) + - [CounterResult](docs/CounterResult.md) + - [CreateNamespaceRequest](docs/CreateNamespaceRequest.md) + - [CreateNamespaceResponse](docs/CreateNamespaceResponse.md) + - [CreateTableRequest](docs/CreateTableRequest.md) + - [CreateViewRequest](docs/CreateViewRequest.md) + - [DataFile](docs/DataFile.md) + - [EqualityDeleteFile](docs/EqualityDeleteFile.md) + - [ErrorModel](docs/ErrorModel.md) + - [Expression](docs/Expression.md) + - [FileFormat](docs/FileFormat.md) + - [GetNamespaceResponse](docs/GetNamespaceResponse.md) + - [IcebergErrorResponse](docs/IcebergErrorResponse.md) + - [ListNamespacesResponse](docs/ListNamespacesResponse.md) + - [ListTablesResponse](docs/ListTablesResponse.md) + - [ListType](docs/ListType.md) + - [LiteralExpression](docs/LiteralExpression.md) + - [LoadTableResult](docs/LoadTableResult.md) + - [LoadViewResult](docs/LoadViewResult.md) + - [MapType](docs/MapType.md) + - [MetadataLogInner](docs/MetadataLogInner.md) + - [MetricResult](docs/MetricResult.md) + - [ModelSchema](docs/ModelSchema.md) + - [NotExpression](docs/NotExpression.md) + - [NotificationRequest](docs/NotificationRequest.md) + - [NotificationType](docs/NotificationType.md) + - [NullOrder](docs/NullOrder.md) + - [OAuthError](docs/OAuthError.md) + - [OAuthTokenResponse](docs/OAuthTokenResponse.md) + - [PartitionField](docs/PartitionField.md) + - [PartitionSpec](docs/PartitionSpec.md) + - [PartitionStatisticsFile](docs/PartitionStatisticsFile.md) + - [PositionDeleteFile](docs/PositionDeleteFile.md) + - [PrimitiveTypeValue](docs/PrimitiveTypeValue.md) + - [RegisterTableRequest](docs/RegisterTableRequest.md) + - [RemovePartitionStatisticsUpdate](docs/RemovePartitionStatisticsUpdate.md) + - [RemovePropertiesUpdate](docs/RemovePropertiesUpdate.md) + - [RemoveSnapshotRefUpdate](docs/RemoveSnapshotRefUpdate.md) + - [RemoveSnapshotsUpdate](docs/RemoveSnapshotsUpdate.md) + - [RemoveStatisticsUpdate](docs/RemoveStatisticsUpdate.md) + - [RenameTableRequest](docs/RenameTableRequest.md) + - [ReportMetricsRequest](docs/ReportMetricsRequest.md) + - [SQLViewRepresentation](docs/SQLViewRepresentation.md) + - [ScanReport](docs/ScanReport.md) + - [SetCurrentSchemaUpdate](docs/SetCurrentSchemaUpdate.md) + - [SetCurrentViewVersionUpdate](docs/SetCurrentViewVersionUpdate.md) + - [SetDefaultSortOrderUpdate](docs/SetDefaultSortOrderUpdate.md) + - [SetDefaultSpecUpdate](docs/SetDefaultSpecUpdate.md) + - [SetExpression](docs/SetExpression.md) + - [SetLocationUpdate](docs/SetLocationUpdate.md) + - [SetPartitionStatisticsUpdate](docs/SetPartitionStatisticsUpdate.md) + - [SetPropertiesUpdate](docs/SetPropertiesUpdate.md) + - [SetSnapshotRefUpdate](docs/SetSnapshotRefUpdate.md) + - [SetStatisticsUpdate](docs/SetStatisticsUpdate.md) + - [Snapshot](docs/Snapshot.md) + - [SnapshotLogInner](docs/SnapshotLogInner.md) + - [SnapshotReference](docs/SnapshotReference.md) + - [SnapshotSummary](docs/SnapshotSummary.md) + - [SortDirection](docs/SortDirection.md) + - [SortField](docs/SortField.md) + - [SortOrder](docs/SortOrder.md) + - [StatisticsFile](docs/StatisticsFile.md) + - [StructField](docs/StructField.md) + - [StructType](docs/StructType.md) + - [TableIdentifier](docs/TableIdentifier.md) + - [TableMetadata](docs/TableMetadata.md) + - [TableRequirement](docs/TableRequirement.md) + - [TableUpdate](docs/TableUpdate.md) + - [TableUpdateNotification](docs/TableUpdateNotification.md) + - [Term](docs/Term.md) + - [TimerResult](docs/TimerResult.md) + - [TokenType](docs/TokenType.md) + - [TransformTerm](docs/TransformTerm.md) + - [Type](docs/Type.md) + - [UnaryExpression](docs/UnaryExpression.md) + - [UpdateNamespacePropertiesRequest](docs/UpdateNamespacePropertiesRequest.md) + - [UpdateNamespacePropertiesResponse](docs/UpdateNamespacePropertiesResponse.md) + - [UpgradeFormatVersionUpdate](docs/UpgradeFormatVersionUpdate.md) + - [ValueMap](docs/ValueMap.md) + - [ViewHistoryEntry](docs/ViewHistoryEntry.md) + - [ViewMetadata](docs/ViewMetadata.md) + - [ViewRepresentation](docs/ViewRepresentation.md) + - [ViewRequirement](docs/ViewRequirement.md) + - [ViewUpdate](docs/ViewUpdate.md) + - [ViewVersion](docs/ViewVersion.md) + + + +## Documentation For Authorization + + +Authentication schemes defined for the API: + +### OAuth2 + +- **Type**: OAuth +- **Flow**: application +- **Authorization URL**: +- **Scopes**: + - **catalog**: Allows interacting with the Config and Catalog APIs + + +### BearerAuth + +- **Type**: Bearer authentication + + +## Author + + + + diff --git a/regtests/client/python/cli/__init__.py b/regtests/client/python/cli/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/regtests/client/python/cli/command/__init__.py b/regtests/client/python/cli/command/__init__.py new file mode 100644 index 0000000000..f9887a4623 --- /dev/null +++ b/regtests/client/python/cli/command/__init__.py @@ -0,0 +1,107 @@ +import argparse +from abc import ABC + +from cli.constants import Commands, Arguments +from cli.options.parser import Parser +from polaris.management import PolarisDefaultApi + + +class Command(ABC): + """ + An abstract base class for commands. Implementations are expected to override the class methods `validate` and + `execute`. The static method `Command.from_options` can be used to parse a argparse Namespace into the appropriate + Command implementation if one exists. + """ + + @staticmethod + def from_options(options: argparse.Namespace) -> 'Command': + + def options_get(key, f=lambda x: x): + return f(getattr(options, key)) if hasattr(options, key) else None + + properties = Parser.parse_properties(options_get(Arguments.PROPERTY)) + + command = None + if options.command == Commands.CATALOGS: + from cli.command.catalogs import CatalogsCommand + command = CatalogsCommand( + options_get(f'{Commands.CATALOGS}_subcommand'), + catalog_type=options_get(Arguments.TYPE), + remote_url=options_get(Arguments.REMOTE_URL), + default_base_location=options_get(Arguments.DEFAULT_BASE_LOCATION), + storage_type=options_get(Arguments.STORAGE_TYPE), + allowed_locations=options_get(Arguments.ALLOWED_LOCATION), + role_arn=options_get(Arguments.ROLE_ARN), + external_id=options_get(Arguments.EXTERNAL_ID), + user_arn=options_get(Arguments.USER_ARN), + tenant_id=options_get(Arguments.TENANT_ID), + multi_tenant_app_name=options_get(Arguments.MULTI_TENANT_APP_NAME), + consent_url=options_get(Arguments.CONSENT_URL), + service_account=options_get(Arguments.SERVICE_ACCOUNT), + catalog_name=options_get(Arguments.CATALOG), + properties={} if properties is None else properties + ) + elif options.command == Commands.PRINCIPALS: + from cli.command.principals import PrincipalsCommand + command = PrincipalsCommand( + options_get(f'{Commands.PRINCIPALS}_subcommand'), + type=options_get(Arguments.TYPE), + principal_name=options_get(Arguments.PRINCIPAL), + client_id=options_get(Arguments.CLIENT_ID), + principal_role=options_get(Arguments.PRINCIPAL_ROLE), + properties=properties + ) + elif options.command == Commands.PRINCIPAL_ROLES: + from cli.command.principal_roles import PrincipalRolesCommand + command = PrincipalRolesCommand( + options_get(f'{Commands.PRINCIPAL_ROLES}_subcommand'), + principal_role_name=options_get(Arguments.PRINCIPAL_ROLE), + principal_name=options_get(Arguments.PRINCIPAL), + catalog_name=options_get(Arguments.CATALOG), + catalog_role_name=options_get(Arguments.CATALOG_ROLE), + properties=properties + ) + elif options.command == Commands.CATALOG_ROLES: + from cli.command.catalog_roles import CatalogRolesCommand + command = CatalogRolesCommand( + options_get(f'{Commands.CATALOG_ROLES}_subcommand'), + catalog_name=options_get(Arguments.CATALOG), + catalog_role_name=options_get(Arguments.CATALOG_ROLE), + principal_role_name=options_get(Arguments.PRINCIPAL_ROLE), + properties=properties + ) + elif options.command == Commands.PRIVILEGES: + from cli.command.privileges import PrivilegesCommand + subcommand = options_get(f'{Commands.PRIVILEGES}_subcommand') + command = PrivilegesCommand( + subcommand, + action=options_get(f'{subcommand}_subcommand'), + catalog_name=options_get(Arguments.CATALOG), + catalog_role_name=options_get(Arguments.CATALOG_ROLE), + namespace=options_get(Arguments.NAMESPACE, lambda s: s.split('.')), + view=options_get(Arguments.VIEW), + table=options_get(Arguments.TABLE), + privilege=options_get(Arguments.PRIVILEGE), + cascade=options_get(Arguments.CASCADE) + ) + + if command is not None: + command.validate() + return command + else: + raise Exception("Please specify a command or run ./polaris --help to view the available commands") + + def execute(self, api: PolarisDefaultApi) -> None: + """ + Execute a given command and, where applicable, print the response as JSON. + """ + raise Exception("`execute` called on abstract `Command`") + + def validate(self) -> None: + """ + Used to validate a command. Should always be called before `execute`. The arg parser will catch many issues + with options, but this is used to apply additional constraints that the arg parser can't currently handle. + One example is that a catalog cannot be created with the `s3` storage type without a `--role-arn` option, but + one can be created without this flag if it's using the `gcs` storage type. + """ + raise Exception("`validate` called on abstract `Command`") diff --git a/regtests/client/python/cli/command/catalog_roles.py b/regtests/client/python/cli/command/catalog_roles.py new file mode 100644 index 0000000000..033dcf6982 --- /dev/null +++ b/regtests/client/python/cli/command/catalog_roles.py @@ -0,0 +1,78 @@ +from dataclasses import dataclass +from typing import Dict, Optional + +from pydantic import StrictStr + +from cli.command import Command +from cli.constants import Subcommands +from polaris.management import PolarisDefaultApi, CreateCatalogRoleRequest, CatalogRole, UpdateCatalogRoleRequest, \ + GrantCatalogRoleRequest + + +@dataclass +class CatalogRolesCommand(Command): + """ + A Command implementation to represent `polaris catalog-roles`. The instance attributes correspond to parameters + that can be provided to various subcommands, except `catalog_roles_subcommand` which represents the subcommand + itself. + + Example commands: + * ./polaris catalog-roles create --catalog bronze_catalog cat_role + * ./polaris catalog-roles list --catalog bronze_catalog --principal-role data-analyst + * ./polaris catalog-roles grant --catalog bronze_catalog --principal-role data-engineer etl_role + """ + + catalog_roles_subcommand: str + catalog_name: str + catalog_role_name: str + principal_role_name: str + properties: Optional[Dict[str, StrictStr]] + + def validate(self): + if not self.catalog_name: + raise Exception("Missing required argument: --catalog") + if self.catalog_roles_subcommand in {Subcommands.GRANT, Subcommands.REVOKE}: + if not self.principal_role_name: + raise Exception("Missing required argument: --principal") + + def execute(self, api: PolarisDefaultApi) -> None: + if self.catalog_roles_subcommand == Subcommands.CREATE: + request = CreateCatalogRoleRequest( + catalog_role=CatalogRole( + name=self.catalog_role_name, + properties=self.properties + ) + ) + api.create_catalog_role(self.catalog_name, request) + elif self.catalog_roles_subcommand == Subcommands.DELETE: + api.delete_catalog_role(self.catalog_name, self.catalog_role_name) + elif self.catalog_roles_subcommand == Subcommands.GET: + print(api.get_catalog_role(self.catalog_name, self.catalog_role_name).to_json()) + elif self.catalog_roles_subcommand == Subcommands.LIST: + if self.principal_role_name: + for catalog_role in api.list_catalog_roles_for_principal_role( + self.principal_role_name, self.catalog_name).roles: + print(catalog_role.to_json()) + else: + for catalog_role in api.list_catalog_roles(self.catalog_name).roles: + print(catalog_role.to_json()) + elif self.catalog_roles_subcommand == Subcommands.UPDATE: + catalog_role = api.get_catalog_role(self.catalog_name, self.catalog_role_name) + request = UpdateCatalogRoleRequest( + current_entity_version=catalog_role.entity_version, + properties=self.properties + ) + api.update_catalog_role(self.catalog_name, self.catalog_role_name, request) + elif self.catalog_roles_subcommand == Subcommands.GRANT: + request = GrantCatalogRoleRequest( + catalog_role=CatalogRole( + name=self.catalog_role_name + ), + properties=self.properties + ) + api.assign_catalog_role_to_principal_role(self.principal_role_name, self.catalog_name, request) + elif self.catalog_roles_subcommand == Subcommands.REVOKE: + api.revoke_catalog_role_from_principal_role( + self.principal_role_name, self.catalog_name, self.catalog_role_name) + else: + raise Exception(f"{self.catalog_roles_subcommand} is not supported in the CLI") diff --git a/regtests/client/python/cli/command/catalogs.py b/regtests/client/python/cli/command/catalogs.py new file mode 100644 index 0000000000..9ce81f5714 --- /dev/null +++ b/regtests/client/python/cli/command/catalogs.py @@ -0,0 +1,169 @@ +from dataclasses import dataclass, field +from typing import Dict, Optional, List + +from pydantic import StrictStr + +from cli.command import Command +from cli.constants import StorageType, CatalogType, Subcommands +from polaris.management import PolarisDefaultApi, Catalog, CreateCatalogRequest, UpdateCatalogRequest, \ + StorageConfigInfo, ExternalCatalog, AwsStorageConfigInfo, AzureStorageConfigInfo, GcpStorageConfigInfo, \ + PolarisCatalog, CatalogProperties + + +@dataclass +class CatalogsCommand(Command): + """ + A Command implementation to represent `polaris catalogs`. The instance attributes correspond to parameters + that can be provided to various subcommands, except `catalogs_subcommand` which represents the subcommand + itself. + + Example commands: + * ./polaris catalogs create cat_name --storage-type s3 --default-base-location s3://bucket/path --role-arn ... + * ./polaris catalogs update cat_name --default-base-location s3://new-bucket/new-location + * ./polaris catalogs list + """ + + catalogs_subcommand: str + catalog_type: str + remote_url: str + default_base_location: str + storage_type: str + allowed_locations: List[str] + role_arn: str + external_id: str + user_arn: str + tenant_id: str + multi_tenant_app_name: str + consent_url: str + service_account: str + catalog_name: str + properties: Dict[str, StrictStr] + + def validate(self): + if self.catalogs_subcommand == Subcommands.CREATE: + if not self.storage_type: + raise Exception(f"Missing required argument:" + f" --storage-type") + if not self.default_base_location: + raise Exception(f"Missing required argument:" + f" --default-base-location") + if self.catalog_type == CatalogType.EXTERNAL.value: + if not self.remote_url: + raise Exception(f"Missing required argument for {CatalogType.EXTERNAL.value} catalog:" + f" --remote-url") + if self.catalogs_subcommand == Subcommands.UPDATE: + if self.allowed_locations: + if not self.storage_type: + raise Exception(f"Missing required argument when updating allowed locations for a catalog:" + f" --storage-type") + + if self.storage_type == StorageType.S3.value: + if not self.role_arn: + raise Exception("Missing required argument for storage type 's3': --role-arn") + if self._has_azure_storage_info() or self._has_gcs_storage_info(): + raise Exception("Storage type 's3' supports the storage configurations --role-arn, " + "--external-id, and --user-arn") + elif self.storage_type == StorageType.AZURE.value: + if not self.tenant_id: + raise Exception("Missing required argument for storage type 'azure': --tenant-id") + if self._has_aws_storage_info() or self._has_gcs_storage_info(): + raise Exception("Storage type 'azure' supports the storage configurations --tenant-id, " + "--multi-tenant-app-name, and --consent-url") + elif self._has_aws_storage_info() or self._has_azure_storage_info(): + raise Exception("Storage type 'gcs' supports the storage configuration: --service-account") + + def _has_aws_storage_info(self): + return self.role_arn or self.external_id or self.user_arn + + def _has_azure_storage_info(self): + return self.tenant_id or self.multi_tenant_app_name or self.consent_url + + def _has_gcs_storage_info(self): + return self.service_account + + def _build_storage_config_info(self): + config = None + if self.storage_type == StorageType.S3.value: + config = AwsStorageConfigInfo( + storage_type=self.storage_type.upper(), + allowed_locations=self.allowed_locations, + role_arn=self.role_arn, + external_id=self.external_id, + user_arn=self.user_arn + ) + elif self.storage_type == StorageType.AZURE.value: + config = AzureStorageConfigInfo( + storage_type=self.storage_type.upper(), + allowed_locations=self.allowed_locations, + tenant_id=self.tenant_id, + multi_tenant_app_name=self.multi_tenant_app_name, + consent_url=self.consent_url, + ) + elif self.storage_type == StorageType.GCS.value: + config = GcpStorageConfigInfo( + storage_type=self.storage_type.upper(), + allowed_locations=self.allowed_locations, + tenant_id=self.tenant_id, + multi_tenant_app_name=self.multi_tenant_app_name + ) + return config + + def execute(self, api: PolarisDefaultApi) -> None: + if self.catalogs_subcommand == Subcommands.CREATE: + config = self._build_storage_config_info() + if self.catalog_type == CatalogType.EXTERNAL.value: + request = CreateCatalogRequest( + catalog=ExternalCatalog( + type=self.catalog_type.upper(), + name=self.catalog_name, + storage_config_info=config, + remote_url=self.remote_url, + properties=CatalogProperties( + default_base_location=self.default_base_location, + additional_properties=self.properties + ) + ) + ) + else: + request = CreateCatalogRequest( + catalog=PolarisCatalog( + type=self.catalog_type.upper(), + name=self.catalog_name, + storage_config_info=config, + properties=CatalogProperties( + default_base_location=self.default_base_location, + additional_properties=self.properties + ) + ) + ) + api.create_catalog(request) + elif self.catalogs_subcommand == Subcommands.DELETE: + api.delete_catalog(self.catalog_name) + elif self.catalogs_subcommand == Subcommands.GET: + print(api.get_catalog(self.catalog_name).to_json()) + elif self.catalogs_subcommand == Subcommands.LIST: + for catalog in api.list_catalogs().catalogs: + print(catalog.to_json()) + elif self.catalogs_subcommand == Subcommands.UPDATE: + catalog = api.get_catalog(self.catalog_name) + default_base_location_properties = {} + if self.default_base_location: + default_base_location_properties = {'default-base-location': self.default_base_location} + catalog.properties = {**default_base_location_properties, **self.properties} + + request = UpdateCatalogRequest( + current_entity_version=catalog.entity_version, + catalog=catalog + ) + if (self.allowed_locations or self._has_aws_storage_info() or self._has_azure_storage_info() or + self._has_gcs_storage_info()): + request = UpdateCatalogRequest( + current_entity_version=catalog.entity_version, + catalog=catalog, + storage_config_info=self._build_storage_config_info() + ) + + api.update_catalog(self.catalog_name, request) + else: + raise Exception(f"{self.catalogs_subcommand} is not supported in the CLI") + diff --git a/regtests/client/python/cli/command/principal_roles.py b/regtests/client/python/cli/command/principal_roles.py new file mode 100644 index 0000000000..4c3e27e5c6 --- /dev/null +++ b/regtests/client/python/cli/command/principal_roles.py @@ -0,0 +1,79 @@ +from dataclasses import dataclass +from typing import Dict, Optional + +from pydantic import StrictStr + +from cli.command import Command +from cli.constants import Subcommands +from polaris.management import PolarisDefaultApi, CreatePrincipalRoleRequest, PrincipalRole, UpdatePrincipalRoleRequest, \ + GrantCatalogRoleRequest, CatalogRole, GrantPrincipalRoleRequest + + +@dataclass +class PrincipalRolesCommand(Command): + """ + A Command implementation to represent `polaris principal-roles`. The instance attributes correspond to parameters + that can be provided to various subcommands, except `principal_roles_subcommand` which represents the subcommand + itself. + + Example commands: + * ./polaris principal-roles create user_role + * ./polaris principal-roles list --principal user + """ + + principal_roles_subcommand: str + principal_role_name: str + principal_name: str + catalog_name: str + catalog_role_name: str + properties: Optional[Dict[str, StrictStr]] + + def validate(self): + if self.principal_roles_subcommand == Subcommands.LIST: + if self.principal_name and self.catalog_role_name: + raise Exception('You may provide either --principal or --catalog-role, but not both') + if self.principal_roles_subcommand in {Subcommands.GRANT, Subcommands.REVOKE}: + if not self.principal_name: + raise Exception(f"Missing required argument for {self.principal_roles_subcommand}: --principal") + + def execute(self, api: PolarisDefaultApi) -> None: + if self.principal_roles_subcommand == Subcommands.CREATE: + request = CreatePrincipalRoleRequest( + principal_role=PrincipalRole( + name=self.principal_role_name, + properties=self.properties + ) + ) + api.create_principal_role(request) + elif self.principal_roles_subcommand == Subcommands.DELETE: + api.delete_principal_role(self.principal_role_name) + elif self.principal_roles_subcommand == Subcommands.GET: + print(api.get_principal_role(self.principal_role_name).to_json()) + elif self.principal_roles_subcommand == Subcommands.LIST: + if self.catalog_role_name: + for principal_role in api.list_principal_roles(self.catalog_role_name).roles: + print(principal_role.to_json()) + elif self.principal_name: + for principal_role in api.list_principal_roles_assigned(self.principal_name).roles: + print(principal_role.to_json()) + else: + for principal_role in api.list_principal_roles().roles: + print(principal_role.to_json()) + elif self.principal_roles_subcommand == Subcommands.UPDATE: + principal_role = api.get_principal_role(self.principal_role_name) + request = UpdatePrincipalRoleRequest( + current_entity_version=principal_role.entity_version, + properties=self.properties + ) + api.update_principal_role(self.principal_role_name, request) + elif self.principal_roles_subcommand == Subcommands.GRANT: + request = GrantPrincipalRoleRequest( + principal_role=PrincipalRole( + name=self.principal_role_name + ), + ) + api.assign_principal_role(self.principal_name, request) + elif self.principal_roles_subcommand == Subcommands.REVOKE: + api.revoke_principal_role(self.principal_name, self.principal_role_name) + else: + raise Exception(f"{self.principal_roles_subcommand} is not supported in the CLI") diff --git a/regtests/client/python/cli/command/principals.py b/regtests/client/python/cli/command/principals.py new file mode 100644 index 0000000000..25f8196de3 --- /dev/null +++ b/regtests/client/python/cli/command/principals.py @@ -0,0 +1,67 @@ +from dataclasses import dataclass +from typing import Dict, Optional + +from pydantic import StrictStr + +from cli.command import Command +from cli.constants import Subcommands +from polaris.management import PolarisDefaultApi, CreatePrincipalRequest, Principal, UpdatePrincipalRequest, \ + GrantPrincipalRoleRequest, PrincipalRole + + +@dataclass +class PrincipalsCommand(Command): + """ + A Command implementation to represent `polaris principals`. The instance attributes correspond to parameters + that can be provided to various subcommands, except `principals_subcommand` which represents the subcommand + itself. + + Example commands: + * ./polaris principals create user + * ./polaris principals list + * ./polaris principals list --principal-role filter-to-this-role + """ + + principals_subcommand: str + type: str + principal_name: str + client_id: str + principal_role: str + properties: Optional[Dict[str, StrictStr]] + + def validate(self): + pass + + def execute(self, api: PolarisDefaultApi) -> None: + if self.principals_subcommand == Subcommands.CREATE: + request = CreatePrincipalRequest( + principal=Principal( + type=self.type.upper(), + name=self.principal_name, + client_id=self.client_id, + properties=self.properties + ) + ) + print(api.create_principal(request).credentials.to_json()) + elif self.principals_subcommand == Subcommands.DELETE: + api.delete_principal(self.principal_name) + elif self.principals_subcommand == Subcommands.GET: + print(api.get_principal(self.principal_name).to_json()) + elif self.principals_subcommand == Subcommands.LIST: + if self.principal_role: + for principal in api.list_assignee_principals_for_principal_role(self.principal_role).principals: + print(principal.to_json()) + else: + for principal in api.list_principals().principals: + print(principal.to_json()) + elif self.principals_subcommand == Subcommands.ROTATE_CREDENTIALS: + print(api.rotate_credentials(self.principal_name).to_json()) + elif self.principals_subcommand == Subcommands.UPDATE: + principal = api.get_principal(self.principal_name) + request = UpdatePrincipalRequest( + current_entity_version=principal.current_entity_version, + properties=self.properties + ) + api.update_principal(self.principal_name, request) + else: + raise Exception(f"{self.principals_subcommand} is not supported in the CLI") diff --git a/regtests/client/python/cli/command/privileges.py b/regtests/client/python/cli/command/privileges.py new file mode 100644 index 0000000000..dbd3f0d264 --- /dev/null +++ b/regtests/client/python/cli/command/privileges.py @@ -0,0 +1,107 @@ +from dataclasses import dataclass +from typing import List + +from pydantic import StrictStr + +from cli.command import Command +from cli.constants import Subcommands, Actions +from polaris.management import PolarisDefaultApi, AddGrantRequest, NamespaceGrant, \ + RevokeGrantRequest, CatalogGrant, TableGrant, ViewGrant, CatalogPrivilege, NamespacePrivilege, TablePrivilege, \ + ViewPrivilege + + +@dataclass +class PrivilegesCommand(Command): + """ + A Command implementation to represent `polaris privileges`. Unlike other commands, `privileges` itself takes two + parameters -- catalog_name and catalog_role_name. The other instance attributes, besides `privileges_subcommand` and + `action`, represent parameters provided to either the `grant` or `revoke` action. + + Example commands: + * ./polaris privileges --catalog c --catalog-role cr table grant --namespace n --table t PRIVILEGE_NAME + * ./polaris privileges --catalog c --catalog-role cr namespace revoke --namespace n PRIVILEGE_NAME + * ./polaris privileges -catalog c --catalog-role cr list + """ + + privileges_subcommand: str + action: str + catalog_name: str + catalog_role_name: str + namespace: List[StrictStr] + view: str + table: str + privilege: str + cascade: bool + + def validate(self): + if not self.catalog_name: + raise Exception('Missing required argument: --catalog') + if not self.catalog_role_name: + raise Exception('Missing required argument: --catalog-role') + + if (self.privileges_subcommand in {Subcommands.NAMESPACE, Subcommands.TABLE, Subcommands.VIEW} + and not self.namespace): + raise Exception('Missing required argument: --namespace') + + if self.action == Actions.GRANT and self.cascade: + raise Exception('Unrecognized argument for GRANT: --cascade') + + if self.privileges_subcommand == Subcommands.CATALOG: + if self.privilege not in {i.value for i in CatalogPrivilege}: + raise Exception(f'Invalid catalog privilege: {self.privilege}') + if self.privileges_subcommand == Subcommands.NAMESPACE: + if self.privilege not in {i.value for i in NamespacePrivilege}: + raise Exception(f'Invalid namespace privilege: {self.privilege}') + if self.privileges_subcommand == Subcommands.TABLE: + if self.privilege not in {i.value for i in TablePrivilege}: + raise Exception(f'Invalid table privilege: {self.privilege}') + if self.privileges_subcommand == Subcommands.VIEW: + if self.privilege not in {i.value for i in ViewPrivilege}: + raise Exception(f'Invalid view privilege: {self.privilege}') + + def execute(self, api: PolarisDefaultApi) -> None: + if self.privileges_subcommand == Subcommands.LIST: + for grant in api.list_grants_for_catalog_role(self.catalog_name, self.catalog_role_name).grants: + print(grant.to_json()) + else: + grant = None + if self.privileges_subcommand == Subcommands.CATALOG: + grant = CatalogGrant( + type=Subcommands.CATALOG, + privilege=CatalogPrivilege(self.privilege) + ) + elif self.privileges_subcommand == Subcommands.NAMESPACE: + grant = NamespaceGrant( + type=Subcommands.NAMESPACE, + namespace=self.namespace, + privilege=NamespacePrivilege(self.privilege) + ) + elif self.privileges_subcommand == Subcommands.TABLE: + grant = TableGrant( + type=Subcommands.TABLE, + namespace=self.namespace, + table_name=self.table, + privilege=TablePrivilege(self.privilege) + ) + elif self.privileges_subcommand == Subcommands.VIEW: + grant = ViewGrant( + type=Subcommands.VIEW, + namespace=self.namespace, + view_name=self.view, + privilege=ViewPrivilege(self.privilege) + ) + + if not grant: + raise Exception(f'{self.privileges_subcommand} is not supported in the CLI') + elif self.action == Actions.GRANT: + request = AddGrantRequest( + grant=grant + ) + api.add_grant_to_catalog_role(self.catalog_name, self.catalog_role_name, request) + elif self.action == Actions.REVOKE: + request = RevokeGrantRequest( + grant=grant + ) + api.revoke_grant_from_catalog_role(self.catalog_name, self.catalog_role_name, self.cascade, request) + else: + raise Exception(f'{self.action} is not supported in the CLI') diff --git a/regtests/client/python/cli/constants.py b/regtests/client/python/cli/constants.py new file mode 100644 index 0000000000..310c8bcdfa --- /dev/null +++ b/regtests/client/python/cli/constants.py @@ -0,0 +1,185 @@ +from enum import Enum + + +class StorageType(Enum): + """ + Represents a Storage Type within the Polaris API -- `s3`, `azure`, or `gcs`. + """ + + S3 = 's3' + AZURE = 'azure' + GCS = 'gcs' + + +class CatalogType(Enum): + """ + Represents a Catalog Type within the Polaris API -- `internal` or `external` + """ + + INTERNAL = 'internal' + EXTERNAL = 'external' + + +class PrincipalType(Enum): + """ + Represents a Principal Type within the Polaris API -- currently only `service` + """ + + SERVICE = 'service' + + +class Commands: + """ + Represents the various commands available in the CLI + """ + + CATALOGS = 'catalogs' + PRINCIPALS = 'principals' + PRINCIPAL_ROLES = 'principal-roles' + CATALOG_ROLES = 'catalog-roles' + PRIVILEGES = 'privileges' + + +class Subcommands: + """ + Represents the various subcommands available in the CLI. This is a flattened view, and no one command supports + all these subcommands. + """ + + CREATE = 'create' + DELETE = 'delete' + GET = 'get' + LIST = 'list' + UPDATE = 'update' + ROTATE_CREDENTIALS = 'rotate-credentials' + CATALOG = 'catalog' + NAMESPACE = 'namespace' + TABLE = 'table' + VIEW = 'view' + GRANT = 'grant' + REVOKE = 'revoke' + + +class Actions: + """ + Represents actions available to different subcommands available in the CLI. Currently, only some subcommands of the + `privileges` command support actions. + """ + + GRANT = 'grant' + REVOKE = 'revoke' + + +class Arguments: + """ + Constants to represent different arguments used by various commands. This is a flattened view, and no one + subcommand supports all these arguments. These argument names map directly to the parameters that the CLI expects + and to the attribute names within the argparse Namespace generated by parsing. + + These values should be snake_case, but they will get mapped to kebab-case in `Parser.parse` + """ + + TYPE = 'type' + REMOTE_URL = 'remote_url' + DEFAULT_BASE_LOCATION = 'default_base_location' + STORAGE_TYPE = 'storage_type' + ALLOWED_LOCATION = 'allowed_location' + ROLE_ARN = 'role_arn' + EXTERNAL_ID = 'external_id' + USER_ARN = 'user_arn' + TENANT_ID = 'tenant_id' + MULTI_TENANT_APP_NAME = 'multi_tenant_app_name' + CONSENT_URL = 'consent_url' + SERVICE_ACCOUNT = 'service_account' + CATALOG_ROLE = 'catalog_role' + CATALOG = 'catalog' + PRINCIPAL = 'principal' + CLIENT_ID = 'client_id' + PRINCIPAL_ROLE = 'principal_role' + PROPERTY = 'property' + PRIVILEGE = 'privilege' + NAMESPACE = 'namespace' + TABLE = 'table' + VIEW = 'view' + CASCADE = 'cascade' + + +class Hints: + """ + Constants used as hints by the various --help outputs. These are arranged within subclasses for readability, but + there is no strict mapping between these subclasses and commands. For example, the hint for the `--catalog` + parameter used by `catalog-roles create` and `catalog-roles delete` may be the same. + """ + + PROPERTY = ('A key/value pair such as: tag=value. Multiple can be provided by specifying this option' + ' more than once') + + class Catalogs: + GRANT = 'Grant a catalog role to a catalog' + REVOKE = 'Revoke a catalog role from a catalog' + + class Create: + TYPE = 'The type of catalog to create in [INTERNAL, EXTERNAL]. INTERNAL by default.' + REMOTE_URL = '(Only for external catalogs) The remote URL to use' + DEFAULT_BASE_LOCATION = '(Required for internal catalogs) Default base location of the catalog' + STORAGE_TYPE = '(Required for internal catalogs) The type of storage to use for the catalog' + ALLOWED_LOCATION = ('(For internal catalogs) An allowed location for files tracked by the catalog. ' + 'Multiple locations can be provided by specifying this option more than once.') + + ROLE_ARN = '(Required for AWS) A role ARN to use when connecting to S3' + EXTERNAL_ID = '(Only for AWS) The external Id to use when connecting to S3' + USER_ARN = '(Only for AWS) A user ARN to use when connecting to S3' + + TENANT_ID = '(Required for Azure) A tenant ID to use when connecting to Azure Storage' + MULTI_TENANT_APP_NAME = '(Only for Azure) The app name to use when connecting to Azure Storage' + CONSENT_URL = '(Only for Azure) A consent URL granting permissions for the Azure Storage location' + + SERVICE_ACCOUNT = '(Only for GCP) The service account to use when connecting to GCS' + + class Principals: + class Create: + NAME = 'The principal name' + CLIENT_ID = 'The output-only OAuth clientId associated with this principal if applicable' + + class Revoke: + PRINCIPAL_ROLE = 'A principal role to revoke from this principal' + + class PrincipalRoles: + PRINCIPAL_ROLE = 'The name of a principal role' + LIST = 'List principal roles, optionally limited to those held a given principal' + + GRANT = 'Grant a principal role to a principal' + REVOKE = 'Revoke a principal role from a principal' + + class Grant: + PRINCIPAL = 'A principal to grant this principal role to' + + class Revoke: + PRINCIPAL = 'A principal to revoke this principal role from' + + class List: + CATALOG_ROLE = ('The name of a catalog role. If provided, show only principal roles assigned to this' + ' catalog role.') + PRINCIPAL_NAME = ('The name of a principal. If provided, show only principal roles assigned to this' + ' principal.') + + class CatalogRoles: + CATALOG_NAME = 'The name of a catalog' + CATALOG_ROLE = 'The name of a catalog role' + LIST = 'List catalog roles within a catalog. Optionally, specify a principal role.' + REVOKE_CATALOG_ROLE = 'Revoke a catalog role from a principal role' + GRANT_CATALOG_ROLE = 'Grant a catalog role to a principal role' + + class Create: + CATALOG_NAME = 'The name of an existing catalog' + + class Grant: + CATALOG_NAME = 'The name of a catalog' + CATALOG_ROLE = 'The name of a catalog role' + PRIVILEGE = 'The privilege to grant or revoke' + NAMESPACE = 'A period-delimited namespace' + TABLE = 'The name of a table' + VIEW = 'The name of a view' + ADD = 'Add a grant. Either this or --revoke must be specified except when the subcommand is `list`' + REVOKE = 'Revoke a grant. Either this or --add must be specified except when the subcommand is `list`' + CASCADE = 'When revoking privileges, additionally revoke privileges that depend on the specified privilege' diff --git a/regtests/client/python/cli/options/__init__.py b/regtests/client/python/cli/options/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/regtests/client/python/cli/options/option_tree.py b/regtests/client/python/cli/options/option_tree.py new file mode 100644 index 0000000000..c9b9efdd35 --- /dev/null +++ b/regtests/client/python/cli/options/option_tree.py @@ -0,0 +1,189 @@ +from dataclasses import dataclass, field +from typing import List + +from cli.constants import StorageType, CatalogType, PrincipalType, Hints, Commands, Arguments, Subcommands, Actions + + +@dataclass +class Argument: + """ + A data class for representing a single argument within the CLI, such as `--host`. + """ + + name: str + type: type + hint: str + choices: List[str] = None + lower: bool = False + allow_repeats: bool = False + default: object = None + flag_name = None + + def __post_init__(self): + if self.name.startswith('--'): + raise Exception(f'Argument name {self.name} starts with `--`: should this be a flag_name?') + + def get_flag_name(self): + return self.flag_name or ('--' + self.name.replace('_', '-')) + + +@dataclass +class Option: + """ + A data class that represents a subcommand within the CLI, such as `catalogs`. Each Option can have child Options, + a collection of Arguments, or both. + """ + + name: str + hint: str = None + input_name: str = None + args: List[Argument] = field(default_factory=list) + children: List['Option'] = field(default_factory=list) + + +class OptionTree: + """ + `OptionTree.get_tree()` returns the full set of Options supported by the CLI. This structure is used to simplify + configuration of the CLI and to generate a custom `--help` message including nested commands. + """ + + _STORAGE_CONFIG_INFO = [ + Argument(Arguments.STORAGE_TYPE, str, Hints.Catalogs.Create.STORAGE_TYPE, lower=True, + choices=[st.value for st in StorageType]), + Argument(Arguments.ALLOWED_LOCATION, str, Hints.Catalogs.Create.ALLOWED_LOCATION, allow_repeats=True), + Argument(Arguments.ROLE_ARN, str, Hints.Catalogs.Create.ROLE_ARN), + Argument(Arguments.EXTERNAL_ID, str, Hints.Catalogs.Create.EXTERNAL_ID), + Argument(Arguments.USER_ARN, str, Hints.Catalogs.Create.USER_ARN), + Argument(Arguments.TENANT_ID, str, Hints.Catalogs.Create.TENANT_ID), + Argument(Arguments.MULTI_TENANT_APP_NAME, str, Hints.Catalogs.Create.MULTI_TENANT_APP_NAME), + Argument(Arguments.CONSENT_URL, str, Hints.Catalogs.Create.CONSENT_URL), + Argument(Arguments.SERVICE_ACCOUNT, str, Hints.Catalogs.Create.SERVICE_ACCOUNT), + ] + + @staticmethod + def get_tree() -> List[Option]: + return [ + Option(Commands.CATALOGS, 'manage catalogs', children=[ + Option(Subcommands.CREATE, args=[ + Argument(Arguments.TYPE, str, Hints.Catalogs.Create.TYPE, lower=True, + choices=[ct.value for ct in CatalogType], default=CatalogType.INTERNAL.value), + Argument(Arguments.REMOTE_URL, str, Hints.Catalogs.Create.REMOTE_URL), + Argument(Arguments.DEFAULT_BASE_LOCATION, str, Hints.Catalogs.Create.DEFAULT_BASE_LOCATION), + Argument(Arguments.PROPERTY, str, Hints.PROPERTY, allow_repeats=True), + ] + OptionTree._STORAGE_CONFIG_INFO, input_name=Arguments.CATALOG), + Option(Subcommands.DELETE, input_name=Arguments.CATALOG), + Option(Subcommands.GET, input_name=Arguments.CATALOG), + Option(Subcommands.LIST, args=[ + Argument(Arguments.PRINCIPAL_ROLE, str, Hints.PrincipalRoles.PRINCIPAL_ROLE) + ]), + Option(Subcommands.UPDATE, args=[ + Argument(Arguments.PROPERTY, str, Hints.PROPERTY, allow_repeats=True), + Argument(Arguments.DEFAULT_BASE_LOCATION, str, Hints.Catalogs.Create.DEFAULT_BASE_LOCATION), + ] + OptionTree._STORAGE_CONFIG_INFO, input_name=Arguments.CATALOG) + ]), + Option(Commands.PRINCIPALS, 'manage principals', children=[ + Option(Subcommands.CREATE, args=[ + Argument(Arguments.TYPE, str, Hints.Catalogs.Create.TYPE, lower=True, + choices=[pt.value for pt in PrincipalType], default=PrincipalType.SERVICE.value), + Argument(Arguments.CLIENT_ID, str, Hints.Principals.Create.CLIENT_ID), + Argument(Arguments.PROPERTY, str, Hints.PROPERTY, allow_repeats=True) + ], input_name=Arguments.PRINCIPAL), + Option(Subcommands.DELETE, input_name=Arguments.PRINCIPAL), + Option(Subcommands.GET, input_name=Arguments.PRINCIPAL), + Option(Subcommands.LIST), + Option(Subcommands.ROTATE_CREDENTIALS, input_name=Arguments.PRINCIPAL), + Option(Subcommands.UPDATE, args=[ + Argument(Arguments.PROPERTY, str, Hints.PROPERTY, allow_repeats=True) + ], input_name=Arguments.PRINCIPAL) + ]), + Option(Commands.PRINCIPAL_ROLES, 'manage principal roles', children=[ + Option(Subcommands.CREATE, args=[ + Argument(Arguments.PROPERTY, str, Hints.PROPERTY, allow_repeats=True) + ], input_name=Arguments.PRINCIPAL_ROLE), + Option(Subcommands.DELETE, input_name=Arguments.PRINCIPAL_ROLE), + Option(Subcommands.GET, input_name=Arguments.PRINCIPAL_ROLE), + Option(Subcommands.LIST, hint=Hints.PrincipalRoles.LIST, args=[ + Argument(Arguments.CATALOG_ROLE, str, Hints.PrincipalRoles.List.CATALOG_ROLE), + Argument(Arguments.PRINCIPAL, str, Hints.PrincipalRoles.List.PRINCIPAL_NAME) + ]), + Option(Subcommands.UPDATE, args=[ + Argument(Arguments.PROPERTY, str, Hints.PROPERTY, allow_repeats=True) + ], input_name=Arguments.PRINCIPAL_ROLE), + Option(Subcommands.GRANT, hint=Hints.PrincipalRoles.GRANT, args=[ + Argument(Arguments.PRINCIPAL, str, Hints.PrincipalRoles.Grant.PRINCIPAL) + ], input_name=Arguments.PRINCIPAL_ROLE), + Option(Subcommands.REVOKE, hint=Hints.PrincipalRoles.REVOKE, args=[ + Argument(Arguments.PRINCIPAL, str, Hints.PrincipalRoles.Revoke.PRINCIPAL) + ], input_name=Arguments.PRINCIPAL_ROLE) + ]), + Option(Commands.CATALOG_ROLES, 'manage catalog roles', children=[ + Option(Subcommands.CREATE, args=[ + Argument(Arguments.CATALOG, str, Hints.CatalogRoles.Create.CATALOG_NAME), + Argument(Arguments.PROPERTY, str, Hints.PROPERTY, allow_repeats=True) + ], input_name=Arguments.CATALOG_ROLE), + Option(Subcommands.DELETE, args=[ + Argument(Arguments.CATALOG, str, Hints.CatalogRoles.Create.CATALOG_NAME), + ], input_name=Arguments.CATALOG_ROLE), + Option(Subcommands.GET, args=[ + Argument(Arguments.CATALOG, str, Hints.CatalogRoles.Create.CATALOG_NAME), + ], input_name=Arguments.CATALOG_ROLE), + Option(Subcommands.LIST, hint=Hints.CatalogRoles.LIST, args=[ + Argument(Arguments.PRINCIPAL_ROLE, str, Hints.PrincipalRoles.PRINCIPAL_ROLE) + ], input_name=Arguments.CATALOG), + Option(Subcommands.UPDATE, args=[ + Argument(Arguments.CATALOG, str, Hints.CatalogRoles.Create.CATALOG_NAME), + Argument(Arguments.PROPERTY, str, Hints.PROPERTY, allow_repeats=True) + ], input_name=Arguments.CATALOG_ROLE), + Option(Subcommands.GRANT, hint=Hints.CatalogRoles.GRANT_CATALOG_ROLE, args=[ + Argument(Arguments.CATALOG, str, Hints.CatalogRoles.CATALOG_NAME), + Argument(Arguments.PRINCIPAL_ROLE, str, Hints.CatalogRoles.CATALOG_ROLE) + ], input_name=Arguments.CATALOG_ROLE), + Option(Subcommands.REVOKE, hint=Hints.CatalogRoles.GRANT_CATALOG_ROLE, args=[ + Argument(Arguments.CATALOG, str, Hints.CatalogRoles.CATALOG_NAME), + Argument(Arguments.PRINCIPAL_ROLE, str, Hints.CatalogRoles.CATALOG_ROLE) + ], input_name=Arguments.CATALOG_ROLE) + ]), + Option(Commands.PRIVILEGES, 'manage privileges for a catalog role', args=[ + Argument(Arguments.CATALOG, str, Hints.CatalogRoles.Create.CATALOG_NAME), + Argument(Arguments.CATALOG_ROLE, str, Hints.CatalogRoles.CATALOG_ROLE) + ], children=[ + Option(Subcommands.LIST), + Option(Subcommands.CATALOG, children=[ + Option(Actions.GRANT, input_name=Arguments.PRIVILEGE), + Option(Actions.REVOKE, args=[ + Argument(Arguments.CASCADE, bool, Hints.Grant.CASCADE) + ], input_name=Arguments.PRIVILEGE), + ]), + Option(Subcommands.NAMESPACE, children=[ + Option(Actions.GRANT, args=[ + Argument(Arguments.NAMESPACE, str, Hints.Grant.NAMESPACE) + ], input_name=Arguments.PRIVILEGE), + Option(Actions.REVOKE, args=[ + Argument(Arguments.NAMESPACE, str, Hints.Grant.NAMESPACE), + Argument(Arguments.CASCADE, bool, Hints.Grant.CASCADE) + ], input_name=Arguments.PRIVILEGE), + ]), + Option(Subcommands.TABLE, children=[ + Option(Actions.GRANT, args=[ + Argument(Arguments.NAMESPACE, str, Hints.Grant.NAMESPACE), + Argument(Arguments.TABLE, str, Hints.Grant.TABLE) + ], input_name=Arguments.PRIVILEGE), + Option(Actions.REVOKE, args=[ + Argument(Arguments.NAMESPACE, str, Hints.Grant.NAMESPACE), + Argument(Arguments.TABLE, str, Hints.Grant.TABLE), + Argument(Arguments.CASCADE, bool, Hints.Grant.CASCADE) + ], input_name=Arguments.PRIVILEGE), + ]), + Option(Subcommands.VIEW, children=[ + Option(Actions.GRANT, args=[ + Argument(Arguments.NAMESPACE, str, Hints.Grant.NAMESPACE), + Argument(Arguments.VIEW, str, Hints.Grant.VIEW) + ], input_name=Arguments.PRIVILEGE), + Option(Actions.REVOKE, args=[ + Argument(Arguments.NAMESPACE, str, Hints.Grant.NAMESPACE), + Argument(Arguments.VIEW, str, Hints.Grant.VIEW), + Argument(Arguments.CASCADE, bool, Hints.Grant.CASCADE) + ], input_name=Arguments.PRIVILEGE), + ]) + ]) + ] diff --git a/regtests/client/python/cli/options/parser.py b/regtests/client/python/cli/options/parser.py new file mode 100644 index 0000000000..0e0260da43 --- /dev/null +++ b/regtests/client/python/cli/options/parser.py @@ -0,0 +1,178 @@ +import argparse +import sys +from typing import List, Optional, Dict + +from cli.options.option_tree import OptionTree, Option, Argument + + +class Parser(object): + """ + `Parser.parse()` is used to parse CLI input into an argparse.Namespace. The arguments expected by the parser are + defined by `OptionTree.getTree()` and by the arguments in `Parser._ROOT_ARGUMENTS`. This class is responsible for + translating the option tree into an ArgumentParser, for applying that ArgumentParser to the user input, and for + generating a custom help message based on the option tree. + """ + + """ + Generates an argparse parser based on the option tree. + """ + + _ROOT_ARGUMENTS = [ + Argument('host', str, hint='hostname', default='localhost'), + Argument('port', int, hint='port', default=8181), + Argument('client-id', str, hint='client ID for token-based authentication'), + Argument('client-secret', str, hint='client secret for token-based authentication'), + Argument('access-token', str, hint='access token for token-based authentication'), + ] + + @staticmethod + def _build_parser() -> argparse.ArgumentParser: + parser = TreeHelpParser(description='Polaris CLI') + + for arg in Parser._ROOT_ARGUMENTS: + if arg.default is not None: + parser.add_argument(arg.get_flag_name(), type=arg.type, help=arg.hint, default=arg.default) + else: + parser.add_argument(arg.get_flag_name(), type=arg.type, help=arg.hint) + + # Add everything from the option tree to the parser: + def add_arguments(parser, args: List[Argument]): + for arg in args: + kwargs = {'help': arg.hint, 'type': arg.type} + if arg.choices: + kwargs['choices'] = arg.choices + if arg.lower: + kwargs['type'] = kwargs['type'].lower + if arg.default: + kwargs['default'] = arg.default + + if arg.type == bool: + del kwargs['type'] + parser.add_argument(arg.get_flag_name(), **kwargs, action='store_true') + elif arg.allow_repeats: + parser.add_argument(arg.get_flag_name(), **kwargs, action='append') + else: + parser.add_argument(arg.get_flag_name(), **kwargs) + + def recurse_options(subparser, options: List[Option]): + for option in options: + option_parser = subparser.add_parser(option.name, help=option.hint or option.name) + add_arguments(option_parser, option.args) + if option.input_name: + option_parser.add_argument(option.input_name, type=str, + help=option.input_name.replace('_', ' '), default=None) + if option.children: + children_subparser = option_parser.add_subparsers(dest=f'{option.name}_subcommand', required=False) + recurse_options(children_subparser, option.children) + + subparser = parser.add_subparsers(dest='command', required=False) + recurse_options(subparser, OptionTree.get_tree()) + return parser + + @staticmethod + def parse(input: Optional[List[str]] = None) -> argparse.Namespace: + parser = Parser._build_parser() + return parser.parse_args(input) + + @staticmethod + def parse_properties(properties: List[str]) -> Optional[Dict[str, str]]: + if not properties: + return None + results = dict() + for property in properties: + if '=' not in property: + raise Exception(f'Could not parse property `{property}`') + key, value = property.split('=', 1) + if '=' in value or not value: + raise Exception(f'Could not parse property `{property}`') + if key in results: + raise Exception(f'Duplicate property key `{key}`') + results[key] = value + return results + + +class TreeHelpParser(argparse.ArgumentParser): + """ + Replaces the default help behavior with a more readable message. + """ + + INDENT = ' ' * 2 + + def parse_args(self, args=None, namespace=None): + if args is None: + args = sys.argv[1:] + help_index = min([float('inf')] + [args.index(x) for x in ['-h', '--help'] if x in args]) + if help_index < float('inf'): + tree_str = self._get_tree_str(args[:help_index]) + if tree_str: + print(f'input: polaris {" ".join(args)}') + print(f'options:') + print(tree_str) + print('\n') + self.print_usage() + super().exit() + else: + return super().parse_args(args, namespace) + else: + return super().parse_args(args, namespace) + + def _get_tree_str(self, args: List[str]) -> Optional[str]: + command_path = self._get_command_path(args, OptionTree.get_tree()) + if len(command_path) == 0: + result = TreeHelpParser.INDENT + 'polaris' + for arg in Parser._ROOT_ARGUMENTS: + result += '\n' + (TreeHelpParser.INDENT * 2) + f"{arg.get_flag_name()} {arg.hint}" + for option in OptionTree.get_tree(): + result += '\n' + self._get_tree_for_option(option, indent=2) + return result + else: + option_node = self._get_option_node(command_path, OptionTree.get_tree()) + if option_node is None: + return None + else: + return self._get_tree_for_option(option_node) + + def _get_tree_for_option(self, option: Option, indent=1) -> str: + result = "" + result += (TreeHelpParser.INDENT * indent) + option.name + + for arg in option.args: + result += '\n' + (TreeHelpParser.INDENT * (indent + 1)) + f"{arg.get_flag_name()} {arg.hint}" + + if len(option.args) > 0 and len(option.children) > 0: + result += '\n' + + for child in option.children: + result += '\n' + self._get_tree_for_option(child, indent + 1) + + return result + + def _get_command_path(self, args: List[str], options: List[Option]) -> List[str]: + command_path = [] + parser = self + + while args: + arg = args.pop(0) + if arg in {o.name for o in options}: + command_path.append(arg) + try: + parser = parser._subparsers._group_actions[0].choices.get(arg) + if not parser: + break + except Exception as e: + break + options = list(filter(lambda o: o.name == arg, options))[0].children + if options is None: + break + return command_path + + def _get_option_node(self, command_path: List[str], nodes: List[Option]) -> Optional[Option]: + if len(command_path) > 0: + for node in nodes: + if node.name == command_path[0]: + if len(command_path) == 1: + return node + else: + return self._get_option_node(command_path[1:], node.children) + return None + diff --git a/regtests/client/python/cli/polaris_cli.py b/regtests/client/python/cli/polaris_cli.py new file mode 100644 index 0000000000..564879fc06 --- /dev/null +++ b/regtests/client/python/cli/polaris_cli.py @@ -0,0 +1,58 @@ +from cli.options.parser import Parser +from polaris.management import ApiClient, Configuration, ApiException +from polaris.management import PolarisDefaultApi + + +class PolarisCli: + """ + Implements a basic Command-Line Interface (CLI) for interacting with a Polaris service. The CLI can be used to + manage entities like catalogs, principals, and grants within Polaris and can perform most operations that are + available in the Python client API. + + Example usage: + * ./polaris --client-id ${id} --client-secret ${secret} --host ${hostname} principals create example_user + * ./polaris --client-id ${id} --client-secret ${secret} --host ${hostname} principal-roles create example_role + * ./polaris --client-id ${id} --client-secret ${secret} --host ${hostname} catalog-roles list + """ + + @staticmethod + def execute(): + options = Parser.parse() + client_builder = PolarisCli._get_client_builder(options) + with client_builder() as api_client: + try: + from cli.command import Command + admin_api = PolarisDefaultApi(api_client) + command = Command.from_options(options) + command.execute(admin_api) + except ApiException as e: + import json + error = json.loads(e.body)['error'] + print(f'Exception when communicating with the Polaris server. {error["type"]}: {error["message"]}') + + @staticmethod + def _get_client_builder(options): + + # Validate + has_access_token = options.access_token is not None + has_client_secret = options.client_id is not None and options.client_secret is not None + if has_access_token and has_client_secret: + raise Exception("Please provide credentials via either --client-id / --client-secret or " + "--access-token, but not both") + + # Authenticate accordingly + polaris_catalog_url = f'http://{options.host}:{options.port}/api/management/v1' + if has_access_token: + return lambda: ApiClient( + Configuration(host=polaris_catalog_url, access_token=options.access_token), + ) + elif has_client_secret: + return lambda: ApiClient( + Configuration(host=polaris_catalog_url, username=options.client_id, password=options.client_secret), + ) + else: + raise Exception("Please provide credentials via --client-id & --client-secret or via --access-token") + + +if __name__ == '__main__': + PolarisCli.execute() diff --git a/regtests/client/python/docs/AddGrantRequest.md b/regtests/client/python/docs/AddGrantRequest.md new file mode 100644 index 0000000000..d36e73027b --- /dev/null +++ b/regtests/client/python/docs/AddGrantRequest.md @@ -0,0 +1,29 @@ +# AddGrantRequest + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**grant** | [**GrantResource**](GrantResource.md) | | [optional] + +## Example + +```python +from polaris.management.models.add_grant_request import AddGrantRequest + +# TODO update the JSON string below +json = "{}" +# create an instance of AddGrantRequest from a JSON string +add_grant_request_instance = AddGrantRequest.from_json(json) +# print the JSON string representation of the object +print(AddGrantRequest.to_json()) + +# convert the object into a dict +add_grant_request_dict = add_grant_request_instance.to_dict() +# create an instance of AddGrantRequest from a dict +add_grant_request_from_dict = AddGrantRequest.from_dict(add_grant_request_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/AddPartitionSpecUpdate.md b/regtests/client/python/docs/AddPartitionSpecUpdate.md new file mode 100644 index 0000000000..31a8448c6d --- /dev/null +++ b/regtests/client/python/docs/AddPartitionSpecUpdate.md @@ -0,0 +1,30 @@ +# AddPartitionSpecUpdate + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**action** | **str** | | +**spec** | [**PartitionSpec**](PartitionSpec.md) | | + +## Example + +```python +from polaris.catalog.models.add_partition_spec_update import AddPartitionSpecUpdate + +# TODO update the JSON string below +json = "{}" +# create an instance of AddPartitionSpecUpdate from a JSON string +add_partition_spec_update_instance = AddPartitionSpecUpdate.from_json(json) +# print the JSON string representation of the object +print(AddPartitionSpecUpdate.to_json()) + +# convert the object into a dict +add_partition_spec_update_dict = add_partition_spec_update_instance.to_dict() +# create an instance of AddPartitionSpecUpdate from a dict +add_partition_spec_update_from_dict = AddPartitionSpecUpdate.from_dict(add_partition_spec_update_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/AddSchemaUpdate.md b/regtests/client/python/docs/AddSchemaUpdate.md new file mode 100644 index 0000000000..0957686695 --- /dev/null +++ b/regtests/client/python/docs/AddSchemaUpdate.md @@ -0,0 +1,31 @@ +# AddSchemaUpdate + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**action** | **str** | | +**var_schema** | [**ModelSchema**](ModelSchema.md) | | +**last_column_id** | **int** | The highest assigned column ID for the table. This is used to ensure columns are always assigned an unused ID when evolving schemas. When omitted, it will be computed on the server side. | [optional] + +## Example + +```python +from polaris.catalog.models.add_schema_update import AddSchemaUpdate + +# TODO update the JSON string below +json = "{}" +# create an instance of AddSchemaUpdate from a JSON string +add_schema_update_instance = AddSchemaUpdate.from_json(json) +# print the JSON string representation of the object +print(AddSchemaUpdate.to_json()) + +# convert the object into a dict +add_schema_update_dict = add_schema_update_instance.to_dict() +# create an instance of AddSchemaUpdate from a dict +add_schema_update_from_dict = AddSchemaUpdate.from_dict(add_schema_update_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/AddSnapshotUpdate.md b/regtests/client/python/docs/AddSnapshotUpdate.md new file mode 100644 index 0000000000..e29a4f6b70 --- /dev/null +++ b/regtests/client/python/docs/AddSnapshotUpdate.md @@ -0,0 +1,30 @@ +# AddSnapshotUpdate + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**action** | **str** | | +**snapshot** | [**Snapshot**](Snapshot.md) | | + +## Example + +```python +from polaris.catalog.models.add_snapshot_update import AddSnapshotUpdate + +# TODO update the JSON string below +json = "{}" +# create an instance of AddSnapshotUpdate from a JSON string +add_snapshot_update_instance = AddSnapshotUpdate.from_json(json) +# print the JSON string representation of the object +print(AddSnapshotUpdate.to_json()) + +# convert the object into a dict +add_snapshot_update_dict = add_snapshot_update_instance.to_dict() +# create an instance of AddSnapshotUpdate from a dict +add_snapshot_update_from_dict = AddSnapshotUpdate.from_dict(add_snapshot_update_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/AddSortOrderUpdate.md b/regtests/client/python/docs/AddSortOrderUpdate.md new file mode 100644 index 0000000000..21c2ec9831 --- /dev/null +++ b/regtests/client/python/docs/AddSortOrderUpdate.md @@ -0,0 +1,30 @@ +# AddSortOrderUpdate + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**action** | **str** | | +**sort_order** | [**SortOrder**](SortOrder.md) | | + +## Example + +```python +from polaris.catalog.models.add_sort_order_update import AddSortOrderUpdate + +# TODO update the JSON string below +json = "{}" +# create an instance of AddSortOrderUpdate from a JSON string +add_sort_order_update_instance = AddSortOrderUpdate.from_json(json) +# print the JSON string representation of the object +print(AddSortOrderUpdate.to_json()) + +# convert the object into a dict +add_sort_order_update_dict = add_sort_order_update_instance.to_dict() +# create an instance of AddSortOrderUpdate from a dict +add_sort_order_update_from_dict = AddSortOrderUpdate.from_dict(add_sort_order_update_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/AddViewVersionUpdate.md b/regtests/client/python/docs/AddViewVersionUpdate.md new file mode 100644 index 0000000000..5c1c4ec3ae --- /dev/null +++ b/regtests/client/python/docs/AddViewVersionUpdate.md @@ -0,0 +1,30 @@ +# AddViewVersionUpdate + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**action** | **str** | | +**view_version** | [**ViewVersion**](ViewVersion.md) | | + +## Example + +```python +from polaris.catalog.models.add_view_version_update import AddViewVersionUpdate + +# TODO update the JSON string below +json = "{}" +# create an instance of AddViewVersionUpdate from a JSON string +add_view_version_update_instance = AddViewVersionUpdate.from_json(json) +# print the JSON string representation of the object +print(AddViewVersionUpdate.to_json()) + +# convert the object into a dict +add_view_version_update_dict = add_view_version_update_instance.to_dict() +# create an instance of AddViewVersionUpdate from a dict +add_view_version_update_from_dict = AddViewVersionUpdate.from_dict(add_view_version_update_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/AndOrExpression.md b/regtests/client/python/docs/AndOrExpression.md new file mode 100644 index 0000000000..44438c4d8a --- /dev/null +++ b/regtests/client/python/docs/AndOrExpression.md @@ -0,0 +1,31 @@ +# AndOrExpression + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**type** | **str** | | +**left** | [**Expression**](Expression.md) | | +**right** | [**Expression**](Expression.md) | | + +## Example + +```python +from polaris.catalog.models.and_or_expression import AndOrExpression + +# TODO update the JSON string below +json = "{}" +# create an instance of AndOrExpression from a JSON string +and_or_expression_instance = AndOrExpression.from_json(json) +# print the JSON string representation of the object +print(AndOrExpression.to_json()) + +# convert the object into a dict +and_or_expression_dict = and_or_expression_instance.to_dict() +# create an instance of AndOrExpression from a dict +and_or_expression_from_dict = AndOrExpression.from_dict(and_or_expression_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/AssertCreate.md b/regtests/client/python/docs/AssertCreate.md new file mode 100644 index 0000000000..80e9ab1e89 --- /dev/null +++ b/regtests/client/python/docs/AssertCreate.md @@ -0,0 +1,30 @@ +# AssertCreate + +The table must not already exist; used for create transactions + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**type** | **str** | | + +## Example + +```python +from polaris.catalog.models.assert_create import AssertCreate + +# TODO update the JSON string below +json = "{}" +# create an instance of AssertCreate from a JSON string +assert_create_instance = AssertCreate.from_json(json) +# print the JSON string representation of the object +print(AssertCreate.to_json()) + +# convert the object into a dict +assert_create_dict = assert_create_instance.to_dict() +# create an instance of AssertCreate from a dict +assert_create_from_dict = AssertCreate.from_dict(assert_create_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/AssertCurrentSchemaId.md b/regtests/client/python/docs/AssertCurrentSchemaId.md new file mode 100644 index 0000000000..f1e81ba30a --- /dev/null +++ b/regtests/client/python/docs/AssertCurrentSchemaId.md @@ -0,0 +1,31 @@ +# AssertCurrentSchemaId + +The table's current schema id must match the requirement's `current-schema-id` + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**type** | **str** | | +**current_schema_id** | **int** | | + +## Example + +```python +from polaris.catalog.models.assert_current_schema_id import AssertCurrentSchemaId + +# TODO update the JSON string below +json = "{}" +# create an instance of AssertCurrentSchemaId from a JSON string +assert_current_schema_id_instance = AssertCurrentSchemaId.from_json(json) +# print the JSON string representation of the object +print(AssertCurrentSchemaId.to_json()) + +# convert the object into a dict +assert_current_schema_id_dict = assert_current_schema_id_instance.to_dict() +# create an instance of AssertCurrentSchemaId from a dict +assert_current_schema_id_from_dict = AssertCurrentSchemaId.from_dict(assert_current_schema_id_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/AssertDefaultSortOrderId.md b/regtests/client/python/docs/AssertDefaultSortOrderId.md new file mode 100644 index 0000000000..6a50a52e3b --- /dev/null +++ b/regtests/client/python/docs/AssertDefaultSortOrderId.md @@ -0,0 +1,31 @@ +# AssertDefaultSortOrderId + +The table's default sort order id must match the requirement's `default-sort-order-id` + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**type** | **str** | | +**default_sort_order_id** | **int** | | + +## Example + +```python +from polaris.catalog.models.assert_default_sort_order_id import AssertDefaultSortOrderId + +# TODO update the JSON string below +json = "{}" +# create an instance of AssertDefaultSortOrderId from a JSON string +assert_default_sort_order_id_instance = AssertDefaultSortOrderId.from_json(json) +# print the JSON string representation of the object +print(AssertDefaultSortOrderId.to_json()) + +# convert the object into a dict +assert_default_sort_order_id_dict = assert_default_sort_order_id_instance.to_dict() +# create an instance of AssertDefaultSortOrderId from a dict +assert_default_sort_order_id_from_dict = AssertDefaultSortOrderId.from_dict(assert_default_sort_order_id_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/AssertDefaultSpecId.md b/regtests/client/python/docs/AssertDefaultSpecId.md new file mode 100644 index 0000000000..5d952d7a2a --- /dev/null +++ b/regtests/client/python/docs/AssertDefaultSpecId.md @@ -0,0 +1,31 @@ +# AssertDefaultSpecId + +The table's default spec id must match the requirement's `default-spec-id` + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**type** | **str** | | +**default_spec_id** | **int** | | + +## Example + +```python +from polaris.catalog.models.assert_default_spec_id import AssertDefaultSpecId + +# TODO update the JSON string below +json = "{}" +# create an instance of AssertDefaultSpecId from a JSON string +assert_default_spec_id_instance = AssertDefaultSpecId.from_json(json) +# print the JSON string representation of the object +print(AssertDefaultSpecId.to_json()) + +# convert the object into a dict +assert_default_spec_id_dict = assert_default_spec_id_instance.to_dict() +# create an instance of AssertDefaultSpecId from a dict +assert_default_spec_id_from_dict = AssertDefaultSpecId.from_dict(assert_default_spec_id_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/AssertLastAssignedFieldId.md b/regtests/client/python/docs/AssertLastAssignedFieldId.md new file mode 100644 index 0000000000..55927c54d8 --- /dev/null +++ b/regtests/client/python/docs/AssertLastAssignedFieldId.md @@ -0,0 +1,31 @@ +# AssertLastAssignedFieldId + +The table's last assigned column id must match the requirement's `last-assigned-field-id` + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**type** | **str** | | +**last_assigned_field_id** | **int** | | + +## Example + +```python +from polaris.catalog.models.assert_last_assigned_field_id import AssertLastAssignedFieldId + +# TODO update the JSON string below +json = "{}" +# create an instance of AssertLastAssignedFieldId from a JSON string +assert_last_assigned_field_id_instance = AssertLastAssignedFieldId.from_json(json) +# print the JSON string representation of the object +print(AssertLastAssignedFieldId.to_json()) + +# convert the object into a dict +assert_last_assigned_field_id_dict = assert_last_assigned_field_id_instance.to_dict() +# create an instance of AssertLastAssignedFieldId from a dict +assert_last_assigned_field_id_from_dict = AssertLastAssignedFieldId.from_dict(assert_last_assigned_field_id_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/AssertLastAssignedPartitionId.md b/regtests/client/python/docs/AssertLastAssignedPartitionId.md new file mode 100644 index 0000000000..381c62aa13 --- /dev/null +++ b/regtests/client/python/docs/AssertLastAssignedPartitionId.md @@ -0,0 +1,31 @@ +# AssertLastAssignedPartitionId + +The table's last assigned partition id must match the requirement's `last-assigned-partition-id` + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**type** | **str** | | +**last_assigned_partition_id** | **int** | | + +## Example + +```python +from polaris.catalog.models.assert_last_assigned_partition_id import AssertLastAssignedPartitionId + +# TODO update the JSON string below +json = "{}" +# create an instance of AssertLastAssignedPartitionId from a JSON string +assert_last_assigned_partition_id_instance = AssertLastAssignedPartitionId.from_json(json) +# print the JSON string representation of the object +print(AssertLastAssignedPartitionId.to_json()) + +# convert the object into a dict +assert_last_assigned_partition_id_dict = assert_last_assigned_partition_id_instance.to_dict() +# create an instance of AssertLastAssignedPartitionId from a dict +assert_last_assigned_partition_id_from_dict = AssertLastAssignedPartitionId.from_dict(assert_last_assigned_partition_id_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/AssertRefSnapshotId.md b/regtests/client/python/docs/AssertRefSnapshotId.md new file mode 100644 index 0000000000..924951e9fd --- /dev/null +++ b/regtests/client/python/docs/AssertRefSnapshotId.md @@ -0,0 +1,32 @@ +# AssertRefSnapshotId + +The table branch or tag identified by the requirement's `ref` must reference the requirement's `snapshot-id`; if `snapshot-id` is `null` or missing, the ref must not already exist + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**type** | **str** | | +**ref** | **str** | | +**snapshot_id** | **int** | | + +## Example + +```python +from polaris.catalog.models.assert_ref_snapshot_id import AssertRefSnapshotId + +# TODO update the JSON string below +json = "{}" +# create an instance of AssertRefSnapshotId from a JSON string +assert_ref_snapshot_id_instance = AssertRefSnapshotId.from_json(json) +# print the JSON string representation of the object +print(AssertRefSnapshotId.to_json()) + +# convert the object into a dict +assert_ref_snapshot_id_dict = assert_ref_snapshot_id_instance.to_dict() +# create an instance of AssertRefSnapshotId from a dict +assert_ref_snapshot_id_from_dict = AssertRefSnapshotId.from_dict(assert_ref_snapshot_id_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/AssertTableUUID.md b/regtests/client/python/docs/AssertTableUUID.md new file mode 100644 index 0000000000..6875919f8f --- /dev/null +++ b/regtests/client/python/docs/AssertTableUUID.md @@ -0,0 +1,31 @@ +# AssertTableUUID + +The table UUID must match the requirement's `uuid` + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**type** | **str** | | +**uuid** | **str** | | + +## Example + +```python +from polaris.catalog.models.assert_table_uuid import AssertTableUUID + +# TODO update the JSON string below +json = "{}" +# create an instance of AssertTableUUID from a JSON string +assert_table_uuid_instance = AssertTableUUID.from_json(json) +# print the JSON string representation of the object +print(AssertTableUUID.to_json()) + +# convert the object into a dict +assert_table_uuid_dict = assert_table_uuid_instance.to_dict() +# create an instance of AssertTableUUID from a dict +assert_table_uuid_from_dict = AssertTableUUID.from_dict(assert_table_uuid_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/AssertViewUUID.md b/regtests/client/python/docs/AssertViewUUID.md new file mode 100644 index 0000000000..3d9d79fc89 --- /dev/null +++ b/regtests/client/python/docs/AssertViewUUID.md @@ -0,0 +1,31 @@ +# AssertViewUUID + +The view UUID must match the requirement's `uuid` + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**type** | **str** | | +**uuid** | **str** | | + +## Example + +```python +from polaris.catalog.models.assert_view_uuid import AssertViewUUID + +# TODO update the JSON string below +json = "{}" +# create an instance of AssertViewUUID from a JSON string +assert_view_uuid_instance = AssertViewUUID.from_json(json) +# print the JSON string representation of the object +print(AssertViewUUID.to_json()) + +# convert the object into a dict +assert_view_uuid_dict = assert_view_uuid_instance.to_dict() +# create an instance of AssertViewUUID from a dict +assert_view_uuid_from_dict = AssertViewUUID.from_dict(assert_view_uuid_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/AssignUUIDUpdate.md b/regtests/client/python/docs/AssignUUIDUpdate.md new file mode 100644 index 0000000000..89a97aab37 --- /dev/null +++ b/regtests/client/python/docs/AssignUUIDUpdate.md @@ -0,0 +1,31 @@ +# AssignUUIDUpdate + +Assigning a UUID to a table/view should only be done when creating the table/view. It is not safe to re-assign the UUID if a table/view already has a UUID assigned + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**action** | **str** | | +**uuid** | **str** | | + +## Example + +```python +from polaris.catalog.models.assign_uuid_update import AssignUUIDUpdate + +# TODO update the JSON string below +json = "{}" +# create an instance of AssignUUIDUpdate from a JSON string +assign_uuid_update_instance = AssignUUIDUpdate.from_json(json) +# print the JSON string representation of the object +print(AssignUUIDUpdate.to_json()) + +# convert the object into a dict +assign_uuid_update_dict = assign_uuid_update_instance.to_dict() +# create an instance of AssignUUIDUpdate from a dict +assign_uuid_update_from_dict = AssignUUIDUpdate.from_dict(assign_uuid_update_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/AwsStorageConfigInfo.md b/regtests/client/python/docs/AwsStorageConfigInfo.md new file mode 100644 index 0000000000..7b4d97d5b0 --- /dev/null +++ b/regtests/client/python/docs/AwsStorageConfigInfo.md @@ -0,0 +1,32 @@ +# AwsStorageConfigInfo + +aws storage configuration info + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**role_arn** | **str** | the aws role arn that grants privileges on the S3 buckets | +**external_id** | **str** | an optional external id used to establish a trust relationship with AWS in the trust policy | [optional] +**user_arn** | **str** | the aws user arn used to assume the aws role | [optional] + +## Example + +```python +from polaris.management.models.aws_storage_config_info import AwsStorageConfigInfo + +# TODO update the JSON string below +json = "{}" +# create an instance of AwsStorageConfigInfo from a JSON string +aws_storage_config_info_instance = AwsStorageConfigInfo.from_json(json) +# print the JSON string representation of the object +print(AwsStorageConfigInfo.to_json()) + +# convert the object into a dict +aws_storage_config_info_dict = aws_storage_config_info_instance.to_dict() +# create an instance of AwsStorageConfigInfo from a dict +aws_storage_config_info_from_dict = AwsStorageConfigInfo.from_dict(aws_storage_config_info_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/AzureStorageConfigInfo.md b/regtests/client/python/docs/AzureStorageConfigInfo.md new file mode 100644 index 0000000000..9b74f953d6 --- /dev/null +++ b/regtests/client/python/docs/AzureStorageConfigInfo.md @@ -0,0 +1,32 @@ +# AzureStorageConfigInfo + +azure storage configuration info + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**tenant_id** | **str** | the tenant id that the storage accounts belong to | +**multi_tenant_app_name** | **str** | the name of the azure client application | [optional] +**consent_url** | **str** | URL to the Azure permissions request page | [optional] + +## Example + +```python +from polaris.management.models.azure_storage_config_info import AzureStorageConfigInfo + +# TODO update the JSON string below +json = "{}" +# create an instance of AzureStorageConfigInfo from a JSON string +azure_storage_config_info_instance = AzureStorageConfigInfo.from_json(json) +# print the JSON string representation of the object +print(AzureStorageConfigInfo.to_json()) + +# convert the object into a dict +azure_storage_config_info_dict = azure_storage_config_info_instance.to_dict() +# create an instance of AzureStorageConfigInfo from a dict +azure_storage_config_info_from_dict = AzureStorageConfigInfo.from_dict(azure_storage_config_info_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/BaseUpdate.md b/regtests/client/python/docs/BaseUpdate.md new file mode 100644 index 0000000000..ba1241dd4f --- /dev/null +++ b/regtests/client/python/docs/BaseUpdate.md @@ -0,0 +1,29 @@ +# BaseUpdate + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**action** | **str** | | + +## Example + +```python +from polaris.catalog.models.base_update import BaseUpdate + +# TODO update the JSON string below +json = "{}" +# create an instance of BaseUpdate from a JSON string +base_update_instance = BaseUpdate.from_json(json) +# print the JSON string representation of the object +print(BaseUpdate.to_json()) + +# convert the object into a dict +base_update_dict = base_update_instance.to_dict() +# create an instance of BaseUpdate from a dict +base_update_from_dict = BaseUpdate.from_dict(base_update_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/BlobMetadata.md b/regtests/client/python/docs/BlobMetadata.md new file mode 100644 index 0000000000..5918824fac --- /dev/null +++ b/regtests/client/python/docs/BlobMetadata.md @@ -0,0 +1,33 @@ +# BlobMetadata + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**type** | **str** | | +**snapshot_id** | **int** | | +**sequence_number** | **int** | | +**fields** | **List[int]** | | +**properties** | **object** | | [optional] + +## Example + +```python +from polaris.catalog.models.blob_metadata import BlobMetadata + +# TODO update the JSON string below +json = "{}" +# create an instance of BlobMetadata from a JSON string +blob_metadata_instance = BlobMetadata.from_json(json) +# print the JSON string representation of the object +print(BlobMetadata.to_json()) + +# convert the object into a dict +blob_metadata_dict = blob_metadata_instance.to_dict() +# create an instance of BlobMetadata from a dict +blob_metadata_from_dict = BlobMetadata.from_dict(blob_metadata_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/Catalog.md b/regtests/client/python/docs/Catalog.md new file mode 100644 index 0000000000..585ddc11d1 --- /dev/null +++ b/regtests/client/python/docs/Catalog.md @@ -0,0 +1,36 @@ +# Catalog + +A catalog object. A catalog may be internal or external. Internal catalogs are managed entirely by an external catalog interface. Third party catalogs may be other Iceberg REST implementations or other services with their own proprietary APIs + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**type** | **str** | the type of catalog - internal or external | [default to 'INTERNAL'] +**name** | **str** | The name of the catalog | +**properties** | [**CatalogProperties**](CatalogProperties.md) | | +**create_timestamp** | **int** | The creation time represented as unix epoch timestamp in milliseconds | [optional] +**last_update_timestamp** | **int** | The last update time represented as unix epoch timestamp in milliseconds | [optional] +**entity_version** | **int** | The version of the catalog object used to determine if the catalog metadata has changed | [optional] +**storage_config_info** | [**StorageConfigInfo**](StorageConfigInfo.md) | | + +## Example + +```python +from polaris.management.models.catalog import Catalog + +# TODO update the JSON string below +json = "{}" +# create an instance of Catalog from a JSON string +catalog_instance = Catalog.from_json(json) +# print the JSON string representation of the object +print(Catalog.to_json()) + +# convert the object into a dict +catalog_dict = catalog_instance.to_dict() +# create an instance of Catalog from a dict +catalog_from_dict = Catalog.from_dict(catalog_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/CatalogConfig.md b/regtests/client/python/docs/CatalogConfig.md new file mode 100644 index 0000000000..77cfa5e9bd --- /dev/null +++ b/regtests/client/python/docs/CatalogConfig.md @@ -0,0 +1,31 @@ +# CatalogConfig + +Server-provided configuration for the catalog. + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**overrides** | **Dict[str, str]** | Properties that should be used to override client configuration; applied after defaults and client configuration. | +**defaults** | **Dict[str, str]** | Properties that should be used as default configuration; applied before client configuration. | + +## Example + +```python +from polaris.catalog.models.catalog_config import CatalogConfig + +# TODO update the JSON string below +json = "{}" +# create an instance of CatalogConfig from a JSON string +catalog_config_instance = CatalogConfig.from_json(json) +# print the JSON string representation of the object +print(CatalogConfig.to_json()) + +# convert the object into a dict +catalog_config_dict = catalog_config_instance.to_dict() +# create an instance of CatalogConfig from a dict +catalog_config_from_dict = CatalogConfig.from_dict(catalog_config_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/CatalogGrant.md b/regtests/client/python/docs/CatalogGrant.md new file mode 100644 index 0000000000..f7dbd9bb9d --- /dev/null +++ b/regtests/client/python/docs/CatalogGrant.md @@ -0,0 +1,29 @@ +# CatalogGrant + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**privilege** | [**CatalogPrivilege**](CatalogPrivilege.md) | | + +## Example + +```python +from polaris.management.models.catalog_grant import CatalogGrant + +# TODO update the JSON string below +json = "{}" +# create an instance of CatalogGrant from a JSON string +catalog_grant_instance = CatalogGrant.from_json(json) +# print the JSON string representation of the object +print(CatalogGrant.to_json()) + +# convert the object into a dict +catalog_grant_dict = catalog_grant_instance.to_dict() +# create an instance of CatalogGrant from a dict +catalog_grant_from_dict = CatalogGrant.from_dict(catalog_grant_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/CatalogPrivilege.md b/regtests/client/python/docs/CatalogPrivilege.md new file mode 100644 index 0000000000..12dd0bcb61 --- /dev/null +++ b/regtests/client/python/docs/CatalogPrivilege.md @@ -0,0 +1,58 @@ +# CatalogPrivilege + + +## Enum + +* `CATALOG_MANAGE_ACCESS` (value: `'CATALOG_MANAGE_ACCESS'`) + +* `CATALOG_MANAGE_CONTENT` (value: `'CATALOG_MANAGE_CONTENT'`) + +* `CATALOG_MANAGE_METADATA` (value: `'CATALOG_MANAGE_METADATA'`) + +* `CATALOG_READ_PROPERTIES` (value: `'CATALOG_READ_PROPERTIES'`) + +* `CATALOG_WRITE_PROPERTIES` (value: `'CATALOG_WRITE_PROPERTIES'`) + +* `NAMESPACE_CREATE` (value: `'NAMESPACE_CREATE'`) + +* `TABLE_CREATE` (value: `'TABLE_CREATE'`) + +* `VIEW_CREATE` (value: `'VIEW_CREATE'`) + +* `NAMESPACE_DROP` (value: `'NAMESPACE_DROP'`) + +* `TABLE_DROP` (value: `'TABLE_DROP'`) + +* `VIEW_DROP` (value: `'VIEW_DROP'`) + +* `NAMESPACE_LIST` (value: `'NAMESPACE_LIST'`) + +* `TABLE_LIST` (value: `'TABLE_LIST'`) + +* `VIEW_LIST` (value: `'VIEW_LIST'`) + +* `NAMESPACE_READ_PROPERTIES` (value: `'NAMESPACE_READ_PROPERTIES'`) + +* `TABLE_READ_PROPERTIES` (value: `'TABLE_READ_PROPERTIES'`) + +* `VIEW_READ_PROPERTIES` (value: `'VIEW_READ_PROPERTIES'`) + +* `NAMESPACE_WRITE_PROPERTIES` (value: `'NAMESPACE_WRITE_PROPERTIES'`) + +* `TABLE_WRITE_PROPERTIES` (value: `'TABLE_WRITE_PROPERTIES'`) + +* `VIEW_WRITE_PROPERTIES` (value: `'VIEW_WRITE_PROPERTIES'`) + +* `TABLE_READ_DATA` (value: `'TABLE_READ_DATA'`) + +* `TABLE_WRITE_DATA` (value: `'TABLE_WRITE_DATA'`) + +* `NAMESPACE_FULL_METADATA` (value: `'NAMESPACE_FULL_METADATA'`) + +* `TABLE_FULL_METADATA` (value: `'TABLE_FULL_METADATA'`) + +* `VIEW_FULL_METADATA` (value: `'VIEW_FULL_METADATA'`) + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/CatalogProperties.md b/regtests/client/python/docs/CatalogProperties.md new file mode 100644 index 0000000000..464a3f89e0 --- /dev/null +++ b/regtests/client/python/docs/CatalogProperties.md @@ -0,0 +1,29 @@ +# CatalogProperties + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**default_base_location** | **str** | | + +## Example + +```python +from polaris.management.models.catalog_properties import CatalogProperties + +# TODO update the JSON string below +json = "{}" +# create an instance of CatalogProperties from a JSON string +catalog_properties_instance = CatalogProperties.from_json(json) +# print the JSON string representation of the object +print(CatalogProperties.to_json()) + +# convert the object into a dict +catalog_properties_dict = catalog_properties_instance.to_dict() +# create an instance of CatalogProperties from a dict +catalog_properties_from_dict = CatalogProperties.from_dict(catalog_properties_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/CatalogRole.md b/regtests/client/python/docs/CatalogRole.md new file mode 100644 index 0000000000..cd052ca7ec --- /dev/null +++ b/regtests/client/python/docs/CatalogRole.md @@ -0,0 +1,33 @@ +# CatalogRole + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**name** | **str** | The name of the role | +**properties** | **Dict[str, str]** | | [optional] +**create_timestamp** | **int** | | [optional] +**last_update_timestamp** | **int** | | [optional] +**entity_version** | **int** | The version of the catalog role object used to determine if the catalog role metadata has changed | [optional] + +## Example + +```python +from polaris.management.models.catalog_role import CatalogRole + +# TODO update the JSON string below +json = "{}" +# create an instance of CatalogRole from a JSON string +catalog_role_instance = CatalogRole.from_json(json) +# print the JSON string representation of the object +print(CatalogRole.to_json()) + +# convert the object into a dict +catalog_role_dict = catalog_role_instance.to_dict() +# create an instance of CatalogRole from a dict +catalog_role_from_dict = CatalogRole.from_dict(catalog_role_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/CatalogRoles.md b/regtests/client/python/docs/CatalogRoles.md new file mode 100644 index 0000000000..d7be37d45d --- /dev/null +++ b/regtests/client/python/docs/CatalogRoles.md @@ -0,0 +1,29 @@ +# CatalogRoles + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**roles** | [**List[CatalogRole]**](CatalogRole.md) | The list of catalog roles | + +## Example + +```python +from polaris.management.models.catalog_roles import CatalogRoles + +# TODO update the JSON string below +json = "{}" +# create an instance of CatalogRoles from a JSON string +catalog_roles_instance = CatalogRoles.from_json(json) +# print the JSON string representation of the object +print(CatalogRoles.to_json()) + +# convert the object into a dict +catalog_roles_dict = catalog_roles_instance.to_dict() +# create an instance of CatalogRoles from a dict +catalog_roles_from_dict = CatalogRoles.from_dict(catalog_roles_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/Catalogs.md b/regtests/client/python/docs/Catalogs.md new file mode 100644 index 0000000000..d1f0ebe58f --- /dev/null +++ b/regtests/client/python/docs/Catalogs.md @@ -0,0 +1,30 @@ +# Catalogs + +A list of Catalog objects + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**catalogs** | [**List[Catalog]**](Catalog.md) | | + +## Example + +```python +from polaris.management.models.catalogs import Catalogs + +# TODO update the JSON string below +json = "{}" +# create an instance of Catalogs from a JSON string +catalogs_instance = Catalogs.from_json(json) +# print the JSON string representation of the object +print(Catalogs.to_json()) + +# convert the object into a dict +catalogs_dict = catalogs_instance.to_dict() +# create an instance of Catalogs from a dict +catalogs_from_dict = Catalogs.from_dict(catalogs_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/CommitReport.md b/regtests/client/python/docs/CommitReport.md new file mode 100644 index 0000000000..57b22e3271 --- /dev/null +++ b/regtests/client/python/docs/CommitReport.md @@ -0,0 +1,34 @@ +# CommitReport + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**table_name** | **str** | | +**snapshot_id** | **int** | | +**sequence_number** | **int** | | +**operation** | **str** | | +**metrics** | [**Dict[str, MetricResult]**](MetricResult.md) | | +**metadata** | **Dict[str, str]** | | [optional] + +## Example + +```python +from polaris.catalog.models.commit_report import CommitReport + +# TODO update the JSON string below +json = "{}" +# create an instance of CommitReport from a JSON string +commit_report_instance = CommitReport.from_json(json) +# print the JSON string representation of the object +print(CommitReport.to_json()) + +# convert the object into a dict +commit_report_dict = commit_report_instance.to_dict() +# create an instance of CommitReport from a dict +commit_report_from_dict = CommitReport.from_dict(commit_report_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/CommitTableRequest.md b/regtests/client/python/docs/CommitTableRequest.md new file mode 100644 index 0000000000..9d490cbd61 --- /dev/null +++ b/regtests/client/python/docs/CommitTableRequest.md @@ -0,0 +1,31 @@ +# CommitTableRequest + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**identifier** | [**TableIdentifier**](TableIdentifier.md) | | [optional] +**requirements** | [**List[TableRequirement]**](TableRequirement.md) | | +**updates** | [**List[TableUpdate]**](TableUpdate.md) | | + +## Example + +```python +from polaris.catalog.models.commit_table_request import CommitTableRequest + +# TODO update the JSON string below +json = "{}" +# create an instance of CommitTableRequest from a JSON string +commit_table_request_instance = CommitTableRequest.from_json(json) +# print the JSON string representation of the object +print(CommitTableRequest.to_json()) + +# convert the object into a dict +commit_table_request_dict = commit_table_request_instance.to_dict() +# create an instance of CommitTableRequest from a dict +commit_table_request_from_dict = CommitTableRequest.from_dict(commit_table_request_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/CommitTableResponse.md b/regtests/client/python/docs/CommitTableResponse.md new file mode 100644 index 0000000000..24430f2a46 --- /dev/null +++ b/regtests/client/python/docs/CommitTableResponse.md @@ -0,0 +1,30 @@ +# CommitTableResponse + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**metadata_location** | **str** | | +**metadata** | [**TableMetadata**](TableMetadata.md) | | + +## Example + +```python +from polaris.catalog.models.commit_table_response import CommitTableResponse + +# TODO update the JSON string below +json = "{}" +# create an instance of CommitTableResponse from a JSON string +commit_table_response_instance = CommitTableResponse.from_json(json) +# print the JSON string representation of the object +print(CommitTableResponse.to_json()) + +# convert the object into a dict +commit_table_response_dict = commit_table_response_instance.to_dict() +# create an instance of CommitTableResponse from a dict +commit_table_response_from_dict = CommitTableResponse.from_dict(commit_table_response_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/CommitTransactionRequest.md b/regtests/client/python/docs/CommitTransactionRequest.md new file mode 100644 index 0000000000..cc3323e4a5 --- /dev/null +++ b/regtests/client/python/docs/CommitTransactionRequest.md @@ -0,0 +1,29 @@ +# CommitTransactionRequest + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**table_changes** | [**List[CommitTableRequest]**](CommitTableRequest.md) | | + +## Example + +```python +from polaris.catalog.models.commit_transaction_request import CommitTransactionRequest + +# TODO update the JSON string below +json = "{}" +# create an instance of CommitTransactionRequest from a JSON string +commit_transaction_request_instance = CommitTransactionRequest.from_json(json) +# print the JSON string representation of the object +print(CommitTransactionRequest.to_json()) + +# convert the object into a dict +commit_transaction_request_dict = commit_transaction_request_instance.to_dict() +# create an instance of CommitTransactionRequest from a dict +commit_transaction_request_from_dict = CommitTransactionRequest.from_dict(commit_transaction_request_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/CommitViewRequest.md b/regtests/client/python/docs/CommitViewRequest.md new file mode 100644 index 0000000000..5867bed269 --- /dev/null +++ b/regtests/client/python/docs/CommitViewRequest.md @@ -0,0 +1,31 @@ +# CommitViewRequest + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**identifier** | [**TableIdentifier**](TableIdentifier.md) | | [optional] +**requirements** | [**List[ViewRequirement]**](ViewRequirement.md) | | [optional] +**updates** | [**List[ViewUpdate]**](ViewUpdate.md) | | + +## Example + +```python +from polaris.catalog.models.commit_view_request import CommitViewRequest + +# TODO update the JSON string below +json = "{}" +# create an instance of CommitViewRequest from a JSON string +commit_view_request_instance = CommitViewRequest.from_json(json) +# print the JSON string representation of the object +print(CommitViewRequest.to_json()) + +# convert the object into a dict +commit_view_request_dict = commit_view_request_instance.to_dict() +# create an instance of CommitViewRequest from a dict +commit_view_request_from_dict = CommitViewRequest.from_dict(commit_view_request_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/ContentFile.md b/regtests/client/python/docs/ContentFile.md new file mode 100644 index 0000000000..fe06ffde2d --- /dev/null +++ b/regtests/client/python/docs/ContentFile.md @@ -0,0 +1,38 @@ +# ContentFile + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**content** | **str** | | +**file_path** | **str** | | +**file_format** | [**FileFormat**](FileFormat.md) | | +**spec_id** | **int** | | +**partition** | [**List[PrimitiveTypeValue]**](PrimitiveTypeValue.md) | A list of partition field values ordered based on the fields of the partition spec specified by the `spec-id` | [optional] +**file_size_in_bytes** | **int** | Total file size in bytes | +**record_count** | **int** | Number of records in the file | +**key_metadata** | **str** | Encryption key metadata blob | [optional] +**split_offsets** | **List[int]** | List of splittable offsets | [optional] +**sort_order_id** | **int** | | [optional] + +## Example + +```python +from polaris.catalog.models.content_file import ContentFile + +# TODO update the JSON string below +json = "{}" +# create an instance of ContentFile from a JSON string +content_file_instance = ContentFile.from_json(json) +# print the JSON string representation of the object +print(ContentFile.to_json()) + +# convert the object into a dict +content_file_dict = content_file_instance.to_dict() +# create an instance of ContentFile from a dict +content_file_from_dict = ContentFile.from_dict(content_file_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/CountMap.md b/regtests/client/python/docs/CountMap.md new file mode 100644 index 0000000000..ac4ce65cac --- /dev/null +++ b/regtests/client/python/docs/CountMap.md @@ -0,0 +1,30 @@ +# CountMap + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**keys** | **List[int]** | List of integer column ids for each corresponding value | [optional] +**values** | **List[int]** | List of Long values, matched to 'keys' by index | [optional] + +## Example + +```python +from polaris.catalog.models.count_map import CountMap + +# TODO update the JSON string below +json = "{}" +# create an instance of CountMap from a JSON string +count_map_instance = CountMap.from_json(json) +# print the JSON string representation of the object +print(CountMap.to_json()) + +# convert the object into a dict +count_map_dict = count_map_instance.to_dict() +# create an instance of CountMap from a dict +count_map_from_dict = CountMap.from_dict(count_map_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/CounterResult.md b/regtests/client/python/docs/CounterResult.md new file mode 100644 index 0000000000..4efe56d321 --- /dev/null +++ b/regtests/client/python/docs/CounterResult.md @@ -0,0 +1,30 @@ +# CounterResult + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**unit** | **str** | | +**value** | **int** | | + +## Example + +```python +from polaris.catalog.models.counter_result import CounterResult + +# TODO update the JSON string below +json = "{}" +# create an instance of CounterResult from a JSON string +counter_result_instance = CounterResult.from_json(json) +# print the JSON string representation of the object +print(CounterResult.to_json()) + +# convert the object into a dict +counter_result_dict = counter_result_instance.to_dict() +# create an instance of CounterResult from a dict +counter_result_from_dict = CounterResult.from_dict(counter_result_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/CreateCatalogRequest.md b/regtests/client/python/docs/CreateCatalogRequest.md new file mode 100644 index 0000000000..160e873c01 --- /dev/null +++ b/regtests/client/python/docs/CreateCatalogRequest.md @@ -0,0 +1,30 @@ +# CreateCatalogRequest + +Request to create a new catalog + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**catalog** | [**Catalog**](Catalog.md) | | + +## Example + +```python +from polaris.management.models.create_catalog_request import CreateCatalogRequest + +# TODO update the JSON string below +json = "{}" +# create an instance of CreateCatalogRequest from a JSON string +create_catalog_request_instance = CreateCatalogRequest.from_json(json) +# print the JSON string representation of the object +print(CreateCatalogRequest.to_json()) + +# convert the object into a dict +create_catalog_request_dict = create_catalog_request_instance.to_dict() +# create an instance of CreateCatalogRequest from a dict +create_catalog_request_from_dict = CreateCatalogRequest.from_dict(create_catalog_request_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/CreateCatalogRoleRequest.md b/regtests/client/python/docs/CreateCatalogRoleRequest.md new file mode 100644 index 0000000000..b10da6c54c --- /dev/null +++ b/regtests/client/python/docs/CreateCatalogRoleRequest.md @@ -0,0 +1,29 @@ +# CreateCatalogRoleRequest + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**catalog_role** | [**CatalogRole**](CatalogRole.md) | | [optional] + +## Example + +```python +from polaris.management.models.create_catalog_role_request import CreateCatalogRoleRequest + +# TODO update the JSON string below +json = "{}" +# create an instance of CreateCatalogRoleRequest from a JSON string +create_catalog_role_request_instance = CreateCatalogRoleRequest.from_json(json) +# print the JSON string representation of the object +print(CreateCatalogRoleRequest.to_json()) + +# convert the object into a dict +create_catalog_role_request_dict = create_catalog_role_request_instance.to_dict() +# create an instance of CreateCatalogRoleRequest from a dict +create_catalog_role_request_from_dict = CreateCatalogRoleRequest.from_dict(create_catalog_role_request_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/CreateNamespaceRequest.md b/regtests/client/python/docs/CreateNamespaceRequest.md new file mode 100644 index 0000000000..8ab25bdb33 --- /dev/null +++ b/regtests/client/python/docs/CreateNamespaceRequest.md @@ -0,0 +1,30 @@ +# CreateNamespaceRequest + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**namespace** | **List[str]** | Reference to one or more levels of a namespace | +**properties** | **Dict[str, str]** | Configured string to string map of properties for the namespace | [optional] + +## Example + +```python +from polaris.catalog.models.create_namespace_request import CreateNamespaceRequest + +# TODO update the JSON string below +json = "{}" +# create an instance of CreateNamespaceRequest from a JSON string +create_namespace_request_instance = CreateNamespaceRequest.from_json(json) +# print the JSON string representation of the object +print(CreateNamespaceRequest.to_json()) + +# convert the object into a dict +create_namespace_request_dict = create_namespace_request_instance.to_dict() +# create an instance of CreateNamespaceRequest from a dict +create_namespace_request_from_dict = CreateNamespaceRequest.from_dict(create_namespace_request_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/CreateNamespaceResponse.md b/regtests/client/python/docs/CreateNamespaceResponse.md new file mode 100644 index 0000000000..144cb7d17d --- /dev/null +++ b/regtests/client/python/docs/CreateNamespaceResponse.md @@ -0,0 +1,30 @@ +# CreateNamespaceResponse + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**namespace** | **List[str]** | Reference to one or more levels of a namespace | +**properties** | **Dict[str, str]** | Properties stored on the namespace, if supported by the server. | [optional] + +## Example + +```python +from polaris.catalog.models.create_namespace_response import CreateNamespaceResponse + +# TODO update the JSON string below +json = "{}" +# create an instance of CreateNamespaceResponse from a JSON string +create_namespace_response_instance = CreateNamespaceResponse.from_json(json) +# print the JSON string representation of the object +print(CreateNamespaceResponse.to_json()) + +# convert the object into a dict +create_namespace_response_dict = create_namespace_response_instance.to_dict() +# create an instance of CreateNamespaceResponse from a dict +create_namespace_response_from_dict = CreateNamespaceResponse.from_dict(create_namespace_response_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/CreatePrincipalRequest.md b/regtests/client/python/docs/CreatePrincipalRequest.md new file mode 100644 index 0000000000..8b02f3c882 --- /dev/null +++ b/regtests/client/python/docs/CreatePrincipalRequest.md @@ -0,0 +1,30 @@ +# CreatePrincipalRequest + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**principal** | [**Principal**](Principal.md) | | [optional] +**credential_rotation_required** | **bool** | If true, the initial credentials can only be used to call rotateCredentials | [optional] + +## Example + +```python +from polaris.management.models.create_principal_request import CreatePrincipalRequest + +# TODO update the JSON string below +json = "{}" +# create an instance of CreatePrincipalRequest from a JSON string +create_principal_request_instance = CreatePrincipalRequest.from_json(json) +# print the JSON string representation of the object +print(CreatePrincipalRequest.to_json()) + +# convert the object into a dict +create_principal_request_dict = create_principal_request_instance.to_dict() +# create an instance of CreatePrincipalRequest from a dict +create_principal_request_from_dict = CreatePrincipalRequest.from_dict(create_principal_request_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/CreatePrincipalRoleRequest.md b/regtests/client/python/docs/CreatePrincipalRoleRequest.md new file mode 100644 index 0000000000..3990833023 --- /dev/null +++ b/regtests/client/python/docs/CreatePrincipalRoleRequest.md @@ -0,0 +1,29 @@ +# CreatePrincipalRoleRequest + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**principal_role** | [**PrincipalRole**](PrincipalRole.md) | | [optional] + +## Example + +```python +from polaris.management.models.create_principal_role_request import CreatePrincipalRoleRequest + +# TODO update the JSON string below +json = "{}" +# create an instance of CreatePrincipalRoleRequest from a JSON string +create_principal_role_request_instance = CreatePrincipalRoleRequest.from_json(json) +# print the JSON string representation of the object +print(CreatePrincipalRoleRequest.to_json()) + +# convert the object into a dict +create_principal_role_request_dict = create_principal_role_request_instance.to_dict() +# create an instance of CreatePrincipalRoleRequest from a dict +create_principal_role_request_from_dict = CreatePrincipalRoleRequest.from_dict(create_principal_role_request_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/CreateTableRequest.md b/regtests/client/python/docs/CreateTableRequest.md new file mode 100644 index 0000000000..d87f8aa294 --- /dev/null +++ b/regtests/client/python/docs/CreateTableRequest.md @@ -0,0 +1,35 @@ +# CreateTableRequest + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**name** | **str** | | +**location** | **str** | | [optional] +**var_schema** | [**ModelSchema**](ModelSchema.md) | | +**partition_spec** | [**PartitionSpec**](PartitionSpec.md) | | [optional] +**write_order** | [**SortOrder**](SortOrder.md) | | [optional] +**stage_create** | **bool** | | [optional] +**properties** | **Dict[str, str]** | | [optional] + +## Example + +```python +from polaris.catalog.models.create_table_request import CreateTableRequest + +# TODO update the JSON string below +json = "{}" +# create an instance of CreateTableRequest from a JSON string +create_table_request_instance = CreateTableRequest.from_json(json) +# print the JSON string representation of the object +print(CreateTableRequest.to_json()) + +# convert the object into a dict +create_table_request_dict = create_table_request_instance.to_dict() +# create an instance of CreateTableRequest from a dict +create_table_request_from_dict = CreateTableRequest.from_dict(create_table_request_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/CreateViewRequest.md b/regtests/client/python/docs/CreateViewRequest.md new file mode 100644 index 0000000000..e0c53722de --- /dev/null +++ b/regtests/client/python/docs/CreateViewRequest.md @@ -0,0 +1,33 @@ +# CreateViewRequest + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**name** | **str** | | +**location** | **str** | | [optional] +**var_schema** | [**ModelSchema**](ModelSchema.md) | | +**view_version** | [**ViewVersion**](ViewVersion.md) | | +**properties** | **Dict[str, str]** | | + +## Example + +```python +from polaris.catalog.models.create_view_request import CreateViewRequest + +# TODO update the JSON string below +json = "{}" +# create an instance of CreateViewRequest from a JSON string +create_view_request_instance = CreateViewRequest.from_json(json) +# print the JSON string representation of the object +print(CreateViewRequest.to_json()) + +# convert the object into a dict +create_view_request_dict = create_view_request_instance.to_dict() +# create an instance of CreateViewRequest from a dict +create_view_request_from_dict = CreateViewRequest.from_dict(create_view_request_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/DataFile.md b/regtests/client/python/docs/DataFile.md new file mode 100644 index 0000000000..49cb15cf88 --- /dev/null +++ b/regtests/client/python/docs/DataFile.md @@ -0,0 +1,35 @@ +# DataFile + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**content** | **str** | | +**column_sizes** | [**CountMap**](CountMap.md) | Map of column id to total count, including null and NaN | [optional] +**value_counts** | [**CountMap**](CountMap.md) | Map of column id to null value count | [optional] +**null_value_counts** | [**CountMap**](CountMap.md) | Map of column id to null value count | [optional] +**nan_value_counts** | [**CountMap**](CountMap.md) | Map of column id to number of NaN values in the column | [optional] +**lower_bounds** | [**ValueMap**](ValueMap.md) | Map of column id to lower bound primitive type values | [optional] +**upper_bounds** | [**ValueMap**](ValueMap.md) | Map of column id to upper bound primitive type values | [optional] + +## Example + +```python +from polaris.catalog.models.data_file import DataFile + +# TODO update the JSON string below +json = "{}" +# create an instance of DataFile from a JSON string +data_file_instance = DataFile.from_json(json) +# print the JSON string representation of the object +print(DataFile.to_json()) + +# convert the object into a dict +data_file_dict = data_file_instance.to_dict() +# create an instance of DataFile from a dict +data_file_from_dict = DataFile.from_dict(data_file_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/EqualityDeleteFile.md b/regtests/client/python/docs/EqualityDeleteFile.md new file mode 100644 index 0000000000..da8af51645 --- /dev/null +++ b/regtests/client/python/docs/EqualityDeleteFile.md @@ -0,0 +1,30 @@ +# EqualityDeleteFile + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**content** | **str** | | +**equality_ids** | **List[int]** | List of equality field IDs | [optional] + +## Example + +```python +from polaris.catalog.models.equality_delete_file import EqualityDeleteFile + +# TODO update the JSON string below +json = "{}" +# create an instance of EqualityDeleteFile from a JSON string +equality_delete_file_instance = EqualityDeleteFile.from_json(json) +# print the JSON string representation of the object +print(EqualityDeleteFile.to_json()) + +# convert the object into a dict +equality_delete_file_dict = equality_delete_file_instance.to_dict() +# create an instance of EqualityDeleteFile from a dict +equality_delete_file_from_dict = EqualityDeleteFile.from_dict(equality_delete_file_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/ErrorModel.md b/regtests/client/python/docs/ErrorModel.md new file mode 100644 index 0000000000..ee4280a5c8 --- /dev/null +++ b/regtests/client/python/docs/ErrorModel.md @@ -0,0 +1,33 @@ +# ErrorModel + +JSON error payload returned in a response with further details on the error + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**message** | **str** | Human-readable error message | +**type** | **str** | Internal type definition of the error | +**code** | **int** | HTTP response code | +**stack** | **List[str]** | | [optional] + +## Example + +```python +from polaris.catalog.models.error_model import ErrorModel + +# TODO update the JSON string below +json = "{}" +# create an instance of ErrorModel from a JSON string +error_model_instance = ErrorModel.from_json(json) +# print the JSON string representation of the object +print(ErrorModel.to_json()) + +# convert the object into a dict +error_model_dict = error_model_instance.to_dict() +# create an instance of ErrorModel from a dict +error_model_from_dict = ErrorModel.from_dict(error_model_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/Expression.md b/regtests/client/python/docs/Expression.md new file mode 100644 index 0000000000..b9ed48b908 --- /dev/null +++ b/regtests/client/python/docs/Expression.md @@ -0,0 +1,35 @@ +# Expression + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**type** | **str** | | +**left** | [**Expression**](Expression.md) | | +**right** | [**Expression**](Expression.md) | | +**child** | [**Expression**](Expression.md) | | +**term** | [**Term**](Term.md) | | +**values** | **List[object]** | | +**value** | **object** | | + +## Example + +```python +from polaris.catalog.models.expression import Expression + +# TODO update the JSON string below +json = "{}" +# create an instance of Expression from a JSON string +expression_instance = Expression.from_json(json) +# print the JSON string representation of the object +print(Expression.to_json()) + +# convert the object into a dict +expression_dict = expression_instance.to_dict() +# create an instance of Expression from a dict +expression_from_dict = Expression.from_dict(expression_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/ExternalCatalog.md b/regtests/client/python/docs/ExternalCatalog.md new file mode 100644 index 0000000000..6db96d1b8e --- /dev/null +++ b/regtests/client/python/docs/ExternalCatalog.md @@ -0,0 +1,30 @@ +# ExternalCatalog + +An externally managed catalog + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**remote_url** | **str** | URL to the remote catalog API | [optional] + +## Example + +```python +from polaris.management.models.external_catalog import ExternalCatalog + +# TODO update the JSON string below +json = "{}" +# create an instance of ExternalCatalog from a JSON string +external_catalog_instance = ExternalCatalog.from_json(json) +# print the JSON string representation of the object +print(ExternalCatalog.to_json()) + +# convert the object into a dict +external_catalog_dict = external_catalog_instance.to_dict() +# create an instance of ExternalCatalog from a dict +external_catalog_from_dict = ExternalCatalog.from_dict(external_catalog_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/FileFormat.md b/regtests/client/python/docs/FileFormat.md new file mode 100644 index 0000000000..32d47c400d --- /dev/null +++ b/regtests/client/python/docs/FileFormat.md @@ -0,0 +1,14 @@ +# FileFormat + + +## Enum + +* `AVRO` (value: `'avro'`) + +* `ORC` (value: `'orc'`) + +* `PARQUET` (value: `'parquet'`) + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/FileStorageConfigInfo.md b/regtests/client/python/docs/FileStorageConfigInfo.md new file mode 100644 index 0000000000..8433842625 --- /dev/null +++ b/regtests/client/python/docs/FileStorageConfigInfo.md @@ -0,0 +1,29 @@ +# FileStorageConfigInfo + +gcp storage configuration info + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- + +## Example + +```python +from polaris.management.models.file_storage_config_info import FileStorageConfigInfo + +# TODO update the JSON string below +json = "{}" +# create an instance of FileStorageConfigInfo from a JSON string +file_storage_config_info_instance = FileStorageConfigInfo.from_json(json) +# print the JSON string representation of the object +print(FileStorageConfigInfo.to_json()) + +# convert the object into a dict +file_storage_config_info_dict = file_storage_config_info_instance.to_dict() +# create an instance of FileStorageConfigInfo from a dict +file_storage_config_info_from_dict = FileStorageConfigInfo.from_dict(file_storage_config_info_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/GcpStorageConfigInfo.md b/regtests/client/python/docs/GcpStorageConfigInfo.md new file mode 100644 index 0000000000..0fa5f202f5 --- /dev/null +++ b/regtests/client/python/docs/GcpStorageConfigInfo.md @@ -0,0 +1,30 @@ +# GcpStorageConfigInfo + +gcp storage configuration info + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**gcs_service_account** | **str** | a Google cloud storage service account | [optional] + +## Example + +```python +from polaris.management.models.gcp_storage_config_info import GcpStorageConfigInfo + +# TODO update the JSON string below +json = "{}" +# create an instance of GcpStorageConfigInfo from a JSON string +gcp_storage_config_info_instance = GcpStorageConfigInfo.from_json(json) +# print the JSON string representation of the object +print(GcpStorageConfigInfo.to_json()) + +# convert the object into a dict +gcp_storage_config_info_dict = gcp_storage_config_info_instance.to_dict() +# create an instance of GcpStorageConfigInfo from a dict +gcp_storage_config_info_from_dict = GcpStorageConfigInfo.from_dict(gcp_storage_config_info_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/GetNamespaceResponse.md b/regtests/client/python/docs/GetNamespaceResponse.md new file mode 100644 index 0000000000..f751daeb09 --- /dev/null +++ b/regtests/client/python/docs/GetNamespaceResponse.md @@ -0,0 +1,30 @@ +# GetNamespaceResponse + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**namespace** | **List[str]** | Reference to one or more levels of a namespace | +**properties** | **Dict[str, str]** | Properties stored on the namespace, if supported by the server. If the server does not support namespace properties, it should return null for this field. If namespace properties are supported, but none are set, it should return an empty object. | [optional] + +## Example + +```python +from polaris.catalog.models.get_namespace_response import GetNamespaceResponse + +# TODO update the JSON string below +json = "{}" +# create an instance of GetNamespaceResponse from a JSON string +get_namespace_response_instance = GetNamespaceResponse.from_json(json) +# print the JSON string representation of the object +print(GetNamespaceResponse.to_json()) + +# convert the object into a dict +get_namespace_response_dict = get_namespace_response_instance.to_dict() +# create an instance of GetNamespaceResponse from a dict +get_namespace_response_from_dict = GetNamespaceResponse.from_dict(get_namespace_response_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/GrantCatalogRoleRequest.md b/regtests/client/python/docs/GrantCatalogRoleRequest.md new file mode 100644 index 0000000000..74f95c1ee0 --- /dev/null +++ b/regtests/client/python/docs/GrantCatalogRoleRequest.md @@ -0,0 +1,29 @@ +# GrantCatalogRoleRequest + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**catalog_role** | [**CatalogRole**](CatalogRole.md) | | [optional] + +## Example + +```python +from polaris.management.models.grant_catalog_role_request import GrantCatalogRoleRequest + +# TODO update the JSON string below +json = "{}" +# create an instance of GrantCatalogRoleRequest from a JSON string +grant_catalog_role_request_instance = GrantCatalogRoleRequest.from_json(json) +# print the JSON string representation of the object +print(GrantCatalogRoleRequest.to_json()) + +# convert the object into a dict +grant_catalog_role_request_dict = grant_catalog_role_request_instance.to_dict() +# create an instance of GrantCatalogRoleRequest from a dict +grant_catalog_role_request_from_dict = GrantCatalogRoleRequest.from_dict(grant_catalog_role_request_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/GrantPrincipalRoleRequest.md b/regtests/client/python/docs/GrantPrincipalRoleRequest.md new file mode 100644 index 0000000000..58c780072f --- /dev/null +++ b/regtests/client/python/docs/GrantPrincipalRoleRequest.md @@ -0,0 +1,29 @@ +# GrantPrincipalRoleRequest + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**principal_role** | [**PrincipalRole**](PrincipalRole.md) | | [optional] + +## Example + +```python +from polaris.management.models.grant_principal_role_request import GrantPrincipalRoleRequest + +# TODO update the JSON string below +json = "{}" +# create an instance of GrantPrincipalRoleRequest from a JSON string +grant_principal_role_request_instance = GrantPrincipalRoleRequest.from_json(json) +# print the JSON string representation of the object +print(GrantPrincipalRoleRequest.to_json()) + +# convert the object into a dict +grant_principal_role_request_dict = grant_principal_role_request_instance.to_dict() +# create an instance of GrantPrincipalRoleRequest from a dict +grant_principal_role_request_from_dict = GrantPrincipalRoleRequest.from_dict(grant_principal_role_request_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/GrantResource.md b/regtests/client/python/docs/GrantResource.md new file mode 100644 index 0000000000..5f6b10e18b --- /dev/null +++ b/regtests/client/python/docs/GrantResource.md @@ -0,0 +1,29 @@ +# GrantResource + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**type** | **str** | | + +## Example + +```python +from polaris.management.models.grant_resource import GrantResource + +# TODO update the JSON string below +json = "{}" +# create an instance of GrantResource from a JSON string +grant_resource_instance = GrantResource.from_json(json) +# print the JSON string representation of the object +print(GrantResource.to_json()) + +# convert the object into a dict +grant_resource_dict = grant_resource_instance.to_dict() +# create an instance of GrantResource from a dict +grant_resource_from_dict = GrantResource.from_dict(grant_resource_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/GrantResources.md b/regtests/client/python/docs/GrantResources.md new file mode 100644 index 0000000000..26708628a8 --- /dev/null +++ b/regtests/client/python/docs/GrantResources.md @@ -0,0 +1,29 @@ +# GrantResources + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**grants** | [**List[GrantResource]**](GrantResource.md) | | + +## Example + +```python +from polaris.management.models.grant_resources import GrantResources + +# TODO update the JSON string below +json = "{}" +# create an instance of GrantResources from a JSON string +grant_resources_instance = GrantResources.from_json(json) +# print the JSON string representation of the object +print(GrantResources.to_json()) + +# convert the object into a dict +grant_resources_dict = grant_resources_instance.to_dict() +# create an instance of GrantResources from a dict +grant_resources_from_dict = GrantResources.from_dict(grant_resources_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/IcebergCatalogAPI.md b/regtests/client/python/docs/IcebergCatalogAPI.md new file mode 100644 index 0000000000..3630897c0a --- /dev/null +++ b/regtests/client/python/docs/IcebergCatalogAPI.md @@ -0,0 +1,2240 @@ +# polaris.catalog.IcebergCatalogAPI + +All URIs are relative to *https://localhost* + +Method | HTTP request | Description +------------- | ------------- | ------------- +[**commit_transaction**](IcebergCatalogAPI.md#commit_transaction) | **POST** /v1/{prefix}/transactions/commit | Commit updates to multiple tables in an atomic operation +[**create_namespace**](IcebergCatalogAPI.md#create_namespace) | **POST** /v1/{prefix}/namespaces | Create a namespace +[**create_table**](IcebergCatalogAPI.md#create_table) | **POST** /v1/{prefix}/namespaces/{namespace}/tables | Create a table in the given namespace +[**create_view**](IcebergCatalogAPI.md#create_view) | **POST** /v1/{prefix}/namespaces/{namespace}/views | Create a view in the given namespace +[**drop_namespace**](IcebergCatalogAPI.md#drop_namespace) | **DELETE** /v1/{prefix}/namespaces/{namespace} | Drop a namespace from the catalog. Namespace must be empty. +[**drop_table**](IcebergCatalogAPI.md#drop_table) | **DELETE** /v1/{prefix}/namespaces/{namespace}/tables/{table} | Drop a table from the catalog +[**drop_view**](IcebergCatalogAPI.md#drop_view) | **DELETE** /v1/{prefix}/namespaces/{namespace}/views/{view} | Drop a view from the catalog +[**list_namespaces**](IcebergCatalogAPI.md#list_namespaces) | **GET** /v1/{prefix}/namespaces | List namespaces, optionally providing a parent namespace to list underneath +[**list_tables**](IcebergCatalogAPI.md#list_tables) | **GET** /v1/{prefix}/namespaces/{namespace}/tables | List all table identifiers underneath a given namespace +[**list_views**](IcebergCatalogAPI.md#list_views) | **GET** /v1/{prefix}/namespaces/{namespace}/views | List all view identifiers underneath a given namespace +[**load_namespace_metadata**](IcebergCatalogAPI.md#load_namespace_metadata) | **GET** /v1/{prefix}/namespaces/{namespace} | Load the metadata properties for a namespace +[**load_table**](IcebergCatalogAPI.md#load_table) | **GET** /v1/{prefix}/namespaces/{namespace}/tables/{table} | Load a table from the catalog +[**load_view**](IcebergCatalogAPI.md#load_view) | **GET** /v1/{prefix}/namespaces/{namespace}/views/{view} | Load a view from the catalog +[**namespace_exists**](IcebergCatalogAPI.md#namespace_exists) | **HEAD** /v1/{prefix}/namespaces/{namespace} | Check if a namespace exists +[**register_table**](IcebergCatalogAPI.md#register_table) | **POST** /v1/{prefix}/namespaces/{namespace}/register | Register a table in the given namespace using given metadata file location +[**rename_table**](IcebergCatalogAPI.md#rename_table) | **POST** /v1/{prefix}/tables/rename | Rename a table from its current name to a new name +[**rename_view**](IcebergCatalogAPI.md#rename_view) | **POST** /v1/{prefix}/views/rename | Rename a view from its current name to a new name +[**replace_view**](IcebergCatalogAPI.md#replace_view) | **POST** /v1/{prefix}/namespaces/{namespace}/views/{view} | Replace a view +[**report_metrics**](IcebergCatalogAPI.md#report_metrics) | **POST** /v1/{prefix}/namespaces/{namespace}/tables/{table}/metrics | Send a metrics report to this endpoint to be processed by the backend +[**send_notification**](IcebergCatalogAPI.md#send_notification) | **POST** /v1/{prefix}/namespaces/{namespace}/tables/{table}/notifications | Sends a notification to the table +[**table_exists**](IcebergCatalogAPI.md#table_exists) | **HEAD** /v1/{prefix}/namespaces/{namespace}/tables/{table} | Check if a table exists +[**update_properties**](IcebergCatalogAPI.md#update_properties) | **POST** /v1/{prefix}/namespaces/{namespace}/properties | Set or remove properties on a namespace +[**update_table**](IcebergCatalogAPI.md#update_table) | **POST** /v1/{prefix}/namespaces/{namespace}/tables/{table} | Commit updates to a table +[**view_exists**](IcebergCatalogAPI.md#view_exists) | **HEAD** /v1/{prefix}/namespaces/{namespace}/views/{view} | Check if a view exists + + +# **commit_transaction** +> commit_transaction(prefix, commit_transaction_request) + +Commit updates to multiple tables in an atomic operation + +### Example + +* OAuth Authentication (OAuth2): +* Bearer Authentication (BearerAuth): + +```python +import polaris.catalog +from polaris.catalog.models.commit_transaction_request import CommitTransactionRequest +from polaris.catalog.rest import ApiException +from pprint import pprint + +# Defining the host is optional and defaults to https://localhost +# See configuration.py for a list of all supported configuration parameters. +configuration = polaris.catalog.Configuration( + host = "https://localhost" +) + +# The client must configure the authentication and authorization parameters +# in accordance with the API server security policy. +# Examples for each auth method are provided below, use the example that +# satisfies your auth use case. + +configuration.access_token = os.environ["ACCESS_TOKEN"] + +# Configure Bearer authorization: BearerAuth +configuration = polaris.catalog.Configuration( + access_token = os.environ["BEARER_TOKEN"] +) + +# Enter a context with an instance of the API client +with polaris.catalog.ApiClient(configuration) as api_client: + # Create an instance of the API class + api_instance = polaris.catalog.IcebergCatalogAPI(api_client) + prefix = 'prefix_example' # str | An optional prefix in the path + commit_transaction_request = polaris.catalog.CommitTransactionRequest() # CommitTransactionRequest | Commit updates to multiple tables in an atomic operation A commit for a single table consists of a table identifier with requirements and updates. Requirements are assertions that will be validated before attempting to make and commit changes. For example, `assert-ref-snapshot-id` will check that a named ref's snapshot ID has a certain value. Updates are changes to make to table metadata. For example, after asserting that the current main ref is at the expected snapshot, a commit may add a new child snapshot and set the ref to the new snapshot id. + + try: + # Commit updates to multiple tables in an atomic operation + api_instance.commit_transaction(prefix, commit_transaction_request) + except Exception as e: + print("Exception when calling IcebergCatalogAPI->commit_transaction: %s\n" % e) +``` + + + +### Parameters + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **prefix** | **str**| An optional prefix in the path | + **commit_transaction_request** | [**CommitTransactionRequest**](CommitTransactionRequest.md)| Commit updates to multiple tables in an atomic operation A commit for a single table consists of a table identifier with requirements and updates. Requirements are assertions that will be validated before attempting to make and commit changes. For example, `assert-ref-snapshot-id` will check that a named ref's snapshot ID has a certain value. Updates are changes to make to table metadata. For example, after asserting that the current main ref is at the expected snapshot, a commit may add a new child snapshot and set the ref to the new snapshot id. | + +### Return type + +void (empty response body) + +### Authorization + +[OAuth2](../README.md#OAuth2), [BearerAuth](../README.md#BearerAuth) + +### HTTP request headers + + - **Content-Type**: application/json + - **Accept**: application/json + +### HTTP response details + +| Status code | Description | Response headers | +|-------------|-------------|------------------| +**204** | Success, no content | - | +**400** | Indicates a bad request error. It could be caused by an unexpected request body format or other forms of request validation failure, such as invalid json. Usually serves application/json content, although in some cases simple text/plain content might be returned by the server's middleware. | - | +**401** | Unauthorized. Authentication is required and has failed or has not yet been provided. | - | +**403** | Forbidden. Authenticated user does not have the necessary permissions. | - | +**404** | Not Found - NoSuchTableException, table to load does not exist | - | +**409** | Conflict - CommitFailedException, one or more requirements failed. The client may retry. | - | +**419** | Credentials have timed out. If possible, the client should refresh credentials and retry. | - | +**500** | An unknown server-side problem occurred; the commit state is unknown. | - | +**503** | The service is not ready to handle the request. The client should wait and retry. The service may additionally send a Retry-After header to indicate when to retry. | - | +**502** | A gateway or proxy received an invalid response from the upstream server; the commit state is unknown. | - | +**504** | A server-side gateway timeout occurred; the commit state is unknown. | - | +**5XX** | A server-side problem that might not be addressable on the client. | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **create_namespace** +> CreateNamespaceResponse create_namespace(prefix, create_namespace_request) + +Create a namespace + +Create a namespace, with an optional set of properties. The server might also add properties, such as `last_modified_time` etc. + +### Example + +* OAuth Authentication (OAuth2): +* Bearer Authentication (BearerAuth): + +```python +import polaris.catalog +from polaris.catalog.models.create_namespace_request import CreateNamespaceRequest +from polaris.catalog.models.create_namespace_response import CreateNamespaceResponse +from polaris.catalog.rest import ApiException +from pprint import pprint + +# Defining the host is optional and defaults to https://localhost +# See configuration.py for a list of all supported configuration parameters. +configuration = polaris.catalog.Configuration( + host = "https://localhost" +) + +# The client must configure the authentication and authorization parameters +# in accordance with the API server security policy. +# Examples for each auth method are provided below, use the example that +# satisfies your auth use case. + +configuration.access_token = os.environ["ACCESS_TOKEN"] + +# Configure Bearer authorization: BearerAuth +configuration = polaris.catalog.Configuration( + access_token = os.environ["BEARER_TOKEN"] +) + +# Enter a context with an instance of the API client +with polaris.catalog.ApiClient(configuration) as api_client: + # Create an instance of the API class + api_instance = polaris.catalog.IcebergCatalogAPI(api_client) + prefix = 'prefix_example' # str | An optional prefix in the path + create_namespace_request = polaris.catalog.CreateNamespaceRequest() # CreateNamespaceRequest | + + try: + # Create a namespace + api_response = api_instance.create_namespace(prefix, create_namespace_request) + print("The response of IcebergCatalogAPI->create_namespace:\n") + pprint(api_response) + except Exception as e: + print("Exception when calling IcebergCatalogAPI->create_namespace: %s\n" % e) +``` + + + +### Parameters + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **prefix** | **str**| An optional prefix in the path | + **create_namespace_request** | [**CreateNamespaceRequest**](CreateNamespaceRequest.md)| | + +### Return type + +[**CreateNamespaceResponse**](CreateNamespaceResponse.md) + +### Authorization + +[OAuth2](../README.md#OAuth2), [BearerAuth](../README.md#BearerAuth) + +### HTTP request headers + + - **Content-Type**: application/json + - **Accept**: application/json + +### HTTP response details + +| Status code | Description | Response headers | +|-------------|-------------|------------------| +**200** | Represents a successful call to create a namespace. Returns the namespace created, as well as any properties that were stored for the namespace, including those the server might have added. Implementations are not required to support namespace properties. | - | +**400** | Indicates a bad request error. It could be caused by an unexpected request body format or other forms of request validation failure, such as invalid json. Usually serves application/json content, although in some cases simple text/plain content might be returned by the server's middleware. | - | +**401** | Unauthorized. Authentication is required and has failed or has not yet been provided. | - | +**403** | Forbidden. Authenticated user does not have the necessary permissions. | - | +**406** | Not Acceptable / Unsupported Operation. The server does not support this operation. | - | +**409** | Conflict - The namespace already exists | - | +**419** | Credentials have timed out. If possible, the client should refresh credentials and retry. | - | +**503** | The service is not ready to handle the request. The client should wait and retry. The service may additionally send a Retry-After header to indicate when to retry. | - | +**5XX** | A server-side problem that might not be addressable from the client side. Used for server 5xx errors without more specific documentation in individual routes. | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **create_table** +> LoadTableResult create_table(prefix, namespace, create_table_request, x_iceberg_access_delegation=x_iceberg_access_delegation) + +Create a table in the given namespace + +Create a table or start a create transaction, like atomic CTAS. If `stage-create` is false, the table is created immediately. If `stage-create` is true, the table is not created, but table metadata is initialized and returned. The service should prepare as needed for a commit to the table commit endpoint to complete the create transaction. The client uses the returned metadata to begin a transaction. To commit the transaction, the client sends all create and subsequent changes to the table commit route. Changes from the table create operation include changes like AddSchemaUpdate and SetCurrentSchemaUpdate that set the initial table state. + +### Example + +* OAuth Authentication (OAuth2): +* Bearer Authentication (BearerAuth): + +```python +import polaris.catalog +from polaris.catalog.models.create_table_request import CreateTableRequest +from polaris.catalog.models.load_table_result import LoadTableResult +from polaris.catalog.rest import ApiException +from pprint import pprint + +# Defining the host is optional and defaults to https://localhost +# See configuration.py for a list of all supported configuration parameters. +configuration = polaris.catalog.Configuration( + host = "https://localhost" +) + +# The client must configure the authentication and authorization parameters +# in accordance with the API server security policy. +# Examples for each auth method are provided below, use the example that +# satisfies your auth use case. + +configuration.access_token = os.environ["ACCESS_TOKEN"] + +# Configure Bearer authorization: BearerAuth +configuration = polaris.catalog.Configuration( + access_token = os.environ["BEARER_TOKEN"] +) + +# Enter a context with an instance of the API client +with polaris.catalog.ApiClient(configuration) as api_client: + # Create an instance of the API class + api_instance = polaris.catalog.IcebergCatalogAPI(api_client) + prefix = 'prefix_example' # str | An optional prefix in the path + namespace = 'accounting' # str | A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. + create_table_request = polaris.catalog.CreateTableRequest() # CreateTableRequest | + x_iceberg_access_delegation = 'vended-credentials,remote-signing' # str | Optional signal to the server that the client supports delegated access via a comma-separated list of access mechanisms. The server may choose to supply access via any or none of the requested mechanisms. Specific properties and handling for `vended-credentials` is documented in the `LoadTableResult` schema section of this spec document. The protocol and specification for `remote-signing` is documented in the `s3-signer-open-api.yaml` OpenApi spec in the `aws` module. (optional) + + try: + # Create a table in the given namespace + api_response = api_instance.create_table(prefix, namespace, create_table_request, x_iceberg_access_delegation=x_iceberg_access_delegation) + print("The response of IcebergCatalogAPI->create_table:\n") + pprint(api_response) + except Exception as e: + print("Exception when calling IcebergCatalogAPI->create_table: %s\n" % e) +``` + + + +### Parameters + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **prefix** | **str**| An optional prefix in the path | + **namespace** | **str**| A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. | + **create_table_request** | [**CreateTableRequest**](CreateTableRequest.md)| | + **x_iceberg_access_delegation** | **str**| Optional signal to the server that the client supports delegated access via a comma-separated list of access mechanisms. The server may choose to supply access via any or none of the requested mechanisms. Specific properties and handling for `vended-credentials` is documented in the `LoadTableResult` schema section of this spec document. The protocol and specification for `remote-signing` is documented in the `s3-signer-open-api.yaml` OpenApi spec in the `aws` module. | [optional] + +### Return type + +[**LoadTableResult**](LoadTableResult.md) + +### Authorization + +[OAuth2](../README.md#OAuth2), [BearerAuth](../README.md#BearerAuth) + +### HTTP request headers + + - **Content-Type**: application/json + - **Accept**: application/json + +### HTTP response details + +| Status code | Description | Response headers | +|-------------|-------------|------------------| +**200** | Table metadata result after creating a table | - | +**400** | Indicates a bad request error. It could be caused by an unexpected request body format or other forms of request validation failure, such as invalid json. Usually serves application/json content, although in some cases simple text/plain content might be returned by the server's middleware. | - | +**401** | Unauthorized. Authentication is required and has failed or has not yet been provided. | - | +**403** | Forbidden. Authenticated user does not have the necessary permissions. | - | +**404** | Not Found - The namespace specified does not exist | - | +**409** | Conflict - The table already exists | - | +**419** | Credentials have timed out. If possible, the client should refresh credentials and retry. | - | +**503** | The service is not ready to handle the request. The client should wait and retry. The service may additionally send a Retry-After header to indicate when to retry. | - | +**5XX** | A server-side problem that might not be addressable from the client side. Used for server 5xx errors without more specific documentation in individual routes. | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **create_view** +> LoadViewResult create_view(prefix, namespace, create_view_request) + +Create a view in the given namespace + +Create a view in the given namespace. + +### Example + +* OAuth Authentication (OAuth2): +* Bearer Authentication (BearerAuth): + +```python +import polaris.catalog +from polaris.catalog.models.create_view_request import CreateViewRequest +from polaris.catalog.models.load_view_result import LoadViewResult +from polaris.catalog.rest import ApiException +from pprint import pprint + +# Defining the host is optional and defaults to https://localhost +# See configuration.py for a list of all supported configuration parameters. +configuration = polaris.catalog.Configuration( + host = "https://localhost" +) + +# The client must configure the authentication and authorization parameters +# in accordance with the API server security policy. +# Examples for each auth method are provided below, use the example that +# satisfies your auth use case. + +configuration.access_token = os.environ["ACCESS_TOKEN"] + +# Configure Bearer authorization: BearerAuth +configuration = polaris.catalog.Configuration( + access_token = os.environ["BEARER_TOKEN"] +) + +# Enter a context with an instance of the API client +with polaris.catalog.ApiClient(configuration) as api_client: + # Create an instance of the API class + api_instance = polaris.catalog.IcebergCatalogAPI(api_client) + prefix = 'prefix_example' # str | An optional prefix in the path + namespace = 'accounting' # str | A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. + create_view_request = polaris.catalog.CreateViewRequest() # CreateViewRequest | + + try: + # Create a view in the given namespace + api_response = api_instance.create_view(prefix, namespace, create_view_request) + print("The response of IcebergCatalogAPI->create_view:\n") + pprint(api_response) + except Exception as e: + print("Exception when calling IcebergCatalogAPI->create_view: %s\n" % e) +``` + + + +### Parameters + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **prefix** | **str**| An optional prefix in the path | + **namespace** | **str**| A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. | + **create_view_request** | [**CreateViewRequest**](CreateViewRequest.md)| | + +### Return type + +[**LoadViewResult**](LoadViewResult.md) + +### Authorization + +[OAuth2](../README.md#OAuth2), [BearerAuth](../README.md#BearerAuth) + +### HTTP request headers + + - **Content-Type**: application/json + - **Accept**: application/json + +### HTTP response details + +| Status code | Description | Response headers | +|-------------|-------------|------------------| +**200** | View metadata result when loading a view | - | +**400** | Indicates a bad request error. It could be caused by an unexpected request body format or other forms of request validation failure, such as invalid json. Usually serves application/json content, although in some cases simple text/plain content might be returned by the server's middleware. | - | +**401** | Unauthorized. Authentication is required and has failed or has not yet been provided. | - | +**403** | Forbidden. Authenticated user does not have the necessary permissions. | - | +**404** | Not Found - The namespace specified does not exist | - | +**409** | Conflict - The view already exists | - | +**419** | Credentials have timed out. If possible, the client should refresh credentials and retry. | - | +**503** | The service is not ready to handle the request. The client should wait and retry. The service may additionally send a Retry-After header to indicate when to retry. | - | +**5XX** | A server-side problem that might not be addressable from the client side. Used for server 5xx errors without more specific documentation in individual routes. | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **drop_namespace** +> drop_namespace(prefix, namespace) + +Drop a namespace from the catalog. Namespace must be empty. + +### Example + +* OAuth Authentication (OAuth2): +* Bearer Authentication (BearerAuth): + +```python +import polaris.catalog +from polaris.catalog.rest import ApiException +from pprint import pprint + +# Defining the host is optional and defaults to https://localhost +# See configuration.py for a list of all supported configuration parameters. +configuration = polaris.catalog.Configuration( + host = "https://localhost" +) + +# The client must configure the authentication and authorization parameters +# in accordance with the API server security policy. +# Examples for each auth method are provided below, use the example that +# satisfies your auth use case. + +configuration.access_token = os.environ["ACCESS_TOKEN"] + +# Configure Bearer authorization: BearerAuth +configuration = polaris.catalog.Configuration( + access_token = os.environ["BEARER_TOKEN"] +) + +# Enter a context with an instance of the API client +with polaris.catalog.ApiClient(configuration) as api_client: + # Create an instance of the API class + api_instance = polaris.catalog.IcebergCatalogAPI(api_client) + prefix = 'prefix_example' # str | An optional prefix in the path + namespace = 'accounting' # str | A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. + + try: + # Drop a namespace from the catalog. Namespace must be empty. + api_instance.drop_namespace(prefix, namespace) + except Exception as e: + print("Exception when calling IcebergCatalogAPI->drop_namespace: %s\n" % e) +``` + + + +### Parameters + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **prefix** | **str**| An optional prefix in the path | + **namespace** | **str**| A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. | + +### Return type + +void (empty response body) + +### Authorization + +[OAuth2](../README.md#OAuth2), [BearerAuth](../README.md#BearerAuth) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + +### HTTP response details + +| Status code | Description | Response headers | +|-------------|-------------|------------------| +**204** | Success, no content | - | +**400** | Indicates a bad request error. It could be caused by an unexpected request body format or other forms of request validation failure, such as invalid json. Usually serves application/json content, although in some cases simple text/plain content might be returned by the server's middleware. | - | +**401** | Unauthorized. Authentication is required and has failed or has not yet been provided. | - | +**403** | Forbidden. Authenticated user does not have the necessary permissions. | - | +**404** | Not Found - Namespace to delete does not exist. | - | +**419** | Credentials have timed out. If possible, the client should refresh credentials and retry. | - | +**503** | The service is not ready to handle the request. The client should wait and retry. The service may additionally send a Retry-After header to indicate when to retry. | - | +**5XX** | A server-side problem that might not be addressable from the client side. Used for server 5xx errors without more specific documentation in individual routes. | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **drop_table** +> drop_table(prefix, namespace, table, purge_requested=purge_requested) + +Drop a table from the catalog + +Remove a table from the catalog + +### Example + +* OAuth Authentication (OAuth2): +* Bearer Authentication (BearerAuth): + +```python +import polaris.catalog +from polaris.catalog.rest import ApiException +from pprint import pprint + +# Defining the host is optional and defaults to https://localhost +# See configuration.py for a list of all supported configuration parameters. +configuration = polaris.catalog.Configuration( + host = "https://localhost" +) + +# The client must configure the authentication and authorization parameters +# in accordance with the API server security policy. +# Examples for each auth method are provided below, use the example that +# satisfies your auth use case. + +configuration.access_token = os.environ["ACCESS_TOKEN"] + +# Configure Bearer authorization: BearerAuth +configuration = polaris.catalog.Configuration( + access_token = os.environ["BEARER_TOKEN"] +) + +# Enter a context with an instance of the API client +with polaris.catalog.ApiClient(configuration) as api_client: + # Create an instance of the API class + api_instance = polaris.catalog.IcebergCatalogAPI(api_client) + prefix = 'prefix_example' # str | An optional prefix in the path + namespace = 'accounting' # str | A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. + table = 'sales' # str | A table name + purge_requested = False # bool | Whether the user requested to purge the underlying table's data and metadata (optional) (default to False) + + try: + # Drop a table from the catalog + api_instance.drop_table(prefix, namespace, table, purge_requested=purge_requested) + except Exception as e: + print("Exception when calling IcebergCatalogAPI->drop_table: %s\n" % e) +``` + + + +### Parameters + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **prefix** | **str**| An optional prefix in the path | + **namespace** | **str**| A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. | + **table** | **str**| A table name | + **purge_requested** | **bool**| Whether the user requested to purge the underlying table's data and metadata | [optional] [default to False] + +### Return type + +void (empty response body) + +### Authorization + +[OAuth2](../README.md#OAuth2), [BearerAuth](../README.md#BearerAuth) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + +### HTTP response details + +| Status code | Description | Response headers | +|-------------|-------------|------------------| +**204** | Success, no content | - | +**400** | Indicates a bad request error. It could be caused by an unexpected request body format or other forms of request validation failure, such as invalid json. Usually serves application/json content, although in some cases simple text/plain content might be returned by the server's middleware. | - | +**401** | Unauthorized. Authentication is required and has failed or has not yet been provided. | - | +**403** | Forbidden. Authenticated user does not have the necessary permissions. | - | +**404** | Not Found - NoSuchTableException, Table to drop does not exist | - | +**419** | Credentials have timed out. If possible, the client should refresh credentials and retry. | - | +**503** | The service is not ready to handle the request. The client should wait and retry. The service may additionally send a Retry-After header to indicate when to retry. | - | +**5XX** | A server-side problem that might not be addressable from the client side. Used for server 5xx errors without more specific documentation in individual routes. | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **drop_view** +> drop_view(prefix, namespace, view) + +Drop a view from the catalog + +Remove a view from the catalog + +### Example + +* OAuth Authentication (OAuth2): +* Bearer Authentication (BearerAuth): + +```python +import polaris.catalog +from polaris.catalog.rest import ApiException +from pprint import pprint + +# Defining the host is optional and defaults to https://localhost +# See configuration.py for a list of all supported configuration parameters. +configuration = polaris.catalog.Configuration( + host = "https://localhost" +) + +# The client must configure the authentication and authorization parameters +# in accordance with the API server security policy. +# Examples for each auth method are provided below, use the example that +# satisfies your auth use case. + +configuration.access_token = os.environ["ACCESS_TOKEN"] + +# Configure Bearer authorization: BearerAuth +configuration = polaris.catalog.Configuration( + access_token = os.environ["BEARER_TOKEN"] +) + +# Enter a context with an instance of the API client +with polaris.catalog.ApiClient(configuration) as api_client: + # Create an instance of the API class + api_instance = polaris.catalog.IcebergCatalogAPI(api_client) + prefix = 'prefix_example' # str | An optional prefix in the path + namespace = 'accounting' # str | A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. + view = 'sales' # str | A view name + + try: + # Drop a view from the catalog + api_instance.drop_view(prefix, namespace, view) + except Exception as e: + print("Exception when calling IcebergCatalogAPI->drop_view: %s\n" % e) +``` + + + +### Parameters + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **prefix** | **str**| An optional prefix in the path | + **namespace** | **str**| A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. | + **view** | **str**| A view name | + +### Return type + +void (empty response body) + +### Authorization + +[OAuth2](../README.md#OAuth2), [BearerAuth](../README.md#BearerAuth) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + +### HTTP response details + +| Status code | Description | Response headers | +|-------------|-------------|------------------| +**204** | Success, no content | - | +**400** | Indicates a bad request error. It could be caused by an unexpected request body format or other forms of request validation failure, such as invalid json. Usually serves application/json content, although in some cases simple text/plain content might be returned by the server's middleware. | - | +**401** | Unauthorized. Authentication is required and has failed or has not yet been provided. | - | +**403** | Forbidden. Authenticated user does not have the necessary permissions. | - | +**404** | Not Found - NoSuchViewException, view to drop does not exist | - | +**419** | Credentials have timed out. If possible, the client should refresh credentials and retry. | - | +**503** | The service is not ready to handle the request. The client should wait and retry. The service may additionally send a Retry-After header to indicate when to retry. | - | +**5XX** | A server-side problem that might not be addressable from the client side. Used for server 5xx errors without more specific documentation in individual routes. | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **list_namespaces** +> ListNamespacesResponse list_namespaces(prefix, page_token=page_token, page_size=page_size, parent=parent) + +List namespaces, optionally providing a parent namespace to list underneath + +List all namespaces at a certain level, optionally starting from a given parent namespace. If table accounting.tax.paid.info exists, using 'SELECT NAMESPACE IN accounting' would translate into `GET /namespaces?parent=accounting` and must return a namespace, [\"accounting\", \"tax\"] only. Using 'SELECT NAMESPACE IN accounting.tax' would translate into `GET /namespaces?parent=accounting%1Ftax` and must return a namespace, [\"accounting\", \"tax\", \"paid\"]. If `parent` is not provided, all top-level namespaces should be listed. + +### Example + +* OAuth Authentication (OAuth2): +* Bearer Authentication (BearerAuth): + +```python +import polaris.catalog +from polaris.catalog.models.list_namespaces_response import ListNamespacesResponse +from polaris.catalog.rest import ApiException +from pprint import pprint + +# Defining the host is optional and defaults to https://localhost +# See configuration.py for a list of all supported configuration parameters. +configuration = polaris.catalog.Configuration( + host = "https://localhost" +) + +# The client must configure the authentication and authorization parameters +# in accordance with the API server security policy. +# Examples for each auth method are provided below, use the example that +# satisfies your auth use case. + +configuration.access_token = os.environ["ACCESS_TOKEN"] + +# Configure Bearer authorization: BearerAuth +configuration = polaris.catalog.Configuration( + access_token = os.environ["BEARER_TOKEN"] +) + +# Enter a context with an instance of the API client +with polaris.catalog.ApiClient(configuration) as api_client: + # Create an instance of the API class + api_instance = polaris.catalog.IcebergCatalogAPI(api_client) + prefix = 'prefix_example' # str | An optional prefix in the path + page_token = 'page_token_example' # str | (optional) + page_size = 56 # int | For servers that support pagination, this signals an upper bound of the number of results that a client will receive. For servers that do not support pagination, clients may receive results larger than the indicated `pageSize`. (optional) + parent = 'accounting%1Ftax' # str | An optional namespace, underneath which to list namespaces. If not provided or empty, all top-level namespaces should be listed. If parent is a multipart namespace, the parts must be separated by the unit separator (`0x1F`) byte. (optional) + + try: + # List namespaces, optionally providing a parent namespace to list underneath + api_response = api_instance.list_namespaces(prefix, page_token=page_token, page_size=page_size, parent=parent) + print("The response of IcebergCatalogAPI->list_namespaces:\n") + pprint(api_response) + except Exception as e: + print("Exception when calling IcebergCatalogAPI->list_namespaces: %s\n" % e) +``` + + + +### Parameters + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **prefix** | **str**| An optional prefix in the path | + **page_token** | **str**| | [optional] + **page_size** | **int**| For servers that support pagination, this signals an upper bound of the number of results that a client will receive. For servers that do not support pagination, clients may receive results larger than the indicated `pageSize`. | [optional] + **parent** | **str**| An optional namespace, underneath which to list namespaces. If not provided or empty, all top-level namespaces should be listed. If parent is a multipart namespace, the parts must be separated by the unit separator (`0x1F`) byte. | [optional] + +### Return type + +[**ListNamespacesResponse**](ListNamespacesResponse.md) + +### Authorization + +[OAuth2](../README.md#OAuth2), [BearerAuth](../README.md#BearerAuth) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + +### HTTP response details + +| Status code | Description | Response headers | +|-------------|-------------|------------------| +**200** | A list of namespaces | - | +**400** | Indicates a bad request error. It could be caused by an unexpected request body format or other forms of request validation failure, such as invalid json. Usually serves application/json content, although in some cases simple text/plain content might be returned by the server's middleware. | - | +**401** | Unauthorized. Authentication is required and has failed or has not yet been provided. | - | +**403** | Forbidden. Authenticated user does not have the necessary permissions. | - | +**404** | Not Found - Namespace provided in the `parent` query parameter is not found. | - | +**419** | Credentials have timed out. If possible, the client should refresh credentials and retry. | - | +**503** | The service is not ready to handle the request. The client should wait and retry. The service may additionally send a Retry-After header to indicate when to retry. | - | +**5XX** | A server-side problem that might not be addressable from the client side. Used for server 5xx errors without more specific documentation in individual routes. | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **list_tables** +> ListTablesResponse list_tables(prefix, namespace, page_token=page_token, page_size=page_size) + +List all table identifiers underneath a given namespace + +Return all table identifiers under this namespace + +### Example + +* OAuth Authentication (OAuth2): +* Bearer Authentication (BearerAuth): + +```python +import polaris.catalog +from polaris.catalog.models.list_tables_response import ListTablesResponse +from polaris.catalog.rest import ApiException +from pprint import pprint + +# Defining the host is optional and defaults to https://localhost +# See configuration.py for a list of all supported configuration parameters. +configuration = polaris.catalog.Configuration( + host = "https://localhost" +) + +# The client must configure the authentication and authorization parameters +# in accordance with the API server security policy. +# Examples for each auth method are provided below, use the example that +# satisfies your auth use case. + +configuration.access_token = os.environ["ACCESS_TOKEN"] + +# Configure Bearer authorization: BearerAuth +configuration = polaris.catalog.Configuration( + access_token = os.environ["BEARER_TOKEN"] +) + +# Enter a context with an instance of the API client +with polaris.catalog.ApiClient(configuration) as api_client: + # Create an instance of the API class + api_instance = polaris.catalog.IcebergCatalogAPI(api_client) + prefix = 'prefix_example' # str | An optional prefix in the path + namespace = 'accounting' # str | A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. + page_token = 'page_token_example' # str | (optional) + page_size = 56 # int | For servers that support pagination, this signals an upper bound of the number of results that a client will receive. For servers that do not support pagination, clients may receive results larger than the indicated `pageSize`. (optional) + + try: + # List all table identifiers underneath a given namespace + api_response = api_instance.list_tables(prefix, namespace, page_token=page_token, page_size=page_size) + print("The response of IcebergCatalogAPI->list_tables:\n") + pprint(api_response) + except Exception as e: + print("Exception when calling IcebergCatalogAPI->list_tables: %s\n" % e) +``` + + + +### Parameters + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **prefix** | **str**| An optional prefix in the path | + **namespace** | **str**| A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. | + **page_token** | **str**| | [optional] + **page_size** | **int**| For servers that support pagination, this signals an upper bound of the number of results that a client will receive. For servers that do not support pagination, clients may receive results larger than the indicated `pageSize`. | [optional] + +### Return type + +[**ListTablesResponse**](ListTablesResponse.md) + +### Authorization + +[OAuth2](../README.md#OAuth2), [BearerAuth](../README.md#BearerAuth) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + +### HTTP response details + +| Status code | Description | Response headers | +|-------------|-------------|------------------| +**200** | A list of table identifiers | - | +**400** | Indicates a bad request error. It could be caused by an unexpected request body format or other forms of request validation failure, such as invalid json. Usually serves application/json content, although in some cases simple text/plain content might be returned by the server's middleware. | - | +**401** | Unauthorized. Authentication is required and has failed or has not yet been provided. | - | +**403** | Forbidden. Authenticated user does not have the necessary permissions. | - | +**404** | Not Found - The namespace specified does not exist | - | +**419** | Credentials have timed out. If possible, the client should refresh credentials and retry. | - | +**503** | The service is not ready to handle the request. The client should wait and retry. The service may additionally send a Retry-After header to indicate when to retry. | - | +**5XX** | A server-side problem that might not be addressable from the client side. Used for server 5xx errors without more specific documentation in individual routes. | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **list_views** +> ListTablesResponse list_views(prefix, namespace, page_token=page_token, page_size=page_size) + +List all view identifiers underneath a given namespace + +Return all view identifiers under this namespace + +### Example + +* OAuth Authentication (OAuth2): +* Bearer Authentication (BearerAuth): + +```python +import polaris.catalog +from polaris.catalog.models.list_tables_response import ListTablesResponse +from polaris.catalog.rest import ApiException +from pprint import pprint + +# Defining the host is optional and defaults to https://localhost +# See configuration.py for a list of all supported configuration parameters. +configuration = polaris.catalog.Configuration( + host = "https://localhost" +) + +# The client must configure the authentication and authorization parameters +# in accordance with the API server security policy. +# Examples for each auth method are provided below, use the example that +# satisfies your auth use case. + +configuration.access_token = os.environ["ACCESS_TOKEN"] + +# Configure Bearer authorization: BearerAuth +configuration = polaris.catalog.Configuration( + access_token = os.environ["BEARER_TOKEN"] +) + +# Enter a context with an instance of the API client +with polaris.catalog.ApiClient(configuration) as api_client: + # Create an instance of the API class + api_instance = polaris.catalog.IcebergCatalogAPI(api_client) + prefix = 'prefix_example' # str | An optional prefix in the path + namespace = 'accounting' # str | A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. + page_token = 'page_token_example' # str | (optional) + page_size = 56 # int | For servers that support pagination, this signals an upper bound of the number of results that a client will receive. For servers that do not support pagination, clients may receive results larger than the indicated `pageSize`. (optional) + + try: + # List all view identifiers underneath a given namespace + api_response = api_instance.list_views(prefix, namespace, page_token=page_token, page_size=page_size) + print("The response of IcebergCatalogAPI->list_views:\n") + pprint(api_response) + except Exception as e: + print("Exception when calling IcebergCatalogAPI->list_views: %s\n" % e) +``` + + + +### Parameters + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **prefix** | **str**| An optional prefix in the path | + **namespace** | **str**| A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. | + **page_token** | **str**| | [optional] + **page_size** | **int**| For servers that support pagination, this signals an upper bound of the number of results that a client will receive. For servers that do not support pagination, clients may receive results larger than the indicated `pageSize`. | [optional] + +### Return type + +[**ListTablesResponse**](ListTablesResponse.md) + +### Authorization + +[OAuth2](../README.md#OAuth2), [BearerAuth](../README.md#BearerAuth) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + +### HTTP response details + +| Status code | Description | Response headers | +|-------------|-------------|------------------| +**200** | A list of table identifiers | - | +**400** | Indicates a bad request error. It could be caused by an unexpected request body format or other forms of request validation failure, such as invalid json. Usually serves application/json content, although in some cases simple text/plain content might be returned by the server's middleware. | - | +**401** | Unauthorized. Authentication is required and has failed or has not yet been provided. | - | +**403** | Forbidden. Authenticated user does not have the necessary permissions. | - | +**404** | Not Found - The namespace specified does not exist | - | +**419** | Credentials have timed out. If possible, the client should refresh credentials and retry. | - | +**503** | The service is not ready to handle the request. The client should wait and retry. The service may additionally send a Retry-After header to indicate when to retry. | - | +**5XX** | A server-side problem that might not be addressable from the client side. Used for server 5xx errors without more specific documentation in individual routes. | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **load_namespace_metadata** +> GetNamespaceResponse load_namespace_metadata(prefix, namespace) + +Load the metadata properties for a namespace + +Return all stored metadata properties for a given namespace + +### Example + +* OAuth Authentication (OAuth2): +* Bearer Authentication (BearerAuth): + +```python +import polaris.catalog +from polaris.catalog.models.get_namespace_response import GetNamespaceResponse +from polaris.catalog.rest import ApiException +from pprint import pprint + +# Defining the host is optional and defaults to https://localhost +# See configuration.py for a list of all supported configuration parameters. +configuration = polaris.catalog.Configuration( + host = "https://localhost" +) + +# The client must configure the authentication and authorization parameters +# in accordance with the API server security policy. +# Examples for each auth method are provided below, use the example that +# satisfies your auth use case. + +configuration.access_token = os.environ["ACCESS_TOKEN"] + +# Configure Bearer authorization: BearerAuth +configuration = polaris.catalog.Configuration( + access_token = os.environ["BEARER_TOKEN"] +) + +# Enter a context with an instance of the API client +with polaris.catalog.ApiClient(configuration) as api_client: + # Create an instance of the API class + api_instance = polaris.catalog.IcebergCatalogAPI(api_client) + prefix = 'prefix_example' # str | An optional prefix in the path + namespace = 'accounting' # str | A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. + + try: + # Load the metadata properties for a namespace + api_response = api_instance.load_namespace_metadata(prefix, namespace) + print("The response of IcebergCatalogAPI->load_namespace_metadata:\n") + pprint(api_response) + except Exception as e: + print("Exception when calling IcebergCatalogAPI->load_namespace_metadata: %s\n" % e) +``` + + + +### Parameters + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **prefix** | **str**| An optional prefix in the path | + **namespace** | **str**| A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. | + +### Return type + +[**GetNamespaceResponse**](GetNamespaceResponse.md) + +### Authorization + +[OAuth2](../README.md#OAuth2), [BearerAuth](../README.md#BearerAuth) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + +### HTTP response details + +| Status code | Description | Response headers | +|-------------|-------------|------------------| +**200** | Returns a namespace, as well as any properties stored on the namespace if namespace properties are supported by the server. | - | +**400** | Indicates a bad request error. It could be caused by an unexpected request body format or other forms of request validation failure, such as invalid json. Usually serves application/json content, although in some cases simple text/plain content might be returned by the server's middleware. | - | +**401** | Unauthorized. Authentication is required and has failed or has not yet been provided. | - | +**403** | Forbidden. Authenticated user does not have the necessary permissions. | - | +**404** | Not Found - Namespace not found | - | +**419** | Credentials have timed out. If possible, the client should refresh credentials and retry. | - | +**503** | The service is not ready to handle the request. The client should wait and retry. The service may additionally send a Retry-After header to indicate when to retry. | - | +**5XX** | A server-side problem that might not be addressable from the client side. Used for server 5xx errors without more specific documentation in individual routes. | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **load_table** +> LoadTableResult load_table(prefix, namespace, table, x_iceberg_access_delegation=x_iceberg_access_delegation, snapshots=snapshots) + +Load a table from the catalog + +Load a table from the catalog. The response contains both configuration and table metadata. The configuration, if non-empty is used as additional configuration for the table that overrides catalog configuration. For example, this configuration may change the FileIO implementation to be used for the table. The response also contains the table's full metadata, matching the table metadata JSON file. The catalog configuration may contain credentials that should be used for subsequent requests for the table. The configuration key \"token\" is used to pass an access token to be used as a bearer token for table requests. Otherwise, a token may be passed using a RFC 8693 token type as a configuration key. For example, \"urn:ietf:params:oauth:token-type:jwt=\". + +### Example + +* OAuth Authentication (OAuth2): +* Bearer Authentication (BearerAuth): + +```python +import polaris.catalog +from polaris.catalog.models.load_table_result import LoadTableResult +from polaris.catalog.rest import ApiException +from pprint import pprint + +# Defining the host is optional and defaults to https://localhost +# See configuration.py for a list of all supported configuration parameters. +configuration = polaris.catalog.Configuration( + host = "https://localhost" +) + +# The client must configure the authentication and authorization parameters +# in accordance with the API server security policy. +# Examples for each auth method are provided below, use the example that +# satisfies your auth use case. + +configuration.access_token = os.environ["ACCESS_TOKEN"] + +# Configure Bearer authorization: BearerAuth +configuration = polaris.catalog.Configuration( + access_token = os.environ["BEARER_TOKEN"] +) + +# Enter a context with an instance of the API client +with polaris.catalog.ApiClient(configuration) as api_client: + # Create an instance of the API class + api_instance = polaris.catalog.IcebergCatalogAPI(api_client) + prefix = 'prefix_example' # str | An optional prefix in the path + namespace = 'accounting' # str | A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. + table = 'sales' # str | A table name + x_iceberg_access_delegation = 'vended-credentials,remote-signing' # str | Optional signal to the server that the client supports delegated access via a comma-separated list of access mechanisms. The server may choose to supply access via any or none of the requested mechanisms. Specific properties and handling for `vended-credentials` is documented in the `LoadTableResult` schema section of this spec document. The protocol and specification for `remote-signing` is documented in the `s3-signer-open-api.yaml` OpenApi spec in the `aws` module. (optional) + snapshots = 'snapshots_example' # str | The snapshots to return in the body of the metadata. Setting the value to `all` would return the full set of snapshots currently valid for the table. Setting the value to `refs` would load all snapshots referenced by branches or tags. Default if no param is provided is `all`. (optional) + + try: + # Load a table from the catalog + api_response = api_instance.load_table(prefix, namespace, table, x_iceberg_access_delegation=x_iceberg_access_delegation, snapshots=snapshots) + print("The response of IcebergCatalogAPI->load_table:\n") + pprint(api_response) + except Exception as e: + print("Exception when calling IcebergCatalogAPI->load_table: %s\n" % e) +``` + + + +### Parameters + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **prefix** | **str**| An optional prefix in the path | + **namespace** | **str**| A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. | + **table** | **str**| A table name | + **x_iceberg_access_delegation** | **str**| Optional signal to the server that the client supports delegated access via a comma-separated list of access mechanisms. The server may choose to supply access via any or none of the requested mechanisms. Specific properties and handling for `vended-credentials` is documented in the `LoadTableResult` schema section of this spec document. The protocol and specification for `remote-signing` is documented in the `s3-signer-open-api.yaml` OpenApi spec in the `aws` module. | [optional] + **snapshots** | **str**| The snapshots to return in the body of the metadata. Setting the value to `all` would return the full set of snapshots currently valid for the table. Setting the value to `refs` would load all snapshots referenced by branches or tags. Default if no param is provided is `all`. | [optional] + +### Return type + +[**LoadTableResult**](LoadTableResult.md) + +### Authorization + +[OAuth2](../README.md#OAuth2), [BearerAuth](../README.md#BearerAuth) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + +### HTTP response details + +| Status code | Description | Response headers | +|-------------|-------------|------------------| +**200** | Table metadata result when loading a table | - | +**400** | Indicates a bad request error. It could be caused by an unexpected request body format or other forms of request validation failure, such as invalid json. Usually serves application/json content, although in some cases simple text/plain content might be returned by the server's middleware. | - | +**401** | Unauthorized. Authentication is required and has failed or has not yet been provided. | - | +**403** | Forbidden. Authenticated user does not have the necessary permissions. | - | +**404** | Not Found - NoSuchTableException, table to load does not exist | - | +**419** | Credentials have timed out. If possible, the client should refresh credentials and retry. | - | +**503** | The service is not ready to handle the request. The client should wait and retry. The service may additionally send a Retry-After header to indicate when to retry. | - | +**5XX** | A server-side problem that might not be addressable from the client side. Used for server 5xx errors without more specific documentation in individual routes. | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **load_view** +> LoadViewResult load_view(prefix, namespace, view) + +Load a view from the catalog + +Load a view from the catalog. The response contains both configuration and view metadata. The configuration, if non-empty is used as additional configuration for the view that overrides catalog configuration. The response also contains the view's full metadata, matching the view metadata JSON file. The catalog configuration may contain credentials that should be used for subsequent requests for the view. The configuration key \"token\" is used to pass an access token to be used as a bearer token for view requests. Otherwise, a token may be passed using a RFC 8693 token type as a configuration key. For example, \"urn:ietf:params:oauth:token-type:jwt=\". + +### Example + +* OAuth Authentication (OAuth2): +* Bearer Authentication (BearerAuth): + +```python +import polaris.catalog +from polaris.catalog.models.load_view_result import LoadViewResult +from polaris.catalog.rest import ApiException +from pprint import pprint + +# Defining the host is optional and defaults to https://localhost +# See configuration.py for a list of all supported configuration parameters. +configuration = polaris.catalog.Configuration( + host = "https://localhost" +) + +# The client must configure the authentication and authorization parameters +# in accordance with the API server security policy. +# Examples for each auth method are provided below, use the example that +# satisfies your auth use case. + +configuration.access_token = os.environ["ACCESS_TOKEN"] + +# Configure Bearer authorization: BearerAuth +configuration = polaris.catalog.Configuration( + access_token = os.environ["BEARER_TOKEN"] +) + +# Enter a context with an instance of the API client +with polaris.catalog.ApiClient(configuration) as api_client: + # Create an instance of the API class + api_instance = polaris.catalog.IcebergCatalogAPI(api_client) + prefix = 'prefix_example' # str | An optional prefix in the path + namespace = 'accounting' # str | A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. + view = 'sales' # str | A view name + + try: + # Load a view from the catalog + api_response = api_instance.load_view(prefix, namespace, view) + print("The response of IcebergCatalogAPI->load_view:\n") + pprint(api_response) + except Exception as e: + print("Exception when calling IcebergCatalogAPI->load_view: %s\n" % e) +``` + + + +### Parameters + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **prefix** | **str**| An optional prefix in the path | + **namespace** | **str**| A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. | + **view** | **str**| A view name | + +### Return type + +[**LoadViewResult**](LoadViewResult.md) + +### Authorization + +[OAuth2](../README.md#OAuth2), [BearerAuth](../README.md#BearerAuth) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + +### HTTP response details + +| Status code | Description | Response headers | +|-------------|-------------|------------------| +**200** | View metadata result when loading a view | - | +**400** | Indicates a bad request error. It could be caused by an unexpected request body format or other forms of request validation failure, such as invalid json. Usually serves application/json content, although in some cases simple text/plain content might be returned by the server's middleware. | - | +**401** | Unauthorized. Authentication is required and has failed or has not yet been provided. | - | +**403** | Forbidden. Authenticated user does not have the necessary permissions. | - | +**404** | Not Found - NoSuchViewException, view to load does not exist | - | +**419** | Credentials have timed out. If possible, the client should refresh credentials and retry. | - | +**503** | The service is not ready to handle the request. The client should wait and retry. The service may additionally send a Retry-After header to indicate when to retry. | - | +**5XX** | A server-side problem that might not be addressable from the client side. Used for server 5xx errors without more specific documentation in individual routes. | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **namespace_exists** +> namespace_exists(prefix, namespace) + +Check if a namespace exists + +Check if a namespace exists. The response does not contain a body. + +### Example + +* OAuth Authentication (OAuth2): +* Bearer Authentication (BearerAuth): + +```python +import polaris.catalog +from polaris.catalog.rest import ApiException +from pprint import pprint + +# Defining the host is optional and defaults to https://localhost +# See configuration.py for a list of all supported configuration parameters. +configuration = polaris.catalog.Configuration( + host = "https://localhost" +) + +# The client must configure the authentication and authorization parameters +# in accordance with the API server security policy. +# Examples for each auth method are provided below, use the example that +# satisfies your auth use case. + +configuration.access_token = os.environ["ACCESS_TOKEN"] + +# Configure Bearer authorization: BearerAuth +configuration = polaris.catalog.Configuration( + access_token = os.environ["BEARER_TOKEN"] +) + +# Enter a context with an instance of the API client +with polaris.catalog.ApiClient(configuration) as api_client: + # Create an instance of the API class + api_instance = polaris.catalog.IcebergCatalogAPI(api_client) + prefix = 'prefix_example' # str | An optional prefix in the path + namespace = 'accounting' # str | A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. + + try: + # Check if a namespace exists + api_instance.namespace_exists(prefix, namespace) + except Exception as e: + print("Exception when calling IcebergCatalogAPI->namespace_exists: %s\n" % e) +``` + + + +### Parameters + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **prefix** | **str**| An optional prefix in the path | + **namespace** | **str**| A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. | + +### Return type + +void (empty response body) + +### Authorization + +[OAuth2](../README.md#OAuth2), [BearerAuth](../README.md#BearerAuth) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + +### HTTP response details + +| Status code | Description | Response headers | +|-------------|-------------|------------------| +**204** | Success, no content | - | +**400** | Indicates a bad request error. It could be caused by an unexpected request body format or other forms of request validation failure, such as invalid json. Usually serves application/json content, although in some cases simple text/plain content might be returned by the server's middleware. | - | +**401** | Unauthorized. Authentication is required and has failed or has not yet been provided. | - | +**403** | Forbidden. Authenticated user does not have the necessary permissions. | - | +**404** | Not Found - Namespace not found | - | +**419** | Credentials have timed out. If possible, the client should refresh credentials and retry. | - | +**503** | The service is not ready to handle the request. The client should wait and retry. The service may additionally send a Retry-After header to indicate when to retry. | - | +**5XX** | A server-side problem that might not be addressable from the client side. Used for server 5xx errors without more specific documentation in individual routes. | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **register_table** +> LoadTableResult register_table(prefix, namespace, register_table_request) + +Register a table in the given namespace using given metadata file location + +Register a table using given metadata file location. + +### Example + +* OAuth Authentication (OAuth2): +* Bearer Authentication (BearerAuth): + +```python +import polaris.catalog +from polaris.catalog.models.load_table_result import LoadTableResult +from polaris.catalog.models.register_table_request import RegisterTableRequest +from polaris.catalog.rest import ApiException +from pprint import pprint + +# Defining the host is optional and defaults to https://localhost +# See configuration.py for a list of all supported configuration parameters. +configuration = polaris.catalog.Configuration( + host = "https://localhost" +) + +# The client must configure the authentication and authorization parameters +# in accordance with the API server security policy. +# Examples for each auth method are provided below, use the example that +# satisfies your auth use case. + +configuration.access_token = os.environ["ACCESS_TOKEN"] + +# Configure Bearer authorization: BearerAuth +configuration = polaris.catalog.Configuration( + access_token = os.environ["BEARER_TOKEN"] +) + +# Enter a context with an instance of the API client +with polaris.catalog.ApiClient(configuration) as api_client: + # Create an instance of the API class + api_instance = polaris.catalog.IcebergCatalogAPI(api_client) + prefix = 'prefix_example' # str | An optional prefix in the path + namespace = 'accounting' # str | A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. + register_table_request = polaris.catalog.RegisterTableRequest() # RegisterTableRequest | + + try: + # Register a table in the given namespace using given metadata file location + api_response = api_instance.register_table(prefix, namespace, register_table_request) + print("The response of IcebergCatalogAPI->register_table:\n") + pprint(api_response) + except Exception as e: + print("Exception when calling IcebergCatalogAPI->register_table: %s\n" % e) +``` + + + +### Parameters + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **prefix** | **str**| An optional prefix in the path | + **namespace** | **str**| A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. | + **register_table_request** | [**RegisterTableRequest**](RegisterTableRequest.md)| | + +### Return type + +[**LoadTableResult**](LoadTableResult.md) + +### Authorization + +[OAuth2](../README.md#OAuth2), [BearerAuth](../README.md#BearerAuth) + +### HTTP request headers + + - **Content-Type**: application/json + - **Accept**: application/json + +### HTTP response details + +| Status code | Description | Response headers | +|-------------|-------------|------------------| +**200** | Table metadata result when loading a table | - | +**400** | Indicates a bad request error. It could be caused by an unexpected request body format or other forms of request validation failure, such as invalid json. Usually serves application/json content, although in some cases simple text/plain content might be returned by the server's middleware. | - | +**401** | Unauthorized. Authentication is required and has failed or has not yet been provided. | - | +**403** | Forbidden. Authenticated user does not have the necessary permissions. | - | +**404** | Not Found - The namespace specified does not exist | - | +**409** | Conflict - The table already exists | - | +**419** | Credentials have timed out. If possible, the client should refresh credentials and retry. | - | +**503** | The service is not ready to handle the request. The client should wait and retry. The service may additionally send a Retry-After header to indicate when to retry. | - | +**5XX** | A server-side problem that might not be addressable from the client side. Used for server 5xx errors without more specific documentation in individual routes. | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **rename_table** +> rename_table(prefix, rename_table_request) + +Rename a table from its current name to a new name + +Rename a table from one identifier to another. It's valid to move a table across namespaces, but the server implementation is not required to support it. + +### Example + +* OAuth Authentication (OAuth2): +* Bearer Authentication (BearerAuth): + +```python +import polaris.catalog +from polaris.catalog.models.rename_table_request import RenameTableRequest +from polaris.catalog.rest import ApiException +from pprint import pprint + +# Defining the host is optional and defaults to https://localhost +# See configuration.py for a list of all supported configuration parameters. +configuration = polaris.catalog.Configuration( + host = "https://localhost" +) + +# The client must configure the authentication and authorization parameters +# in accordance with the API server security policy. +# Examples for each auth method are provided below, use the example that +# satisfies your auth use case. + +configuration.access_token = os.environ["ACCESS_TOKEN"] + +# Configure Bearer authorization: BearerAuth +configuration = polaris.catalog.Configuration( + access_token = os.environ["BEARER_TOKEN"] +) + +# Enter a context with an instance of the API client +with polaris.catalog.ApiClient(configuration) as api_client: + # Create an instance of the API class + api_instance = polaris.catalog.IcebergCatalogAPI(api_client) + prefix = 'prefix_example' # str | An optional prefix in the path + rename_table_request = polaris.catalog.RenameTableRequest() # RenameTableRequest | Current table identifier to rename and new table identifier to rename to + + try: + # Rename a table from its current name to a new name + api_instance.rename_table(prefix, rename_table_request) + except Exception as e: + print("Exception when calling IcebergCatalogAPI->rename_table: %s\n" % e) +``` + + + +### Parameters + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **prefix** | **str**| An optional prefix in the path | + **rename_table_request** | [**RenameTableRequest**](RenameTableRequest.md)| Current table identifier to rename and new table identifier to rename to | + +### Return type + +void (empty response body) + +### Authorization + +[OAuth2](../README.md#OAuth2), [BearerAuth](../README.md#BearerAuth) + +### HTTP request headers + + - **Content-Type**: application/json + - **Accept**: application/json + +### HTTP response details + +| Status code | Description | Response headers | +|-------------|-------------|------------------| +**204** | Success, no content | - | +**400** | Indicates a bad request error. It could be caused by an unexpected request body format or other forms of request validation failure, such as invalid json. Usually serves application/json content, although in some cases simple text/plain content might be returned by the server's middleware. | - | +**401** | Unauthorized. Authentication is required and has failed or has not yet been provided. | - | +**403** | Forbidden. Authenticated user does not have the necessary permissions. | - | +**404** | Not Found - NoSuchTableException, Table to rename does not exist - NoSuchNamespaceException, The target namespace of the new table identifier does not exist | - | +**406** | Not Acceptable / Unsupported Operation. The server does not support this operation. | - | +**409** | Conflict - The target identifier to rename to already exists as a table or view | - | +**419** | Credentials have timed out. If possible, the client should refresh credentials and retry. | - | +**503** | The service is not ready to handle the request. The client should wait and retry. The service may additionally send a Retry-After header to indicate when to retry. | - | +**5XX** | A server-side problem that might not be addressable from the client side. Used for server 5xx errors without more specific documentation in individual routes. | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **rename_view** +> rename_view(prefix, rename_table_request) + +Rename a view from its current name to a new name + +Rename a view from one identifier to another. It's valid to move a view across namespaces, but the server implementation is not required to support it. + +### Example + +* OAuth Authentication (OAuth2): +* Bearer Authentication (BearerAuth): + +```python +import polaris.catalog +from polaris.catalog.models.rename_table_request import RenameTableRequest +from polaris.catalog.rest import ApiException +from pprint import pprint + +# Defining the host is optional and defaults to https://localhost +# See configuration.py for a list of all supported configuration parameters. +configuration = polaris.catalog.Configuration( + host = "https://localhost" +) + +# The client must configure the authentication and authorization parameters +# in accordance with the API server security policy. +# Examples for each auth method are provided below, use the example that +# satisfies your auth use case. + +configuration.access_token = os.environ["ACCESS_TOKEN"] + +# Configure Bearer authorization: BearerAuth +configuration = polaris.catalog.Configuration( + access_token = os.environ["BEARER_TOKEN"] +) + +# Enter a context with an instance of the API client +with polaris.catalog.ApiClient(configuration) as api_client: + # Create an instance of the API class + api_instance = polaris.catalog.IcebergCatalogAPI(api_client) + prefix = 'prefix_example' # str | An optional prefix in the path + rename_table_request = polaris.catalog.RenameTableRequest() # RenameTableRequest | Current view identifier to rename and new view identifier to rename to + + try: + # Rename a view from its current name to a new name + api_instance.rename_view(prefix, rename_table_request) + except Exception as e: + print("Exception when calling IcebergCatalogAPI->rename_view: %s\n" % e) +``` + + + +### Parameters + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **prefix** | **str**| An optional prefix in the path | + **rename_table_request** | [**RenameTableRequest**](RenameTableRequest.md)| Current view identifier to rename and new view identifier to rename to | + +### Return type + +void (empty response body) + +### Authorization + +[OAuth2](../README.md#OAuth2), [BearerAuth](../README.md#BearerAuth) + +### HTTP request headers + + - **Content-Type**: application/json + - **Accept**: application/json + +### HTTP response details + +| Status code | Description | Response headers | +|-------------|-------------|------------------| +**204** | Success, no content | - | +**400** | Indicates a bad request error. It could be caused by an unexpected request body format or other forms of request validation failure, such as invalid json. Usually serves application/json content, although in some cases simple text/plain content might be returned by the server's middleware. | - | +**401** | Unauthorized. Authentication is required and has failed or has not yet been provided. | - | +**403** | Forbidden. Authenticated user does not have the necessary permissions. | - | +**404** | Not Found - NoSuchViewException, view to rename does not exist - NoSuchNamespaceException, The target namespace of the new identifier does not exist | - | +**406** | Not Acceptable / Unsupported Operation. The server does not support this operation. | - | +**409** | Conflict - The target identifier to rename to already exists as a table or view | - | +**419** | Credentials have timed out. If possible, the client should refresh credentials and retry. | - | +**503** | The service is not ready to handle the request. The client should wait and retry. The service may additionally send a Retry-After header to indicate when to retry. | - | +**5XX** | A server-side problem that might not be addressable from the client side. Used for server 5xx errors without more specific documentation in individual routes. | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **replace_view** +> LoadViewResult replace_view(prefix, namespace, view, commit_view_request) + +Replace a view + +Commit updates to a view. + +### Example + +* OAuth Authentication (OAuth2): +* Bearer Authentication (BearerAuth): + +```python +import polaris.catalog +from polaris.catalog.models.commit_view_request import CommitViewRequest +from polaris.catalog.models.load_view_result import LoadViewResult +from polaris.catalog.rest import ApiException +from pprint import pprint + +# Defining the host is optional and defaults to https://localhost +# See configuration.py for a list of all supported configuration parameters. +configuration = polaris.catalog.Configuration( + host = "https://localhost" +) + +# The client must configure the authentication and authorization parameters +# in accordance with the API server security policy. +# Examples for each auth method are provided below, use the example that +# satisfies your auth use case. + +configuration.access_token = os.environ["ACCESS_TOKEN"] + +# Configure Bearer authorization: BearerAuth +configuration = polaris.catalog.Configuration( + access_token = os.environ["BEARER_TOKEN"] +) + +# Enter a context with an instance of the API client +with polaris.catalog.ApiClient(configuration) as api_client: + # Create an instance of the API class + api_instance = polaris.catalog.IcebergCatalogAPI(api_client) + prefix = 'prefix_example' # str | An optional prefix in the path + namespace = 'accounting' # str | A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. + view = 'sales' # str | A view name + commit_view_request = polaris.catalog.CommitViewRequest() # CommitViewRequest | + + try: + # Replace a view + api_response = api_instance.replace_view(prefix, namespace, view, commit_view_request) + print("The response of IcebergCatalogAPI->replace_view:\n") + pprint(api_response) + except Exception as e: + print("Exception when calling IcebergCatalogAPI->replace_view: %s\n" % e) +``` + + + +### Parameters + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **prefix** | **str**| An optional prefix in the path | + **namespace** | **str**| A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. | + **view** | **str**| A view name | + **commit_view_request** | [**CommitViewRequest**](CommitViewRequest.md)| | + +### Return type + +[**LoadViewResult**](LoadViewResult.md) + +### Authorization + +[OAuth2](../README.md#OAuth2), [BearerAuth](../README.md#BearerAuth) + +### HTTP request headers + + - **Content-Type**: application/json + - **Accept**: application/json + +### HTTP response details + +| Status code | Description | Response headers | +|-------------|-------------|------------------| +**200** | View metadata result when loading a view | - | +**400** | Indicates a bad request error. It could be caused by an unexpected request body format or other forms of request validation failure, such as invalid json. Usually serves application/json content, although in some cases simple text/plain content might be returned by the server's middleware. | - | +**401** | Unauthorized. Authentication is required and has failed or has not yet been provided. | - | +**403** | Forbidden. Authenticated user does not have the necessary permissions. | - | +**404** | Not Found - NoSuchViewException, view to load does not exist | - | +**409** | Conflict - CommitFailedException. The client may retry. | - | +**419** | Credentials have timed out. If possible, the client should refresh credentials and retry. | - | +**500** | An unknown server-side problem occurred; the commit state is unknown. | - | +**503** | The service is not ready to handle the request. The client should wait and retry. The service may additionally send a Retry-After header to indicate when to retry. | - | +**502** | A gateway or proxy received an invalid response from the upstream server; the commit state is unknown. | - | +**504** | A server-side gateway timeout occurred; the commit state is unknown. | - | +**5XX** | A server-side problem that might not be addressable on the client. | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **report_metrics** +> report_metrics(prefix, namespace, table, report_metrics_request) + +Send a metrics report to this endpoint to be processed by the backend + +### Example + +* OAuth Authentication (OAuth2): +* Bearer Authentication (BearerAuth): + +```python +import polaris.catalog +from polaris.catalog.models.report_metrics_request import ReportMetricsRequest +from polaris.catalog.rest import ApiException +from pprint import pprint + +# Defining the host is optional and defaults to https://localhost +# See configuration.py for a list of all supported configuration parameters. +configuration = polaris.catalog.Configuration( + host = "https://localhost" +) + +# The client must configure the authentication and authorization parameters +# in accordance with the API server security policy. +# Examples for each auth method are provided below, use the example that +# satisfies your auth use case. + +configuration.access_token = os.environ["ACCESS_TOKEN"] + +# Configure Bearer authorization: BearerAuth +configuration = polaris.catalog.Configuration( + access_token = os.environ["BEARER_TOKEN"] +) + +# Enter a context with an instance of the API client +with polaris.catalog.ApiClient(configuration) as api_client: + # Create an instance of the API class + api_instance = polaris.catalog.IcebergCatalogAPI(api_client) + prefix = 'prefix_example' # str | An optional prefix in the path + namespace = 'accounting' # str | A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. + table = 'sales' # str | A table name + report_metrics_request = polaris.catalog.ReportMetricsRequest() # ReportMetricsRequest | The request containing the metrics report to be sent + + try: + # Send a metrics report to this endpoint to be processed by the backend + api_instance.report_metrics(prefix, namespace, table, report_metrics_request) + except Exception as e: + print("Exception when calling IcebergCatalogAPI->report_metrics: %s\n" % e) +``` + + + +### Parameters + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **prefix** | **str**| An optional prefix in the path | + **namespace** | **str**| A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. | + **table** | **str**| A table name | + **report_metrics_request** | [**ReportMetricsRequest**](ReportMetricsRequest.md)| The request containing the metrics report to be sent | + +### Return type + +void (empty response body) + +### Authorization + +[OAuth2](../README.md#OAuth2), [BearerAuth](../README.md#BearerAuth) + +### HTTP request headers + + - **Content-Type**: application/json + - **Accept**: application/json + +### HTTP response details + +| Status code | Description | Response headers | +|-------------|-------------|------------------| +**204** | Success, no content | - | +**400** | Indicates a bad request error. It could be caused by an unexpected request body format or other forms of request validation failure, such as invalid json. Usually serves application/json content, although in some cases simple text/plain content might be returned by the server's middleware. | - | +**401** | Unauthorized. Authentication is required and has failed or has not yet been provided. | - | +**403** | Forbidden. Authenticated user does not have the necessary permissions. | - | +**404** | Not Found - NoSuchTableException, table to load does not exist | - | +**419** | Credentials have timed out. If possible, the client should refresh credentials and retry. | - | +**503** | The service is not ready to handle the request. The client should wait and retry. The service may additionally send a Retry-After header to indicate when to retry. | - | +**5XX** | A server-side problem that might not be addressable from the client side. Used for server 5xx errors without more specific documentation in individual routes. | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **send_notification** +> send_notification(prefix, namespace, table, notification_request) + +Sends a notification to the table + +### Example + +* OAuth Authentication (OAuth2): +* Bearer Authentication (BearerAuth): + +```python +import polaris.catalog +from polaris.catalog.models.notification_request import NotificationRequest +from polaris.catalog.rest import ApiException +from pprint import pprint + +# Defining the host is optional and defaults to https://localhost +# See configuration.py for a list of all supported configuration parameters. +configuration = polaris.catalog.Configuration( + host = "https://localhost" +) + +# The client must configure the authentication and authorization parameters +# in accordance with the API server security policy. +# Examples for each auth method are provided below, use the example that +# satisfies your auth use case. + +configuration.access_token = os.environ["ACCESS_TOKEN"] + +# Configure Bearer authorization: BearerAuth +configuration = polaris.catalog.Configuration( + access_token = os.environ["BEARER_TOKEN"] +) + +# Enter a context with an instance of the API client +with polaris.catalog.ApiClient(configuration) as api_client: + # Create an instance of the API class + api_instance = polaris.catalog.IcebergCatalogAPI(api_client) + prefix = 'prefix_example' # str | An optional prefix in the path + namespace = 'accounting' # str | A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. + table = 'sales' # str | A table name + notification_request = polaris.catalog.NotificationRequest() # NotificationRequest | The request containing the notification to be sent + + try: + # Sends a notification to the table + api_instance.send_notification(prefix, namespace, table, notification_request) + except Exception as e: + print("Exception when calling IcebergCatalogAPI->send_notification: %s\n" % e) +``` + + + +### Parameters + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **prefix** | **str**| An optional prefix in the path | + **namespace** | **str**| A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. | + **table** | **str**| A table name | + **notification_request** | [**NotificationRequest**](NotificationRequest.md)| The request containing the notification to be sent | + +### Return type + +void (empty response body) + +### Authorization + +[OAuth2](../README.md#OAuth2), [BearerAuth](../README.md#BearerAuth) + +### HTTP request headers + + - **Content-Type**: application/json + - **Accept**: application/json + +### HTTP response details + +| Status code | Description | Response headers | +|-------------|-------------|------------------| +**204** | Success, no content | - | +**400** | Indicates a bad request error. It could be caused by an unexpected request body format or other forms of request validation failure, such as invalid json. Usually serves application/json content, although in some cases simple text/plain content might be returned by the server's middleware. | - | +**401** | Unauthorized. Authentication is required and has failed or has not yet been provided. | - | +**403** | Forbidden. Authenticated user does not have the necessary permissions. | - | +**404** | Not Found - NoSuchTableException, table to load does not exist | - | +**419** | Credentials have timed out. If possible, the client should refresh credentials and retry. | - | +**503** | The service is not ready to handle the request. The client should wait and retry. The service may additionally send a Retry-After header to indicate when to retry. | - | +**5XX** | A server-side problem that might not be addressable from the client side. Used for server 5xx errors without more specific documentation in individual routes. | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **table_exists** +> table_exists(prefix, namespace, table) + +Check if a table exists + +Check if a table exists within a given namespace. The response does not contain a body. + +### Example + +* OAuth Authentication (OAuth2): +* Bearer Authentication (BearerAuth): + +```python +import polaris.catalog +from polaris.catalog.rest import ApiException +from pprint import pprint + +# Defining the host is optional and defaults to https://localhost +# See configuration.py for a list of all supported configuration parameters. +configuration = polaris.catalog.Configuration( + host = "https://localhost" +) + +# The client must configure the authentication and authorization parameters +# in accordance with the API server security policy. +# Examples for each auth method are provided below, use the example that +# satisfies your auth use case. + +configuration.access_token = os.environ["ACCESS_TOKEN"] + +# Configure Bearer authorization: BearerAuth +configuration = polaris.catalog.Configuration( + access_token = os.environ["BEARER_TOKEN"] +) + +# Enter a context with an instance of the API client +with polaris.catalog.ApiClient(configuration) as api_client: + # Create an instance of the API class + api_instance = polaris.catalog.IcebergCatalogAPI(api_client) + prefix = 'prefix_example' # str | An optional prefix in the path + namespace = 'accounting' # str | A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. + table = 'sales' # str | A table name + + try: + # Check if a table exists + api_instance.table_exists(prefix, namespace, table) + except Exception as e: + print("Exception when calling IcebergCatalogAPI->table_exists: %s\n" % e) +``` + + + +### Parameters + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **prefix** | **str**| An optional prefix in the path | + **namespace** | **str**| A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. | + **table** | **str**| A table name | + +### Return type + +void (empty response body) + +### Authorization + +[OAuth2](../README.md#OAuth2), [BearerAuth](../README.md#BearerAuth) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + +### HTTP response details + +| Status code | Description | Response headers | +|-------------|-------------|------------------| +**204** | Success, no content | - | +**400** | Indicates a bad request error. It could be caused by an unexpected request body format or other forms of request validation failure, such as invalid json. Usually serves application/json content, although in some cases simple text/plain content might be returned by the server's middleware. | - | +**401** | Unauthorized. Authentication is required and has failed or has not yet been provided. | - | +**403** | Forbidden. Authenticated user does not have the necessary permissions. | - | +**404** | Not Found - NoSuchTableException, Table not found | - | +**419** | Credentials have timed out. If possible, the client should refresh credentials and retry. | - | +**503** | The service is not ready to handle the request. The client should wait and retry. The service may additionally send a Retry-After header to indicate when to retry. | - | +**5XX** | A server-side problem that might not be addressable from the client side. Used for server 5xx errors without more specific documentation in individual routes. | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **update_properties** +> UpdateNamespacePropertiesResponse update_properties(prefix, namespace, update_namespace_properties_request) + +Set or remove properties on a namespace + +Set and/or remove properties on a namespace. The request body specifies a list of properties to remove and a map of key value pairs to update. Properties that are not in the request are not modified or removed by this call. Server implementations are not required to support namespace properties. + +### Example + +* OAuth Authentication (OAuth2): +* Bearer Authentication (BearerAuth): + +```python +import polaris.catalog +from polaris.catalog.models.update_namespace_properties_request import UpdateNamespacePropertiesRequest +from polaris.catalog.models.update_namespace_properties_response import UpdateNamespacePropertiesResponse +from polaris.catalog.rest import ApiException +from pprint import pprint + +# Defining the host is optional and defaults to https://localhost +# See configuration.py for a list of all supported configuration parameters. +configuration = polaris.catalog.Configuration( + host = "https://localhost" +) + +# The client must configure the authentication and authorization parameters +# in accordance with the API server security policy. +# Examples for each auth method are provided below, use the example that +# satisfies your auth use case. + +configuration.access_token = os.environ["ACCESS_TOKEN"] + +# Configure Bearer authorization: BearerAuth +configuration = polaris.catalog.Configuration( + access_token = os.environ["BEARER_TOKEN"] +) + +# Enter a context with an instance of the API client +with polaris.catalog.ApiClient(configuration) as api_client: + # Create an instance of the API class + api_instance = polaris.catalog.IcebergCatalogAPI(api_client) + prefix = 'prefix_example' # str | An optional prefix in the path + namespace = 'accounting' # str | A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. + update_namespace_properties_request = polaris.catalog.UpdateNamespacePropertiesRequest() # UpdateNamespacePropertiesRequest | + + try: + # Set or remove properties on a namespace + api_response = api_instance.update_properties(prefix, namespace, update_namespace_properties_request) + print("The response of IcebergCatalogAPI->update_properties:\n") + pprint(api_response) + except Exception as e: + print("Exception when calling IcebergCatalogAPI->update_properties: %s\n" % e) +``` + + + +### Parameters + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **prefix** | **str**| An optional prefix in the path | + **namespace** | **str**| A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. | + **update_namespace_properties_request** | [**UpdateNamespacePropertiesRequest**](UpdateNamespacePropertiesRequest.md)| | + +### Return type + +[**UpdateNamespacePropertiesResponse**](UpdateNamespacePropertiesResponse.md) + +### Authorization + +[OAuth2](../README.md#OAuth2), [BearerAuth](../README.md#BearerAuth) + +### HTTP request headers + + - **Content-Type**: application/json + - **Accept**: application/json + +### HTTP response details + +| Status code | Description | Response headers | +|-------------|-------------|------------------| +**200** | JSON data response for a synchronous update properties request. | - | +**400** | Indicates a bad request error. It could be caused by an unexpected request body format or other forms of request validation failure, such as invalid json. Usually serves application/json content, although in some cases simple text/plain content might be returned by the server's middleware. | - | +**401** | Unauthorized. Authentication is required and has failed or has not yet been provided. | - | +**403** | Forbidden. Authenticated user does not have the necessary permissions. | - | +**404** | Not Found - Namespace not found | - | +**406** | Not Acceptable / Unsupported Operation. The server does not support this operation. | - | +**422** | Unprocessable Entity - A property key was included in both `removals` and `updates` | - | +**419** | Credentials have timed out. If possible, the client should refresh credentials and retry. | - | +**503** | The service is not ready to handle the request. The client should wait and retry. The service may additionally send a Retry-After header to indicate when to retry. | - | +**5XX** | A server-side problem that might not be addressable from the client side. Used for server 5xx errors without more specific documentation in individual routes. | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **update_table** +> CommitTableResponse update_table(prefix, namespace, table, commit_table_request) + +Commit updates to a table + +Commit updates to a table. Commits have two parts, requirements and updates. Requirements are assertions that will be validated before attempting to make and commit changes. For example, `assert-ref-snapshot-id` will check that a named ref's snapshot ID has a certain value. Updates are changes to make to table metadata. For example, after asserting that the current main ref is at the expected snapshot, a commit may add a new child snapshot and set the ref to the new snapshot id. Create table transactions that are started by createTable with `stage-create` set to true are committed using this route. Transactions should include all changes to the table, including table initialization, like AddSchemaUpdate and SetCurrentSchemaUpdate. The `assert-create` requirement is used to ensure that the table was not created concurrently. + +### Example + +* OAuth Authentication (OAuth2): +* Bearer Authentication (BearerAuth): + +```python +import polaris.catalog +from polaris.catalog.models.commit_table_request import CommitTableRequest +from polaris.catalog.models.commit_table_response import CommitTableResponse +from polaris.catalog.rest import ApiException +from pprint import pprint + +# Defining the host is optional and defaults to https://localhost +# See configuration.py for a list of all supported configuration parameters. +configuration = polaris.catalog.Configuration( + host = "https://localhost" +) + +# The client must configure the authentication and authorization parameters +# in accordance with the API server security policy. +# Examples for each auth method are provided below, use the example that +# satisfies your auth use case. + +configuration.access_token = os.environ["ACCESS_TOKEN"] + +# Configure Bearer authorization: BearerAuth +configuration = polaris.catalog.Configuration( + access_token = os.environ["BEARER_TOKEN"] +) + +# Enter a context with an instance of the API client +with polaris.catalog.ApiClient(configuration) as api_client: + # Create an instance of the API class + api_instance = polaris.catalog.IcebergCatalogAPI(api_client) + prefix = 'prefix_example' # str | An optional prefix in the path + namespace = 'accounting' # str | A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. + table = 'sales' # str | A table name + commit_table_request = polaris.catalog.CommitTableRequest() # CommitTableRequest | + + try: + # Commit updates to a table + api_response = api_instance.update_table(prefix, namespace, table, commit_table_request) + print("The response of IcebergCatalogAPI->update_table:\n") + pprint(api_response) + except Exception as e: + print("Exception when calling IcebergCatalogAPI->update_table: %s\n" % e) +``` + + + +### Parameters + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **prefix** | **str**| An optional prefix in the path | + **namespace** | **str**| A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. | + **table** | **str**| A table name | + **commit_table_request** | [**CommitTableRequest**](CommitTableRequest.md)| | + +### Return type + +[**CommitTableResponse**](CommitTableResponse.md) + +### Authorization + +[OAuth2](../README.md#OAuth2), [BearerAuth](../README.md#BearerAuth) + +### HTTP request headers + + - **Content-Type**: application/json + - **Accept**: application/json + +### HTTP response details + +| Status code | Description | Response headers | +|-------------|-------------|------------------| +**200** | Response used when a table is successfully updated. The table metadata JSON is returned in the metadata field. The corresponding file location of table metadata must be returned in the metadata-location field. Clients can check whether metadata has changed by comparing metadata locations. | - | +**400** | Indicates a bad request error. It could be caused by an unexpected request body format or other forms of request validation failure, such as invalid json. Usually serves application/json content, although in some cases simple text/plain content might be returned by the server's middleware. | - | +**401** | Unauthorized. Authentication is required and has failed or has not yet been provided. | - | +**403** | Forbidden. Authenticated user does not have the necessary permissions. | - | +**404** | Not Found - NoSuchTableException, table to load does not exist | - | +**409** | Conflict - CommitFailedException, one or more requirements failed. The client may retry. | - | +**419** | Credentials have timed out. If possible, the client should refresh credentials and retry. | - | +**500** | An unknown server-side problem occurred; the commit state is unknown. | - | +**503** | The service is not ready to handle the request. The client should wait and retry. The service may additionally send a Retry-After header to indicate when to retry. | - | +**502** | A gateway or proxy received an invalid response from the upstream server; the commit state is unknown. | - | +**504** | A server-side gateway timeout occurred; the commit state is unknown. | - | +**5XX** | A server-side problem that might not be addressable on the client. | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **view_exists** +> view_exists(prefix, namespace, view) + +Check if a view exists + +Check if a view exists within a given namespace. This request does not return a response body. + +### Example + +* OAuth Authentication (OAuth2): +* Bearer Authentication (BearerAuth): + +```python +import polaris.catalog +from polaris.catalog.rest import ApiException +from pprint import pprint + +# Defining the host is optional and defaults to https://localhost +# See configuration.py for a list of all supported configuration parameters. +configuration = polaris.catalog.Configuration( + host = "https://localhost" +) + +# The client must configure the authentication and authorization parameters +# in accordance with the API server security policy. +# Examples for each auth method are provided below, use the example that +# satisfies your auth use case. + +configuration.access_token = os.environ["ACCESS_TOKEN"] + +# Configure Bearer authorization: BearerAuth +configuration = polaris.catalog.Configuration( + access_token = os.environ["BEARER_TOKEN"] +) + +# Enter a context with an instance of the API client +with polaris.catalog.ApiClient(configuration) as api_client: + # Create an instance of the API class + api_instance = polaris.catalog.IcebergCatalogAPI(api_client) + prefix = 'prefix_example' # str | An optional prefix in the path + namespace = 'accounting' # str | A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. + view = 'sales' # str | A view name + + try: + # Check if a view exists + api_instance.view_exists(prefix, namespace, view) + except Exception as e: + print("Exception when calling IcebergCatalogAPI->view_exists: %s\n" % e) +``` + + + +### Parameters + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **prefix** | **str**| An optional prefix in the path | + **namespace** | **str**| A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. | + **view** | **str**| A view name | + +### Return type + +void (empty response body) + +### Authorization + +[OAuth2](../README.md#OAuth2), [BearerAuth](../README.md#BearerAuth) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + +### HTTP response details + +| Status code | Description | Response headers | +|-------------|-------------|------------------| +**204** | Success, no content | - | +**400** | Bad Request | - | +**401** | Unauthorized | - | +**404** | Not Found | - | +**419** | Credentials have timed out. If possible, the client should refresh credentials and retry. | - | +**503** | The service is not ready to handle the request. The client should wait and retry. The service may additionally send a Retry-After header to indicate when to retry. | - | +**5XX** | A server-side problem that might not be addressable from the client side. Used for server 5xx errors without more specific documentation in individual routes. | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + diff --git a/regtests/client/python/docs/IcebergConfigurationAPI.md b/regtests/client/python/docs/IcebergConfigurationAPI.md new file mode 100644 index 0000000000..f3b9007dbd --- /dev/null +++ b/regtests/client/python/docs/IcebergConfigurationAPI.md @@ -0,0 +1,96 @@ +# polaris.catalog.IcebergConfigurationAPI + +All URIs are relative to *https://localhost* + +Method | HTTP request | Description +------------- | ------------- | ------------- +[**get_config**](IcebergConfigurationAPI.md#get_config) | **GET** /v1/config | List all catalog configuration settings + + +# **get_config** +> CatalogConfig get_config(warehouse=warehouse) + +List all catalog configuration settings + + All REST clients should first call this route to get catalog configuration properties from the server to configure the catalog and its HTTP client. Configuration from the server consists of two sets of key/value pairs. - defaults - properties that should be used as default configuration; applied before client configuration - overrides - properties that should be used to override client configuration; applied after defaults and client configuration Catalog configuration is constructed by setting the defaults, then client- provided configuration, and finally overrides. The final property set is then used to configure the catalog. For example, a default configuration property might set the size of the client pool, which can be replaced with a client-specific setting. An override might be used to set the warehouse location, which is stored on the server rather than in client configuration. Common catalog configuration settings are documented at https://iceberg.apache.org/docs/latest/configuration/#catalog-properties + +### Example + +* OAuth Authentication (OAuth2): +* Bearer Authentication (BearerAuth): + +```python +import polaris.catalog +from polaris.catalog.models.catalog_config import CatalogConfig +from polaris.catalog.rest import ApiException +from pprint import pprint + +# Defining the host is optional and defaults to https://localhost +# See configuration.py for a list of all supported configuration parameters. +configuration = polaris.catalog.Configuration( + host = "https://localhost" +) + +# The client must configure the authentication and authorization parameters +# in accordance with the API server security policy. +# Examples for each auth method are provided below, use the example that +# satisfies your auth use case. + +configuration.access_token = os.environ["ACCESS_TOKEN"] + +# Configure Bearer authorization: BearerAuth +configuration = polaris.catalog.Configuration( + access_token = os.environ["BEARER_TOKEN"] +) + +# Enter a context with an instance of the API client +with polaris.catalog.ApiClient(configuration) as api_client: + # Create an instance of the API class + api_instance = polaris.catalog.IcebergConfigurationAPI(api_client) + warehouse = 'warehouse_example' # str | Warehouse location or identifier to request from the service (optional) + + try: + # List all catalog configuration settings + api_response = api_instance.get_config(warehouse=warehouse) + print("The response of IcebergConfigurationAPI->get_config:\n") + pprint(api_response) + except Exception as e: + print("Exception when calling IcebergConfigurationAPI->get_config: %s\n" % e) +``` + + + +### Parameters + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **warehouse** | **str**| Warehouse location or identifier to request from the service | [optional] + +### Return type + +[**CatalogConfig**](CatalogConfig.md) + +### Authorization + +[OAuth2](../README.md#OAuth2), [BearerAuth](../README.md#BearerAuth) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + +### HTTP response details + +| Status code | Description | Response headers | +|-------------|-------------|------------------| +**200** | Server specified configuration values. | - | +**400** | Indicates a bad request error. It could be caused by an unexpected request body format or other forms of request validation failure, such as invalid json. Usually serves application/json content, although in some cases simple text/plain content might be returned by the server's middleware. | - | +**401** | Unauthorized. Authentication is required and has failed or has not yet been provided. | - | +**403** | Forbidden. Authenticated user does not have the necessary permissions. | - | +**419** | Credentials have timed out. If possible, the client should refresh credentials and retry. | - | +**503** | The service is not ready to handle the request. The client should wait and retry. The service may additionally send a Retry-After header to indicate when to retry. | - | +**5XX** | A server-side problem that might not be addressable from the client side. Used for server 5xx errors without more specific documentation in individual routes. | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + diff --git a/regtests/client/python/docs/IcebergErrorResponse.md b/regtests/client/python/docs/IcebergErrorResponse.md new file mode 100644 index 0000000000..de5a9542fc --- /dev/null +++ b/regtests/client/python/docs/IcebergErrorResponse.md @@ -0,0 +1,30 @@ +# IcebergErrorResponse + +JSON wrapper for all error responses (non-2xx) + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**error** | [**ErrorModel**](ErrorModel.md) | | + +## Example + +```python +from polaris.catalog.models.iceberg_error_response import IcebergErrorResponse + +# TODO update the JSON string below +json = "{}" +# create an instance of IcebergErrorResponse from a JSON string +iceberg_error_response_instance = IcebergErrorResponse.from_json(json) +# print the JSON string representation of the object +print(IcebergErrorResponse.to_json()) + +# convert the object into a dict +iceberg_error_response_dict = iceberg_error_response_instance.to_dict() +# create an instance of IcebergErrorResponse from a dict +iceberg_error_response_from_dict = IcebergErrorResponse.from_dict(iceberg_error_response_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/IcebergOAuth2API.md b/regtests/client/python/docs/IcebergOAuth2API.md new file mode 100644 index 0000000000..20e8d297b9 --- /dev/null +++ b/regtests/client/python/docs/IcebergOAuth2API.md @@ -0,0 +1,107 @@ +# polaris.catalog.IcebergOAuth2API + +All URIs are relative to *https://localhost* + +Method | HTTP request | Description +------------- | ------------- | ------------- +[**get_token**](IcebergOAuth2API.md#get_token) | **POST** /v1/oauth/tokens | Get a token using an OAuth2 flow + + +# **get_token** +> OAuthTokenResponse get_token(grant_type=grant_type, scope=scope, client_id=client_id, client_secret=client_secret, requested_token_type=requested_token_type, subject_token=subject_token, subject_token_type=subject_token_type, actor_token=actor_token, actor_token_type=actor_token_type) + +Get a token using an OAuth2 flow + +Exchange credentials for a token using the OAuth2 client credentials flow or token exchange. This endpoint is used for three purposes - 1. To exchange client credentials (client ID and secret) for an access token This uses the client credentials flow. 2. To exchange a client token and an identity token for a more specific access token This uses the token exchange flow. 3. To exchange an access token for one with the same claims and a refreshed expiration period This uses the token exchange flow. For example, a catalog client may be configured with client credentials from the OAuth2 Authorization flow. This client would exchange its client ID and secret for an access token using the client credentials request with this endpoint (1). Subsequent requests would then use that access token. Some clients may also handle sessions that have additional user context. These clients would use the token exchange flow to exchange a user token (the \"subject\" token) from the session for a more specific access token for that user, using the catalog's access token as the \"actor\" token (2). The user ID token is the \"subject\" token and can be any token type allowed by the OAuth2 token exchange flow, including a unsecured JWT token with a sub claim. This request should use the catalog's bearer token in the \"Authorization\" header. Clients may also use the token exchange flow to refresh a token that is about to expire by sending a token exchange request (3). The request's \"subject\" token should be the expiring token. This request should use the subject token in the \"Authorization\" header. + +### Example + +* Bearer Authentication (BearerAuth): + +```python +import polaris.catalog +from polaris.catalog.models.o_auth_token_response import OAuthTokenResponse +from polaris.catalog.models.token_type import TokenType +from polaris.catalog.rest import ApiException +from pprint import pprint + +# Defining the host is optional and defaults to https://localhost +# See configuration.py for a list of all supported configuration parameters. +configuration = polaris.catalog.Configuration( + host = "https://localhost" +) + +# The client must configure the authentication and authorization parameters +# in accordance with the API server security policy. +# Examples for each auth method are provided below, use the example that +# satisfies your auth use case. + +# Configure Bearer authorization: BearerAuth +configuration = polaris.catalog.Configuration( + access_token = os.environ["BEARER_TOKEN"] +) + +# Enter a context with an instance of the API client +with polaris.catalog.ApiClient(configuration) as api_client: + # Create an instance of the API class + api_instance = polaris.catalog.IcebergOAuth2API(api_client) + grant_type = 'grant_type_example' # str | (optional) + scope = 'scope_example' # str | (optional) + client_id = 'client_id_example' # str | Client ID This can be sent in the request body, but OAuth2 recommends sending it in a Basic Authorization header. (optional) + client_secret = 'client_secret_example' # str | Client secret This can be sent in the request body, but OAuth2 recommends sending it in a Basic Authorization header. (optional) + requested_token_type = polaris.catalog.TokenType() # TokenType | (optional) + subject_token = 'subject_token_example' # str | Subject token for token exchange request (optional) + subject_token_type = polaris.catalog.TokenType() # TokenType | (optional) + actor_token = 'actor_token_example' # str | Actor token for token exchange request (optional) + actor_token_type = polaris.catalog.TokenType() # TokenType | (optional) + + try: + # Get a token using an OAuth2 flow + api_response = api_instance.get_token(grant_type=grant_type, scope=scope, client_id=client_id, client_secret=client_secret, requested_token_type=requested_token_type, subject_token=subject_token, subject_token_type=subject_token_type, actor_token=actor_token, actor_token_type=actor_token_type) + print("The response of IcebergOAuth2API->get_token:\n") + pprint(api_response) + except Exception as e: + print("Exception when calling IcebergOAuth2API->get_token: %s\n" % e) +``` + + + +### Parameters + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **grant_type** | **str**| | [optional] + **scope** | **str**| | [optional] + **client_id** | **str**| Client ID This can be sent in the request body, but OAuth2 recommends sending it in a Basic Authorization header. | [optional] + **client_secret** | **str**| Client secret This can be sent in the request body, but OAuth2 recommends sending it in a Basic Authorization header. | [optional] + **requested_token_type** | [**TokenType**](TokenType.md)| | [optional] + **subject_token** | **str**| Subject token for token exchange request | [optional] + **subject_token_type** | [**TokenType**](TokenType.md)| | [optional] + **actor_token** | **str**| Actor token for token exchange request | [optional] + **actor_token_type** | [**TokenType**](TokenType.md)| | [optional] + +### Return type + +[**OAuthTokenResponse**](OAuthTokenResponse.md) + +### Authorization + +[BearerAuth](../README.md#BearerAuth) + +### HTTP request headers + + - **Content-Type**: application/x-www-form-urlencoded + - **Accept**: application/json + +### HTTP response details + +| Status code | Description | Response headers | +|-------------|-------------|------------------| +**200** | OAuth2 token response for client credentials or token exchange | - | +**400** | OAuth2 error response | - | +**401** | OAuth2 error response | - | +**5XX** | OAuth2 error response | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + diff --git a/regtests/client/python/docs/ListNamespacesResponse.md b/regtests/client/python/docs/ListNamespacesResponse.md new file mode 100644 index 0000000000..faafb191ef --- /dev/null +++ b/regtests/client/python/docs/ListNamespacesResponse.md @@ -0,0 +1,30 @@ +# ListNamespacesResponse + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**next_page_token** | **str** | An opaque token that allows clients to make use of pagination for list APIs (e.g. ListTables). Clients may initiate the first paginated request by sending an empty query parameter `pageToken` to the server. Servers that support pagination should identify the `pageToken` parameter and return a `next-page-token` in the response if there are more results available. After the initial request, the value of `next-page-token` from each response must be used as the `pageToken` parameter value for the next request. The server must return `null` value for the `next-page-token` in the last response. Servers that support pagination must return all results in a single response with the value of `next-page-token` set to `null` if the query parameter `pageToken` is not set in the request. Servers that do not support pagination should ignore the `pageToken` parameter and return all results in a single response. The `next-page-token` must be omitted from the response. Clients must interpret either `null` or missing response value of `next-page-token` as the end of the listing results. | [optional] +**namespaces** | **List[List[str]]** | | [optional] + +## Example + +```python +from polaris.catalog.models.list_namespaces_response import ListNamespacesResponse + +# TODO update the JSON string below +json = "{}" +# create an instance of ListNamespacesResponse from a JSON string +list_namespaces_response_instance = ListNamespacesResponse.from_json(json) +# print the JSON string representation of the object +print(ListNamespacesResponse.to_json()) + +# convert the object into a dict +list_namespaces_response_dict = list_namespaces_response_instance.to_dict() +# create an instance of ListNamespacesResponse from a dict +list_namespaces_response_from_dict = ListNamespacesResponse.from_dict(list_namespaces_response_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/ListTablesResponse.md b/regtests/client/python/docs/ListTablesResponse.md new file mode 100644 index 0000000000..24df6f7874 --- /dev/null +++ b/regtests/client/python/docs/ListTablesResponse.md @@ -0,0 +1,30 @@ +# ListTablesResponse + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**next_page_token** | **str** | An opaque token that allows clients to make use of pagination for list APIs (e.g. ListTables). Clients may initiate the first paginated request by sending an empty query parameter `pageToken` to the server. Servers that support pagination should identify the `pageToken` parameter and return a `next-page-token` in the response if there are more results available. After the initial request, the value of `next-page-token` from each response must be used as the `pageToken` parameter value for the next request. The server must return `null` value for the `next-page-token` in the last response. Servers that support pagination must return all results in a single response with the value of `next-page-token` set to `null` if the query parameter `pageToken` is not set in the request. Servers that do not support pagination should ignore the `pageToken` parameter and return all results in a single response. The `next-page-token` must be omitted from the response. Clients must interpret either `null` or missing response value of `next-page-token` as the end of the listing results. | [optional] +**identifiers** | [**List[TableIdentifier]**](TableIdentifier.md) | | [optional] + +## Example + +```python +from polaris.catalog.models.list_tables_response import ListTablesResponse + +# TODO update the JSON string below +json = "{}" +# create an instance of ListTablesResponse from a JSON string +list_tables_response_instance = ListTablesResponse.from_json(json) +# print the JSON string representation of the object +print(ListTablesResponse.to_json()) + +# convert the object into a dict +list_tables_response_dict = list_tables_response_instance.to_dict() +# create an instance of ListTablesResponse from a dict +list_tables_response_from_dict = ListTablesResponse.from_dict(list_tables_response_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/ListType.md b/regtests/client/python/docs/ListType.md new file mode 100644 index 0000000000..63fbd215e9 --- /dev/null +++ b/regtests/client/python/docs/ListType.md @@ -0,0 +1,32 @@ +# ListType + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**type** | **str** | | +**element_id** | **int** | | +**element** | [**Type**](Type.md) | | +**element_required** | **bool** | | + +## Example + +```python +from polaris.catalog.models.list_type import ListType + +# TODO update the JSON string below +json = "{}" +# create an instance of ListType from a JSON string +list_type_instance = ListType.from_json(json) +# print the JSON string representation of the object +print(ListType.to_json()) + +# convert the object into a dict +list_type_dict = list_type_instance.to_dict() +# create an instance of ListType from a dict +list_type_from_dict = ListType.from_dict(list_type_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/LiteralExpression.md b/regtests/client/python/docs/LiteralExpression.md new file mode 100644 index 0000000000..5c414fafd2 --- /dev/null +++ b/regtests/client/python/docs/LiteralExpression.md @@ -0,0 +1,31 @@ +# LiteralExpression + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**type** | **str** | | +**term** | [**Term**](Term.md) | | +**value** | **object** | | + +## Example + +```python +from polaris.catalog.models.literal_expression import LiteralExpression + +# TODO update the JSON string below +json = "{}" +# create an instance of LiteralExpression from a JSON string +literal_expression_instance = LiteralExpression.from_json(json) +# print the JSON string representation of the object +print(LiteralExpression.to_json()) + +# convert the object into a dict +literal_expression_dict = literal_expression_instance.to_dict() +# create an instance of LiteralExpression from a dict +literal_expression_from_dict = LiteralExpression.from_dict(literal_expression_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/LoadTableResult.md b/regtests/client/python/docs/LoadTableResult.md new file mode 100644 index 0000000000..82154d946d --- /dev/null +++ b/regtests/client/python/docs/LoadTableResult.md @@ -0,0 +1,32 @@ +# LoadTableResult + +Result used when a table is successfully loaded. The table metadata JSON is returned in the `metadata` field. The corresponding file location of table metadata should be returned in the `metadata-location` field, unless the metadata is not yet committed. For example, a create transaction may return metadata that is staged but not committed. Clients can check whether metadata has changed by comparing metadata locations after the table has been created. The `config` map returns table-specific configuration for the table's resources, including its HTTP client and FileIO. For example, config may contain a specific FileIO implementation class for the table depending on its underlying storage. The following configurations should be respected by clients: ## General Configurations - `token`: Authorization bearer token to use for table requests if OAuth2 security is enabled ## AWS Configurations The following configurations should be respected when working with tables stored in AWS S3 - `client.region`: region to configure client for making requests to AWS - `s3.access-key-id`: id for for credentials that provide access to the data in S3 - `s3.secret-access-key`: secret for credentials that provide access to data in S3 - `s3.session-token`: if present, this value should be used for as the session token - `s3.remote-signing-enabled`: if `true` remote signing should be performed as described in the `s3-signer-open-api.yaml` specification + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**metadata_location** | **str** | May be null if the table is staged as part of a transaction | [optional] +**metadata** | [**TableMetadata**](TableMetadata.md) | | +**config** | **Dict[str, str]** | | [optional] + +## Example + +```python +from polaris.catalog.models.load_table_result import LoadTableResult + +# TODO update the JSON string below +json = "{}" +# create an instance of LoadTableResult from a JSON string +load_table_result_instance = LoadTableResult.from_json(json) +# print the JSON string representation of the object +print(LoadTableResult.to_json()) + +# convert the object into a dict +load_table_result_dict = load_table_result_instance.to_dict() +# create an instance of LoadTableResult from a dict +load_table_result_from_dict = LoadTableResult.from_dict(load_table_result_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/LoadViewResult.md b/regtests/client/python/docs/LoadViewResult.md new file mode 100644 index 0000000000..3e861f6b4c --- /dev/null +++ b/regtests/client/python/docs/LoadViewResult.md @@ -0,0 +1,32 @@ +# LoadViewResult + +Result used when a view is successfully loaded. The view metadata JSON is returned in the `metadata` field. The corresponding file location of view metadata is returned in the `metadata-location` field. Clients can check whether metadata has changed by comparing metadata locations after the view has been created. The `config` map returns view-specific configuration for the view's resources. The following configurations should be respected by clients: ## General Configurations - `token`: Authorization bearer token to use for view requests if OAuth2 security is enabled + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**metadata_location** | **str** | | +**metadata** | [**ViewMetadata**](ViewMetadata.md) | | +**config** | **Dict[str, str]** | | [optional] + +## Example + +```python +from polaris.catalog.models.load_view_result import LoadViewResult + +# TODO update the JSON string below +json = "{}" +# create an instance of LoadViewResult from a JSON string +load_view_result_instance = LoadViewResult.from_json(json) +# print the JSON string representation of the object +print(LoadViewResult.to_json()) + +# convert the object into a dict +load_view_result_dict = load_view_result_instance.to_dict() +# create an instance of LoadViewResult from a dict +load_view_result_from_dict = LoadViewResult.from_dict(load_view_result_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/MapType.md b/regtests/client/python/docs/MapType.md new file mode 100644 index 0000000000..56bbc1db05 --- /dev/null +++ b/regtests/client/python/docs/MapType.md @@ -0,0 +1,34 @@ +# MapType + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**type** | **str** | | +**key_id** | **int** | | +**key** | [**Type**](Type.md) | | +**value_id** | **int** | | +**value** | [**Type**](Type.md) | | +**value_required** | **bool** | | + +## Example + +```python +from polaris.catalog.models.map_type import MapType + +# TODO update the JSON string below +json = "{}" +# create an instance of MapType from a JSON string +map_type_instance = MapType.from_json(json) +# print the JSON string representation of the object +print(MapType.to_json()) + +# convert the object into a dict +map_type_dict = map_type_instance.to_dict() +# create an instance of MapType from a dict +map_type_from_dict = MapType.from_dict(map_type_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/MetadataLogInner.md b/regtests/client/python/docs/MetadataLogInner.md new file mode 100644 index 0000000000..8b132cb185 --- /dev/null +++ b/regtests/client/python/docs/MetadataLogInner.md @@ -0,0 +1,30 @@ +# MetadataLogInner + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**metadata_file** | **str** | | +**timestamp_ms** | **int** | | + +## Example + +```python +from polaris.catalog.models.metadata_log_inner import MetadataLogInner + +# TODO update the JSON string below +json = "{}" +# create an instance of MetadataLogInner from a JSON string +metadata_log_inner_instance = MetadataLogInner.from_json(json) +# print the JSON string representation of the object +print(MetadataLogInner.to_json()) + +# convert the object into a dict +metadata_log_inner_dict = metadata_log_inner_instance.to_dict() +# create an instance of MetadataLogInner from a dict +metadata_log_inner_from_dict = MetadataLogInner.from_dict(metadata_log_inner_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/MetricResult.md b/regtests/client/python/docs/MetricResult.md new file mode 100644 index 0000000000..8763169072 --- /dev/null +++ b/regtests/client/python/docs/MetricResult.md @@ -0,0 +1,33 @@ +# MetricResult + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**unit** | **str** | | +**value** | **int** | | +**time_unit** | **str** | | +**count** | **int** | | +**total_duration** | **int** | | + +## Example + +```python +from polaris.catalog.models.metric_result import MetricResult + +# TODO update the JSON string below +json = "{}" +# create an instance of MetricResult from a JSON string +metric_result_instance = MetricResult.from_json(json) +# print the JSON string representation of the object +print(MetricResult.to_json()) + +# convert the object into a dict +metric_result_dict = metric_result_instance.to_dict() +# create an instance of MetricResult from a dict +metric_result_from_dict = MetricResult.from_dict(metric_result_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/ModelSchema.md b/regtests/client/python/docs/ModelSchema.md new file mode 100644 index 0000000000..66e58d077f --- /dev/null +++ b/regtests/client/python/docs/ModelSchema.md @@ -0,0 +1,32 @@ +# ModelSchema + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**type** | **str** | | +**fields** | [**List[StructField]**](StructField.md) | | +**schema_id** | **int** | | [optional] [readonly] +**identifier_field_ids** | **List[int]** | | [optional] + +## Example + +```python +from polaris.catalog.models.model_schema import ModelSchema + +# TODO update the JSON string below +json = "{}" +# create an instance of ModelSchema from a JSON string +model_schema_instance = ModelSchema.from_json(json) +# print the JSON string representation of the object +print(ModelSchema.to_json()) + +# convert the object into a dict +model_schema_dict = model_schema_instance.to_dict() +# create an instance of ModelSchema from a dict +model_schema_from_dict = ModelSchema.from_dict(model_schema_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/NamespaceGrant.md b/regtests/client/python/docs/NamespaceGrant.md new file mode 100644 index 0000000000..6ec24670db --- /dev/null +++ b/regtests/client/python/docs/NamespaceGrant.md @@ -0,0 +1,30 @@ +# NamespaceGrant + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**namespace** | **List[str]** | | +**privilege** | [**NamespacePrivilege**](NamespacePrivilege.md) | | + +## Example + +```python +from polaris.management.models.namespace_grant import NamespaceGrant + +# TODO update the JSON string below +json = "{}" +# create an instance of NamespaceGrant from a JSON string +namespace_grant_instance = NamespaceGrant.from_json(json) +# print the JSON string representation of the object +print(NamespaceGrant.to_json()) + +# convert the object into a dict +namespace_grant_dict = namespace_grant_instance.to_dict() +# create an instance of NamespaceGrant from a dict +namespace_grant_from_dict = NamespaceGrant.from_dict(namespace_grant_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/NamespacePrivilege.md b/regtests/client/python/docs/NamespacePrivilege.md new file mode 100644 index 0000000000..47756c10b2 --- /dev/null +++ b/regtests/client/python/docs/NamespacePrivilege.md @@ -0,0 +1,54 @@ +# NamespacePrivilege + + +## Enum + +* `CATALOG_MANAGE_ACCESS` (value: `'CATALOG_MANAGE_ACCESS'`) + +* `CATALOG_MANAGE_CONTENT` (value: `'CATALOG_MANAGE_CONTENT'`) + +* `CATALOG_MANAGE_METADATA` (value: `'CATALOG_MANAGE_METADATA'`) + +* `NAMESPACE_CREATE` (value: `'NAMESPACE_CREATE'`) + +* `TABLE_CREATE` (value: `'TABLE_CREATE'`) + +* `VIEW_CREATE` (value: `'VIEW_CREATE'`) + +* `NAMESPACE_DROP` (value: `'NAMESPACE_DROP'`) + +* `TABLE_DROP` (value: `'TABLE_DROP'`) + +* `VIEW_DROP` (value: `'VIEW_DROP'`) + +* `NAMESPACE_LIST` (value: `'NAMESPACE_LIST'`) + +* `TABLE_LIST` (value: `'TABLE_LIST'`) + +* `VIEW_LIST` (value: `'VIEW_LIST'`) + +* `NAMESPACE_READ_PROPERTIES` (value: `'NAMESPACE_READ_PROPERTIES'`) + +* `TABLE_READ_PROPERTIES` (value: `'TABLE_READ_PROPERTIES'`) + +* `VIEW_READ_PROPERTIES` (value: `'VIEW_READ_PROPERTIES'`) + +* `NAMESPACE_WRITE_PROPERTIES` (value: `'NAMESPACE_WRITE_PROPERTIES'`) + +* `TABLE_WRITE_PROPERTIES` (value: `'TABLE_WRITE_PROPERTIES'`) + +* `VIEW_WRITE_PROPERTIES` (value: `'VIEW_WRITE_PROPERTIES'`) + +* `TABLE_READ_DATA` (value: `'TABLE_READ_DATA'`) + +* `TABLE_WRITE_DATA` (value: `'TABLE_WRITE_DATA'`) + +* `NAMESPACE_FULL_METADATA` (value: `'NAMESPACE_FULL_METADATA'`) + +* `TABLE_FULL_METADATA` (value: `'TABLE_FULL_METADATA'`) + +* `VIEW_FULL_METADATA` (value: `'VIEW_FULL_METADATA'`) + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/NotExpression.md b/regtests/client/python/docs/NotExpression.md new file mode 100644 index 0000000000..0d19c6e22d --- /dev/null +++ b/regtests/client/python/docs/NotExpression.md @@ -0,0 +1,30 @@ +# NotExpression + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**type** | **str** | | +**child** | [**Expression**](Expression.md) | | + +## Example + +```python +from polaris.catalog.models.not_expression import NotExpression + +# TODO update the JSON string below +json = "{}" +# create an instance of NotExpression from a JSON string +not_expression_instance = NotExpression.from_json(json) +# print the JSON string representation of the object +print(NotExpression.to_json()) + +# convert the object into a dict +not_expression_dict = not_expression_instance.to_dict() +# create an instance of NotExpression from a dict +not_expression_from_dict = NotExpression.from_dict(not_expression_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/NotificationRequest.md b/regtests/client/python/docs/NotificationRequest.md new file mode 100644 index 0000000000..d6cbb8555f --- /dev/null +++ b/regtests/client/python/docs/NotificationRequest.md @@ -0,0 +1,30 @@ +# NotificationRequest + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**notification_type** | [**NotificationType**](NotificationType.md) | | +**payload** | [**TableUpdateNotification**](TableUpdateNotification.md) | | [optional] + +## Example + +```python +from polaris.catalog.models.notification_request import NotificationRequest + +# TODO update the JSON string below +json = "{}" +# create an instance of NotificationRequest from a JSON string +notification_request_instance = NotificationRequest.from_json(json) +# print the JSON string representation of the object +print(NotificationRequest.to_json()) + +# convert the object into a dict +notification_request_dict = notification_request_instance.to_dict() +# create an instance of NotificationRequest from a dict +notification_request_from_dict = NotificationRequest.from_dict(notification_request_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/NotificationType.md b/regtests/client/python/docs/NotificationType.md new file mode 100644 index 0000000000..0e6f189949 --- /dev/null +++ b/regtests/client/python/docs/NotificationType.md @@ -0,0 +1,16 @@ +# NotificationType + + +## Enum + +* `UNKNOWN` (value: `'UNKNOWN'`) + +* `CREATE` (value: `'CREATE'`) + +* `UPDATE` (value: `'UPDATE'`) + +* `DROP` (value: `'DROP'`) + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/NullOrder.md b/regtests/client/python/docs/NullOrder.md new file mode 100644 index 0000000000..2558e8ad24 --- /dev/null +++ b/regtests/client/python/docs/NullOrder.md @@ -0,0 +1,12 @@ +# NullOrder + + +## Enum + +* `NULLS_MINUS_FIRST` (value: `'nulls-first'`) + +* `NULLS_MINUS_LAST` (value: `'nulls-last'`) + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/OAuthError.md b/regtests/client/python/docs/OAuthError.md new file mode 100644 index 0000000000..b800a505ce --- /dev/null +++ b/regtests/client/python/docs/OAuthError.md @@ -0,0 +1,31 @@ +# OAuthError + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**error** | **str** | | +**error_description** | **str** | | [optional] +**error_uri** | **str** | | [optional] + +## Example + +```python +from polaris.catalog.models.o_auth_error import OAuthError + +# TODO update the JSON string below +json = "{}" +# create an instance of OAuthError from a JSON string +o_auth_error_instance = OAuthError.from_json(json) +# print the JSON string representation of the object +print(OAuthError.to_json()) + +# convert the object into a dict +o_auth_error_dict = o_auth_error_instance.to_dict() +# create an instance of OAuthError from a dict +o_auth_error_from_dict = OAuthError.from_dict(o_auth_error_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/OAuthTokenResponse.md b/regtests/client/python/docs/OAuthTokenResponse.md new file mode 100644 index 0000000000..3505f49939 --- /dev/null +++ b/regtests/client/python/docs/OAuthTokenResponse.md @@ -0,0 +1,34 @@ +# OAuthTokenResponse + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**access_token** | **str** | The access token, for client credentials or token exchange | +**token_type** | **str** | Access token type for client credentials or token exchange See https://datatracker.ietf.org/doc/html/rfc6749#section-7.1 | +**expires_in** | **int** | Lifetime of the access token in seconds for client credentials or token exchange | [optional] +**issued_token_type** | [**TokenType**](TokenType.md) | | [optional] +**refresh_token** | **str** | Refresh token for client credentials or token exchange | [optional] +**scope** | **str** | Authorization scope for client credentials or token exchange | [optional] + +## Example + +```python +from polaris.catalog.models.o_auth_token_response import OAuthTokenResponse + +# TODO update the JSON string below +json = "{}" +# create an instance of OAuthTokenResponse from a JSON string +o_auth_token_response_instance = OAuthTokenResponse.from_json(json) +# print the JSON string representation of the object +print(OAuthTokenResponse.to_json()) + +# convert the object into a dict +o_auth_token_response_dict = o_auth_token_response_instance.to_dict() +# create an instance of OAuthTokenResponse from a dict +o_auth_token_response_from_dict = OAuthTokenResponse.from_dict(o_auth_token_response_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/PartitionField.md b/regtests/client/python/docs/PartitionField.md new file mode 100644 index 0000000000..1dfdd35a77 --- /dev/null +++ b/regtests/client/python/docs/PartitionField.md @@ -0,0 +1,32 @@ +# PartitionField + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**field_id** | **int** | | [optional] +**source_id** | **int** | | +**name** | **str** | | +**transform** | **str** | | + +## Example + +```python +from polaris.catalog.models.partition_field import PartitionField + +# TODO update the JSON string below +json = "{}" +# create an instance of PartitionField from a JSON string +partition_field_instance = PartitionField.from_json(json) +# print the JSON string representation of the object +print(PartitionField.to_json()) + +# convert the object into a dict +partition_field_dict = partition_field_instance.to_dict() +# create an instance of PartitionField from a dict +partition_field_from_dict = PartitionField.from_dict(partition_field_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/PartitionSpec.md b/regtests/client/python/docs/PartitionSpec.md new file mode 100644 index 0000000000..2014c9e6a4 --- /dev/null +++ b/regtests/client/python/docs/PartitionSpec.md @@ -0,0 +1,30 @@ +# PartitionSpec + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**spec_id** | **int** | | [optional] [readonly] +**fields** | [**List[PartitionField]**](PartitionField.md) | | + +## Example + +```python +from polaris.catalog.models.partition_spec import PartitionSpec + +# TODO update the JSON string below +json = "{}" +# create an instance of PartitionSpec from a JSON string +partition_spec_instance = PartitionSpec.from_json(json) +# print the JSON string representation of the object +print(PartitionSpec.to_json()) + +# convert the object into a dict +partition_spec_dict = partition_spec_instance.to_dict() +# create an instance of PartitionSpec from a dict +partition_spec_from_dict = PartitionSpec.from_dict(partition_spec_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/PartitionStatisticsFile.md b/regtests/client/python/docs/PartitionStatisticsFile.md new file mode 100644 index 0000000000..db25c98708 --- /dev/null +++ b/regtests/client/python/docs/PartitionStatisticsFile.md @@ -0,0 +1,31 @@ +# PartitionStatisticsFile + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**snapshot_id** | **int** | | +**statistics_path** | **str** | | +**file_size_in_bytes** | **int** | | + +## Example + +```python +from polaris.catalog.models.partition_statistics_file import PartitionStatisticsFile + +# TODO update the JSON string below +json = "{}" +# create an instance of PartitionStatisticsFile from a JSON string +partition_statistics_file_instance = PartitionStatisticsFile.from_json(json) +# print the JSON string representation of the object +print(PartitionStatisticsFile.to_json()) + +# convert the object into a dict +partition_statistics_file_dict = partition_statistics_file_instance.to_dict() +# create an instance of PartitionStatisticsFile from a dict +partition_statistics_file_from_dict = PartitionStatisticsFile.from_dict(partition_statistics_file_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/PolarisCatalog.md b/regtests/client/python/docs/PolarisCatalog.md new file mode 100644 index 0000000000..8f29559ec5 --- /dev/null +++ b/regtests/client/python/docs/PolarisCatalog.md @@ -0,0 +1,29 @@ +# PolarisCatalog + +The base catalog type - this contains all the fields necessary to construct an INTERNAL catalog + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- + +## Example + +```python +from polaris.management.models.polaris_catalog import PolarisCatalog + +# TODO update the JSON string below +json = "{}" +# create an instance of PolarisCatalog from a JSON string +polaris_catalog_instance = PolarisCatalog.from_json(json) +# print the JSON string representation of the object +print(PolarisCatalog.to_json()) + +# convert the object into a dict +polaris_catalog_dict = polaris_catalog_instance.to_dict() +# create an instance of PolarisCatalog from a dict +polaris_catalog_from_dict = PolarisCatalog.from_dict(polaris_catalog_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/PolarisDefaultApi.md b/regtests/client/python/docs/PolarisDefaultApi.md new file mode 100644 index 0000000000..cefe48e142 --- /dev/null +++ b/regtests/client/python/docs/PolarisDefaultApi.md @@ -0,0 +1,2474 @@ +# polaris.management.PolarisDefaultApi + +All URIs are relative to *https://localhost/api/management/v1* + +Method | HTTP request | Description +------------- | ------------- | ------------- +[**add_grant_to_catalog_role**](PolarisDefaultApi.md#add_grant_to_catalog_role) | **PUT** /catalogs/{catalogName}/catalog-roles/{catalogRoleName}/grants | +[**assign_catalog_role_to_principal_role**](PolarisDefaultApi.md#assign_catalog_role_to_principal_role) | **PUT** /principal-roles/{principalRoleName}/catalog-roles/{catalogName} | +[**assign_principal_role**](PolarisDefaultApi.md#assign_principal_role) | **PUT** /principals/{principalName}/principal-roles | +[**create_catalog**](PolarisDefaultApi.md#create_catalog) | **POST** /catalogs | +[**create_catalog_role**](PolarisDefaultApi.md#create_catalog_role) | **POST** /catalogs/{catalogName}/catalog-roles | +[**create_principal**](PolarisDefaultApi.md#create_principal) | **POST** /principals | +[**create_principal_role**](PolarisDefaultApi.md#create_principal_role) | **POST** /principal-roles | +[**delete_catalog**](PolarisDefaultApi.md#delete_catalog) | **DELETE** /catalogs/{catalogName} | +[**delete_catalog_role**](PolarisDefaultApi.md#delete_catalog_role) | **DELETE** /catalogs/{catalogName}/catalog-roles/{catalogRoleName} | +[**delete_principal**](PolarisDefaultApi.md#delete_principal) | **DELETE** /principals/{principalName} | +[**delete_principal_role**](PolarisDefaultApi.md#delete_principal_role) | **DELETE** /principal-roles/{principalRoleName} | +[**get_catalog**](PolarisDefaultApi.md#get_catalog) | **GET** /catalogs/{catalogName} | +[**get_catalog_role**](PolarisDefaultApi.md#get_catalog_role) | **GET** /catalogs/{catalogName}/catalog-roles/{catalogRoleName} | +[**get_principal**](PolarisDefaultApi.md#get_principal) | **GET** /principals/{principalName} | +[**get_principal_role**](PolarisDefaultApi.md#get_principal_role) | **GET** /principal-roles/{principalRoleName} | +[**list_assignee_principal_roles_for_catalog_role**](PolarisDefaultApi.md#list_assignee_principal_roles_for_catalog_role) | **GET** /catalogs/{catalogName}/catalog-roles/{catalogRoleName}/principal-roles | +[**list_assignee_principals_for_principal_role**](PolarisDefaultApi.md#list_assignee_principals_for_principal_role) | **GET** /principal-roles/{principalRoleName}/principals | +[**list_catalog_roles**](PolarisDefaultApi.md#list_catalog_roles) | **GET** /catalogs/{catalogName}/catalog-roles | +[**list_catalog_roles_for_principal_role**](PolarisDefaultApi.md#list_catalog_roles_for_principal_role) | **GET** /principal-roles/{principalRoleName}/catalog-roles/{catalogName} | +[**list_catalogs**](PolarisDefaultApi.md#list_catalogs) | **GET** /catalogs | +[**list_grants_for_catalog_role**](PolarisDefaultApi.md#list_grants_for_catalog_role) | **GET** /catalogs/{catalogName}/catalog-roles/{catalogRoleName}/grants | +[**list_principal_roles**](PolarisDefaultApi.md#list_principal_roles) | **GET** /principal-roles | +[**list_principal_roles_assigned**](PolarisDefaultApi.md#list_principal_roles_assigned) | **GET** /principals/{principalName}/principal-roles | +[**list_principals**](PolarisDefaultApi.md#list_principals) | **GET** /principals | +[**revoke_catalog_role_from_principal_role**](PolarisDefaultApi.md#revoke_catalog_role_from_principal_role) | **DELETE** /principal-roles/{principalRoleName}/catalog-roles/{catalogName}/{catalogRoleName} | +[**revoke_grant_from_catalog_role**](PolarisDefaultApi.md#revoke_grant_from_catalog_role) | **POST** /catalogs/{catalogName}/catalog-roles/{catalogRoleName}/grants | +[**revoke_principal_role**](PolarisDefaultApi.md#revoke_principal_role) | **DELETE** /principals/{principalName}/principal-roles/{principalRoleName} | +[**rotate_credentials**](PolarisDefaultApi.md#rotate_credentials) | **POST** /principals/{principalName}/rotate | +[**update_catalog**](PolarisDefaultApi.md#update_catalog) | **PUT** /catalogs/{catalogName} | +[**update_catalog_role**](PolarisDefaultApi.md#update_catalog_role) | **PUT** /catalogs/{catalogName}/catalog-roles/{catalogRoleName} | +[**update_principal**](PolarisDefaultApi.md#update_principal) | **PUT** /principals/{principalName} | +[**update_principal_role**](PolarisDefaultApi.md#update_principal_role) | **PUT** /principal-roles/{principalRoleName} | + + +# **add_grant_to_catalog_role** +> add_grant_to_catalog_role(catalog_name, catalog_role_name, add_grant_request=add_grant_request) + + + +Add a new grant to the catalog role + +### Example + +* OAuth Authentication (OAuth2): + +```python +import polaris.management +from polaris.management.models.add_grant_request import AddGrantRequest +from polaris.management.rest import ApiException +from pprint import pprint + +# Defining the host is optional and defaults to https://localhost/api/management/v1 +# See configuration.py for a list of all supported configuration parameters. +configuration = polaris.management.Configuration( + host = "https://localhost/api/management/v1" +) + +# The client must configure the authentication and authorization parameters +# in accordance with the API server security policy. +# Examples for each auth method are provided below, use the example that +# satisfies your auth use case. + +configuration.access_token = os.environ["ACCESS_TOKEN"] + +# Enter a context with an instance of the API client +with polaris.management.ApiClient(configuration) as api_client: + # Create an instance of the API class + api_instance = polaris.management.PolarisDefaultApi(api_client) + catalog_name = 'catalog_name_example' # str | The name of the catalog where the role will receive the grant + catalog_role_name = 'catalog_role_name_example' # str | The name of the role receiving the grant (must exist) + add_grant_request = polaris.management.AddGrantRequest() # AddGrantRequest | (optional) + + try: + api_instance.add_grant_to_catalog_role(catalog_name, catalog_role_name, add_grant_request=add_grant_request) + except Exception as e: + print("Exception when calling PolarisDefaultApi->add_grant_to_catalog_role: %s\n" % e) +``` + + + +### Parameters + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **catalog_name** | **str**| The name of the catalog where the role will receive the grant | + **catalog_role_name** | **str**| The name of the role receiving the grant (must exist) | + **add_grant_request** | [**AddGrantRequest**](AddGrantRequest.md)| | [optional] + +### Return type + +void (empty response body) + +### Authorization + +[OAuth2](../README.md#OAuth2) + +### HTTP request headers + + - **Content-Type**: application/json + - **Accept**: Not defined + +### HTTP response details + +| Status code | Description | Response headers | +|-------------|-------------|------------------| +**201** | Successful response | - | +**403** | The principal is not authorized to create grants | - | +**404** | The catalog or the role does not exist | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **assign_catalog_role_to_principal_role** +> assign_catalog_role_to_principal_role(principal_role_name, catalog_name, grant_catalog_role_request) + + + +Assign a catalog role to a principal role + +### Example + +* OAuth Authentication (OAuth2): + +```python +import polaris.management +from polaris.management.models.grant_catalog_role_request import GrantCatalogRoleRequest +from polaris.management.rest import ApiException +from pprint import pprint + +# Defining the host is optional and defaults to https://localhost/api/management/v1 +# See configuration.py for a list of all supported configuration parameters. +configuration = polaris.management.Configuration( + host = "https://localhost/api/management/v1" +) + +# The client must configure the authentication and authorization parameters +# in accordance with the API server security policy. +# Examples for each auth method are provided below, use the example that +# satisfies your auth use case. + +configuration.access_token = os.environ["ACCESS_TOKEN"] + +# Enter a context with an instance of the API client +with polaris.management.ApiClient(configuration) as api_client: + # Create an instance of the API class + api_instance = polaris.management.PolarisDefaultApi(api_client) + principal_role_name = 'principal_role_name_example' # str | The principal role name + catalog_name = 'catalog_name_example' # str | The name of the catalog where the catalogRoles reside + grant_catalog_role_request = polaris.management.GrantCatalogRoleRequest() # GrantCatalogRoleRequest | The principal to create + + try: + api_instance.assign_catalog_role_to_principal_role(principal_role_name, catalog_name, grant_catalog_role_request) + except Exception as e: + print("Exception when calling PolarisDefaultApi->assign_catalog_role_to_principal_role: %s\n" % e) +``` + + + +### Parameters + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **principal_role_name** | **str**| The principal role name | + **catalog_name** | **str**| The name of the catalog where the catalogRoles reside | + **grant_catalog_role_request** | [**GrantCatalogRoleRequest**](GrantCatalogRoleRequest.md)| The principal to create | + +### Return type + +void (empty response body) + +### Authorization + +[OAuth2](../README.md#OAuth2) + +### HTTP request headers + + - **Content-Type**: application/json + - **Accept**: Not defined + +### HTTP response details + +| Status code | Description | Response headers | +|-------------|-------------|------------------| +**201** | Successful response | - | +**403** | The caller does not have permission to assign a catalog role | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **assign_principal_role** +> assign_principal_role(principal_name, grant_principal_role_request) + + + +Add a role to the principal + +### Example + +* OAuth Authentication (OAuth2): + +```python +import polaris.management +from polaris.management.models.grant_principal_role_request import GrantPrincipalRoleRequest +from polaris.management.rest import ApiException +from pprint import pprint + +# Defining the host is optional and defaults to https://localhost/api/management/v1 +# See configuration.py for a list of all supported configuration parameters. +configuration = polaris.management.Configuration( + host = "https://localhost/api/management/v1" +) + +# The client must configure the authentication and authorization parameters +# in accordance with the API server security policy. +# Examples for each auth method are provided below, use the example that +# satisfies your auth use case. + +configuration.access_token = os.environ["ACCESS_TOKEN"] + +# Enter a context with an instance of the API client +with polaris.management.ApiClient(configuration) as api_client: + # Create an instance of the API class + api_instance = polaris.management.PolarisDefaultApi(api_client) + principal_name = 'principal_name_example' # str | The name of the target principal + grant_principal_role_request = polaris.management.GrantPrincipalRoleRequest() # GrantPrincipalRoleRequest | The principal role to assign + + try: + api_instance.assign_principal_role(principal_name, grant_principal_role_request) + except Exception as e: + print("Exception when calling PolarisDefaultApi->assign_principal_role: %s\n" % e) +``` + + + +### Parameters + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **principal_name** | **str**| The name of the target principal | + **grant_principal_role_request** | [**GrantPrincipalRoleRequest**](GrantPrincipalRoleRequest.md)| The principal role to assign | + +### Return type + +void (empty response body) + +### Authorization + +[OAuth2](../README.md#OAuth2) + +### HTTP request headers + + - **Content-Type**: application/json + - **Accept**: Not defined + +### HTTP response details + +| Status code | Description | Response headers | +|-------------|-------------|------------------| +**201** | Successful response | - | +**403** | The caller does not have permission to add assign a role to the principal | - | +**404** | The catalog, the principal, or the role does not exist | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **create_catalog** +> create_catalog(create_catalog_request) + + + +Add a new Catalog + +### Example + +* OAuth Authentication (OAuth2): + +```python +import polaris.management +from polaris.management.models.create_catalog_request import CreateCatalogRequest +from polaris.management.rest import ApiException +from pprint import pprint + +# Defining the host is optional and defaults to https://localhost/api/management/v1 +# See configuration.py for a list of all supported configuration parameters. +configuration = polaris.management.Configuration( + host = "https://localhost/api/management/v1" +) + +# The client must configure the authentication and authorization parameters +# in accordance with the API server security policy. +# Examples for each auth method are provided below, use the example that +# satisfies your auth use case. + +configuration.access_token = os.environ["ACCESS_TOKEN"] + +# Enter a context with an instance of the API client +with polaris.management.ApiClient(configuration) as api_client: + # Create an instance of the API class + api_instance = polaris.management.PolarisDefaultApi(api_client) + create_catalog_request = polaris.management.CreateCatalogRequest() # CreateCatalogRequest | The Catalog to create + + try: + api_instance.create_catalog(create_catalog_request) + except Exception as e: + print("Exception when calling PolarisDefaultApi->create_catalog: %s\n" % e) +``` + + + +### Parameters + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **create_catalog_request** | [**CreateCatalogRequest**](CreateCatalogRequest.md)| The Catalog to create | + +### Return type + +void (empty response body) + +### Authorization + +[OAuth2](../README.md#OAuth2) + +### HTTP request headers + + - **Content-Type**: application/json + - **Accept**: Not defined + +### HTTP response details + +| Status code | Description | Response headers | +|-------------|-------------|------------------| +**201** | Successful response | - | +**403** | The caller does not have permission to create a catalog | - | +**404** | The catalog does not exist | - | +**409** | A catalog with the specified name already exists | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **create_catalog_role** +> create_catalog_role(catalog_name, create_catalog_role_request=create_catalog_role_request) + + + +Create a new role in the catalog + +### Example + +* OAuth Authentication (OAuth2): + +```python +import polaris.management +from polaris.management.models.create_catalog_role_request import CreateCatalogRoleRequest +from polaris.management.rest import ApiException +from pprint import pprint + +# Defining the host is optional and defaults to https://localhost/api/management/v1 +# See configuration.py for a list of all supported configuration parameters. +configuration = polaris.management.Configuration( + host = "https://localhost/api/management/v1" +) + +# The client must configure the authentication and authorization parameters +# in accordance with the API server security policy. +# Examples for each auth method are provided below, use the example that +# satisfies your auth use case. + +configuration.access_token = os.environ["ACCESS_TOKEN"] + +# Enter a context with an instance of the API client +with polaris.management.ApiClient(configuration) as api_client: + # Create an instance of the API class + api_instance = polaris.management.PolarisDefaultApi(api_client) + catalog_name = 'catalog_name_example' # str | The catalog for which we are reading/updating roles + create_catalog_role_request = polaris.management.CreateCatalogRoleRequest() # CreateCatalogRoleRequest | (optional) + + try: + api_instance.create_catalog_role(catalog_name, create_catalog_role_request=create_catalog_role_request) + except Exception as e: + print("Exception when calling PolarisDefaultApi->create_catalog_role: %s\n" % e) +``` + + + +### Parameters + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **catalog_name** | **str**| The catalog for which we are reading/updating roles | + **create_catalog_role_request** | [**CreateCatalogRoleRequest**](CreateCatalogRoleRequest.md)| | [optional] + +### Return type + +void (empty response body) + +### Authorization + +[OAuth2](../README.md#OAuth2) + +### HTTP request headers + + - **Content-Type**: application/json + - **Accept**: Not defined + +### HTTP response details + +| Status code | Description | Response headers | +|-------------|-------------|------------------| +**201** | Successful response | - | +**403** | The principal is not authorized to create roles | - | +**404** | The catalog does not exist | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **create_principal** +> PrincipalWithCredentials create_principal(create_principal_request) + + + +Create a principal + +### Example + +* OAuth Authentication (OAuth2): + +```python +import polaris.management +from polaris.management.models.create_principal_request import CreatePrincipalRequest +from polaris.management.models.principal_with_credentials import PrincipalWithCredentials +from polaris.management.rest import ApiException +from pprint import pprint + +# Defining the host is optional and defaults to https://localhost/api/management/v1 +# See configuration.py for a list of all supported configuration parameters. +configuration = polaris.management.Configuration( + host = "https://localhost/api/management/v1" +) + +# The client must configure the authentication and authorization parameters +# in accordance with the API server security policy. +# Examples for each auth method are provided below, use the example that +# satisfies your auth use case. + +configuration.access_token = os.environ["ACCESS_TOKEN"] + +# Enter a context with an instance of the API client +with polaris.management.ApiClient(configuration) as api_client: + # Create an instance of the API class + api_instance = polaris.management.PolarisDefaultApi(api_client) + create_principal_request = polaris.management.CreatePrincipalRequest() # CreatePrincipalRequest | The principal to create + + try: + api_response = api_instance.create_principal(create_principal_request) + print("The response of PolarisDefaultApi->create_principal:\n") + pprint(api_response) + except Exception as e: + print("Exception when calling PolarisDefaultApi->create_principal: %s\n" % e) +``` + + + +### Parameters + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **create_principal_request** | [**CreatePrincipalRequest**](CreatePrincipalRequest.md)| The principal to create | + +### Return type + +[**PrincipalWithCredentials**](PrincipalWithCredentials.md) + +### Authorization + +[OAuth2](../README.md#OAuth2) + +### HTTP request headers + + - **Content-Type**: application/json + - **Accept**: application/json + +### HTTP response details + +| Status code | Description | Response headers | +|-------------|-------------|------------------| +**201** | Successful response | - | +**403** | The caller does not have permission to add a principal | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **create_principal_role** +> create_principal_role(create_principal_role_request) + + + +Create a principal role + +### Example + +* OAuth Authentication (OAuth2): + +```python +import polaris.management +from polaris.management.models.create_principal_role_request import CreatePrincipalRoleRequest +from polaris.management.rest import ApiException +from pprint import pprint + +# Defining the host is optional and defaults to https://localhost/api/management/v1 +# See configuration.py for a list of all supported configuration parameters. +configuration = polaris.management.Configuration( + host = "https://localhost/api/management/v1" +) + +# The client must configure the authentication and authorization parameters +# in accordance with the API server security policy. +# Examples for each auth method are provided below, use the example that +# satisfies your auth use case. + +configuration.access_token = os.environ["ACCESS_TOKEN"] + +# Enter a context with an instance of the API client +with polaris.management.ApiClient(configuration) as api_client: + # Create an instance of the API class + api_instance = polaris.management.PolarisDefaultApi(api_client) + create_principal_role_request = polaris.management.CreatePrincipalRoleRequest() # CreatePrincipalRoleRequest | The principal to create + + try: + api_instance.create_principal_role(create_principal_role_request) + except Exception as e: + print("Exception when calling PolarisDefaultApi->create_principal_role: %s\n" % e) +``` + + + +### Parameters + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **create_principal_role_request** | [**CreatePrincipalRoleRequest**](CreatePrincipalRoleRequest.md)| The principal to create | + +### Return type + +void (empty response body) + +### Authorization + +[OAuth2](../README.md#OAuth2) + +### HTTP request headers + + - **Content-Type**: application/json + - **Accept**: Not defined + +### HTTP response details + +| Status code | Description | Response headers | +|-------------|-------------|------------------| +**201** | Successful response | - | +**403** | The caller does not have permission to add a principal role | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **delete_catalog** +> delete_catalog(catalog_name) + + + +Delete an existing catalog. This is a cascading operation that deletes all metadata, including principals, roles and grants. If the catalog is an internal catalog, all tables and namespaces are dropped without purge. + +### Example + +* OAuth Authentication (OAuth2): + +```python +import polaris.management +from polaris.management.rest import ApiException +from pprint import pprint + +# Defining the host is optional and defaults to https://localhost/api/management/v1 +# See configuration.py for a list of all supported configuration parameters. +configuration = polaris.management.Configuration( + host = "https://localhost/api/management/v1" +) + +# The client must configure the authentication and authorization parameters +# in accordance with the API server security policy. +# Examples for each auth method are provided below, use the example that +# satisfies your auth use case. + +configuration.access_token = os.environ["ACCESS_TOKEN"] + +# Enter a context with an instance of the API client +with polaris.management.ApiClient(configuration) as api_client: + # Create an instance of the API class + api_instance = polaris.management.PolarisDefaultApi(api_client) + catalog_name = 'catalog_name_example' # str | The name of the catalog + + try: + api_instance.delete_catalog(catalog_name) + except Exception as e: + print("Exception when calling PolarisDefaultApi->delete_catalog: %s\n" % e) +``` + + + +### Parameters + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **catalog_name** | **str**| The name of the catalog | + +### Return type + +void (empty response body) + +### Authorization + +[OAuth2](../README.md#OAuth2) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: Not defined + +### HTTP response details + +| Status code | Description | Response headers | +|-------------|-------------|------------------| +**204** | Success, no content | - | +**403** | The caller does not have permission to delete a catalog | - | +**404** | The catalog does not exist | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **delete_catalog_role** +> delete_catalog_role(catalog_name, catalog_role_name) + + + +Delete an existing role from the catalog. All associated grants will also be deleted + +### Example + +* OAuth Authentication (OAuth2): + +```python +import polaris.management +from polaris.management.rest import ApiException +from pprint import pprint + +# Defining the host is optional and defaults to https://localhost/api/management/v1 +# See configuration.py for a list of all supported configuration parameters. +configuration = polaris.management.Configuration( + host = "https://localhost/api/management/v1" +) + +# The client must configure the authentication and authorization parameters +# in accordance with the API server security policy. +# Examples for each auth method are provided below, use the example that +# satisfies your auth use case. + +configuration.access_token = os.environ["ACCESS_TOKEN"] + +# Enter a context with an instance of the API client +with polaris.management.ApiClient(configuration) as api_client: + # Create an instance of the API class + api_instance = polaris.management.PolarisDefaultApi(api_client) + catalog_name = 'catalog_name_example' # str | The catalog for which we are retrieving roles + catalog_role_name = 'catalog_role_name_example' # str | The name of the role + + try: + api_instance.delete_catalog_role(catalog_name, catalog_role_name) + except Exception as e: + print("Exception when calling PolarisDefaultApi->delete_catalog_role: %s\n" % e) +``` + + + +### Parameters + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **catalog_name** | **str**| The catalog for which we are retrieving roles | + **catalog_role_name** | **str**| The name of the role | + +### Return type + +void (empty response body) + +### Authorization + +[OAuth2](../README.md#OAuth2) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: Not defined + +### HTTP response details + +| Status code | Description | Response headers | +|-------------|-------------|------------------| +**204** | Success, no content | - | +**403** | The principal is not authorized to delete roles | - | +**404** | The catalog or the role does not exist | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **delete_principal** +> delete_principal(principal_name) + + + +Remove a principal from polaris + +### Example + +* OAuth Authentication (OAuth2): + +```python +import polaris.management +from polaris.management.rest import ApiException +from pprint import pprint + +# Defining the host is optional and defaults to https://localhost/api/management/v1 +# See configuration.py for a list of all supported configuration parameters. +configuration = polaris.management.Configuration( + host = "https://localhost/api/management/v1" +) + +# The client must configure the authentication and authorization parameters +# in accordance with the API server security policy. +# Examples for each auth method are provided below, use the example that +# satisfies your auth use case. + +configuration.access_token = os.environ["ACCESS_TOKEN"] + +# Enter a context with an instance of the API client +with polaris.management.ApiClient(configuration) as api_client: + # Create an instance of the API class + api_instance = polaris.management.PolarisDefaultApi(api_client) + principal_name = 'principal_name_example' # str | The principal name + + try: + api_instance.delete_principal(principal_name) + except Exception as e: + print("Exception when calling PolarisDefaultApi->delete_principal: %s\n" % e) +``` + + + +### Parameters + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **principal_name** | **str**| The principal name | + +### Return type + +void (empty response body) + +### Authorization + +[OAuth2](../README.md#OAuth2) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: Not defined + +### HTTP response details + +| Status code | Description | Response headers | +|-------------|-------------|------------------| +**204** | Success, no content | - | +**403** | The caller does not have permission to delete a principal | - | +**404** | The principal does not exist | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **delete_principal_role** +> delete_principal_role(principal_role_name) + + + +Remove a principal role from polaris + +### Example + +* OAuth Authentication (OAuth2): + +```python +import polaris.management +from polaris.management.rest import ApiException +from pprint import pprint + +# Defining the host is optional and defaults to https://localhost/api/management/v1 +# See configuration.py for a list of all supported configuration parameters. +configuration = polaris.management.Configuration( + host = "https://localhost/api/management/v1" +) + +# The client must configure the authentication and authorization parameters +# in accordance with the API server security policy. +# Examples for each auth method are provided below, use the example that +# satisfies your auth use case. + +configuration.access_token = os.environ["ACCESS_TOKEN"] + +# Enter a context with an instance of the API client +with polaris.management.ApiClient(configuration) as api_client: + # Create an instance of the API class + api_instance = polaris.management.PolarisDefaultApi(api_client) + principal_role_name = 'principal_role_name_example' # str | The principal role name + + try: + api_instance.delete_principal_role(principal_role_name) + except Exception as e: + print("Exception when calling PolarisDefaultApi->delete_principal_role: %s\n" % e) +``` + + + +### Parameters + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **principal_role_name** | **str**| The principal role name | + +### Return type + +void (empty response body) + +### Authorization + +[OAuth2](../README.md#OAuth2) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: Not defined + +### HTTP response details + +| Status code | Description | Response headers | +|-------------|-------------|------------------| +**204** | Success, no content | - | +**403** | The caller does not have permission to delete a principal role | - | +**404** | The principal role does not exist | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **get_catalog** +> Catalog get_catalog(catalog_name) + + + +Get the details of a catalog + +### Example + +* OAuth Authentication (OAuth2): + +```python +import polaris.management +from polaris.management.models.catalog import Catalog +from polaris.management.rest import ApiException +from pprint import pprint + +# Defining the host is optional and defaults to https://localhost/api/management/v1 +# See configuration.py for a list of all supported configuration parameters. +configuration = polaris.management.Configuration( + host = "https://localhost/api/management/v1" +) + +# The client must configure the authentication and authorization parameters +# in accordance with the API server security policy. +# Examples for each auth method are provided below, use the example that +# satisfies your auth use case. + +configuration.access_token = os.environ["ACCESS_TOKEN"] + +# Enter a context with an instance of the API client +with polaris.management.ApiClient(configuration) as api_client: + # Create an instance of the API class + api_instance = polaris.management.PolarisDefaultApi(api_client) + catalog_name = 'catalog_name_example' # str | The name of the catalog + + try: + api_response = api_instance.get_catalog(catalog_name) + print("The response of PolarisDefaultApi->get_catalog:\n") + pprint(api_response) + except Exception as e: + print("Exception when calling PolarisDefaultApi->get_catalog: %s\n" % e) +``` + + + +### Parameters + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **catalog_name** | **str**| The name of the catalog | + +### Return type + +[**Catalog**](Catalog.md) + +### Authorization + +[OAuth2](../README.md#OAuth2) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + +### HTTP response details + +| Status code | Description | Response headers | +|-------------|-------------|------------------| +**200** | The catalog details | - | +**403** | The caller does not have permission to read catalog details | - | +**404** | The catalog does not exist | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **get_catalog_role** +> CatalogRole get_catalog_role(catalog_name, catalog_role_name) + + + +Get the details of an existing role + +### Example + +* OAuth Authentication (OAuth2): + +```python +import polaris.management +from polaris.management.models.catalog_role import CatalogRole +from polaris.management.rest import ApiException +from pprint import pprint + +# Defining the host is optional and defaults to https://localhost/api/management/v1 +# See configuration.py for a list of all supported configuration parameters. +configuration = polaris.management.Configuration( + host = "https://localhost/api/management/v1" +) + +# The client must configure the authentication and authorization parameters +# in accordance with the API server security policy. +# Examples for each auth method are provided below, use the example that +# satisfies your auth use case. + +configuration.access_token = os.environ["ACCESS_TOKEN"] + +# Enter a context with an instance of the API client +with polaris.management.ApiClient(configuration) as api_client: + # Create an instance of the API class + api_instance = polaris.management.PolarisDefaultApi(api_client) + catalog_name = 'catalog_name_example' # str | The catalog for which we are retrieving roles + catalog_role_name = 'catalog_role_name_example' # str | The name of the role + + try: + api_response = api_instance.get_catalog_role(catalog_name, catalog_role_name) + print("The response of PolarisDefaultApi->get_catalog_role:\n") + pprint(api_response) + except Exception as e: + print("Exception when calling PolarisDefaultApi->get_catalog_role: %s\n" % e) +``` + + + +### Parameters + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **catalog_name** | **str**| The catalog for which we are retrieving roles | + **catalog_role_name** | **str**| The name of the role | + +### Return type + +[**CatalogRole**](CatalogRole.md) + +### Authorization + +[OAuth2](../README.md#OAuth2) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + +### HTTP response details + +| Status code | Description | Response headers | +|-------------|-------------|------------------| +**200** | The specified role details | - | +**403** | The principal is not authorized to read role data | - | +**404** | The catalog or the role does not exist | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **get_principal** +> Principal get_principal(principal_name) + + + +Get the principal details + +### Example + +* OAuth Authentication (OAuth2): + +```python +import polaris.management +from polaris.management.models.principal import Principal +from polaris.management.rest import ApiException +from pprint import pprint + +# Defining the host is optional and defaults to https://localhost/api/management/v1 +# See configuration.py for a list of all supported configuration parameters. +configuration = polaris.management.Configuration( + host = "https://localhost/api/management/v1" +) + +# The client must configure the authentication and authorization parameters +# in accordance with the API server security policy. +# Examples for each auth method are provided below, use the example that +# satisfies your auth use case. + +configuration.access_token = os.environ["ACCESS_TOKEN"] + +# Enter a context with an instance of the API client +with polaris.management.ApiClient(configuration) as api_client: + # Create an instance of the API class + api_instance = polaris.management.PolarisDefaultApi(api_client) + principal_name = 'principal_name_example' # str | The principal name + + try: + api_response = api_instance.get_principal(principal_name) + print("The response of PolarisDefaultApi->get_principal:\n") + pprint(api_response) + except Exception as e: + print("Exception when calling PolarisDefaultApi->get_principal: %s\n" % e) +``` + + + +### Parameters + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **principal_name** | **str**| The principal name | + +### Return type + +[**Principal**](Principal.md) + +### Authorization + +[OAuth2](../README.md#OAuth2) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + +### HTTP response details + +| Status code | Description | Response headers | +|-------------|-------------|------------------| +**200** | The requested principal | - | +**403** | The caller does not have permission to get principal details | - | +**404** | The catalog or principal does not exist | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **get_principal_role** +> PrincipalRole get_principal_role(principal_role_name) + + + +Get the principal role details + +### Example + +* OAuth Authentication (OAuth2): + +```python +import polaris.management +from polaris.management.models.principal_role import PrincipalRole +from polaris.management.rest import ApiException +from pprint import pprint + +# Defining the host is optional and defaults to https://localhost/api/management/v1 +# See configuration.py for a list of all supported configuration parameters. +configuration = polaris.management.Configuration( + host = "https://localhost/api/management/v1" +) + +# The client must configure the authentication and authorization parameters +# in accordance with the API server security policy. +# Examples for each auth method are provided below, use the example that +# satisfies your auth use case. + +configuration.access_token = os.environ["ACCESS_TOKEN"] + +# Enter a context with an instance of the API client +with polaris.management.ApiClient(configuration) as api_client: + # Create an instance of the API class + api_instance = polaris.management.PolarisDefaultApi(api_client) + principal_role_name = 'principal_role_name_example' # str | The principal role name + + try: + api_response = api_instance.get_principal_role(principal_role_name) + print("The response of PolarisDefaultApi->get_principal_role:\n") + pprint(api_response) + except Exception as e: + print("Exception when calling PolarisDefaultApi->get_principal_role: %s\n" % e) +``` + + + +### Parameters + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **principal_role_name** | **str**| The principal role name | + +### Return type + +[**PrincipalRole**](PrincipalRole.md) + +### Authorization + +[OAuth2](../README.md#OAuth2) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + +### HTTP response details + +| Status code | Description | Response headers | +|-------------|-------------|------------------| +**200** | The requested principal role | - | +**403** | The caller does not have permission to get principal role details | - | +**404** | The principal role does not exist | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **list_assignee_principal_roles_for_catalog_role** +> PrincipalRoles list_assignee_principal_roles_for_catalog_role(catalog_name, catalog_role_name) + + + +List the PrincipalRoles to whome the tagetcatalog role has been assigned + +### Example + +* OAuth Authentication (OAuth2): + +```python +import polaris.management +from polaris.management.models.principal_roles import PrincipalRoles +from polaris.management.rest import ApiException +from pprint import pprint + +# Defining the host is optional and defaults to https://localhost/api/management/v1 +# See configuration.py for a list of all supported configuration parameters. +configuration = polaris.management.Configuration( + host = "https://localhost/api/management/v1" +) + +# The client must configure the authentication and authorization parameters +# in accordance with the API server security policy. +# Examples for each auth method are provided below, use the example that +# satisfies your auth use case. + +configuration.access_token = os.environ["ACCESS_TOKEN"] + +# Enter a context with an instance of the API client +with polaris.management.ApiClient(configuration) as api_client: + # Create an instance of the API class + api_instance = polaris.management.PolarisDefaultApi(api_client) + catalog_name = 'catalog_name_example' # str | The name of the catalog where the catalog role resides + catalog_role_name = 'catalog_role_name_example' # str | The name of the catalog role + + try: + api_response = api_instance.list_assignee_principal_roles_for_catalog_role(catalog_name, catalog_role_name) + print("The response of PolarisDefaultApi->list_assignee_principal_roles_for_catalog_role:\n") + pprint(api_response) + except Exception as e: + print("Exception when calling PolarisDefaultApi->list_assignee_principal_roles_for_catalog_role: %s\n" % e) +``` + + + +### Parameters + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **catalog_name** | **str**| The name of the catalog where the catalog role resides | + **catalog_role_name** | **str**| The name of the catalog role | + +### Return type + +[**PrincipalRoles**](PrincipalRoles.md) + +### Authorization + +[OAuth2](../README.md#OAuth2) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + +### HTTP response details + +| Status code | Description | Response headers | +|-------------|-------------|------------------| +**200** | List the PrincipalRoles to whome the tagetcatalog role has been assigned | - | +**403** | The caller does not have permission to list principal roles | - | +**404** | The catalog or catalog role does not exist | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **list_assignee_principals_for_principal_role** +> Principals list_assignee_principals_for_principal_role(principal_role_name) + + + +List the Principals to whom the target principal role has been assigned + +### Example + +* OAuth Authentication (OAuth2): + +```python +import polaris.management +from polaris.management.models.principals import Principals +from polaris.management.rest import ApiException +from pprint import pprint + +# Defining the host is optional and defaults to https://localhost/api/management/v1 +# See configuration.py for a list of all supported configuration parameters. +configuration = polaris.management.Configuration( + host = "https://localhost/api/management/v1" +) + +# The client must configure the authentication and authorization parameters +# in accordance with the API server security policy. +# Examples for each auth method are provided below, use the example that +# satisfies your auth use case. + +configuration.access_token = os.environ["ACCESS_TOKEN"] + +# Enter a context with an instance of the API client +with polaris.management.ApiClient(configuration) as api_client: + # Create an instance of the API class + api_instance = polaris.management.PolarisDefaultApi(api_client) + principal_role_name = 'principal_role_name_example' # str | The principal role name + + try: + api_response = api_instance.list_assignee_principals_for_principal_role(principal_role_name) + print("The response of PolarisDefaultApi->list_assignee_principals_for_principal_role:\n") + pprint(api_response) + except Exception as e: + print("Exception when calling PolarisDefaultApi->list_assignee_principals_for_principal_role: %s\n" % e) +``` + + + +### Parameters + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **principal_role_name** | **str**| The principal role name | + +### Return type + +[**Principals**](Principals.md) + +### Authorization + +[OAuth2](../README.md#OAuth2) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + +### HTTP response details + +| Status code | Description | Response headers | +|-------------|-------------|------------------| +**200** | List the Principals to whom the target principal role has been assigned | - | +**403** | The caller does not have permission to list principals | - | +**404** | The principal role does not exist | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **list_catalog_roles** +> CatalogRoles list_catalog_roles(catalog_name) + + + +List existing roles in the catalog + +### Example + +* OAuth Authentication (OAuth2): + +```python +import polaris.management +from polaris.management.models.catalog_roles import CatalogRoles +from polaris.management.rest import ApiException +from pprint import pprint + +# Defining the host is optional and defaults to https://localhost/api/management/v1 +# See configuration.py for a list of all supported configuration parameters. +configuration = polaris.management.Configuration( + host = "https://localhost/api/management/v1" +) + +# The client must configure the authentication and authorization parameters +# in accordance with the API server security policy. +# Examples for each auth method are provided below, use the example that +# satisfies your auth use case. + +configuration.access_token = os.environ["ACCESS_TOKEN"] + +# Enter a context with an instance of the API client +with polaris.management.ApiClient(configuration) as api_client: + # Create an instance of the API class + api_instance = polaris.management.PolarisDefaultApi(api_client) + catalog_name = 'catalog_name_example' # str | The catalog for which we are reading/updating roles + + try: + api_response = api_instance.list_catalog_roles(catalog_name) + print("The response of PolarisDefaultApi->list_catalog_roles:\n") + pprint(api_response) + except Exception as e: + print("Exception when calling PolarisDefaultApi->list_catalog_roles: %s\n" % e) +``` + + + +### Parameters + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **catalog_name** | **str**| The catalog for which we are reading/updating roles | + +### Return type + +[**CatalogRoles**](CatalogRoles.md) + +### Authorization + +[OAuth2](../README.md#OAuth2) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + +### HTTP response details + +| Status code | Description | Response headers | +|-------------|-------------|------------------| +**200** | The list of roles that exist in this catalog | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **list_catalog_roles_for_principal_role** +> CatalogRoles list_catalog_roles_for_principal_role(principal_role_name, catalog_name) + + + +Get the catalog roles mapped to the principal role + +### Example + +* OAuth Authentication (OAuth2): + +```python +import polaris.management +from polaris.management.models.catalog_roles import CatalogRoles +from polaris.management.rest import ApiException +from pprint import pprint + +# Defining the host is optional and defaults to https://localhost/api/management/v1 +# See configuration.py for a list of all supported configuration parameters. +configuration = polaris.management.Configuration( + host = "https://localhost/api/management/v1" +) + +# The client must configure the authentication and authorization parameters +# in accordance with the API server security policy. +# Examples for each auth method are provided below, use the example that +# satisfies your auth use case. + +configuration.access_token = os.environ["ACCESS_TOKEN"] + +# Enter a context with an instance of the API client +with polaris.management.ApiClient(configuration) as api_client: + # Create an instance of the API class + api_instance = polaris.management.PolarisDefaultApi(api_client) + principal_role_name = 'principal_role_name_example' # str | The principal role name + catalog_name = 'catalog_name_example' # str | The name of the catalog where the catalogRoles reside + + try: + api_response = api_instance.list_catalog_roles_for_principal_role(principal_role_name, catalog_name) + print("The response of PolarisDefaultApi->list_catalog_roles_for_principal_role:\n") + pprint(api_response) + except Exception as e: + print("Exception when calling PolarisDefaultApi->list_catalog_roles_for_principal_role: %s\n" % e) +``` + + + +### Parameters + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **principal_role_name** | **str**| The principal role name | + **catalog_name** | **str**| The name of the catalog where the catalogRoles reside | + +### Return type + +[**CatalogRoles**](CatalogRoles.md) + +### Authorization + +[OAuth2](../README.md#OAuth2) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + +### HTTP response details + +| Status code | Description | Response headers | +|-------------|-------------|------------------| +**200** | The list of catalog roles mapped to the principal role | - | +**403** | The caller does not have permission to list catalog roles | - | +**404** | The principal role does not exist | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **list_catalogs** +> Catalogs list_catalogs() + + + +List all catalogs in this polaris service + +### Example + +* OAuth Authentication (OAuth2): + +```python +import polaris.management +from polaris.management.models.catalogs import Catalogs +from polaris.management.rest import ApiException +from pprint import pprint + +# Defining the host is optional and defaults to https://localhost/api/management/v1 +# See configuration.py for a list of all supported configuration parameters. +configuration = polaris.management.Configuration( + host = "https://localhost/api/management/v1" +) + +# The client must configure the authentication and authorization parameters +# in accordance with the API server security policy. +# Examples for each auth method are provided below, use the example that +# satisfies your auth use case. + +configuration.access_token = os.environ["ACCESS_TOKEN"] + +# Enter a context with an instance of the API client +with polaris.management.ApiClient(configuration) as api_client: + # Create an instance of the API class + api_instance = polaris.management.PolarisDefaultApi(api_client) + + try: + api_response = api_instance.list_catalogs() + print("The response of PolarisDefaultApi->list_catalogs:\n") + pprint(api_response) + except Exception as e: + print("Exception when calling PolarisDefaultApi->list_catalogs: %s\n" % e) +``` + + + +### Parameters + +This endpoint does not need any parameter. + +### Return type + +[**Catalogs**](Catalogs.md) + +### Authorization + +[OAuth2](../README.md#OAuth2) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + +### HTTP response details + +| Status code | Description | Response headers | +|-------------|-------------|------------------| +**200** | List of catalogs in the polaris service | - | +**403** | The caller does not have permission to list catalog details | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **list_grants_for_catalog_role** +> GrantResources list_grants_for_catalog_role(catalog_name, catalog_role_name) + + + +List the grants the catalog role holds + +### Example + +* OAuth Authentication (OAuth2): + +```python +import polaris.management +from polaris.management.models.grant_resources import GrantResources +from polaris.management.rest import ApiException +from pprint import pprint + +# Defining the host is optional and defaults to https://localhost/api/management/v1 +# See configuration.py for a list of all supported configuration parameters. +configuration = polaris.management.Configuration( + host = "https://localhost/api/management/v1" +) + +# The client must configure the authentication and authorization parameters +# in accordance with the API server security policy. +# Examples for each auth method are provided below, use the example that +# satisfies your auth use case. + +configuration.access_token = os.environ["ACCESS_TOKEN"] + +# Enter a context with an instance of the API client +with polaris.management.ApiClient(configuration) as api_client: + # Create an instance of the API class + api_instance = polaris.management.PolarisDefaultApi(api_client) + catalog_name = 'catalog_name_example' # str | The name of the catalog where the role will receive the grant + catalog_role_name = 'catalog_role_name_example' # str | The name of the role receiving the grant (must exist) + + try: + api_response = api_instance.list_grants_for_catalog_role(catalog_name, catalog_role_name) + print("The response of PolarisDefaultApi->list_grants_for_catalog_role:\n") + pprint(api_response) + except Exception as e: + print("Exception when calling PolarisDefaultApi->list_grants_for_catalog_role: %s\n" % e) +``` + + + +### Parameters + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **catalog_name** | **str**| The name of the catalog where the role will receive the grant | + **catalog_role_name** | **str**| The name of the role receiving the grant (must exist) | + +### Return type + +[**GrantResources**](GrantResources.md) + +### Authorization + +[OAuth2](../README.md#OAuth2) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + +### HTTP response details + +| Status code | Description | Response headers | +|-------------|-------------|------------------| +**200** | List of all grants given to the role in this catalog | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **list_principal_roles** +> PrincipalRoles list_principal_roles() + + + +List the principal roles + +### Example + +* OAuth Authentication (OAuth2): + +```python +import polaris.management +from polaris.management.models.principal_roles import PrincipalRoles +from polaris.management.rest import ApiException +from pprint import pprint + +# Defining the host is optional and defaults to https://localhost/api/management/v1 +# See configuration.py for a list of all supported configuration parameters. +configuration = polaris.management.Configuration( + host = "https://localhost/api/management/v1" +) + +# The client must configure the authentication and authorization parameters +# in accordance with the API server security policy. +# Examples for each auth method are provided below, use the example that +# satisfies your auth use case. + +configuration.access_token = os.environ["ACCESS_TOKEN"] + +# Enter a context with an instance of the API client +with polaris.management.ApiClient(configuration) as api_client: + # Create an instance of the API class + api_instance = polaris.management.PolarisDefaultApi(api_client) + + try: + api_response = api_instance.list_principal_roles() + print("The response of PolarisDefaultApi->list_principal_roles:\n") + pprint(api_response) + except Exception as e: + print("Exception when calling PolarisDefaultApi->list_principal_roles: %s\n" % e) +``` + + + +### Parameters + +This endpoint does not need any parameter. + +### Return type + +[**PrincipalRoles**](PrincipalRoles.md) + +### Authorization + +[OAuth2](../README.md#OAuth2) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + +### HTTP response details + +| Status code | Description | Response headers | +|-------------|-------------|------------------| +**200** | List of principal roles | - | +**403** | The caller does not have permission to list principal roles | - | +**404** | The catalog does not exist | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **list_principal_roles_assigned** +> PrincipalRoles list_principal_roles_assigned(principal_name) + + + +List the roles assigned to the principal + +### Example + +* OAuth Authentication (OAuth2): + +```python +import polaris.management +from polaris.management.models.principal_roles import PrincipalRoles +from polaris.management.rest import ApiException +from pprint import pprint + +# Defining the host is optional and defaults to https://localhost/api/management/v1 +# See configuration.py for a list of all supported configuration parameters. +configuration = polaris.management.Configuration( + host = "https://localhost/api/management/v1" +) + +# The client must configure the authentication and authorization parameters +# in accordance with the API server security policy. +# Examples for each auth method are provided below, use the example that +# satisfies your auth use case. + +configuration.access_token = os.environ["ACCESS_TOKEN"] + +# Enter a context with an instance of the API client +with polaris.management.ApiClient(configuration) as api_client: + # Create an instance of the API class + api_instance = polaris.management.PolarisDefaultApi(api_client) + principal_name = 'principal_name_example' # str | The name of the target principal + + try: + api_response = api_instance.list_principal_roles_assigned(principal_name) + print("The response of PolarisDefaultApi->list_principal_roles_assigned:\n") + pprint(api_response) + except Exception as e: + print("Exception when calling PolarisDefaultApi->list_principal_roles_assigned: %s\n" % e) +``` + + + +### Parameters + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **principal_name** | **str**| The name of the target principal | + +### Return type + +[**PrincipalRoles**](PrincipalRoles.md) + +### Authorization + +[OAuth2](../README.md#OAuth2) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + +### HTTP response details + +| Status code | Description | Response headers | +|-------------|-------------|------------------| +**200** | List of roles assigned to this principal | - | +**403** | The caller does not have permission to list roles | - | +**404** | The principal or catalog does not exist | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **list_principals** +> Principals list_principals() + + + +List the principals for the current catalog + +### Example + +* OAuth Authentication (OAuth2): + +```python +import polaris.management +from polaris.management.models.principals import Principals +from polaris.management.rest import ApiException +from pprint import pprint + +# Defining the host is optional and defaults to https://localhost/api/management/v1 +# See configuration.py for a list of all supported configuration parameters. +configuration = polaris.management.Configuration( + host = "https://localhost/api/management/v1" +) + +# The client must configure the authentication and authorization parameters +# in accordance with the API server security policy. +# Examples for each auth method are provided below, use the example that +# satisfies your auth use case. + +configuration.access_token = os.environ["ACCESS_TOKEN"] + +# Enter a context with an instance of the API client +with polaris.management.ApiClient(configuration) as api_client: + # Create an instance of the API class + api_instance = polaris.management.PolarisDefaultApi(api_client) + + try: + api_response = api_instance.list_principals() + print("The response of PolarisDefaultApi->list_principals:\n") + pprint(api_response) + except Exception as e: + print("Exception when calling PolarisDefaultApi->list_principals: %s\n" % e) +``` + + + +### Parameters + +This endpoint does not need any parameter. + +### Return type + +[**Principals**](Principals.md) + +### Authorization + +[OAuth2](../README.md#OAuth2) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + +### HTTP response details + +| Status code | Description | Response headers | +|-------------|-------------|------------------| +**200** | List of principals for this catalog | - | +**403** | The caller does not have permission to list catalog admins | - | +**404** | The catalog does not exist | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **revoke_catalog_role_from_principal_role** +> revoke_catalog_role_from_principal_role(principal_role_name, catalog_name, catalog_role_name) + + + +Remove a catalog role from a principal role + +### Example + +* OAuth Authentication (OAuth2): + +```python +import polaris.management +from polaris.management.rest import ApiException +from pprint import pprint + +# Defining the host is optional and defaults to https://localhost/api/management/v1 +# See configuration.py for a list of all supported configuration parameters. +configuration = polaris.management.Configuration( + host = "https://localhost/api/management/v1" +) + +# The client must configure the authentication and authorization parameters +# in accordance with the API server security policy. +# Examples for each auth method are provided below, use the example that +# satisfies your auth use case. + +configuration.access_token = os.environ["ACCESS_TOKEN"] + +# Enter a context with an instance of the API client +with polaris.management.ApiClient(configuration) as api_client: + # Create an instance of the API class + api_instance = polaris.management.PolarisDefaultApi(api_client) + principal_role_name = 'principal_role_name_example' # str | The principal role name + catalog_name = 'catalog_name_example' # str | The name of the catalog that contains the role to revoke + catalog_role_name = 'catalog_role_name_example' # str | The name of the catalog role that should be revoked + + try: + api_instance.revoke_catalog_role_from_principal_role(principal_role_name, catalog_name, catalog_role_name) + except Exception as e: + print("Exception when calling PolarisDefaultApi->revoke_catalog_role_from_principal_role: %s\n" % e) +``` + + + +### Parameters + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **principal_role_name** | **str**| The principal role name | + **catalog_name** | **str**| The name of the catalog that contains the role to revoke | + **catalog_role_name** | **str**| The name of the catalog role that should be revoked | + +### Return type + +void (empty response body) + +### Authorization + +[OAuth2](../README.md#OAuth2) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: Not defined + +### HTTP response details + +| Status code | Description | Response headers | +|-------------|-------------|------------------| +**204** | Success, no content | - | +**403** | The caller does not have permission to revoke a catalog role | - | +**404** | The principal role does not exist | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **revoke_grant_from_catalog_role** +> revoke_grant_from_catalog_role(catalog_name, catalog_role_name, cascade=cascade, revoke_grant_request=revoke_grant_request) + + + +Delete a specific grant from the role. This may be a subset or a superset of the grants the role has. In case of a subset, the role will retain the grants not specified. If the `cascade` parameter is true, grant revocation will have a cascading effect - that is, if a principal has specific grants on a subresource, and grants are revoked on a parent resource, the grants present on the subresource will be revoked as well. By default, this behavior is disabled and grant revocation only affects the specified resource. + +### Example + +* OAuth Authentication (OAuth2): + +```python +import polaris.management +from polaris.management.models.revoke_grant_request import RevokeGrantRequest +from polaris.management.rest import ApiException +from pprint import pprint + +# Defining the host is optional and defaults to https://localhost/api/management/v1 +# See configuration.py for a list of all supported configuration parameters. +configuration = polaris.management.Configuration( + host = "https://localhost/api/management/v1" +) + +# The client must configure the authentication and authorization parameters +# in accordance with the API server security policy. +# Examples for each auth method are provided below, use the example that +# satisfies your auth use case. + +configuration.access_token = os.environ["ACCESS_TOKEN"] + +# Enter a context with an instance of the API client +with polaris.management.ApiClient(configuration) as api_client: + # Create an instance of the API class + api_instance = polaris.management.PolarisDefaultApi(api_client) + catalog_name = 'catalog_name_example' # str | The name of the catalog where the role will receive the grant + catalog_role_name = 'catalog_role_name_example' # str | The name of the role receiving the grant (must exist) + cascade = False # bool | If true, the grant revocation cascades to all subresources. (optional) (default to False) + revoke_grant_request = polaris.management.RevokeGrantRequest() # RevokeGrantRequest | (optional) + + try: + api_instance.revoke_grant_from_catalog_role(catalog_name, catalog_role_name, cascade=cascade, revoke_grant_request=revoke_grant_request) + except Exception as e: + print("Exception when calling PolarisDefaultApi->revoke_grant_from_catalog_role: %s\n" % e) +``` + + + +### Parameters + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **catalog_name** | **str**| The name of the catalog where the role will receive the grant | + **catalog_role_name** | **str**| The name of the role receiving the grant (must exist) | + **cascade** | **bool**| If true, the grant revocation cascades to all subresources. | [optional] [default to False] + **revoke_grant_request** | [**RevokeGrantRequest**](RevokeGrantRequest.md)| | [optional] + +### Return type + +void (empty response body) + +### Authorization + +[OAuth2](../README.md#OAuth2) + +### HTTP request headers + + - **Content-Type**: application/json + - **Accept**: Not defined + +### HTTP response details + +| Status code | Description | Response headers | +|-------------|-------------|------------------| +**201** | Successful response | - | +**403** | The principal is not authorized to create grants | - | +**404** | The catalog or the role does not exist | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **revoke_principal_role** +> revoke_principal_role(principal_name, principal_role_name) + + + +Remove a role from a catalog principal + +### Example + +* OAuth Authentication (OAuth2): + +```python +import polaris.management +from polaris.management.rest import ApiException +from pprint import pprint + +# Defining the host is optional and defaults to https://localhost/api/management/v1 +# See configuration.py for a list of all supported configuration parameters. +configuration = polaris.management.Configuration( + host = "https://localhost/api/management/v1" +) + +# The client must configure the authentication and authorization parameters +# in accordance with the API server security policy. +# Examples for each auth method are provided below, use the example that +# satisfies your auth use case. + +configuration.access_token = os.environ["ACCESS_TOKEN"] + +# Enter a context with an instance of the API client +with polaris.management.ApiClient(configuration) as api_client: + # Create an instance of the API class + api_instance = polaris.management.PolarisDefaultApi(api_client) + principal_name = 'principal_name_example' # str | The name of the target principal + principal_role_name = 'principal_role_name_example' # str | The name of the role + + try: + api_instance.revoke_principal_role(principal_name, principal_role_name) + except Exception as e: + print("Exception when calling PolarisDefaultApi->revoke_principal_role: %s\n" % e) +``` + + + +### Parameters + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **principal_name** | **str**| The name of the target principal | + **principal_role_name** | **str**| The name of the role | + +### Return type + +void (empty response body) + +### Authorization + +[OAuth2](../README.md#OAuth2) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: Not defined + +### HTTP response details + +| Status code | Description | Response headers | +|-------------|-------------|------------------| +**204** | Success, no content | - | +**403** | The caller does not have permission to remove a role from the principal | - | +**404** | The catalog or principal does not exist | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **rotate_credentials** +> PrincipalWithCredentials rotate_credentials(principal_name) + + + +Rotate a principal's credentials. The new credentials will be returned in the response. This is the only API, aside from createPrincipal, that returns the user's credentials. This API is *not* idempotent. + +### Example + +* OAuth Authentication (OAuth2): + +```python +import polaris.management +from polaris.management.models.principal_with_credentials import PrincipalWithCredentials +from polaris.management.rest import ApiException +from pprint import pprint + +# Defining the host is optional and defaults to https://localhost/api/management/v1 +# See configuration.py for a list of all supported configuration parameters. +configuration = polaris.management.Configuration( + host = "https://localhost/api/management/v1" +) + +# The client must configure the authentication and authorization parameters +# in accordance with the API server security policy. +# Examples for each auth method are provided below, use the example that +# satisfies your auth use case. + +configuration.access_token = os.environ["ACCESS_TOKEN"] + +# Enter a context with an instance of the API client +with polaris.management.ApiClient(configuration) as api_client: + # Create an instance of the API class + api_instance = polaris.management.PolarisDefaultApi(api_client) + principal_name = 'principal_name_example' # str | The user name + + try: + api_response = api_instance.rotate_credentials(principal_name) + print("The response of PolarisDefaultApi->rotate_credentials:\n") + pprint(api_response) + except Exception as e: + print("Exception when calling PolarisDefaultApi->rotate_credentials: %s\n" % e) +``` + + + +### Parameters + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **principal_name** | **str**| The user name | + +### Return type + +[**PrincipalWithCredentials**](PrincipalWithCredentials.md) + +### Authorization + +[OAuth2](../README.md#OAuth2) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + +### HTTP response details + +| Status code | Description | Response headers | +|-------------|-------------|------------------| +**200** | The principal details along with the newly rotated credentials | - | +**403** | The caller does not have permission to rotate credentials | - | +**404** | The principal does not exist | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **update_catalog** +> Catalog update_catalog(catalog_name, update_catalog_request) + + + +Update an existing catalog + +### Example + +* OAuth Authentication (OAuth2): + +```python +import polaris.management +from polaris.management.models.catalog import Catalog +from polaris.management.models.update_catalog_request import UpdateCatalogRequest +from polaris.management.rest import ApiException +from pprint import pprint + +# Defining the host is optional and defaults to https://localhost/api/management/v1 +# See configuration.py for a list of all supported configuration parameters. +configuration = polaris.management.Configuration( + host = "https://localhost/api/management/v1" +) + +# The client must configure the authentication and authorization parameters +# in accordance with the API server security policy. +# Examples for each auth method are provided below, use the example that +# satisfies your auth use case. + +configuration.access_token = os.environ["ACCESS_TOKEN"] + +# Enter a context with an instance of the API client +with polaris.management.ApiClient(configuration) as api_client: + # Create an instance of the API class + api_instance = polaris.management.PolarisDefaultApi(api_client) + catalog_name = 'catalog_name_example' # str | The name of the catalog + update_catalog_request = polaris.management.UpdateCatalogRequest() # UpdateCatalogRequest | The catalog details to use in the update + + try: + api_response = api_instance.update_catalog(catalog_name, update_catalog_request) + print("The response of PolarisDefaultApi->update_catalog:\n") + pprint(api_response) + except Exception as e: + print("Exception when calling PolarisDefaultApi->update_catalog: %s\n" % e) +``` + + + +### Parameters + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **catalog_name** | **str**| The name of the catalog | + **update_catalog_request** | [**UpdateCatalogRequest**](UpdateCatalogRequest.md)| The catalog details to use in the update | + +### Return type + +[**Catalog**](Catalog.md) + +### Authorization + +[OAuth2](../README.md#OAuth2) + +### HTTP request headers + + - **Content-Type**: application/json + - **Accept**: application/json + +### HTTP response details + +| Status code | Description | Response headers | +|-------------|-------------|------------------| +**200** | The catalog details | - | +**403** | The caller does not have permission to update catalog details | - | +**404** | The catalog does not exist | - | +**409** | The entity version doesn't match the currentEntityVersion; retry after fetching latest version | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **update_catalog_role** +> CatalogRole update_catalog_role(catalog_name, catalog_role_name, update_catalog_role_request=update_catalog_role_request) + + + +Update an existing role in the catalog + +### Example + +* OAuth Authentication (OAuth2): + +```python +import polaris.management +from polaris.management.models.catalog_role import CatalogRole +from polaris.management.models.update_catalog_role_request import UpdateCatalogRoleRequest +from polaris.management.rest import ApiException +from pprint import pprint + +# Defining the host is optional and defaults to https://localhost/api/management/v1 +# See configuration.py for a list of all supported configuration parameters. +configuration = polaris.management.Configuration( + host = "https://localhost/api/management/v1" +) + +# The client must configure the authentication and authorization parameters +# in accordance with the API server security policy. +# Examples for each auth method are provided below, use the example that +# satisfies your auth use case. + +configuration.access_token = os.environ["ACCESS_TOKEN"] + +# Enter a context with an instance of the API client +with polaris.management.ApiClient(configuration) as api_client: + # Create an instance of the API class + api_instance = polaris.management.PolarisDefaultApi(api_client) + catalog_name = 'catalog_name_example' # str | The catalog for which we are retrieving roles + catalog_role_name = 'catalog_role_name_example' # str | The name of the role + update_catalog_role_request = polaris.management.UpdateCatalogRoleRequest() # UpdateCatalogRoleRequest | (optional) + + try: + api_response = api_instance.update_catalog_role(catalog_name, catalog_role_name, update_catalog_role_request=update_catalog_role_request) + print("The response of PolarisDefaultApi->update_catalog_role:\n") + pprint(api_response) + except Exception as e: + print("Exception when calling PolarisDefaultApi->update_catalog_role: %s\n" % e) +``` + + + +### Parameters + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **catalog_name** | **str**| The catalog for which we are retrieving roles | + **catalog_role_name** | **str**| The name of the role | + **update_catalog_role_request** | [**UpdateCatalogRoleRequest**](UpdateCatalogRoleRequest.md)| | [optional] + +### Return type + +[**CatalogRole**](CatalogRole.md) + +### Authorization + +[OAuth2](../README.md#OAuth2) + +### HTTP request headers + + - **Content-Type**: application/json + - **Accept**: application/json + +### HTTP response details + +| Status code | Description | Response headers | +|-------------|-------------|------------------| +**200** | The specified role details | - | +**403** | The principal is not authorized to update roles | - | +**404** | The catalog or the role does not exist | - | +**409** | The entity version doesn't match the currentEntityVersion; retry after fetching latest version | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **update_principal** +> Principal update_principal(principal_name, update_principal_request) + + + +Update an existing principal + +### Example + +* OAuth Authentication (OAuth2): + +```python +import polaris.management +from polaris.management.models.principal import Principal +from polaris.management.models.update_principal_request import UpdatePrincipalRequest +from polaris.management.rest import ApiException +from pprint import pprint + +# Defining the host is optional and defaults to https://localhost/api/management/v1 +# See configuration.py for a list of all supported configuration parameters. +configuration = polaris.management.Configuration( + host = "https://localhost/api/management/v1" +) + +# The client must configure the authentication and authorization parameters +# in accordance with the API server security policy. +# Examples for each auth method are provided below, use the example that +# satisfies your auth use case. + +configuration.access_token = os.environ["ACCESS_TOKEN"] + +# Enter a context with an instance of the API client +with polaris.management.ApiClient(configuration) as api_client: + # Create an instance of the API class + api_instance = polaris.management.PolarisDefaultApi(api_client) + principal_name = 'principal_name_example' # str | The principal name + update_principal_request = polaris.management.UpdatePrincipalRequest() # UpdatePrincipalRequest | The principal details to use in the update + + try: + api_response = api_instance.update_principal(principal_name, update_principal_request) + print("The response of PolarisDefaultApi->update_principal:\n") + pprint(api_response) + except Exception as e: + print("Exception when calling PolarisDefaultApi->update_principal: %s\n" % e) +``` + + + +### Parameters + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **principal_name** | **str**| The principal name | + **update_principal_request** | [**UpdatePrincipalRequest**](UpdatePrincipalRequest.md)| The principal details to use in the update | + +### Return type + +[**Principal**](Principal.md) + +### Authorization + +[OAuth2](../README.md#OAuth2) + +### HTTP request headers + + - **Content-Type**: application/json + - **Accept**: application/json + +### HTTP response details + +| Status code | Description | Response headers | +|-------------|-------------|------------------| +**200** | The updated principal | - | +**403** | The caller does not have permission to update principal details | - | +**404** | The principal does not exist | - | +**409** | The entity version doesn't match the currentEntityVersion; retry after fetching latest version | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **update_principal_role** +> PrincipalRole update_principal_role(principal_role_name, update_principal_role_request) + + + +Update an existing principalRole + +### Example + +* OAuth Authentication (OAuth2): + +```python +import polaris.management +from polaris.management.models.principal_role import PrincipalRole +from polaris.management.models.update_principal_role_request import UpdatePrincipalRoleRequest +from polaris.management.rest import ApiException +from pprint import pprint + +# Defining the host is optional and defaults to https://localhost/api/management/v1 +# See configuration.py for a list of all supported configuration parameters. +configuration = polaris.management.Configuration( + host = "https://localhost/api/management/v1" +) + +# The client must configure the authentication and authorization parameters +# in accordance with the API server security policy. +# Examples for each auth method are provided below, use the example that +# satisfies your auth use case. + +configuration.access_token = os.environ["ACCESS_TOKEN"] + +# Enter a context with an instance of the API client +with polaris.management.ApiClient(configuration) as api_client: + # Create an instance of the API class + api_instance = polaris.management.PolarisDefaultApi(api_client) + principal_role_name = 'principal_role_name_example' # str | The principal role name + update_principal_role_request = polaris.management.UpdatePrincipalRoleRequest() # UpdatePrincipalRoleRequest | The principalRole details to use in the update + + try: + api_response = api_instance.update_principal_role(principal_role_name, update_principal_role_request) + print("The response of PolarisDefaultApi->update_principal_role:\n") + pprint(api_response) + except Exception as e: + print("Exception when calling PolarisDefaultApi->update_principal_role: %s\n" % e) +``` + + + +### Parameters + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **principal_role_name** | **str**| The principal role name | + **update_principal_role_request** | [**UpdatePrincipalRoleRequest**](UpdatePrincipalRoleRequest.md)| The principalRole details to use in the update | + +### Return type + +[**PrincipalRole**](PrincipalRole.md) + +### Authorization + +[OAuth2](../README.md#OAuth2) + +### HTTP request headers + + - **Content-Type**: application/json + - **Accept**: application/json + +### HTTP response details + +| Status code | Description | Response headers | +|-------------|-------------|------------------| +**200** | The updated principal role | - | +**403** | The caller does not have permission to update principal role details | - | +**404** | The principal role does not exist | - | +**409** | The entity version doesn't match the currentEntityVersion; retry after fetching latest version | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + diff --git a/regtests/client/python/docs/PositionDeleteFile.md b/regtests/client/python/docs/PositionDeleteFile.md new file mode 100644 index 0000000000..0c276d2cbf --- /dev/null +++ b/regtests/client/python/docs/PositionDeleteFile.md @@ -0,0 +1,29 @@ +# PositionDeleteFile + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**content** | **str** | | + +## Example + +```python +from polaris.catalog.models.position_delete_file import PositionDeleteFile + +# TODO update the JSON string below +json = "{}" +# create an instance of PositionDeleteFile from a JSON string +position_delete_file_instance = PositionDeleteFile.from_json(json) +# print the JSON string representation of the object +print(PositionDeleteFile.to_json()) + +# convert the object into a dict +position_delete_file_dict = position_delete_file_instance.to_dict() +# create an instance of PositionDeleteFile from a dict +position_delete_file_from_dict = PositionDeleteFile.from_dict(position_delete_file_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/PrimitiveTypeValue.md b/regtests/client/python/docs/PrimitiveTypeValue.md new file mode 100644 index 0000000000..d327eb4451 --- /dev/null +++ b/regtests/client/python/docs/PrimitiveTypeValue.md @@ -0,0 +1,28 @@ +# PrimitiveTypeValue + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- + +## Example + +```python +from polaris.catalog.models.primitive_type_value import PrimitiveTypeValue + +# TODO update the JSON string below +json = "{}" +# create an instance of PrimitiveTypeValue from a JSON string +primitive_type_value_instance = PrimitiveTypeValue.from_json(json) +# print the JSON string representation of the object +print(PrimitiveTypeValue.to_json()) + +# convert the object into a dict +primitive_type_value_dict = primitive_type_value_instance.to_dict() +# create an instance of PrimitiveTypeValue from a dict +primitive_type_value_from_dict = PrimitiveTypeValue.from_dict(primitive_type_value_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/Principal.md b/regtests/client/python/docs/Principal.md new file mode 100644 index 0000000000..afa5f290b7 --- /dev/null +++ b/regtests/client/python/docs/Principal.md @@ -0,0 +1,35 @@ +# Principal + +A Polaris principal. + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**name** | **str** | | +**client_id** | **str** | The output-only OAuth clientId associated with this principal if applicable | [optional] +**properties** | **Dict[str, str]** | | [optional] +**create_timestamp** | **int** | | [optional] +**last_update_timestamp** | **int** | | [optional] +**entity_version** | **int** | The version of the principal object used to determine if the principal metadata has changed | [optional] + +## Example + +```python +from polaris.management.models.principal import Principal + +# TODO update the JSON string below +json = "{}" +# create an instance of Principal from a JSON string +principal_instance = Principal.from_json(json) +# print the JSON string representation of the object +print(Principal.to_json()) + +# convert the object into a dict +principal_dict = principal_instance.to_dict() +# create an instance of Principal from a dict +principal_from_dict = Principal.from_dict(principal_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/PrincipalRole.md b/regtests/client/python/docs/PrincipalRole.md new file mode 100644 index 0000000000..519f06456a --- /dev/null +++ b/regtests/client/python/docs/PrincipalRole.md @@ -0,0 +1,33 @@ +# PrincipalRole + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**name** | **str** | The name of the role | +**properties** | **Dict[str, str]** | | [optional] +**create_timestamp** | **int** | | [optional] +**last_update_timestamp** | **int** | | [optional] +**entity_version** | **int** | The version of the principal role object used to determine if the principal role metadata has changed | [optional] + +## Example + +```python +from polaris.management.models.principal_role import PrincipalRole + +# TODO update the JSON string below +json = "{}" +# create an instance of PrincipalRole from a JSON string +principal_role_instance = PrincipalRole.from_json(json) +# print the JSON string representation of the object +print(PrincipalRole.to_json()) + +# convert the object into a dict +principal_role_dict = principal_role_instance.to_dict() +# create an instance of PrincipalRole from a dict +principal_role_from_dict = PrincipalRole.from_dict(principal_role_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/PrincipalRoles.md b/regtests/client/python/docs/PrincipalRoles.md new file mode 100644 index 0000000000..de0bb73e97 --- /dev/null +++ b/regtests/client/python/docs/PrincipalRoles.md @@ -0,0 +1,29 @@ +# PrincipalRoles + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**roles** | [**List[PrincipalRole]**](PrincipalRole.md) | | + +## Example + +```python +from polaris.management.models.principal_roles import PrincipalRoles + +# TODO update the JSON string below +json = "{}" +# create an instance of PrincipalRoles from a JSON string +principal_roles_instance = PrincipalRoles.from_json(json) +# print the JSON string representation of the object +print(PrincipalRoles.to_json()) + +# convert the object into a dict +principal_roles_dict = principal_roles_instance.to_dict() +# create an instance of PrincipalRoles from a dict +principal_roles_from_dict = PrincipalRoles.from_dict(principal_roles_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/PrincipalWithCredentials.md b/regtests/client/python/docs/PrincipalWithCredentials.md new file mode 100644 index 0000000000..0e8256276f --- /dev/null +++ b/regtests/client/python/docs/PrincipalWithCredentials.md @@ -0,0 +1,31 @@ +# PrincipalWithCredentials + +A user with its client id and secret. This type is returned when a new principal is created or when its credentials are rotated + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**principal** | [**Principal**](Principal.md) | | +**credentials** | [**PrincipalWithCredentialsCredentials**](PrincipalWithCredentialsCredentials.md) | | + +## Example + +```python +from polaris.management.models.principal_with_credentials import PrincipalWithCredentials + +# TODO update the JSON string below +json = "{}" +# create an instance of PrincipalWithCredentials from a JSON string +principal_with_credentials_instance = PrincipalWithCredentials.from_json(json) +# print the JSON string representation of the object +print(PrincipalWithCredentials.to_json()) + +# convert the object into a dict +principal_with_credentials_dict = principal_with_credentials_instance.to_dict() +# create an instance of PrincipalWithCredentials from a dict +principal_with_credentials_from_dict = PrincipalWithCredentials.from_dict(principal_with_credentials_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/PrincipalWithCredentialsCredentials.md b/regtests/client/python/docs/PrincipalWithCredentialsCredentials.md new file mode 100644 index 0000000000..2cebdcd6af --- /dev/null +++ b/regtests/client/python/docs/PrincipalWithCredentialsCredentials.md @@ -0,0 +1,30 @@ +# PrincipalWithCredentialsCredentials + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**client_id** | **str** | | [optional] +**client_secret** | **str** | | [optional] + +## Example + +```python +from polaris.management.models.principal_with_credentials_credentials import PrincipalWithCredentialsCredentials + +# TODO update the JSON string below +json = "{}" +# create an instance of PrincipalWithCredentialsCredentials from a JSON string +principal_with_credentials_credentials_instance = PrincipalWithCredentialsCredentials.from_json(json) +# print the JSON string representation of the object +print(PrincipalWithCredentialsCredentials.to_json()) + +# convert the object into a dict +principal_with_credentials_credentials_dict = principal_with_credentials_credentials_instance.to_dict() +# create an instance of PrincipalWithCredentialsCredentials from a dict +principal_with_credentials_credentials_from_dict = PrincipalWithCredentialsCredentials.from_dict(principal_with_credentials_credentials_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/Principals.md b/regtests/client/python/docs/Principals.md new file mode 100644 index 0000000000..3b58434721 --- /dev/null +++ b/regtests/client/python/docs/Principals.md @@ -0,0 +1,30 @@ +# Principals + +A list of Principals + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**principals** | [**List[Principal]**](Principal.md) | | + +## Example + +```python +from polaris.management.models.principals import Principals + +# TODO update the JSON string below +json = "{}" +# create an instance of Principals from a JSON string +principals_instance = Principals.from_json(json) +# print the JSON string representation of the object +print(Principals.to_json()) + +# convert the object into a dict +principals_dict = principals_instance.to_dict() +# create an instance of Principals from a dict +principals_from_dict = Principals.from_dict(principals_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/RegisterTableRequest.md b/regtests/client/python/docs/RegisterTableRequest.md new file mode 100644 index 0000000000..d834072053 --- /dev/null +++ b/regtests/client/python/docs/RegisterTableRequest.md @@ -0,0 +1,30 @@ +# RegisterTableRequest + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**name** | **str** | | +**metadata_location** | **str** | | + +## Example + +```python +from polaris.catalog.models.register_table_request import RegisterTableRequest + +# TODO update the JSON string below +json = "{}" +# create an instance of RegisterTableRequest from a JSON string +register_table_request_instance = RegisterTableRequest.from_json(json) +# print the JSON string representation of the object +print(RegisterTableRequest.to_json()) + +# convert the object into a dict +register_table_request_dict = register_table_request_instance.to_dict() +# create an instance of RegisterTableRequest from a dict +register_table_request_from_dict = RegisterTableRequest.from_dict(register_table_request_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/RemovePartitionStatisticsUpdate.md b/regtests/client/python/docs/RemovePartitionStatisticsUpdate.md new file mode 100644 index 0000000000..9e9dac4896 --- /dev/null +++ b/regtests/client/python/docs/RemovePartitionStatisticsUpdate.md @@ -0,0 +1,30 @@ +# RemovePartitionStatisticsUpdate + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**action** | **str** | | +**snapshot_id** | **int** | | + +## Example + +```python +from polaris.catalog.models.remove_partition_statistics_update import RemovePartitionStatisticsUpdate + +# TODO update the JSON string below +json = "{}" +# create an instance of RemovePartitionStatisticsUpdate from a JSON string +remove_partition_statistics_update_instance = RemovePartitionStatisticsUpdate.from_json(json) +# print the JSON string representation of the object +print(RemovePartitionStatisticsUpdate.to_json()) + +# convert the object into a dict +remove_partition_statistics_update_dict = remove_partition_statistics_update_instance.to_dict() +# create an instance of RemovePartitionStatisticsUpdate from a dict +remove_partition_statistics_update_from_dict = RemovePartitionStatisticsUpdate.from_dict(remove_partition_statistics_update_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/RemovePropertiesUpdate.md b/regtests/client/python/docs/RemovePropertiesUpdate.md new file mode 100644 index 0000000000..764eb76d6b --- /dev/null +++ b/regtests/client/python/docs/RemovePropertiesUpdate.md @@ -0,0 +1,30 @@ +# RemovePropertiesUpdate + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**action** | **str** | | +**removals** | **List[str]** | | + +## Example + +```python +from polaris.catalog.models.remove_properties_update import RemovePropertiesUpdate + +# TODO update the JSON string below +json = "{}" +# create an instance of RemovePropertiesUpdate from a JSON string +remove_properties_update_instance = RemovePropertiesUpdate.from_json(json) +# print the JSON string representation of the object +print(RemovePropertiesUpdate.to_json()) + +# convert the object into a dict +remove_properties_update_dict = remove_properties_update_instance.to_dict() +# create an instance of RemovePropertiesUpdate from a dict +remove_properties_update_from_dict = RemovePropertiesUpdate.from_dict(remove_properties_update_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/RemoveSnapshotRefUpdate.md b/regtests/client/python/docs/RemoveSnapshotRefUpdate.md new file mode 100644 index 0000000000..67d2700e47 --- /dev/null +++ b/regtests/client/python/docs/RemoveSnapshotRefUpdate.md @@ -0,0 +1,30 @@ +# RemoveSnapshotRefUpdate + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**action** | **str** | | +**ref_name** | **str** | | + +## Example + +```python +from polaris.catalog.models.remove_snapshot_ref_update import RemoveSnapshotRefUpdate + +# TODO update the JSON string below +json = "{}" +# create an instance of RemoveSnapshotRefUpdate from a JSON string +remove_snapshot_ref_update_instance = RemoveSnapshotRefUpdate.from_json(json) +# print the JSON string representation of the object +print(RemoveSnapshotRefUpdate.to_json()) + +# convert the object into a dict +remove_snapshot_ref_update_dict = remove_snapshot_ref_update_instance.to_dict() +# create an instance of RemoveSnapshotRefUpdate from a dict +remove_snapshot_ref_update_from_dict = RemoveSnapshotRefUpdate.from_dict(remove_snapshot_ref_update_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/RemoveSnapshotsUpdate.md b/regtests/client/python/docs/RemoveSnapshotsUpdate.md new file mode 100644 index 0000000000..4b50794347 --- /dev/null +++ b/regtests/client/python/docs/RemoveSnapshotsUpdate.md @@ -0,0 +1,30 @@ +# RemoveSnapshotsUpdate + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**action** | **str** | | +**snapshot_ids** | **List[int]** | | + +## Example + +```python +from polaris.catalog.models.remove_snapshots_update import RemoveSnapshotsUpdate + +# TODO update the JSON string below +json = "{}" +# create an instance of RemoveSnapshotsUpdate from a JSON string +remove_snapshots_update_instance = RemoveSnapshotsUpdate.from_json(json) +# print the JSON string representation of the object +print(RemoveSnapshotsUpdate.to_json()) + +# convert the object into a dict +remove_snapshots_update_dict = remove_snapshots_update_instance.to_dict() +# create an instance of RemoveSnapshotsUpdate from a dict +remove_snapshots_update_from_dict = RemoveSnapshotsUpdate.from_dict(remove_snapshots_update_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/RemoveStatisticsUpdate.md b/regtests/client/python/docs/RemoveStatisticsUpdate.md new file mode 100644 index 0000000000..a47aa31441 --- /dev/null +++ b/regtests/client/python/docs/RemoveStatisticsUpdate.md @@ -0,0 +1,30 @@ +# RemoveStatisticsUpdate + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**action** | **str** | | +**snapshot_id** | **int** | | + +## Example + +```python +from polaris.catalog.models.remove_statistics_update import RemoveStatisticsUpdate + +# TODO update the JSON string below +json = "{}" +# create an instance of RemoveStatisticsUpdate from a JSON string +remove_statistics_update_instance = RemoveStatisticsUpdate.from_json(json) +# print the JSON string representation of the object +print(RemoveStatisticsUpdate.to_json()) + +# convert the object into a dict +remove_statistics_update_dict = remove_statistics_update_instance.to_dict() +# create an instance of RemoveStatisticsUpdate from a dict +remove_statistics_update_from_dict = RemoveStatisticsUpdate.from_dict(remove_statistics_update_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/RenameTableRequest.md b/regtests/client/python/docs/RenameTableRequest.md new file mode 100644 index 0000000000..28daddd1d8 --- /dev/null +++ b/regtests/client/python/docs/RenameTableRequest.md @@ -0,0 +1,30 @@ +# RenameTableRequest + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**source** | [**TableIdentifier**](TableIdentifier.md) | | +**destination** | [**TableIdentifier**](TableIdentifier.md) | | + +## Example + +```python +from polaris.catalog.models.rename_table_request import RenameTableRequest + +# TODO update the JSON string below +json = "{}" +# create an instance of RenameTableRequest from a JSON string +rename_table_request_instance = RenameTableRequest.from_json(json) +# print the JSON string representation of the object +print(RenameTableRequest.to_json()) + +# convert the object into a dict +rename_table_request_dict = rename_table_request_instance.to_dict() +# create an instance of RenameTableRequest from a dict +rename_table_request_from_dict = RenameTableRequest.from_dict(rename_table_request_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/ReportMetricsRequest.md b/regtests/client/python/docs/ReportMetricsRequest.md new file mode 100644 index 0000000000..61ddcaebd0 --- /dev/null +++ b/regtests/client/python/docs/ReportMetricsRequest.md @@ -0,0 +1,39 @@ +# ReportMetricsRequest + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**report_type** | **str** | | +**table_name** | **str** | | +**snapshot_id** | **int** | | +**filter** | [**Expression**](Expression.md) | | +**schema_id** | **int** | | +**projected_field_ids** | **List[int]** | | +**projected_field_names** | **List[str]** | | +**metrics** | [**Dict[str, MetricResult]**](MetricResult.md) | | +**metadata** | **Dict[str, str]** | | [optional] +**sequence_number** | **int** | | +**operation** | **str** | | + +## Example + +```python +from polaris.catalog.models.report_metrics_request import ReportMetricsRequest + +# TODO update the JSON string below +json = "{}" +# create an instance of ReportMetricsRequest from a JSON string +report_metrics_request_instance = ReportMetricsRequest.from_json(json) +# print the JSON string representation of the object +print(ReportMetricsRequest.to_json()) + +# convert the object into a dict +report_metrics_request_dict = report_metrics_request_instance.to_dict() +# create an instance of ReportMetricsRequest from a dict +report_metrics_request_from_dict = ReportMetricsRequest.from_dict(report_metrics_request_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/RevokeGrantRequest.md b/regtests/client/python/docs/RevokeGrantRequest.md new file mode 100644 index 0000000000..d6409f5d93 --- /dev/null +++ b/regtests/client/python/docs/RevokeGrantRequest.md @@ -0,0 +1,29 @@ +# RevokeGrantRequest + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**grant** | [**GrantResource**](GrantResource.md) | | [optional] + +## Example + +```python +from polaris.management.models.revoke_grant_request import RevokeGrantRequest + +# TODO update the JSON string below +json = "{}" +# create an instance of RevokeGrantRequest from a JSON string +revoke_grant_request_instance = RevokeGrantRequest.from_json(json) +# print the JSON string representation of the object +print(RevokeGrantRequest.to_json()) + +# convert the object into a dict +revoke_grant_request_dict = revoke_grant_request_instance.to_dict() +# create an instance of RevokeGrantRequest from a dict +revoke_grant_request_from_dict = RevokeGrantRequest.from_dict(revoke_grant_request_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/SQLViewRepresentation.md b/regtests/client/python/docs/SQLViewRepresentation.md new file mode 100644 index 0000000000..7cf648b696 --- /dev/null +++ b/regtests/client/python/docs/SQLViewRepresentation.md @@ -0,0 +1,31 @@ +# SQLViewRepresentation + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**type** | **str** | | +**sql** | **str** | | +**dialect** | **str** | | + +## Example + +```python +from polaris.catalog.models.sql_view_representation import SQLViewRepresentation + +# TODO update the JSON string below +json = "{}" +# create an instance of SQLViewRepresentation from a JSON string +sql_view_representation_instance = SQLViewRepresentation.from_json(json) +# print the JSON string representation of the object +print(SQLViewRepresentation.to_json()) + +# convert the object into a dict +sql_view_representation_dict = sql_view_representation_instance.to_dict() +# create an instance of SQLViewRepresentation from a dict +sql_view_representation_from_dict = SQLViewRepresentation.from_dict(sql_view_representation_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/ScanReport.md b/regtests/client/python/docs/ScanReport.md new file mode 100644 index 0000000000..d2ac51da53 --- /dev/null +++ b/regtests/client/python/docs/ScanReport.md @@ -0,0 +1,36 @@ +# ScanReport + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**table_name** | **str** | | +**snapshot_id** | **int** | | +**filter** | [**Expression**](Expression.md) | | +**schema_id** | **int** | | +**projected_field_ids** | **List[int]** | | +**projected_field_names** | **List[str]** | | +**metrics** | [**Dict[str, MetricResult]**](MetricResult.md) | | +**metadata** | **Dict[str, str]** | | [optional] + +## Example + +```python +from polaris.catalog.models.scan_report import ScanReport + +# TODO update the JSON string below +json = "{}" +# create an instance of ScanReport from a JSON string +scan_report_instance = ScanReport.from_json(json) +# print the JSON string representation of the object +print(ScanReport.to_json()) + +# convert the object into a dict +scan_report_dict = scan_report_instance.to_dict() +# create an instance of ScanReport from a dict +scan_report_from_dict = ScanReport.from_dict(scan_report_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/SetCurrentSchemaUpdate.md b/regtests/client/python/docs/SetCurrentSchemaUpdate.md new file mode 100644 index 0000000000..54a34de9e2 --- /dev/null +++ b/regtests/client/python/docs/SetCurrentSchemaUpdate.md @@ -0,0 +1,30 @@ +# SetCurrentSchemaUpdate + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**action** | **str** | | +**schema_id** | **int** | Schema ID to set as current, or -1 to set last added schema | + +## Example + +```python +from polaris.catalog.models.set_current_schema_update import SetCurrentSchemaUpdate + +# TODO update the JSON string below +json = "{}" +# create an instance of SetCurrentSchemaUpdate from a JSON string +set_current_schema_update_instance = SetCurrentSchemaUpdate.from_json(json) +# print the JSON string representation of the object +print(SetCurrentSchemaUpdate.to_json()) + +# convert the object into a dict +set_current_schema_update_dict = set_current_schema_update_instance.to_dict() +# create an instance of SetCurrentSchemaUpdate from a dict +set_current_schema_update_from_dict = SetCurrentSchemaUpdate.from_dict(set_current_schema_update_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/SetCurrentViewVersionUpdate.md b/regtests/client/python/docs/SetCurrentViewVersionUpdate.md new file mode 100644 index 0000000000..306a5f90d3 --- /dev/null +++ b/regtests/client/python/docs/SetCurrentViewVersionUpdate.md @@ -0,0 +1,30 @@ +# SetCurrentViewVersionUpdate + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**action** | **str** | | +**view_version_id** | **int** | The view version id to set as current, or -1 to set last added view version id | + +## Example + +```python +from polaris.catalog.models.set_current_view_version_update import SetCurrentViewVersionUpdate + +# TODO update the JSON string below +json = "{}" +# create an instance of SetCurrentViewVersionUpdate from a JSON string +set_current_view_version_update_instance = SetCurrentViewVersionUpdate.from_json(json) +# print the JSON string representation of the object +print(SetCurrentViewVersionUpdate.to_json()) + +# convert the object into a dict +set_current_view_version_update_dict = set_current_view_version_update_instance.to_dict() +# create an instance of SetCurrentViewVersionUpdate from a dict +set_current_view_version_update_from_dict = SetCurrentViewVersionUpdate.from_dict(set_current_view_version_update_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/SetDefaultSortOrderUpdate.md b/regtests/client/python/docs/SetDefaultSortOrderUpdate.md new file mode 100644 index 0000000000..be60238f5e --- /dev/null +++ b/regtests/client/python/docs/SetDefaultSortOrderUpdate.md @@ -0,0 +1,30 @@ +# SetDefaultSortOrderUpdate + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**action** | **str** | | +**sort_order_id** | **int** | Sort order ID to set as the default, or -1 to set last added sort order | + +## Example + +```python +from polaris.catalog.models.set_default_sort_order_update import SetDefaultSortOrderUpdate + +# TODO update the JSON string below +json = "{}" +# create an instance of SetDefaultSortOrderUpdate from a JSON string +set_default_sort_order_update_instance = SetDefaultSortOrderUpdate.from_json(json) +# print the JSON string representation of the object +print(SetDefaultSortOrderUpdate.to_json()) + +# convert the object into a dict +set_default_sort_order_update_dict = set_default_sort_order_update_instance.to_dict() +# create an instance of SetDefaultSortOrderUpdate from a dict +set_default_sort_order_update_from_dict = SetDefaultSortOrderUpdate.from_dict(set_default_sort_order_update_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/SetDefaultSpecUpdate.md b/regtests/client/python/docs/SetDefaultSpecUpdate.md new file mode 100644 index 0000000000..762f014b10 --- /dev/null +++ b/regtests/client/python/docs/SetDefaultSpecUpdate.md @@ -0,0 +1,30 @@ +# SetDefaultSpecUpdate + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**action** | **str** | | +**spec_id** | **int** | Partition spec ID to set as the default, or -1 to set last added spec | + +## Example + +```python +from polaris.catalog.models.set_default_spec_update import SetDefaultSpecUpdate + +# TODO update the JSON string below +json = "{}" +# create an instance of SetDefaultSpecUpdate from a JSON string +set_default_spec_update_instance = SetDefaultSpecUpdate.from_json(json) +# print the JSON string representation of the object +print(SetDefaultSpecUpdate.to_json()) + +# convert the object into a dict +set_default_spec_update_dict = set_default_spec_update_instance.to_dict() +# create an instance of SetDefaultSpecUpdate from a dict +set_default_spec_update_from_dict = SetDefaultSpecUpdate.from_dict(set_default_spec_update_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/SetExpression.md b/regtests/client/python/docs/SetExpression.md new file mode 100644 index 0000000000..bb3ec5c5f7 --- /dev/null +++ b/regtests/client/python/docs/SetExpression.md @@ -0,0 +1,31 @@ +# SetExpression + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**type** | **str** | | +**term** | [**Term**](Term.md) | | +**values** | **List[object]** | | + +## Example + +```python +from polaris.catalog.models.set_expression import SetExpression + +# TODO update the JSON string below +json = "{}" +# create an instance of SetExpression from a JSON string +set_expression_instance = SetExpression.from_json(json) +# print the JSON string representation of the object +print(SetExpression.to_json()) + +# convert the object into a dict +set_expression_dict = set_expression_instance.to_dict() +# create an instance of SetExpression from a dict +set_expression_from_dict = SetExpression.from_dict(set_expression_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/SetLocationUpdate.md b/regtests/client/python/docs/SetLocationUpdate.md new file mode 100644 index 0000000000..7209a097e7 --- /dev/null +++ b/regtests/client/python/docs/SetLocationUpdate.md @@ -0,0 +1,30 @@ +# SetLocationUpdate + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**action** | **str** | | +**location** | **str** | | + +## Example + +```python +from polaris.catalog.models.set_location_update import SetLocationUpdate + +# TODO update the JSON string below +json = "{}" +# create an instance of SetLocationUpdate from a JSON string +set_location_update_instance = SetLocationUpdate.from_json(json) +# print the JSON string representation of the object +print(SetLocationUpdate.to_json()) + +# convert the object into a dict +set_location_update_dict = set_location_update_instance.to_dict() +# create an instance of SetLocationUpdate from a dict +set_location_update_from_dict = SetLocationUpdate.from_dict(set_location_update_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/SetPartitionStatisticsUpdate.md b/regtests/client/python/docs/SetPartitionStatisticsUpdate.md new file mode 100644 index 0000000000..2f38fe1574 --- /dev/null +++ b/regtests/client/python/docs/SetPartitionStatisticsUpdate.md @@ -0,0 +1,30 @@ +# SetPartitionStatisticsUpdate + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**action** | **str** | | +**partition_statistics** | [**PartitionStatisticsFile**](PartitionStatisticsFile.md) | | + +## Example + +```python +from polaris.catalog.models.set_partition_statistics_update import SetPartitionStatisticsUpdate + +# TODO update the JSON string below +json = "{}" +# create an instance of SetPartitionStatisticsUpdate from a JSON string +set_partition_statistics_update_instance = SetPartitionStatisticsUpdate.from_json(json) +# print the JSON string representation of the object +print(SetPartitionStatisticsUpdate.to_json()) + +# convert the object into a dict +set_partition_statistics_update_dict = set_partition_statistics_update_instance.to_dict() +# create an instance of SetPartitionStatisticsUpdate from a dict +set_partition_statistics_update_from_dict = SetPartitionStatisticsUpdate.from_dict(set_partition_statistics_update_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/SetPropertiesUpdate.md b/regtests/client/python/docs/SetPropertiesUpdate.md new file mode 100644 index 0000000000..bca9117bdd --- /dev/null +++ b/regtests/client/python/docs/SetPropertiesUpdate.md @@ -0,0 +1,30 @@ +# SetPropertiesUpdate + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**action** | **str** | | +**updates** | **Dict[str, str]** | | + +## Example + +```python +from polaris.catalog.models.set_properties_update import SetPropertiesUpdate + +# TODO update the JSON string below +json = "{}" +# create an instance of SetPropertiesUpdate from a JSON string +set_properties_update_instance = SetPropertiesUpdate.from_json(json) +# print the JSON string representation of the object +print(SetPropertiesUpdate.to_json()) + +# convert the object into a dict +set_properties_update_dict = set_properties_update_instance.to_dict() +# create an instance of SetPropertiesUpdate from a dict +set_properties_update_from_dict = SetPropertiesUpdate.from_dict(set_properties_update_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/SetSnapshotRefUpdate.md b/regtests/client/python/docs/SetSnapshotRefUpdate.md new file mode 100644 index 0000000000..d83b36a881 --- /dev/null +++ b/regtests/client/python/docs/SetSnapshotRefUpdate.md @@ -0,0 +1,35 @@ +# SetSnapshotRefUpdate + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**action** | **str** | | +**ref_name** | **str** | | +**type** | **str** | | +**snapshot_id** | **int** | | +**max_ref_age_ms** | **int** | | [optional] +**max_snapshot_age_ms** | **int** | | [optional] +**min_snapshots_to_keep** | **int** | | [optional] + +## Example + +```python +from polaris.catalog.models.set_snapshot_ref_update import SetSnapshotRefUpdate + +# TODO update the JSON string below +json = "{}" +# create an instance of SetSnapshotRefUpdate from a JSON string +set_snapshot_ref_update_instance = SetSnapshotRefUpdate.from_json(json) +# print the JSON string representation of the object +print(SetSnapshotRefUpdate.to_json()) + +# convert the object into a dict +set_snapshot_ref_update_dict = set_snapshot_ref_update_instance.to_dict() +# create an instance of SetSnapshotRefUpdate from a dict +set_snapshot_ref_update_from_dict = SetSnapshotRefUpdate.from_dict(set_snapshot_ref_update_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/SetStatisticsUpdate.md b/regtests/client/python/docs/SetStatisticsUpdate.md new file mode 100644 index 0000000000..1ebd049e8c --- /dev/null +++ b/regtests/client/python/docs/SetStatisticsUpdate.md @@ -0,0 +1,31 @@ +# SetStatisticsUpdate + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**action** | **str** | | +**snapshot_id** | **int** | | +**statistics** | [**StatisticsFile**](StatisticsFile.md) | | + +## Example + +```python +from polaris.catalog.models.set_statistics_update import SetStatisticsUpdate + +# TODO update the JSON string below +json = "{}" +# create an instance of SetStatisticsUpdate from a JSON string +set_statistics_update_instance = SetStatisticsUpdate.from_json(json) +# print the JSON string representation of the object +print(SetStatisticsUpdate.to_json()) + +# convert the object into a dict +set_statistics_update_dict = set_statistics_update_instance.to_dict() +# create an instance of SetStatisticsUpdate from a dict +set_statistics_update_from_dict = SetStatisticsUpdate.from_dict(set_statistics_update_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/Snapshot.md b/regtests/client/python/docs/Snapshot.md new file mode 100644 index 0000000000..8b9ec27997 --- /dev/null +++ b/regtests/client/python/docs/Snapshot.md @@ -0,0 +1,35 @@ +# Snapshot + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**snapshot_id** | **int** | | +**parent_snapshot_id** | **int** | | [optional] +**sequence_number** | **int** | | [optional] +**timestamp_ms** | **int** | | +**manifest_list** | **str** | Location of the snapshot's manifest list file | +**summary** | [**SnapshotSummary**](SnapshotSummary.md) | | +**schema_id** | **int** | | [optional] + +## Example + +```python +from polaris.catalog.models.snapshot import Snapshot + +# TODO update the JSON string below +json = "{}" +# create an instance of Snapshot from a JSON string +snapshot_instance = Snapshot.from_json(json) +# print the JSON string representation of the object +print(Snapshot.to_json()) + +# convert the object into a dict +snapshot_dict = snapshot_instance.to_dict() +# create an instance of Snapshot from a dict +snapshot_from_dict = Snapshot.from_dict(snapshot_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/SnapshotLogInner.md b/regtests/client/python/docs/SnapshotLogInner.md new file mode 100644 index 0000000000..a19edc4e4c --- /dev/null +++ b/regtests/client/python/docs/SnapshotLogInner.md @@ -0,0 +1,30 @@ +# SnapshotLogInner + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**snapshot_id** | **int** | | +**timestamp_ms** | **int** | | + +## Example + +```python +from polaris.catalog.models.snapshot_log_inner import SnapshotLogInner + +# TODO update the JSON string below +json = "{}" +# create an instance of SnapshotLogInner from a JSON string +snapshot_log_inner_instance = SnapshotLogInner.from_json(json) +# print the JSON string representation of the object +print(SnapshotLogInner.to_json()) + +# convert the object into a dict +snapshot_log_inner_dict = snapshot_log_inner_instance.to_dict() +# create an instance of SnapshotLogInner from a dict +snapshot_log_inner_from_dict = SnapshotLogInner.from_dict(snapshot_log_inner_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/SnapshotReference.md b/regtests/client/python/docs/SnapshotReference.md new file mode 100644 index 0000000000..0128bfa075 --- /dev/null +++ b/regtests/client/python/docs/SnapshotReference.md @@ -0,0 +1,33 @@ +# SnapshotReference + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**type** | **str** | | +**snapshot_id** | **int** | | +**max_ref_age_ms** | **int** | | [optional] +**max_snapshot_age_ms** | **int** | | [optional] +**min_snapshots_to_keep** | **int** | | [optional] + +## Example + +```python +from polaris.catalog.models.snapshot_reference import SnapshotReference + +# TODO update the JSON string below +json = "{}" +# create an instance of SnapshotReference from a JSON string +snapshot_reference_instance = SnapshotReference.from_json(json) +# print the JSON string representation of the object +print(SnapshotReference.to_json()) + +# convert the object into a dict +snapshot_reference_dict = snapshot_reference_instance.to_dict() +# create an instance of SnapshotReference from a dict +snapshot_reference_from_dict = SnapshotReference.from_dict(snapshot_reference_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/SnapshotSummary.md b/regtests/client/python/docs/SnapshotSummary.md new file mode 100644 index 0000000000..4c32e38ab4 --- /dev/null +++ b/regtests/client/python/docs/SnapshotSummary.md @@ -0,0 +1,29 @@ +# SnapshotSummary + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**operation** | **str** | | + +## Example + +```python +from polaris.catalog.models.snapshot_summary import SnapshotSummary + +# TODO update the JSON string below +json = "{}" +# create an instance of SnapshotSummary from a JSON string +snapshot_summary_instance = SnapshotSummary.from_json(json) +# print the JSON string representation of the object +print(SnapshotSummary.to_json()) + +# convert the object into a dict +snapshot_summary_dict = snapshot_summary_instance.to_dict() +# create an instance of SnapshotSummary from a dict +snapshot_summary_from_dict = SnapshotSummary.from_dict(snapshot_summary_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/SortDirection.md b/regtests/client/python/docs/SortDirection.md new file mode 100644 index 0000000000..c5c2266788 --- /dev/null +++ b/regtests/client/python/docs/SortDirection.md @@ -0,0 +1,12 @@ +# SortDirection + + +## Enum + +* `ASC` (value: `'asc'`) + +* `DESC` (value: `'desc'`) + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/SortField.md b/regtests/client/python/docs/SortField.md new file mode 100644 index 0000000000..bc5264e9e3 --- /dev/null +++ b/regtests/client/python/docs/SortField.md @@ -0,0 +1,32 @@ +# SortField + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**source_id** | **int** | | +**transform** | **str** | | +**direction** | [**SortDirection**](SortDirection.md) | | +**null_order** | [**NullOrder**](NullOrder.md) | | + +## Example + +```python +from polaris.catalog.models.sort_field import SortField + +# TODO update the JSON string below +json = "{}" +# create an instance of SortField from a JSON string +sort_field_instance = SortField.from_json(json) +# print the JSON string representation of the object +print(SortField.to_json()) + +# convert the object into a dict +sort_field_dict = sort_field_instance.to_dict() +# create an instance of SortField from a dict +sort_field_from_dict = SortField.from_dict(sort_field_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/SortOrder.md b/regtests/client/python/docs/SortOrder.md new file mode 100644 index 0000000000..a54dc8a995 --- /dev/null +++ b/regtests/client/python/docs/SortOrder.md @@ -0,0 +1,30 @@ +# SortOrder + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**order_id** | **int** | | [readonly] +**fields** | [**List[SortField]**](SortField.md) | | + +## Example + +```python +from polaris.catalog.models.sort_order import SortOrder + +# TODO update the JSON string below +json = "{}" +# create an instance of SortOrder from a JSON string +sort_order_instance = SortOrder.from_json(json) +# print the JSON string representation of the object +print(SortOrder.to_json()) + +# convert the object into a dict +sort_order_dict = sort_order_instance.to_dict() +# create an instance of SortOrder from a dict +sort_order_from_dict = SortOrder.from_dict(sort_order_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/StatisticsFile.md b/regtests/client/python/docs/StatisticsFile.md new file mode 100644 index 0000000000..7c6c8c293a --- /dev/null +++ b/regtests/client/python/docs/StatisticsFile.md @@ -0,0 +1,33 @@ +# StatisticsFile + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**snapshot_id** | **int** | | +**statistics_path** | **str** | | +**file_size_in_bytes** | **int** | | +**file_footer_size_in_bytes** | **int** | | +**blob_metadata** | [**List[BlobMetadata]**](BlobMetadata.md) | | + +## Example + +```python +from polaris.catalog.models.statistics_file import StatisticsFile + +# TODO update the JSON string below +json = "{}" +# create an instance of StatisticsFile from a JSON string +statistics_file_instance = StatisticsFile.from_json(json) +# print the JSON string representation of the object +print(StatisticsFile.to_json()) + +# convert the object into a dict +statistics_file_dict = statistics_file_instance.to_dict() +# create an instance of StatisticsFile from a dict +statistics_file_from_dict = StatisticsFile.from_dict(statistics_file_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/StorageConfigInfo.md b/regtests/client/python/docs/StorageConfigInfo.md new file mode 100644 index 0000000000..7a992db4ff --- /dev/null +++ b/regtests/client/python/docs/StorageConfigInfo.md @@ -0,0 +1,31 @@ +# StorageConfigInfo + +A storage configuration used by catalogs + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**storage_type** | **str** | The cloud provider type this storage is built on. FILE is supported for testing purposes only | +**allowed_locations** | **List[str]** | | [optional] + +## Example + +```python +from polaris.management.models.storage_config_info import StorageConfigInfo + +# TODO update the JSON string below +json = "{}" +# create an instance of StorageConfigInfo from a JSON string +storage_config_info_instance = StorageConfigInfo.from_json(json) +# print the JSON string representation of the object +print(StorageConfigInfo.to_json()) + +# convert the object into a dict +storage_config_info_dict = storage_config_info_instance.to_dict() +# create an instance of StorageConfigInfo from a dict +storage_config_info_from_dict = StorageConfigInfo.from_dict(storage_config_info_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/StructField.md b/regtests/client/python/docs/StructField.md new file mode 100644 index 0000000000..ac95e884eb --- /dev/null +++ b/regtests/client/python/docs/StructField.md @@ -0,0 +1,33 @@ +# StructField + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**id** | **int** | | +**name** | **str** | | +**type** | [**Type**](Type.md) | | +**required** | **bool** | | +**doc** | **str** | | [optional] + +## Example + +```python +from polaris.catalog.models.struct_field import StructField + +# TODO update the JSON string below +json = "{}" +# create an instance of StructField from a JSON string +struct_field_instance = StructField.from_json(json) +# print the JSON string representation of the object +print(StructField.to_json()) + +# convert the object into a dict +struct_field_dict = struct_field_instance.to_dict() +# create an instance of StructField from a dict +struct_field_from_dict = StructField.from_dict(struct_field_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/StructType.md b/regtests/client/python/docs/StructType.md new file mode 100644 index 0000000000..236fb4d7b9 --- /dev/null +++ b/regtests/client/python/docs/StructType.md @@ -0,0 +1,30 @@ +# StructType + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**type** | **str** | | +**fields** | [**List[StructField]**](StructField.md) | | + +## Example + +```python +from polaris.catalog.models.struct_type import StructType + +# TODO update the JSON string below +json = "{}" +# create an instance of StructType from a JSON string +struct_type_instance = StructType.from_json(json) +# print the JSON string representation of the object +print(StructType.to_json()) + +# convert the object into a dict +struct_type_dict = struct_type_instance.to_dict() +# create an instance of StructType from a dict +struct_type_from_dict = StructType.from_dict(struct_type_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/TableGrant.md b/regtests/client/python/docs/TableGrant.md new file mode 100644 index 0000000000..50a4de9bbd --- /dev/null +++ b/regtests/client/python/docs/TableGrant.md @@ -0,0 +1,31 @@ +# TableGrant + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**namespace** | **List[str]** | | +**table_name** | **str** | | +**privilege** | [**TablePrivilege**](TablePrivilege.md) | | + +## Example + +```python +from polaris.management.models.table_grant import TableGrant + +# TODO update the JSON string below +json = "{}" +# create an instance of TableGrant from a JSON string +table_grant_instance = TableGrant.from_json(json) +# print the JSON string representation of the object +print(TableGrant.to_json()) + +# convert the object into a dict +table_grant_dict = table_grant_instance.to_dict() +# create an instance of TableGrant from a dict +table_grant_from_dict = TableGrant.from_dict(table_grant_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/TableIdentifier.md b/regtests/client/python/docs/TableIdentifier.md new file mode 100644 index 0000000000..a3b7a2161b --- /dev/null +++ b/regtests/client/python/docs/TableIdentifier.md @@ -0,0 +1,30 @@ +# TableIdentifier + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**namespace** | **List[str]** | Reference to one or more levels of a namespace | +**name** | **str** | | + +## Example + +```python +from polaris.catalog.models.table_identifier import TableIdentifier + +# TODO update the JSON string below +json = "{}" +# create an instance of TableIdentifier from a JSON string +table_identifier_instance = TableIdentifier.from_json(json) +# print the JSON string representation of the object +print(TableIdentifier.to_json()) + +# convert the object into a dict +table_identifier_dict = table_identifier_instance.to_dict() +# create an instance of TableIdentifier from a dict +table_identifier_from_dict = TableIdentifier.from_dict(table_identifier_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/TableMetadata.md b/regtests/client/python/docs/TableMetadata.md new file mode 100644 index 0000000000..3a1019d612 --- /dev/null +++ b/regtests/client/python/docs/TableMetadata.md @@ -0,0 +1,49 @@ +# TableMetadata + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**format_version** | **int** | | +**table_uuid** | **str** | | +**location** | **str** | | [optional] +**last_updated_ms** | **int** | | [optional] +**properties** | **Dict[str, str]** | | [optional] +**schemas** | [**List[ModelSchema]**](ModelSchema.md) | | [optional] +**current_schema_id** | **int** | | [optional] +**last_column_id** | **int** | | [optional] +**partition_specs** | [**List[PartitionSpec]**](PartitionSpec.md) | | [optional] +**default_spec_id** | **int** | | [optional] +**last_partition_id** | **int** | | [optional] +**sort_orders** | [**List[SortOrder]**](SortOrder.md) | | [optional] +**default_sort_order_id** | **int** | | [optional] +**snapshots** | [**List[Snapshot]**](Snapshot.md) | | [optional] +**refs** | [**Dict[str, SnapshotReference]**](SnapshotReference.md) | | [optional] +**current_snapshot_id** | **int** | | [optional] +**last_sequence_number** | **int** | | [optional] +**snapshot_log** | [**List[SnapshotLogInner]**](SnapshotLogInner.md) | | [optional] +**metadata_log** | [**List[MetadataLogInner]**](MetadataLogInner.md) | | [optional] +**statistics_files** | [**List[StatisticsFile]**](StatisticsFile.md) | | [optional] +**partition_statistics_files** | [**List[PartitionStatisticsFile]**](PartitionStatisticsFile.md) | | [optional] + +## Example + +```python +from polaris.catalog.models.table_metadata import TableMetadata + +# TODO update the JSON string below +json = "{}" +# create an instance of TableMetadata from a JSON string +table_metadata_instance = TableMetadata.from_json(json) +# print the JSON string representation of the object +print(TableMetadata.to_json()) + +# convert the object into a dict +table_metadata_dict = table_metadata_instance.to_dict() +# create an instance of TableMetadata from a dict +table_metadata_from_dict = TableMetadata.from_dict(table_metadata_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/TablePrivilege.md b/regtests/client/python/docs/TablePrivilege.md new file mode 100644 index 0000000000..f4015e82d7 --- /dev/null +++ b/regtests/client/python/docs/TablePrivilege.md @@ -0,0 +1,26 @@ +# TablePrivilege + + +## Enum + +* `CATALOG_MANAGE_ACCESS` (value: `'CATALOG_MANAGE_ACCESS'`) + +* `TABLE_DROP` (value: `'TABLE_DROP'`) + +* `TABLE_LIST` (value: `'TABLE_LIST'`) + +* `TABLE_READ_PROPERTIES` (value: `'TABLE_READ_PROPERTIES'`) + +* `VIEW_READ_PROPERTIES` (value: `'VIEW_READ_PROPERTIES'`) + +* `TABLE_WRITE_PROPERTIES` (value: `'TABLE_WRITE_PROPERTIES'`) + +* `TABLE_READ_DATA` (value: `'TABLE_READ_DATA'`) + +* `TABLE_WRITE_DATA` (value: `'TABLE_WRITE_DATA'`) + +* `TABLE_FULL_METADATA` (value: `'TABLE_FULL_METADATA'`) + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/TableRequirement.md b/regtests/client/python/docs/TableRequirement.md new file mode 100644 index 0000000000..f2b2460e62 --- /dev/null +++ b/regtests/client/python/docs/TableRequirement.md @@ -0,0 +1,29 @@ +# TableRequirement + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**type** | **str** | | + +## Example + +```python +from polaris.catalog.models.table_requirement import TableRequirement + +# TODO update the JSON string below +json = "{}" +# create an instance of TableRequirement from a JSON string +table_requirement_instance = TableRequirement.from_json(json) +# print the JSON string representation of the object +print(TableRequirement.to_json()) + +# convert the object into a dict +table_requirement_dict = table_requirement_instance.to_dict() +# create an instance of TableRequirement from a dict +table_requirement_from_dict = TableRequirement.from_dict(table_requirement_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/TableUpdate.md b/regtests/client/python/docs/TableUpdate.md new file mode 100644 index 0000000000..6dca410f47 --- /dev/null +++ b/regtests/client/python/docs/TableUpdate.md @@ -0,0 +1,49 @@ +# TableUpdate + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**action** | **str** | | +**format_version** | **int** | | +**var_schema** | [**ModelSchema**](ModelSchema.md) | | +**last_column_id** | **int** | The highest assigned column ID for the table. This is used to ensure columns are always assigned an unused ID when evolving schemas. When omitted, it will be computed on the server side. | [optional] +**schema_id** | **int** | Schema ID to set as current, or -1 to set last added schema | +**spec** | [**PartitionSpec**](PartitionSpec.md) | | +**spec_id** | **int** | Partition spec ID to set as the default, or -1 to set last added spec | +**sort_order** | [**SortOrder**](SortOrder.md) | | +**sort_order_id** | **int** | Sort order ID to set as the default, or -1 to set last added sort order | +**snapshot** | [**Snapshot**](Snapshot.md) | | +**ref_name** | **str** | | +**type** | **str** | | +**snapshot_id** | **int** | | +**max_ref_age_ms** | **int** | | [optional] +**max_snapshot_age_ms** | **int** | | [optional] +**min_snapshots_to_keep** | **int** | | [optional] +**snapshot_ids** | **List[int]** | | +**location** | **str** | | +**updates** | **Dict[str, str]** | | +**removals** | **List[str]** | | +**statistics** | [**StatisticsFile**](StatisticsFile.md) | | + +## Example + +```python +from polaris.catalog.models.table_update import TableUpdate + +# TODO update the JSON string below +json = "{}" +# create an instance of TableUpdate from a JSON string +table_update_instance = TableUpdate.from_json(json) +# print the JSON string representation of the object +print(TableUpdate.to_json()) + +# convert the object into a dict +table_update_dict = table_update_instance.to_dict() +# create an instance of TableUpdate from a dict +table_update_from_dict = TableUpdate.from_dict(table_update_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/TableUpdateNotification.md b/regtests/client/python/docs/TableUpdateNotification.md new file mode 100644 index 0000000000..b0a7b5600a --- /dev/null +++ b/regtests/client/python/docs/TableUpdateNotification.md @@ -0,0 +1,33 @@ +# TableUpdateNotification + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**table_name** | **str** | | +**timestamp** | **int** | | +**table_uuid** | **str** | | +**metadata_location** | **str** | | +**metadata** | [**TableMetadata**](TableMetadata.md) | | [optional] + +## Example + +```python +from polaris.catalog.models.table_update_notification import TableUpdateNotification + +# TODO update the JSON string below +json = "{}" +# create an instance of TableUpdateNotification from a JSON string +table_update_notification_instance = TableUpdateNotification.from_json(json) +# print the JSON string representation of the object +print(TableUpdateNotification.to_json()) + +# convert the object into a dict +table_update_notification_dict = table_update_notification_instance.to_dict() +# create an instance of TableUpdateNotification from a dict +table_update_notification_from_dict = TableUpdateNotification.from_dict(table_update_notification_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/Term.md b/regtests/client/python/docs/Term.md new file mode 100644 index 0000000000..cf1e3fb923 --- /dev/null +++ b/regtests/client/python/docs/Term.md @@ -0,0 +1,31 @@ +# Term + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**type** | **str** | | +**transform** | **str** | | +**term** | **str** | | + +## Example + +```python +from polaris.catalog.models.term import Term + +# TODO update the JSON string below +json = "{}" +# create an instance of Term from a JSON string +term_instance = Term.from_json(json) +# print the JSON string representation of the object +print(Term.to_json()) + +# convert the object into a dict +term_dict = term_instance.to_dict() +# create an instance of Term from a dict +term_from_dict = Term.from_dict(term_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/TimerResult.md b/regtests/client/python/docs/TimerResult.md new file mode 100644 index 0000000000..7a4c22a36f --- /dev/null +++ b/regtests/client/python/docs/TimerResult.md @@ -0,0 +1,31 @@ +# TimerResult + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**time_unit** | **str** | | +**count** | **int** | | +**total_duration** | **int** | | + +## Example + +```python +from polaris.catalog.models.timer_result import TimerResult + +# TODO update the JSON string below +json = "{}" +# create an instance of TimerResult from a JSON string +timer_result_instance = TimerResult.from_json(json) +# print the JSON string representation of the object +print(TimerResult.to_json()) + +# convert the object into a dict +timer_result_dict = timer_result_instance.to_dict() +# create an instance of TimerResult from a dict +timer_result_from_dict = TimerResult.from_dict(timer_result_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/TokenType.md b/regtests/client/python/docs/TokenType.md new file mode 100644 index 0000000000..8bb0dc046c --- /dev/null +++ b/regtests/client/python/docs/TokenType.md @@ -0,0 +1,21 @@ +# TokenType + +Token type identifier, from RFC 8693 Section 3 See https://datatracker.ietf.org/doc/html/rfc8693#section-3 + +## Enum + +* `URN_COLON_IETF_COLON_PARAMS_COLON_OAUTH_COLON_TOKEN_MINUS_TYPE_COLON_ACCESS_TOKEN` (value: `'urn:ietf:params:oauth:token-type:access_token'`) + +* `URN_COLON_IETF_COLON_PARAMS_COLON_OAUTH_COLON_TOKEN_MINUS_TYPE_COLON_REFRESH_TOKEN` (value: `'urn:ietf:params:oauth:token-type:refresh_token'`) + +* `URN_COLON_IETF_COLON_PARAMS_COLON_OAUTH_COLON_TOKEN_MINUS_TYPE_COLON_ID_TOKEN` (value: `'urn:ietf:params:oauth:token-type:id_token'`) + +* `URN_COLON_IETF_COLON_PARAMS_COLON_OAUTH_COLON_TOKEN_MINUS_TYPE_COLON_SAML1` (value: `'urn:ietf:params:oauth:token-type:saml1'`) + +* `URN_COLON_IETF_COLON_PARAMS_COLON_OAUTH_COLON_TOKEN_MINUS_TYPE_COLON_SAML2` (value: `'urn:ietf:params:oauth:token-type:saml2'`) + +* `URN_COLON_IETF_COLON_PARAMS_COLON_OAUTH_COLON_TOKEN_MINUS_TYPE_COLON_JWT` (value: `'urn:ietf:params:oauth:token-type:jwt'`) + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/TransformTerm.md b/regtests/client/python/docs/TransformTerm.md new file mode 100644 index 0000000000..14653cdf36 --- /dev/null +++ b/regtests/client/python/docs/TransformTerm.md @@ -0,0 +1,31 @@ +# TransformTerm + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**type** | **str** | | +**transform** | **str** | | +**term** | **str** | | + +## Example + +```python +from polaris.catalog.models.transform_term import TransformTerm + +# TODO update the JSON string below +json = "{}" +# create an instance of TransformTerm from a JSON string +transform_term_instance = TransformTerm.from_json(json) +# print the JSON string representation of the object +print(TransformTerm.to_json()) + +# convert the object into a dict +transform_term_dict = transform_term_instance.to_dict() +# create an instance of TransformTerm from a dict +transform_term_from_dict = TransformTerm.from_dict(transform_term_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/Type.md b/regtests/client/python/docs/Type.md new file mode 100644 index 0000000000..4a7337c956 --- /dev/null +++ b/regtests/client/python/docs/Type.md @@ -0,0 +1,38 @@ +# Type + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**type** | **str** | | +**fields** | [**List[StructField]**](StructField.md) | | +**element_id** | **int** | | +**element** | [**Type**](Type.md) | | +**element_required** | **bool** | | +**key_id** | **int** | | +**key** | [**Type**](Type.md) | | +**value_id** | **int** | | +**value** | [**Type**](Type.md) | | +**value_required** | **bool** | | + +## Example + +```python +from polaris.catalog.models.type import Type + +# TODO update the JSON string below +json = "{}" +# create an instance of Type from a JSON string +type_instance = Type.from_json(json) +# print the JSON string representation of the object +print(Type.to_json()) + +# convert the object into a dict +type_dict = type_instance.to_dict() +# create an instance of Type from a dict +type_from_dict = Type.from_dict(type_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/UnaryExpression.md b/regtests/client/python/docs/UnaryExpression.md new file mode 100644 index 0000000000..5a3e12c1bc --- /dev/null +++ b/regtests/client/python/docs/UnaryExpression.md @@ -0,0 +1,31 @@ +# UnaryExpression + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**type** | **str** | | +**term** | [**Term**](Term.md) | | +**value** | **object** | | + +## Example + +```python +from polaris.catalog.models.unary_expression import UnaryExpression + +# TODO update the JSON string below +json = "{}" +# create an instance of UnaryExpression from a JSON string +unary_expression_instance = UnaryExpression.from_json(json) +# print the JSON string representation of the object +print(UnaryExpression.to_json()) + +# convert the object into a dict +unary_expression_dict = unary_expression_instance.to_dict() +# create an instance of UnaryExpression from a dict +unary_expression_from_dict = UnaryExpression.from_dict(unary_expression_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/UpdateCatalogRequest.md b/regtests/client/python/docs/UpdateCatalogRequest.md new file mode 100644 index 0000000000..5cb5581c9e --- /dev/null +++ b/regtests/client/python/docs/UpdateCatalogRequest.md @@ -0,0 +1,32 @@ +# UpdateCatalogRequest + +Updates to apply to a Catalog + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**current_entity_version** | **int** | The version of the object onto which this update is applied; if the object changed, the update will fail and the caller should retry after fetching the latest version. | [optional] +**properties** | **Dict[str, str]** | | [optional] +**storage_config_info** | [**StorageConfigInfo**](StorageConfigInfo.md) | | [optional] + +## Example + +```python +from polaris.management.models.update_catalog_request import UpdateCatalogRequest + +# TODO update the JSON string below +json = "{}" +# create an instance of UpdateCatalogRequest from a JSON string +update_catalog_request_instance = UpdateCatalogRequest.from_json(json) +# print the JSON string representation of the object +print(UpdateCatalogRequest.to_json()) + +# convert the object into a dict +update_catalog_request_dict = update_catalog_request_instance.to_dict() +# create an instance of UpdateCatalogRequest from a dict +update_catalog_request_from_dict = UpdateCatalogRequest.from_dict(update_catalog_request_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/UpdateCatalogRoleRequest.md b/regtests/client/python/docs/UpdateCatalogRoleRequest.md new file mode 100644 index 0000000000..bc4ace54f2 --- /dev/null +++ b/regtests/client/python/docs/UpdateCatalogRoleRequest.md @@ -0,0 +1,31 @@ +# UpdateCatalogRoleRequest + +Updates to apply to a Catalog Role + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**current_entity_version** | **int** | The version of the object onto which this update is applied; if the object changed, the update will fail and the caller should retry after fetching the latest version. | +**properties** | **Dict[str, str]** | | + +## Example + +```python +from polaris.management.models.update_catalog_role_request import UpdateCatalogRoleRequest + +# TODO update the JSON string below +json = "{}" +# create an instance of UpdateCatalogRoleRequest from a JSON string +update_catalog_role_request_instance = UpdateCatalogRoleRequest.from_json(json) +# print the JSON string representation of the object +print(UpdateCatalogRoleRequest.to_json()) + +# convert the object into a dict +update_catalog_role_request_dict = update_catalog_role_request_instance.to_dict() +# create an instance of UpdateCatalogRoleRequest from a dict +update_catalog_role_request_from_dict = UpdateCatalogRoleRequest.from_dict(update_catalog_role_request_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/UpdateNamespacePropertiesRequest.md b/regtests/client/python/docs/UpdateNamespacePropertiesRequest.md new file mode 100644 index 0000000000..ea2a5a6722 --- /dev/null +++ b/regtests/client/python/docs/UpdateNamespacePropertiesRequest.md @@ -0,0 +1,30 @@ +# UpdateNamespacePropertiesRequest + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**removals** | **List[str]** | | [optional] +**updates** | **Dict[str, str]** | | [optional] + +## Example + +```python +from polaris.catalog.models.update_namespace_properties_request import UpdateNamespacePropertiesRequest + +# TODO update the JSON string below +json = "{}" +# create an instance of UpdateNamespacePropertiesRequest from a JSON string +update_namespace_properties_request_instance = UpdateNamespacePropertiesRequest.from_json(json) +# print the JSON string representation of the object +print(UpdateNamespacePropertiesRequest.to_json()) + +# convert the object into a dict +update_namespace_properties_request_dict = update_namespace_properties_request_instance.to_dict() +# create an instance of UpdateNamespacePropertiesRequest from a dict +update_namespace_properties_request_from_dict = UpdateNamespacePropertiesRequest.from_dict(update_namespace_properties_request_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/UpdateNamespacePropertiesResponse.md b/regtests/client/python/docs/UpdateNamespacePropertiesResponse.md new file mode 100644 index 0000000000..70ebd68baf --- /dev/null +++ b/regtests/client/python/docs/UpdateNamespacePropertiesResponse.md @@ -0,0 +1,31 @@ +# UpdateNamespacePropertiesResponse + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**updated** | **List[str]** | List of property keys that were added or updated | +**removed** | **List[str]** | List of properties that were removed | +**missing** | **List[str]** | List of properties requested for removal that were not found in the namespace's properties. Represents a partial success response. Server's do not need to implement this. | [optional] + +## Example + +```python +from polaris.catalog.models.update_namespace_properties_response import UpdateNamespacePropertiesResponse + +# TODO update the JSON string below +json = "{}" +# create an instance of UpdateNamespacePropertiesResponse from a JSON string +update_namespace_properties_response_instance = UpdateNamespacePropertiesResponse.from_json(json) +# print the JSON string representation of the object +print(UpdateNamespacePropertiesResponse.to_json()) + +# convert the object into a dict +update_namespace_properties_response_dict = update_namespace_properties_response_instance.to_dict() +# create an instance of UpdateNamespacePropertiesResponse from a dict +update_namespace_properties_response_from_dict = UpdateNamespacePropertiesResponse.from_dict(update_namespace_properties_response_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/UpdatePrincipalRequest.md b/regtests/client/python/docs/UpdatePrincipalRequest.md new file mode 100644 index 0000000000..586f60a5f9 --- /dev/null +++ b/regtests/client/python/docs/UpdatePrincipalRequest.md @@ -0,0 +1,31 @@ +# UpdatePrincipalRequest + +Updates to apply to a Principal + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**current_entity_version** | **int** | The version of the object onto which this update is applied; if the object changed, the update will fail and the caller should retry after fetching the latest version. | +**properties** | **Dict[str, str]** | | + +## Example + +```python +from polaris.management.models.update_principal_request import UpdatePrincipalRequest + +# TODO update the JSON string below +json = "{}" +# create an instance of UpdatePrincipalRequest from a JSON string +update_principal_request_instance = UpdatePrincipalRequest.from_json(json) +# print the JSON string representation of the object +print(UpdatePrincipalRequest.to_json()) + +# convert the object into a dict +update_principal_request_dict = update_principal_request_instance.to_dict() +# create an instance of UpdatePrincipalRequest from a dict +update_principal_request_from_dict = UpdatePrincipalRequest.from_dict(update_principal_request_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/UpdatePrincipalRoleRequest.md b/regtests/client/python/docs/UpdatePrincipalRoleRequest.md new file mode 100644 index 0000000000..ecb2f9cb25 --- /dev/null +++ b/regtests/client/python/docs/UpdatePrincipalRoleRequest.md @@ -0,0 +1,31 @@ +# UpdatePrincipalRoleRequest + +Updates to apply to a Principal Role + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**current_entity_version** | **int** | The version of the object onto which this update is applied; if the object changed, the update will fail and the caller should retry after fetching the latest version. | +**properties** | **Dict[str, str]** | | + +## Example + +```python +from polaris.management.models.update_principal_role_request import UpdatePrincipalRoleRequest + +# TODO update the JSON string below +json = "{}" +# create an instance of UpdatePrincipalRoleRequest from a JSON string +update_principal_role_request_instance = UpdatePrincipalRoleRequest.from_json(json) +# print the JSON string representation of the object +print(UpdatePrincipalRoleRequest.to_json()) + +# convert the object into a dict +update_principal_role_request_dict = update_principal_role_request_instance.to_dict() +# create an instance of UpdatePrincipalRoleRequest from a dict +update_principal_role_request_from_dict = UpdatePrincipalRoleRequest.from_dict(update_principal_role_request_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/UpgradeFormatVersionUpdate.md b/regtests/client/python/docs/UpgradeFormatVersionUpdate.md new file mode 100644 index 0000000000..599995ab0f --- /dev/null +++ b/regtests/client/python/docs/UpgradeFormatVersionUpdate.md @@ -0,0 +1,30 @@ +# UpgradeFormatVersionUpdate + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**action** | **str** | | +**format_version** | **int** | | + +## Example + +```python +from polaris.catalog.models.upgrade_format_version_update import UpgradeFormatVersionUpdate + +# TODO update the JSON string below +json = "{}" +# create an instance of UpgradeFormatVersionUpdate from a JSON string +upgrade_format_version_update_instance = UpgradeFormatVersionUpdate.from_json(json) +# print the JSON string representation of the object +print(UpgradeFormatVersionUpdate.to_json()) + +# convert the object into a dict +upgrade_format_version_update_dict = upgrade_format_version_update_instance.to_dict() +# create an instance of UpgradeFormatVersionUpdate from a dict +upgrade_format_version_update_from_dict = UpgradeFormatVersionUpdate.from_dict(upgrade_format_version_update_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/ValueMap.md b/regtests/client/python/docs/ValueMap.md new file mode 100644 index 0000000000..391355735a --- /dev/null +++ b/regtests/client/python/docs/ValueMap.md @@ -0,0 +1,30 @@ +# ValueMap + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**keys** | **List[int]** | List of integer column ids for each corresponding value | [optional] +**values** | [**List[PrimitiveTypeValue]**](PrimitiveTypeValue.md) | List of primitive type values, matched to 'keys' by index | [optional] + +## Example + +```python +from polaris.catalog.models.value_map import ValueMap + +# TODO update the JSON string below +json = "{}" +# create an instance of ValueMap from a JSON string +value_map_instance = ValueMap.from_json(json) +# print the JSON string representation of the object +print(ValueMap.to_json()) + +# convert the object into a dict +value_map_dict = value_map_instance.to_dict() +# create an instance of ValueMap from a dict +value_map_from_dict = ValueMap.from_dict(value_map_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/ViewGrant.md b/regtests/client/python/docs/ViewGrant.md new file mode 100644 index 0000000000..f9e8030ce3 --- /dev/null +++ b/regtests/client/python/docs/ViewGrant.md @@ -0,0 +1,31 @@ +# ViewGrant + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**namespace** | **List[str]** | | +**view_name** | **str** | | +**privilege** | [**ViewPrivilege**](ViewPrivilege.md) | | + +## Example + +```python +from polaris.management.models.view_grant import ViewGrant + +# TODO update the JSON string below +json = "{}" +# create an instance of ViewGrant from a JSON string +view_grant_instance = ViewGrant.from_json(json) +# print the JSON string representation of the object +print(ViewGrant.to_json()) + +# convert the object into a dict +view_grant_dict = view_grant_instance.to_dict() +# create an instance of ViewGrant from a dict +view_grant_from_dict = ViewGrant.from_dict(view_grant_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/ViewHistoryEntry.md b/regtests/client/python/docs/ViewHistoryEntry.md new file mode 100644 index 0000000000..4def78578a --- /dev/null +++ b/regtests/client/python/docs/ViewHistoryEntry.md @@ -0,0 +1,30 @@ +# ViewHistoryEntry + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**version_id** | **int** | | +**timestamp_ms** | **int** | | + +## Example + +```python +from polaris.catalog.models.view_history_entry import ViewHistoryEntry + +# TODO update the JSON string below +json = "{}" +# create an instance of ViewHistoryEntry from a JSON string +view_history_entry_instance = ViewHistoryEntry.from_json(json) +# print the JSON string representation of the object +print(ViewHistoryEntry.to_json()) + +# convert the object into a dict +view_history_entry_dict = view_history_entry_instance.to_dict() +# create an instance of ViewHistoryEntry from a dict +view_history_entry_from_dict = ViewHistoryEntry.from_dict(view_history_entry_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/ViewMetadata.md b/regtests/client/python/docs/ViewMetadata.md new file mode 100644 index 0000000000..174ca820f1 --- /dev/null +++ b/regtests/client/python/docs/ViewMetadata.md @@ -0,0 +1,36 @@ +# ViewMetadata + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**view_uuid** | **str** | | +**format_version** | **int** | | +**location** | **str** | | +**current_version_id** | **int** | | +**versions** | [**List[ViewVersion]**](ViewVersion.md) | | +**version_log** | [**List[ViewHistoryEntry]**](ViewHistoryEntry.md) | | +**schemas** | [**List[ModelSchema]**](ModelSchema.md) | | +**properties** | **Dict[str, str]** | | [optional] + +## Example + +```python +from polaris.catalog.models.view_metadata import ViewMetadata + +# TODO update the JSON string below +json = "{}" +# create an instance of ViewMetadata from a JSON string +view_metadata_instance = ViewMetadata.from_json(json) +# print the JSON string representation of the object +print(ViewMetadata.to_json()) + +# convert the object into a dict +view_metadata_dict = view_metadata_instance.to_dict() +# create an instance of ViewMetadata from a dict +view_metadata_from_dict = ViewMetadata.from_dict(view_metadata_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/ViewPrivilege.md b/regtests/client/python/docs/ViewPrivilege.md new file mode 100644 index 0000000000..27dd07d427 --- /dev/null +++ b/regtests/client/python/docs/ViewPrivilege.md @@ -0,0 +1,22 @@ +# ViewPrivilege + + +## Enum + +* `CATALOG_MANAGE_ACCESS` (value: `'CATALOG_MANAGE_ACCESS'`) + +* `VIEW_CREATE` (value: `'VIEW_CREATE'`) + +* `VIEW_DROP` (value: `'VIEW_DROP'`) + +* `VIEW_LIST` (value: `'VIEW_LIST'`) + +* `VIEW_READ_PROPERTIES` (value: `'VIEW_READ_PROPERTIES'`) + +* `VIEW_WRITE_PROPERTIES` (value: `'VIEW_WRITE_PROPERTIES'`) + +* `VIEW_FULL_METADATA` (value: `'VIEW_FULL_METADATA'`) + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/ViewRepresentation.md b/regtests/client/python/docs/ViewRepresentation.md new file mode 100644 index 0000000000..6c892c9719 --- /dev/null +++ b/regtests/client/python/docs/ViewRepresentation.md @@ -0,0 +1,31 @@ +# ViewRepresentation + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**type** | **str** | | +**sql** | **str** | | +**dialect** | **str** | | + +## Example + +```python +from polaris.catalog.models.view_representation import ViewRepresentation + +# TODO update the JSON string below +json = "{}" +# create an instance of ViewRepresentation from a JSON string +view_representation_instance = ViewRepresentation.from_json(json) +# print the JSON string representation of the object +print(ViewRepresentation.to_json()) + +# convert the object into a dict +view_representation_dict = view_representation_instance.to_dict() +# create an instance of ViewRepresentation from a dict +view_representation_from_dict = ViewRepresentation.from_dict(view_representation_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/ViewRequirement.md b/regtests/client/python/docs/ViewRequirement.md new file mode 100644 index 0000000000..ff869b4354 --- /dev/null +++ b/regtests/client/python/docs/ViewRequirement.md @@ -0,0 +1,29 @@ +# ViewRequirement + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**type** | **str** | | + +## Example + +```python +from polaris.catalog.models.view_requirement import ViewRequirement + +# TODO update the JSON string below +json = "{}" +# create an instance of ViewRequirement from a JSON string +view_requirement_instance = ViewRequirement.from_json(json) +# print the JSON string representation of the object +print(ViewRequirement.to_json()) + +# convert the object into a dict +view_requirement_dict = view_requirement_instance.to_dict() +# create an instance of ViewRequirement from a dict +view_requirement_from_dict = ViewRequirement.from_dict(view_requirement_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/ViewUpdate.md b/regtests/client/python/docs/ViewUpdate.md new file mode 100644 index 0000000000..98d45169ca --- /dev/null +++ b/regtests/client/python/docs/ViewUpdate.md @@ -0,0 +1,37 @@ +# ViewUpdate + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**action** | **str** | | +**format_version** | **int** | | +**var_schema** | [**ModelSchema**](ModelSchema.md) | | +**last_column_id** | **int** | The highest assigned column ID for the table. This is used to ensure columns are always assigned an unused ID when evolving schemas. When omitted, it will be computed on the server side. | [optional] +**location** | **str** | | +**updates** | **Dict[str, str]** | | +**removals** | **List[str]** | | +**view_version** | [**ViewVersion**](ViewVersion.md) | | +**view_version_id** | **int** | The view version id to set as current, or -1 to set last added view version id | + +## Example + +```python +from polaris.catalog.models.view_update import ViewUpdate + +# TODO update the JSON string below +json = "{}" +# create an instance of ViewUpdate from a JSON string +view_update_instance = ViewUpdate.from_json(json) +# print the JSON string representation of the object +print(ViewUpdate.to_json()) + +# convert the object into a dict +view_update_dict = view_update_instance.to_dict() +# create an instance of ViewUpdate from a dict +view_update_from_dict = ViewUpdate.from_dict(view_update_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/docs/ViewVersion.md b/regtests/client/python/docs/ViewVersion.md new file mode 100644 index 0000000000..6ed58a5f04 --- /dev/null +++ b/regtests/client/python/docs/ViewVersion.md @@ -0,0 +1,35 @@ +# ViewVersion + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**version_id** | **int** | | +**timestamp_ms** | **int** | | +**schema_id** | **int** | Schema ID to set as current, or -1 to set last added schema | +**summary** | **Dict[str, str]** | | +**representations** | [**List[ViewRepresentation]**](ViewRepresentation.md) | | +**default_catalog** | **str** | | [optional] +**default_namespace** | **List[str]** | Reference to one or more levels of a namespace | + +## Example + +```python +from polaris.catalog.models.view_version import ViewVersion + +# TODO update the JSON string below +json = "{}" +# create an instance of ViewVersion from a JSON string +view_version_instance = ViewVersion.from_json(json) +# print the JSON string representation of the object +print(ViewVersion.to_json()) + +# convert the object into a dict +view_version_dict = view_version_instance.to_dict() +# create an instance of ViewVersion from a dict +view_version_from_dict = ViewVersion.from_dict(view_version_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/regtests/client/python/git_push.sh b/regtests/client/python/git_push.sh new file mode 100644 index 0000000000..f53a75d4fa --- /dev/null +++ b/regtests/client/python/git_push.sh @@ -0,0 +1,57 @@ +#!/bin/sh +# ref: https://help.github.com/articles/adding-an-existing-project-to-github-using-the-command-line/ +# +# Usage example: /bin/sh ./git_push.sh wing328 openapi-petstore-perl "minor update" "gitlab.com" + +git_user_id=$1 +git_repo_id=$2 +release_note=$3 +git_host=$4 + +if [ "$git_host" = "" ]; then + git_host="github.com" + echo "[INFO] No command line input provided. Set \$git_host to $git_host" +fi + +if [ "$git_user_id" = "" ]; then + git_user_id="GIT_USER_ID" + echo "[INFO] No command line input provided. Set \$git_user_id to $git_user_id" +fi + +if [ "$git_repo_id" = "" ]; then + git_repo_id="GIT_REPO_ID" + echo "[INFO] No command line input provided. Set \$git_repo_id to $git_repo_id" +fi + +if [ "$release_note" = "" ]; then + release_note="Minor update" + echo "[INFO] No command line input provided. Set \$release_note to $release_note" +fi + +# Initialize the local directory as a Git repository +git init + +# Adds the files in the local repository and stages them for commit. +git add . + +# Commits the tracked changes and prepares them to be pushed to a remote repository. +git commit -m "$release_note" + +# Sets the new remote +git_remote=$(git remote) +if [ "$git_remote" = "" ]; then # git remote not defined + + if [ "$GIT_TOKEN" = "" ]; then + echo "[INFO] \$GIT_TOKEN (environment variable) is not set. Using the git credential in your environment." + git remote add origin https://${git_host}/${git_user_id}/${git_repo_id}.git + else + git remote add origin https://${git_user_id}:"${GIT_TOKEN}"@${git_host}/${git_user_id}/${git_repo_id}.git + fi + +fi + +git pull origin master + +# Pushes (Forces) the changes in the local repository up to the remote repository +echo "Git pushing to https://${git_host}/${git_user_id}/${git_repo_id}.git" +git push origin master 2>&1 | grep -v 'To https' diff --git a/regtests/client/python/poetry.lock b/regtests/client/python/poetry.lock new file mode 100644 index 0000000000..25367ce075 --- /dev/null +++ b/regtests/client/python/poetry.lock @@ -0,0 +1,594 @@ +# This file is automatically @generated by Poetry 1.5.0 and should not be changed by hand. + +[[package]] +name = "annotated-types" +version = "0.7.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +files = [ + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.9\""} + +[[package]] +name = "boto3" +version = "1.34.120" +description = "The AWS SDK for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "boto3-1.34.120-py3-none-any.whl", hash = "sha256:3c42bc309246a761413f6e152f307f009e80e7c9fd03dd9e6c0dc8ab8b3a8fc1"}, + {file = "boto3-1.34.120.tar.gz", hash = "sha256:38893db8269d25b72cc6fbab97633bfc863eefde5456847169d06149a16aa6e0"}, +] + +[package.dependencies] +botocore = ">=1.34.120,<1.35.0" +jmespath = ">=0.7.1,<2.0.0" +s3transfer = ">=0.10.0,<0.11.0" + +[package.extras] +crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] + +[[package]] +name = "botocore" +version = "1.34.132" +description = "Low-level, data-driven core of boto 3." +optional = false +python-versions = ">=3.8" +files = [ + {file = "botocore-1.34.132-py3-none-any.whl", hash = "sha256:06ef8b4bd3b3cb5a9b9a4273a543b257be3304030978ba51516b576a65156c39"}, + {file = "botocore-1.34.132.tar.gz", hash = "sha256:372a6cfce29e5de9bcf8c95af901d0bc3e27d8aa2295fadee295424f95f43f16"}, +] + +[package.dependencies] +jmespath = ">=0.7.1,<2.0.0" +python-dateutil = ">=2.1,<3.0.0" +urllib3 = [ + {version = ">=1.25.4,<1.27", markers = "python_version < \"3.10\""}, + {version = ">=1.25.4,<2.2.0 || >2.2.0,<3", markers = "python_version >= \"3.10\""}, +] + +[package.extras] +crt = ["awscrt (==0.20.11)"] + +[[package]] +name = "cachetools" +version = "5.3.3" +description = "Extensible memoizing collections and decorators" +optional = false +python-versions = ">=3.7" +files = [ + {file = "cachetools-5.3.3-py3-none-any.whl", hash = "sha256:0abad1021d3f8325b2fc1d2e9c8b9c9d57b04c3932657a72465447332c24d945"}, + {file = "cachetools-5.3.3.tar.gz", hash = "sha256:ba29e2dfa0b8b556606f097407ed1aa62080ee108ab0dc5ec9d6a723a007d105"}, +] + +[[package]] +name = "chardet" +version = "5.2.0" +description = "Universal encoding detector for Python 3" +optional = false +python-versions = ">=3.7" +files = [ + {file = "chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970"}, + {file = "chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7"}, +] + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "distlib" +version = "0.3.8" +description = "Distribution utilities" +optional = false +python-versions = "*" +files = [ + {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, + {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, +] + +[[package]] +name = "exceptiongroup" +version = "1.2.1" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.2.1-py3-none-any.whl", hash = "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad"}, + {file = "exceptiongroup-1.2.1.tar.gz", hash = "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "filelock" +version = "3.15.4" +description = "A platform independent file lock." +optional = false +python-versions = ">=3.8" +files = [ + {file = "filelock-3.15.4-py3-none-any.whl", hash = "sha256:6ca1fffae96225dab4c6eaf1c4f4f28cd2568d3ec2a44e15a08520504de468e7"}, + {file = "filelock-3.15.4.tar.gz", hash = "sha256:2207938cbc1844345cb01a5a95524dae30f0ce089eba5b00378295a17e3e90cb"}, +] + +[package.extras] +docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-asyncio (>=0.21)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)", "virtualenv (>=20.26.2)"] +typing = ["typing-extensions (>=4.8)"] + +[[package]] +name = "flake8" +version = "5.0.4" +description = "the modular source code checker: pep8 pyflakes and co" +optional = false +python-versions = ">=3.6.1" +files = [ + {file = "flake8-5.0.4-py2.py3-none-any.whl", hash = "sha256:7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248"}, + {file = "flake8-5.0.4.tar.gz", hash = "sha256:6fbe320aad8d6b95cec8b8e47bc933004678dc63095be98528b7bdd2a9f510db"}, +] + +[package.dependencies] +mccabe = ">=0.7.0,<0.8.0" +pycodestyle = ">=2.9.0,<2.10.0" +pyflakes = ">=2.5.0,<2.6.0" + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "jmespath" +version = "1.0.1" +description = "JSON Matching Expressions" +optional = false +python-versions = ">=3.7" +files = [ + {file = "jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980"}, + {file = "jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"}, +] + +[[package]] +name = "mccabe" +version = "0.7.0" +description = "McCabe checker, plugin for flake8" +optional = false +python-versions = ">=3.6" +files = [ + {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, + {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, +] + +[[package]] +name = "mypy" +version = "1.4.1" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "mypy-1.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:566e72b0cd6598503e48ea610e0052d1b8168e60a46e0bfd34b3acf2d57f96a8"}, + {file = "mypy-1.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ca637024ca67ab24a7fd6f65d280572c3794665eaf5edcc7e90a866544076878"}, + {file = "mypy-1.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dde1d180cd84f0624c5dcaaa89c89775550a675aff96b5848de78fb11adabcd"}, + {file = "mypy-1.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8c4d8e89aa7de683e2056a581ce63c46a0c41e31bd2b6d34144e2c80f5ea53dc"}, + {file = "mypy-1.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:bfdca17c36ae01a21274a3c387a63aa1aafe72bff976522886869ef131b937f1"}, + {file = "mypy-1.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7549fbf655e5825d787bbc9ecf6028731973f78088fbca3a1f4145c39ef09462"}, + {file = "mypy-1.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:98324ec3ecf12296e6422939e54763faedbfcc502ea4a4c38502082711867258"}, + {file = "mypy-1.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:141dedfdbfe8a04142881ff30ce6e6653c9685b354876b12e4fe6c78598b45e2"}, + {file = "mypy-1.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8207b7105829eca6f3d774f64a904190bb2231de91b8b186d21ffd98005f14a7"}, + {file = "mypy-1.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:16f0db5b641ba159eff72cff08edc3875f2b62b2fa2bc24f68c1e7a4e8232d01"}, + {file = "mypy-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:470c969bb3f9a9efcedbadcd19a74ffb34a25f8e6b0e02dae7c0e71f8372f97b"}, + {file = "mypy-1.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5952d2d18b79f7dc25e62e014fe5a23eb1a3d2bc66318df8988a01b1a037c5b"}, + {file = "mypy-1.4.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:190b6bab0302cec4e9e6767d3eb66085aef2a1cc98fe04936d8a42ed2ba77bb7"}, + {file = "mypy-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9d40652cc4fe33871ad3338581dca3297ff5f2213d0df345bcfbde5162abf0c9"}, + {file = "mypy-1.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:01fd2e9f85622d981fd9063bfaef1aed6e336eaacca00892cd2d82801ab7c042"}, + {file = "mypy-1.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2460a58faeea905aeb1b9b36f5065f2dc9a9c6e4c992a6499a2360c6c74ceca3"}, + {file = "mypy-1.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2746d69a8196698146a3dbe29104f9eb6a2a4d8a27878d92169a6c0b74435b6"}, + {file = "mypy-1.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ae704dcfaa180ff7c4cfbad23e74321a2b774f92ca77fd94ce1049175a21c97f"}, + {file = "mypy-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:43d24f6437925ce50139a310a64b2ab048cb2d3694c84c71c3f2a1626d8101dc"}, + {file = "mypy-1.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c482e1246726616088532b5e964e39765b6d1520791348e6c9dc3af25b233828"}, + {file = "mypy-1.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:43b592511672017f5b1a483527fd2684347fdffc041c9ef53428c8dc530f79a3"}, + {file = "mypy-1.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:34a9239d5b3502c17f07fd7c0b2ae6b7dd7d7f6af35fbb5072c6208e76295816"}, + {file = "mypy-1.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5703097c4936bbb9e9bce41478c8d08edd2865e177dc4c52be759f81ee4dd26c"}, + {file = "mypy-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:e02d700ec8d9b1859790c0475df4e4092c7bf3272a4fd2c9f33d87fac4427b8f"}, + {file = "mypy-1.4.1-py3-none-any.whl", hash = "sha256:45d32cec14e7b97af848bddd97d85ea4f0db4d5a149ed9676caa4eb2f7402bb4"}, + {file = "mypy-1.4.1.tar.gz", hash = "sha256:9bbcd9ab8ea1f2e1c8031c21445b511442cc45c89951e49bbf852cbb70755b1b"}, +] + +[package.dependencies] +mypy-extensions = ">=1.0.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = ">=4.1.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +install-types = ["pip"] +python2 = ["typed-ast (>=1.4.0,<2)"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.5" +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + +[[package]] +name = "packaging" +version = "24.1" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, + {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, +] + +[[package]] +name = "platformdirs" +version = "4.2.2" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.8" +files = [ + {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, + {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, +] + +[package.extras] +docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] +type = ["mypy (>=1.8)"] + +[[package]] +name = "pluggy" +version = "1.5.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pycodestyle" +version = "2.9.1" +description = "Python style guide checker" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pycodestyle-2.9.1-py2.py3-none-any.whl", hash = "sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b"}, + {file = "pycodestyle-2.9.1.tar.gz", hash = "sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785"}, +] + +[[package]] +name = "pydantic" +version = "2.7.4" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic-2.7.4-py3-none-any.whl", hash = "sha256:ee8538d41ccb9c0a9ad3e0e5f07bf15ed8015b481ced539a1759d8cc89ae90d0"}, + {file = "pydantic-2.7.4.tar.gz", hash = "sha256:0c84efd9548d545f63ac0060c1e4d39bb9b14db8b3c0652338aecc07b5adec52"}, +] + +[package.dependencies] +annotated-types = ">=0.4.0" +pydantic-core = "2.18.4" +typing-extensions = ">=4.6.1" + +[package.extras] +email = ["email-validator (>=2.0.0)"] + +[[package]] +name = "pydantic-core" +version = "2.18.4" +description = "Core functionality for Pydantic validation and serialization" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic_core-2.18.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:f76d0ad001edd426b92233d45c746fd08f467d56100fd8f30e9ace4b005266e4"}, + {file = "pydantic_core-2.18.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:59ff3e89f4eaf14050c8022011862df275b552caef8082e37b542b066ce1ff26"}, + {file = "pydantic_core-2.18.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a55b5b16c839df1070bc113c1f7f94a0af4433fcfa1b41799ce7606e5c79ce0a"}, + {file = "pydantic_core-2.18.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4d0dcc59664fcb8974b356fe0a18a672d6d7cf9f54746c05f43275fc48636851"}, + {file = "pydantic_core-2.18.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8951eee36c57cd128f779e641e21eb40bc5073eb28b2d23f33eb0ef14ffb3f5d"}, + {file = "pydantic_core-2.18.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4701b19f7e3a06ea655513f7938de6f108123bf7c86bbebb1196eb9bd35cf724"}, + {file = "pydantic_core-2.18.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e00a3f196329e08e43d99b79b286d60ce46bed10f2280d25a1718399457e06be"}, + {file = "pydantic_core-2.18.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:97736815b9cc893b2b7f663628e63f436018b75f44854c8027040e05230eeddb"}, + {file = "pydantic_core-2.18.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6891a2ae0e8692679c07728819b6e2b822fb30ca7445f67bbf6509b25a96332c"}, + {file = "pydantic_core-2.18.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bc4ff9805858bd54d1a20efff925ccd89c9d2e7cf4986144b30802bf78091c3e"}, + {file = "pydantic_core-2.18.4-cp310-none-win32.whl", hash = "sha256:1b4de2e51bbcb61fdebd0ab86ef28062704f62c82bbf4addc4e37fa4b00b7cbc"}, + {file = "pydantic_core-2.18.4-cp310-none-win_amd64.whl", hash = "sha256:6a750aec7bf431517a9fd78cb93c97b9b0c496090fee84a47a0d23668976b4b0"}, + {file = "pydantic_core-2.18.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:942ba11e7dfb66dc70f9ae66b33452f51ac7bb90676da39a7345e99ffb55402d"}, + {file = "pydantic_core-2.18.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b2ebef0e0b4454320274f5e83a41844c63438fdc874ea40a8b5b4ecb7693f1c4"}, + {file = "pydantic_core-2.18.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a642295cd0c8df1b86fc3dced1d067874c353a188dc8e0f744626d49e9aa51c4"}, + {file = "pydantic_core-2.18.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f09baa656c904807e832cf9cce799c6460c450c4ad80803517032da0cd062e2"}, + {file = "pydantic_core-2.18.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:98906207f29bc2c459ff64fa007afd10a8c8ac080f7e4d5beff4c97086a3dabd"}, + {file = "pydantic_core-2.18.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:19894b95aacfa98e7cb093cd7881a0c76f55731efad31073db4521e2b6ff5b7d"}, + {file = "pydantic_core-2.18.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0fbbdc827fe5e42e4d196c746b890b3d72876bdbf160b0eafe9f0334525119c8"}, + {file = "pydantic_core-2.18.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f85d05aa0918283cf29a30b547b4df2fbb56b45b135f9e35b6807cb28bc47951"}, + {file = "pydantic_core-2.18.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e85637bc8fe81ddb73fda9e56bab24560bdddfa98aa64f87aaa4e4b6730c23d2"}, + {file = "pydantic_core-2.18.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:2f5966897e5461f818e136b8451d0551a2e77259eb0f73a837027b47dc95dab9"}, + {file = "pydantic_core-2.18.4-cp311-none-win32.whl", hash = "sha256:44c7486a4228413c317952e9d89598bcdfb06399735e49e0f8df643e1ccd0558"}, + {file = "pydantic_core-2.18.4-cp311-none-win_amd64.whl", hash = "sha256:8a7164fe2005d03c64fd3b85649891cd4953a8de53107940bf272500ba8a788b"}, + {file = "pydantic_core-2.18.4-cp311-none-win_arm64.whl", hash = "sha256:4e99bc050fe65c450344421017f98298a97cefc18c53bb2f7b3531eb39bc7805"}, + {file = "pydantic_core-2.18.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:6f5c4d41b2771c730ea1c34e458e781b18cc668d194958e0112455fff4e402b2"}, + {file = "pydantic_core-2.18.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2fdf2156aa3d017fddf8aea5adfba9f777db1d6022d392b682d2a8329e087cef"}, + {file = "pydantic_core-2.18.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4748321b5078216070b151d5271ef3e7cc905ab170bbfd27d5c83ee3ec436695"}, + {file = "pydantic_core-2.18.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:847a35c4d58721c5dc3dba599878ebbdfd96784f3fb8bb2c356e123bdcd73f34"}, + {file = "pydantic_core-2.18.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3c40d4eaad41f78e3bbda31b89edc46a3f3dc6e171bf0ecf097ff7a0ffff7cb1"}, + {file = "pydantic_core-2.18.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:21a5e440dbe315ab9825fcd459b8814bb92b27c974cbc23c3e8baa2b76890077"}, + {file = "pydantic_core-2.18.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:01dd777215e2aa86dfd664daed5957704b769e726626393438f9c87690ce78c3"}, + {file = "pydantic_core-2.18.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4b06beb3b3f1479d32befd1f3079cc47b34fa2da62457cdf6c963393340b56e9"}, + {file = "pydantic_core-2.18.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:564d7922e4b13a16b98772441879fcdcbe82ff50daa622d681dd682175ea918c"}, + {file = "pydantic_core-2.18.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:0eb2a4f660fcd8e2b1c90ad566db2b98d7f3f4717c64fe0a83e0adb39766d5b8"}, + {file = "pydantic_core-2.18.4-cp312-none-win32.whl", hash = "sha256:8b8bab4c97248095ae0c4455b5a1cd1cdd96e4e4769306ab19dda135ea4cdb07"}, + {file = "pydantic_core-2.18.4-cp312-none-win_amd64.whl", hash = "sha256:14601cdb733d741b8958224030e2bfe21a4a881fb3dd6fbb21f071cabd48fa0a"}, + {file = "pydantic_core-2.18.4-cp312-none-win_arm64.whl", hash = "sha256:c1322d7dd74713dcc157a2b7898a564ab091ca6c58302d5c7b4c07296e3fd00f"}, + {file = "pydantic_core-2.18.4-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:823be1deb01793da05ecb0484d6c9e20baebb39bd42b5d72636ae9cf8350dbd2"}, + {file = "pydantic_core-2.18.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ebef0dd9bf9b812bf75bda96743f2a6c5734a02092ae7f721c048d156d5fabae"}, + {file = "pydantic_core-2.18.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ae1d6df168efb88d7d522664693607b80b4080be6750c913eefb77e34c12c71a"}, + {file = "pydantic_core-2.18.4-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f9899c94762343f2cc2fc64c13e7cae4c3cc65cdfc87dd810a31654c9b7358cc"}, + {file = "pydantic_core-2.18.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99457f184ad90235cfe8461c4d70ab7dd2680e28821c29eca00252ba90308c78"}, + {file = "pydantic_core-2.18.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18f469a3d2a2fdafe99296a87e8a4c37748b5080a26b806a707f25a902c040a8"}, + {file = "pydantic_core-2.18.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7cdf28938ac6b8b49ae5e92f2735056a7ba99c9b110a474473fd71185c1af5d"}, + {file = "pydantic_core-2.18.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:938cb21650855054dc54dfd9120a851c974f95450f00683399006aa6e8abb057"}, + {file = "pydantic_core-2.18.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:44cd83ab6a51da80fb5adbd9560e26018e2ac7826f9626bc06ca3dc074cd198b"}, + {file = "pydantic_core-2.18.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:972658f4a72d02b8abfa2581d92d59f59897d2e9f7e708fdabe922f9087773af"}, + {file = "pydantic_core-2.18.4-cp38-none-win32.whl", hash = "sha256:1d886dc848e60cb7666f771e406acae54ab279b9f1e4143babc9c2258213daa2"}, + {file = "pydantic_core-2.18.4-cp38-none-win_amd64.whl", hash = "sha256:bb4462bd43c2460774914b8525f79b00f8f407c945d50881568f294c1d9b4443"}, + {file = "pydantic_core-2.18.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:44a688331d4a4e2129140a8118479443bd6f1905231138971372fcde37e43528"}, + {file = "pydantic_core-2.18.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a2fdd81edd64342c85ac7cf2753ccae0b79bf2dfa063785503cb85a7d3593223"}, + {file = "pydantic_core-2.18.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:86110d7e1907ab36691f80b33eb2da87d780f4739ae773e5fc83fb272f88825f"}, + {file = "pydantic_core-2.18.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:46387e38bd641b3ee5ce247563b60c5ca098da9c56c75c157a05eaa0933ed154"}, + {file = "pydantic_core-2.18.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:123c3cec203e3f5ac7b000bd82235f1a3eced8665b63d18be751f115588fea30"}, + {file = "pydantic_core-2.18.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dc1803ac5c32ec324c5261c7209e8f8ce88e83254c4e1aebdc8b0a39f9ddb443"}, + {file = "pydantic_core-2.18.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53db086f9f6ab2b4061958d9c276d1dbe3690e8dd727d6abf2321d6cce37fa94"}, + {file = "pydantic_core-2.18.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:abc267fa9837245cc28ea6929f19fa335f3dc330a35d2e45509b6566dc18be23"}, + {file = "pydantic_core-2.18.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a0d829524aaefdebccb869eed855e2d04c21d2d7479b6cada7ace5448416597b"}, + {file = "pydantic_core-2.18.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:509daade3b8649f80d4e5ff21aa5673e4ebe58590b25fe42fac5f0f52c6f034a"}, + {file = "pydantic_core-2.18.4-cp39-none-win32.whl", hash = "sha256:ca26a1e73c48cfc54c4a76ff78df3727b9d9f4ccc8dbee4ae3f73306a591676d"}, + {file = "pydantic_core-2.18.4-cp39-none-win_amd64.whl", hash = "sha256:c67598100338d5d985db1b3d21f3619ef392e185e71b8d52bceacc4a7771ea7e"}, + {file = "pydantic_core-2.18.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:574d92eac874f7f4db0ca653514d823a0d22e2354359d0759e3f6a406db5d55d"}, + {file = "pydantic_core-2.18.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1f4d26ceb5eb9eed4af91bebeae4b06c3fb28966ca3a8fb765208cf6b51102ab"}, + {file = "pydantic_core-2.18.4-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77450e6d20016ec41f43ca4a6c63e9fdde03f0ae3fe90e7c27bdbeaece8b1ed4"}, + {file = "pydantic_core-2.18.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d323a01da91851a4f17bf592faf46149c9169d68430b3146dcba2bb5e5719abc"}, + {file = "pydantic_core-2.18.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:43d447dd2ae072a0065389092a231283f62d960030ecd27565672bd40746c507"}, + {file = "pydantic_core-2.18.4-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:578e24f761f3b425834f297b9935e1ce2e30f51400964ce4801002435a1b41ef"}, + {file = "pydantic_core-2.18.4-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:81b5efb2f126454586d0f40c4d834010979cb80785173d1586df845a632e4e6d"}, + {file = "pydantic_core-2.18.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ab86ce7c8f9bea87b9d12c7f0af71102acbf5ecbc66c17796cff45dae54ef9a5"}, + {file = "pydantic_core-2.18.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:90afc12421df2b1b4dcc975f814e21bc1754640d502a2fbcc6d41e77af5ec312"}, + {file = "pydantic_core-2.18.4-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:51991a89639a912c17bef4b45c87bd83593aee0437d8102556af4885811d59f5"}, + {file = "pydantic_core-2.18.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:293afe532740370aba8c060882f7d26cfd00c94cae32fd2e212a3a6e3b7bc15e"}, + {file = "pydantic_core-2.18.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b48ece5bde2e768197a2d0f6e925f9d7e3e826f0ad2271120f8144a9db18d5c8"}, + {file = "pydantic_core-2.18.4-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:eae237477a873ab46e8dd748e515c72c0c804fb380fbe6c85533c7de51f23a8f"}, + {file = "pydantic_core-2.18.4-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:834b5230b5dfc0c1ec37b2fda433b271cbbc0e507560b5d1588e2cc1148cf1ce"}, + {file = "pydantic_core-2.18.4-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e858ac0a25074ba4bce653f9b5d0a85b7456eaddadc0ce82d3878c22489fa4ee"}, + {file = "pydantic_core-2.18.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2fd41f6eff4c20778d717af1cc50eca52f5afe7805ee530a4fbd0bae284f16e9"}, + {file = "pydantic_core-2.18.4.tar.gz", hash = "sha256:ec3beeada09ff865c344ff3bc2f427f5e6c26401cc6113d77e372c3fdac73864"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" + +[[package]] +name = "pyflakes" +version = "2.5.0" +description = "passive checker of Python programs" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pyflakes-2.5.0-py2.py3-none-any.whl", hash = "sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2"}, + {file = "pyflakes-2.5.0.tar.gz", hash = "sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3"}, +] + +[[package]] +name = "pyproject-api" +version = "1.7.1" +description = "API to interact with the python pyproject.toml based projects" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pyproject_api-1.7.1-py3-none-any.whl", hash = "sha256:2dc1654062c2b27733d8fd4cdda672b22fe8741ef1dde8e3a998a9547b071eeb"}, + {file = "pyproject_api-1.7.1.tar.gz", hash = "sha256:7ebc6cd10710f89f4cf2a2731710a98abce37ebff19427116ff2174c9236a827"}, +] + +[package.dependencies] +packaging = ">=24.1" +tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""} + +[package.extras] +docs = ["furo (>=2024.5.6)", "sphinx-autodoc-typehints (>=2.2.1)"] +testing = ["covdefaults (>=2.3)", "pytest (>=8.2.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "setuptools (>=70.1)"] + +[[package]] +name = "pytest" +version = "8.2.2" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-8.2.2-py3-none-any.whl", hash = "sha256:c434598117762e2bd304e526244f67bf66bbd7b5d6cf22138be51ff661980343"}, + {file = "pytest-8.2.2.tar.gz", hash = "sha256:de4bb8104e201939ccdc688b27a89a7be2079b22e2bd2b07f806b6ba71117977"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=1.5,<2.0" +tomli = {version = ">=1", markers = "python_version < \"3.11\""} + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "s3transfer" +version = "0.10.2" +description = "An Amazon S3 Transfer Manager" +optional = false +python-versions = ">=3.8" +files = [ + {file = "s3transfer-0.10.2-py3-none-any.whl", hash = "sha256:eca1c20de70a39daee580aef4986996620f365c4e0fda6a86100231d62f1bf69"}, + {file = "s3transfer-0.10.2.tar.gz", hash = "sha256:0711534e9356d3cc692fdde846b4a1e4b0cb6519971860796e6bc4c7aea00ef6"}, +] + +[package.dependencies] +botocore = ">=1.33.2,<2.0a.0" + +[package.extras] +crt = ["botocore[crt] (>=1.33.2,<2.0a.0)"] + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + +[[package]] +name = "tox" +version = "4.15.1" +description = "tox is a generic virtualenv management and test command line tool" +optional = false +python-versions = ">=3.8" +files = [ + {file = "tox-4.15.1-py3-none-any.whl", hash = "sha256:f00a5dc4222b358e69694e47e3da0227ac41253509bca9f45aa8f012053e8d9d"}, + {file = "tox-4.15.1.tar.gz", hash = "sha256:53a092527d65e873e39213ebd4bd027a64623320b6b0326136384213f95b7076"}, +] + +[package.dependencies] +cachetools = ">=5.3.2" +chardet = ">=5.2" +colorama = ">=0.4.6" +filelock = ">=3.13.1" +packaging = ">=23.2" +platformdirs = ">=4.1" +pluggy = ">=1.3" +pyproject-api = ">=1.6.1" +tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""} +virtualenv = ">=20.25" + +[package.extras] +docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-argparse-cli (>=1.11.1)", "sphinx-autodoc-typehints (>=1.25.2)", "sphinx-copybutton (>=0.5.2)", "sphinx-inline-tabs (>=2023.4.21)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.11)"] +testing = ["build[virtualenv] (>=1.0.3)", "covdefaults (>=2.3)", "detect-test-pollution (>=1.2)", "devpi-process (>=1)", "diff-cover (>=8.0.2)", "distlib (>=0.3.8)", "flaky (>=3.7)", "hatch-vcs (>=0.4)", "hatchling (>=1.21)", "psutil (>=5.9.7)", "pytest (>=7.4.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-xdist (>=3.5)", "re-assert (>=1.1)", "time-machine (>=2.13)", "wheel (>=0.42)"] + +[[package]] +name = "types-python-dateutil" +version = "2.9.0.20240316" +description = "Typing stubs for python-dateutil" +optional = false +python-versions = ">=3.8" +files = [ + {file = "types-python-dateutil-2.9.0.20240316.tar.gz", hash = "sha256:5d2f2e240b86905e40944dd787db6da9263f0deabef1076ddaed797351ec0202"}, + {file = "types_python_dateutil-2.9.0.20240316-py3-none-any.whl", hash = "sha256:6b8cb66d960771ce5ff974e9dd45e38facb81718cc1e208b10b1baccbfdbee3b"}, +] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, +] + +[[package]] +name = "urllib3" +version = "1.26.19" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +files = [ + {file = "urllib3-1.26.19-py2.py3-none-any.whl", hash = "sha256:37a0344459b199fce0e80b0d3569837ec6b6937435c5244e7fd73fa6006830f3"}, + {file = "urllib3-1.26.19.tar.gz", hash = "sha256:3e3d753a8618b86d7de333b4223005f68720bcd6a7d2bcb9fbd2229ec7c1e429"}, +] + +[package.extras] +brotli = ["brotli (==1.0.9)", "brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] +secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] + +[[package]] +name = "virtualenv" +version = "20.26.3" +description = "Virtual Python Environment builder" +optional = false +python-versions = ">=3.7" +files = [ + {file = "virtualenv-20.26.3-py3-none-any.whl", hash = "sha256:8cc4a31139e796e9a7de2cd5cf2489de1217193116a8fd42328f1bd65f434589"}, + {file = "virtualenv-20.26.3.tar.gz", hash = "sha256:4c43a2a236279d9ea36a0d76f98d84bd6ca94ac4e0f4a3b9d46d05e10fea542a"}, +] + +[package.dependencies] +distlib = ">=0.3.7,<1" +filelock = ">=3.12.2,<4" +platformdirs = ">=3.9.1,<5" + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] + +[metadata] +lock-version = "2.0" +python-versions = "^3.8" +content-hash = "c4842ff5ce0e433e93b0d1270e77ba79cf84362da1ce0b63b1940c50cf70da00" diff --git a/regtests/client/python/polaris/__init__.py b/regtests/client/python/polaris/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/regtests/client/python/polaris/catalog/__init__.py b/regtests/client/python/polaris/catalog/__init__.py new file mode 100644 index 0000000000..fd3a10c008 --- /dev/null +++ b/regtests/client/python/polaris/catalog/__init__.py @@ -0,0 +1,145 @@ +# coding: utf-8 + +# flake8: noqa + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +__version__ = "1.0.0" + +# import apis into sdk package +from polaris.catalog.api.iceberg_catalog_api import IcebergCatalogAPI +from polaris.catalog.api.iceberg_configuration_api import IcebergConfigurationAPI +from polaris.catalog.api.iceberg_o_auth2_api import IcebergOAuth2API + +# import ApiClient +from polaris.catalog.api_response import ApiResponse +from polaris.catalog.api_client import ApiClient +from polaris.catalog.configuration import Configuration +from polaris.catalog.exceptions import OpenApiException +from polaris.catalog.exceptions import ApiTypeError +from polaris.catalog.exceptions import ApiValueError +from polaris.catalog.exceptions import ApiKeyError +from polaris.catalog.exceptions import ApiAttributeError +from polaris.catalog.exceptions import ApiException + +# import models into sdk package +from polaris.catalog.models.add_partition_spec_update import AddPartitionSpecUpdate +from polaris.catalog.models.add_schema_update import AddSchemaUpdate +from polaris.catalog.models.add_snapshot_update import AddSnapshotUpdate +from polaris.catalog.models.add_sort_order_update import AddSortOrderUpdate +from polaris.catalog.models.add_view_version_update import AddViewVersionUpdate +from polaris.catalog.models.and_or_expression import AndOrExpression +from polaris.catalog.models.assert_create import AssertCreate +from polaris.catalog.models.assert_current_schema_id import AssertCurrentSchemaId +from polaris.catalog.models.assert_default_sort_order_id import AssertDefaultSortOrderId +from polaris.catalog.models.assert_default_spec_id import AssertDefaultSpecId +from polaris.catalog.models.assert_last_assigned_field_id import AssertLastAssignedFieldId +from polaris.catalog.models.assert_last_assigned_partition_id import AssertLastAssignedPartitionId +from polaris.catalog.models.assert_ref_snapshot_id import AssertRefSnapshotId +from polaris.catalog.models.assert_table_uuid import AssertTableUUID +from polaris.catalog.models.assert_view_uuid import AssertViewUUID +from polaris.catalog.models.assign_uuid_update import AssignUUIDUpdate +from polaris.catalog.models.base_update import BaseUpdate +from polaris.catalog.models.blob_metadata import BlobMetadata +from polaris.catalog.models.catalog_config import CatalogConfig +from polaris.catalog.models.commit_report import CommitReport +from polaris.catalog.models.commit_table_request import CommitTableRequest +from polaris.catalog.models.commit_table_response import CommitTableResponse +from polaris.catalog.models.commit_transaction_request import CommitTransactionRequest +from polaris.catalog.models.commit_view_request import CommitViewRequest +from polaris.catalog.models.content_file import ContentFile +from polaris.catalog.models.count_map import CountMap +from polaris.catalog.models.counter_result import CounterResult +from polaris.catalog.models.create_namespace_request import CreateNamespaceRequest +from polaris.catalog.models.create_namespace_response import CreateNamespaceResponse +from polaris.catalog.models.create_table_request import CreateTableRequest +from polaris.catalog.models.create_view_request import CreateViewRequest +from polaris.catalog.models.data_file import DataFile +from polaris.catalog.models.equality_delete_file import EqualityDeleteFile +from polaris.catalog.models.error_model import ErrorModel +from polaris.catalog.models.expression import Expression +from polaris.catalog.models.file_format import FileFormat +from polaris.catalog.models.get_namespace_response import GetNamespaceResponse +from polaris.catalog.models.iceberg_error_response import IcebergErrorResponse +from polaris.catalog.models.list_namespaces_response import ListNamespacesResponse +from polaris.catalog.models.list_tables_response import ListTablesResponse +from polaris.catalog.models.list_type import ListType +from polaris.catalog.models.literal_expression import LiteralExpression +from polaris.catalog.models.load_table_result import LoadTableResult +from polaris.catalog.models.load_view_result import LoadViewResult +from polaris.catalog.models.map_type import MapType +from polaris.catalog.models.metadata_log_inner import MetadataLogInner +from polaris.catalog.models.metric_result import MetricResult +from polaris.catalog.models.model_schema import ModelSchema +from polaris.catalog.models.not_expression import NotExpression +from polaris.catalog.models.notification_request import NotificationRequest +from polaris.catalog.models.notification_type import NotificationType +from polaris.catalog.models.null_order import NullOrder +from polaris.catalog.models.o_auth_error import OAuthError +from polaris.catalog.models.o_auth_token_response import OAuthTokenResponse +from polaris.catalog.models.partition_field import PartitionField +from polaris.catalog.models.partition_spec import PartitionSpec +from polaris.catalog.models.partition_statistics_file import PartitionStatisticsFile +from polaris.catalog.models.position_delete_file import PositionDeleteFile +from polaris.catalog.models.primitive_type_value import PrimitiveTypeValue +from polaris.catalog.models.register_table_request import RegisterTableRequest +from polaris.catalog.models.remove_partition_statistics_update import RemovePartitionStatisticsUpdate +from polaris.catalog.models.remove_properties_update import RemovePropertiesUpdate +from polaris.catalog.models.remove_snapshot_ref_update import RemoveSnapshotRefUpdate +from polaris.catalog.models.remove_snapshots_update import RemoveSnapshotsUpdate +from polaris.catalog.models.remove_statistics_update import RemoveStatisticsUpdate +from polaris.catalog.models.rename_table_request import RenameTableRequest +from polaris.catalog.models.report_metrics_request import ReportMetricsRequest +from polaris.catalog.models.sql_view_representation import SQLViewRepresentation +from polaris.catalog.models.scan_report import ScanReport +from polaris.catalog.models.set_current_schema_update import SetCurrentSchemaUpdate +from polaris.catalog.models.set_current_view_version_update import SetCurrentViewVersionUpdate +from polaris.catalog.models.set_default_sort_order_update import SetDefaultSortOrderUpdate +from polaris.catalog.models.set_default_spec_update import SetDefaultSpecUpdate +from polaris.catalog.models.set_expression import SetExpression +from polaris.catalog.models.set_location_update import SetLocationUpdate +from polaris.catalog.models.set_partition_statistics_update import SetPartitionStatisticsUpdate +from polaris.catalog.models.set_properties_update import SetPropertiesUpdate +from polaris.catalog.models.set_snapshot_ref_update import SetSnapshotRefUpdate +from polaris.catalog.models.set_statistics_update import SetStatisticsUpdate +from polaris.catalog.models.snapshot import Snapshot +from polaris.catalog.models.snapshot_log_inner import SnapshotLogInner +from polaris.catalog.models.snapshot_reference import SnapshotReference +from polaris.catalog.models.snapshot_summary import SnapshotSummary +from polaris.catalog.models.sort_direction import SortDirection +from polaris.catalog.models.sort_field import SortField +from polaris.catalog.models.sort_order import SortOrder +from polaris.catalog.models.statistics_file import StatisticsFile +from polaris.catalog.models.struct_field import StructField +from polaris.catalog.models.struct_type import StructType +from polaris.catalog.models.table_identifier import TableIdentifier +from polaris.catalog.models.table_metadata import TableMetadata +from polaris.catalog.models.table_requirement import TableRequirement +from polaris.catalog.models.table_update import TableUpdate +from polaris.catalog.models.table_update_notification import TableUpdateNotification +from polaris.catalog.models.term import Term +from polaris.catalog.models.timer_result import TimerResult +from polaris.catalog.models.token_type import TokenType +from polaris.catalog.models.transform_term import TransformTerm +from polaris.catalog.models.type import Type +from polaris.catalog.models.unary_expression import UnaryExpression +from polaris.catalog.models.update_namespace_properties_request import UpdateNamespacePropertiesRequest +from polaris.catalog.models.update_namespace_properties_response import UpdateNamespacePropertiesResponse +from polaris.catalog.models.upgrade_format_version_update import UpgradeFormatVersionUpdate +from polaris.catalog.models.value_map import ValueMap +from polaris.catalog.models.view_history_entry import ViewHistoryEntry +from polaris.catalog.models.view_metadata import ViewMetadata +from polaris.catalog.models.view_representation import ViewRepresentation +from polaris.catalog.models.view_requirement import ViewRequirement +from polaris.catalog.models.view_update import ViewUpdate +from polaris.catalog.models.view_version import ViewVersion diff --git a/regtests/client/python/polaris/catalog/api/__init__.py b/regtests/client/python/polaris/catalog/api/__init__.py new file mode 100644 index 0000000000..63e5ffafcc --- /dev/null +++ b/regtests/client/python/polaris/catalog/api/__init__.py @@ -0,0 +1,7 @@ +# flake8: noqa + +# import apis into api package +from polaris.catalog.api.iceberg_catalog_api import IcebergCatalogAPI +from polaris.catalog.api.iceberg_configuration_api import IcebergConfigurationAPI +from polaris.catalog.api.iceberg_o_auth2_api import IcebergOAuth2API + diff --git a/regtests/client/python/polaris/catalog/api/iceberg_catalog_api.py b/regtests/client/python/polaris/catalog/api/iceberg_catalog_api.py new file mode 100644 index 0000000000..e6ce3fc024 --- /dev/null +++ b/regtests/client/python/polaris/catalog/api/iceberg_catalog_api.py @@ -0,0 +1,7806 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + +import warnings +from pydantic import validate_call, Field, StrictFloat, StrictStr, StrictInt +from typing import Any, Dict, List, Optional, Tuple, Union +from typing_extensions import Annotated + +from pydantic import Field, StrictBool, StrictStr, field_validator +from typing import Optional +from typing_extensions import Annotated +from polaris.catalog.models.commit_table_request import CommitTableRequest +from polaris.catalog.models.commit_table_response import CommitTableResponse +from polaris.catalog.models.commit_transaction_request import CommitTransactionRequest +from polaris.catalog.models.commit_view_request import CommitViewRequest +from polaris.catalog.models.create_namespace_request import CreateNamespaceRequest +from polaris.catalog.models.create_namespace_response import CreateNamespaceResponse +from polaris.catalog.models.create_table_request import CreateTableRequest +from polaris.catalog.models.create_view_request import CreateViewRequest +from polaris.catalog.models.get_namespace_response import GetNamespaceResponse +from polaris.catalog.models.list_namespaces_response import ListNamespacesResponse +from polaris.catalog.models.list_tables_response import ListTablesResponse +from polaris.catalog.models.load_table_result import LoadTableResult +from polaris.catalog.models.load_view_result import LoadViewResult +from polaris.catalog.models.notification_request import NotificationRequest +from polaris.catalog.models.register_table_request import RegisterTableRequest +from polaris.catalog.models.rename_table_request import RenameTableRequest +from polaris.catalog.models.report_metrics_request import ReportMetricsRequest +from polaris.catalog.models.update_namespace_properties_request import UpdateNamespacePropertiesRequest +from polaris.catalog.models.update_namespace_properties_response import UpdateNamespacePropertiesResponse + +from polaris.catalog.api_client import ApiClient, RequestSerialized +from polaris.catalog.api_response import ApiResponse +from polaris.catalog.rest import RESTResponseType + + +class IcebergCatalogAPI: + """NOTE: This class is auto generated by OpenAPI Generator + Ref: https://openapi-generator.tech + + Do not edit the class manually. + """ + + def __init__(self, api_client=None) -> None: + if api_client is None: + api_client = ApiClient.get_default() + self.api_client = api_client + + + @validate_call + def commit_transaction( + self, + prefix: Annotated[StrictStr, Field(description="An optional prefix in the path")], + commit_transaction_request: Annotated[CommitTransactionRequest, Field(description="Commit updates to multiple tables in an atomic operation A commit for a single table consists of a table identifier with requirements and updates. Requirements are assertions that will be validated before attempting to make and commit changes. For example, `assert-ref-snapshot-id` will check that a named ref's snapshot ID has a certain value. Updates are changes to make to table metadata. For example, after asserting that the current main ref is at the expected snapshot, a commit may add a new child snapshot and set the ref to the new snapshot id.")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> None: + """Commit updates to multiple tables in an atomic operation + + + :param prefix: An optional prefix in the path (required) + :type prefix: str + :param commit_transaction_request: Commit updates to multiple tables in an atomic operation A commit for a single table consists of a table identifier with requirements and updates. Requirements are assertions that will be validated before attempting to make and commit changes. For example, `assert-ref-snapshot-id` will check that a named ref's snapshot ID has a certain value. Updates are changes to make to table metadata. For example, after asserting that the current main ref is at the expected snapshot, a commit may add a new child snapshot and set the ref to the new snapshot id. (required) + :type commit_transaction_request: CommitTransactionRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._commit_transaction_serialize( + prefix=prefix, + commit_transaction_request=commit_transaction_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '204': None, + '400': "IcebergErrorResponse", + '401': "IcebergErrorResponse", + '403': "IcebergErrorResponse", + '404': "IcebergErrorResponse", + '409': "IcebergErrorResponse", + '419': "IcebergErrorResponse", + '500': "IcebergErrorResponse", + '503': "IcebergErrorResponse", + '502': "IcebergErrorResponse", + '504': "IcebergErrorResponse", + '5XX': "IcebergErrorResponse", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + def commit_transaction_with_http_info( + self, + prefix: Annotated[StrictStr, Field(description="An optional prefix in the path")], + commit_transaction_request: Annotated[CommitTransactionRequest, Field(description="Commit updates to multiple tables in an atomic operation A commit for a single table consists of a table identifier with requirements and updates. Requirements are assertions that will be validated before attempting to make and commit changes. For example, `assert-ref-snapshot-id` will check that a named ref's snapshot ID has a certain value. Updates are changes to make to table metadata. For example, after asserting that the current main ref is at the expected snapshot, a commit may add a new child snapshot and set the ref to the new snapshot id.")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[None]: + """Commit updates to multiple tables in an atomic operation + + + :param prefix: An optional prefix in the path (required) + :type prefix: str + :param commit_transaction_request: Commit updates to multiple tables in an atomic operation A commit for a single table consists of a table identifier with requirements and updates. Requirements are assertions that will be validated before attempting to make and commit changes. For example, `assert-ref-snapshot-id` will check that a named ref's snapshot ID has a certain value. Updates are changes to make to table metadata. For example, after asserting that the current main ref is at the expected snapshot, a commit may add a new child snapshot and set the ref to the new snapshot id. (required) + :type commit_transaction_request: CommitTransactionRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._commit_transaction_serialize( + prefix=prefix, + commit_transaction_request=commit_transaction_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '204': None, + '400': "IcebergErrorResponse", + '401': "IcebergErrorResponse", + '403': "IcebergErrorResponse", + '404': "IcebergErrorResponse", + '409': "IcebergErrorResponse", + '419': "IcebergErrorResponse", + '500': "IcebergErrorResponse", + '503': "IcebergErrorResponse", + '502': "IcebergErrorResponse", + '504': "IcebergErrorResponse", + '5XX': "IcebergErrorResponse", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + def commit_transaction_without_preload_content( + self, + prefix: Annotated[StrictStr, Field(description="An optional prefix in the path")], + commit_transaction_request: Annotated[CommitTransactionRequest, Field(description="Commit updates to multiple tables in an atomic operation A commit for a single table consists of a table identifier with requirements and updates. Requirements are assertions that will be validated before attempting to make and commit changes. For example, `assert-ref-snapshot-id` will check that a named ref's snapshot ID has a certain value. Updates are changes to make to table metadata. For example, after asserting that the current main ref is at the expected snapshot, a commit may add a new child snapshot and set the ref to the new snapshot id.")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """Commit updates to multiple tables in an atomic operation + + + :param prefix: An optional prefix in the path (required) + :type prefix: str + :param commit_transaction_request: Commit updates to multiple tables in an atomic operation A commit for a single table consists of a table identifier with requirements and updates. Requirements are assertions that will be validated before attempting to make and commit changes. For example, `assert-ref-snapshot-id` will check that a named ref's snapshot ID has a certain value. Updates are changes to make to table metadata. For example, after asserting that the current main ref is at the expected snapshot, a commit may add a new child snapshot and set the ref to the new snapshot id. (required) + :type commit_transaction_request: CommitTransactionRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._commit_transaction_serialize( + prefix=prefix, + commit_transaction_request=commit_transaction_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '204': None, + '400': "IcebergErrorResponse", + '401': "IcebergErrorResponse", + '403': "IcebergErrorResponse", + '404': "IcebergErrorResponse", + '409': "IcebergErrorResponse", + '419': "IcebergErrorResponse", + '500': "IcebergErrorResponse", + '503': "IcebergErrorResponse", + '502': "IcebergErrorResponse", + '504': "IcebergErrorResponse", + '5XX': "IcebergErrorResponse", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _commit_transaction_serialize( + self, + prefix, + commit_transaction_request, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[str, Union[str, bytes]] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + if prefix is not None: + _path_params['prefix'] = prefix + # process the query parameters + # process the header parameters + # process the form parameters + # process the body parameter + if commit_transaction_request is not None: + _body_params = commit_transaction_request + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + + # set the HTTP header `Content-Type` + if _content_type: + _header_params['Content-Type'] = _content_type + else: + _default_content_type = ( + self.api_client.select_header_content_type( + [ + 'application/json' + ] + ) + ) + if _default_content_type is not None: + _header_params['Content-Type'] = _default_content_type + + # authentication setting + _auth_settings: List[str] = [ + 'OAuth2', + 'BearerAuth' + ] + + return self.api_client.param_serialize( + method='POST', + resource_path='/v1/{prefix}/transactions/commit', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + + + + @validate_call + def create_namespace( + self, + prefix: Annotated[StrictStr, Field(description="An optional prefix in the path")], + create_namespace_request: CreateNamespaceRequest, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> CreateNamespaceResponse: + """Create a namespace + + Create a namespace, with an optional set of properties. The server might also add properties, such as `last_modified_time` etc. + + :param prefix: An optional prefix in the path (required) + :type prefix: str + :param create_namespace_request: (required) + :type create_namespace_request: CreateNamespaceRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._create_namespace_serialize( + prefix=prefix, + create_namespace_request=create_namespace_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "CreateNamespaceResponse", + '400': "IcebergErrorResponse", + '401': "IcebergErrorResponse", + '403': "IcebergErrorResponse", + '406': "ErrorModel", + '409': "IcebergErrorResponse", + '419': "IcebergErrorResponse", + '503': "IcebergErrorResponse", + '5XX': "IcebergErrorResponse", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + def create_namespace_with_http_info( + self, + prefix: Annotated[StrictStr, Field(description="An optional prefix in the path")], + create_namespace_request: CreateNamespaceRequest, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[CreateNamespaceResponse]: + """Create a namespace + + Create a namespace, with an optional set of properties. The server might also add properties, such as `last_modified_time` etc. + + :param prefix: An optional prefix in the path (required) + :type prefix: str + :param create_namespace_request: (required) + :type create_namespace_request: CreateNamespaceRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._create_namespace_serialize( + prefix=prefix, + create_namespace_request=create_namespace_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "CreateNamespaceResponse", + '400': "IcebergErrorResponse", + '401': "IcebergErrorResponse", + '403': "IcebergErrorResponse", + '406': "ErrorModel", + '409': "IcebergErrorResponse", + '419': "IcebergErrorResponse", + '503': "IcebergErrorResponse", + '5XX': "IcebergErrorResponse", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + def create_namespace_without_preload_content( + self, + prefix: Annotated[StrictStr, Field(description="An optional prefix in the path")], + create_namespace_request: CreateNamespaceRequest, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """Create a namespace + + Create a namespace, with an optional set of properties. The server might also add properties, such as `last_modified_time` etc. + + :param prefix: An optional prefix in the path (required) + :type prefix: str + :param create_namespace_request: (required) + :type create_namespace_request: CreateNamespaceRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._create_namespace_serialize( + prefix=prefix, + create_namespace_request=create_namespace_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "CreateNamespaceResponse", + '400': "IcebergErrorResponse", + '401': "IcebergErrorResponse", + '403': "IcebergErrorResponse", + '406': "ErrorModel", + '409': "IcebergErrorResponse", + '419': "IcebergErrorResponse", + '503': "IcebergErrorResponse", + '5XX': "IcebergErrorResponse", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _create_namespace_serialize( + self, + prefix, + create_namespace_request, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[str, Union[str, bytes]] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + if prefix is not None: + _path_params['prefix'] = prefix + # process the query parameters + # process the header parameters + # process the form parameters + # process the body parameter + if create_namespace_request is not None: + _body_params = create_namespace_request + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + + # set the HTTP header `Content-Type` + if _content_type: + _header_params['Content-Type'] = _content_type + else: + _default_content_type = ( + self.api_client.select_header_content_type( + [ + 'application/json' + ] + ) + ) + if _default_content_type is not None: + _header_params['Content-Type'] = _default_content_type + + # authentication setting + _auth_settings: List[str] = [ + 'OAuth2', + 'BearerAuth' + ] + + return self.api_client.param_serialize( + method='POST', + resource_path='/v1/{prefix}/namespaces', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + + + + @validate_call + def create_table( + self, + prefix: Annotated[StrictStr, Field(description="An optional prefix in the path")], + namespace: Annotated[StrictStr, Field(description="A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte.")], + create_table_request: CreateTableRequest, + x_iceberg_access_delegation: Annotated[Optional[StrictStr], Field(description="Optional signal to the server that the client supports delegated access via a comma-separated list of access mechanisms. The server may choose to supply access via any or none of the requested mechanisms. Specific properties and handling for `vended-credentials` is documented in the `LoadTableResult` schema section of this spec document. The protocol and specification for `remote-signing` is documented in the `s3-signer-open-api.yaml` OpenApi spec in the `aws` module. ")] = None, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> LoadTableResult: + """Create a table in the given namespace + + Create a table or start a create transaction, like atomic CTAS. If `stage-create` is false, the table is created immediately. If `stage-create` is true, the table is not created, but table metadata is initialized and returned. The service should prepare as needed for a commit to the table commit endpoint to complete the create transaction. The client uses the returned metadata to begin a transaction. To commit the transaction, the client sends all create and subsequent changes to the table commit route. Changes from the table create operation include changes like AddSchemaUpdate and SetCurrentSchemaUpdate that set the initial table state. + + :param prefix: An optional prefix in the path (required) + :type prefix: str + :param namespace: A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. (required) + :type namespace: str + :param create_table_request: (required) + :type create_table_request: CreateTableRequest + :param x_iceberg_access_delegation: Optional signal to the server that the client supports delegated access via a comma-separated list of access mechanisms. The server may choose to supply access via any or none of the requested mechanisms. Specific properties and handling for `vended-credentials` is documented in the `LoadTableResult` schema section of this spec document. The protocol and specification for `remote-signing` is documented in the `s3-signer-open-api.yaml` OpenApi spec in the `aws` module. + :type x_iceberg_access_delegation: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._create_table_serialize( + prefix=prefix, + namespace=namespace, + create_table_request=create_table_request, + x_iceberg_access_delegation=x_iceberg_access_delegation, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "LoadTableResult", + '400': "IcebergErrorResponse", + '401': "IcebergErrorResponse", + '403': "IcebergErrorResponse", + '404': "IcebergErrorResponse", + '409': "IcebergErrorResponse", + '419': "IcebergErrorResponse", + '503': "IcebergErrorResponse", + '5XX': "IcebergErrorResponse", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + def create_table_with_http_info( + self, + prefix: Annotated[StrictStr, Field(description="An optional prefix in the path")], + namespace: Annotated[StrictStr, Field(description="A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte.")], + create_table_request: CreateTableRequest, + x_iceberg_access_delegation: Annotated[Optional[StrictStr], Field(description="Optional signal to the server that the client supports delegated access via a comma-separated list of access mechanisms. The server may choose to supply access via any or none of the requested mechanisms. Specific properties and handling for `vended-credentials` is documented in the `LoadTableResult` schema section of this spec document. The protocol and specification for `remote-signing` is documented in the `s3-signer-open-api.yaml` OpenApi spec in the `aws` module. ")] = None, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[LoadTableResult]: + """Create a table in the given namespace + + Create a table or start a create transaction, like atomic CTAS. If `stage-create` is false, the table is created immediately. If `stage-create` is true, the table is not created, but table metadata is initialized and returned. The service should prepare as needed for a commit to the table commit endpoint to complete the create transaction. The client uses the returned metadata to begin a transaction. To commit the transaction, the client sends all create and subsequent changes to the table commit route. Changes from the table create operation include changes like AddSchemaUpdate and SetCurrentSchemaUpdate that set the initial table state. + + :param prefix: An optional prefix in the path (required) + :type prefix: str + :param namespace: A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. (required) + :type namespace: str + :param create_table_request: (required) + :type create_table_request: CreateTableRequest + :param x_iceberg_access_delegation: Optional signal to the server that the client supports delegated access via a comma-separated list of access mechanisms. The server may choose to supply access via any or none of the requested mechanisms. Specific properties and handling for `vended-credentials` is documented in the `LoadTableResult` schema section of this spec document. The protocol and specification for `remote-signing` is documented in the `s3-signer-open-api.yaml` OpenApi spec in the `aws` module. + :type x_iceberg_access_delegation: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._create_table_serialize( + prefix=prefix, + namespace=namespace, + create_table_request=create_table_request, + x_iceberg_access_delegation=x_iceberg_access_delegation, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "LoadTableResult", + '400': "IcebergErrorResponse", + '401': "IcebergErrorResponse", + '403': "IcebergErrorResponse", + '404': "IcebergErrorResponse", + '409': "IcebergErrorResponse", + '419': "IcebergErrorResponse", + '503': "IcebergErrorResponse", + '5XX': "IcebergErrorResponse", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + def create_table_without_preload_content( + self, + prefix: Annotated[StrictStr, Field(description="An optional prefix in the path")], + namespace: Annotated[StrictStr, Field(description="A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte.")], + create_table_request: CreateTableRequest, + x_iceberg_access_delegation: Annotated[Optional[StrictStr], Field(description="Optional signal to the server that the client supports delegated access via a comma-separated list of access mechanisms. The server may choose to supply access via any or none of the requested mechanisms. Specific properties and handling for `vended-credentials` is documented in the `LoadTableResult` schema section of this spec document. The protocol and specification for `remote-signing` is documented in the `s3-signer-open-api.yaml` OpenApi spec in the `aws` module. ")] = None, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """Create a table in the given namespace + + Create a table or start a create transaction, like atomic CTAS. If `stage-create` is false, the table is created immediately. If `stage-create` is true, the table is not created, but table metadata is initialized and returned. The service should prepare as needed for a commit to the table commit endpoint to complete the create transaction. The client uses the returned metadata to begin a transaction. To commit the transaction, the client sends all create and subsequent changes to the table commit route. Changes from the table create operation include changes like AddSchemaUpdate and SetCurrentSchemaUpdate that set the initial table state. + + :param prefix: An optional prefix in the path (required) + :type prefix: str + :param namespace: A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. (required) + :type namespace: str + :param create_table_request: (required) + :type create_table_request: CreateTableRequest + :param x_iceberg_access_delegation: Optional signal to the server that the client supports delegated access via a comma-separated list of access mechanisms. The server may choose to supply access via any or none of the requested mechanisms. Specific properties and handling for `vended-credentials` is documented in the `LoadTableResult` schema section of this spec document. The protocol and specification for `remote-signing` is documented in the `s3-signer-open-api.yaml` OpenApi spec in the `aws` module. + :type x_iceberg_access_delegation: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._create_table_serialize( + prefix=prefix, + namespace=namespace, + create_table_request=create_table_request, + x_iceberg_access_delegation=x_iceberg_access_delegation, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "LoadTableResult", + '400': "IcebergErrorResponse", + '401': "IcebergErrorResponse", + '403': "IcebergErrorResponse", + '404': "IcebergErrorResponse", + '409': "IcebergErrorResponse", + '419': "IcebergErrorResponse", + '503': "IcebergErrorResponse", + '5XX': "IcebergErrorResponse", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _create_table_serialize( + self, + prefix, + namespace, + create_table_request, + x_iceberg_access_delegation, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[str, Union[str, bytes]] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + if prefix is not None: + _path_params['prefix'] = prefix + if namespace is not None: + _path_params['namespace'] = namespace + # process the query parameters + # process the header parameters + if x_iceberg_access_delegation is not None: + _header_params['X-Iceberg-Access-Delegation'] = x_iceberg_access_delegation + # process the form parameters + # process the body parameter + if create_table_request is not None: + _body_params = create_table_request + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + + # set the HTTP header `Content-Type` + if _content_type: + _header_params['Content-Type'] = _content_type + else: + _default_content_type = ( + self.api_client.select_header_content_type( + [ + 'application/json' + ] + ) + ) + if _default_content_type is not None: + _header_params['Content-Type'] = _default_content_type + + # authentication setting + _auth_settings: List[str] = [ + 'OAuth2', + 'BearerAuth' + ] + + return self.api_client.param_serialize( + method='POST', + resource_path='/v1/{prefix}/namespaces/{namespace}/tables', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + + + + @validate_call + def create_view( + self, + prefix: Annotated[StrictStr, Field(description="An optional prefix in the path")], + namespace: Annotated[StrictStr, Field(description="A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte.")], + create_view_request: CreateViewRequest, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> LoadViewResult: + """Create a view in the given namespace + + Create a view in the given namespace. + + :param prefix: An optional prefix in the path (required) + :type prefix: str + :param namespace: A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. (required) + :type namespace: str + :param create_view_request: (required) + :type create_view_request: CreateViewRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._create_view_serialize( + prefix=prefix, + namespace=namespace, + create_view_request=create_view_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "LoadViewResult", + '400': "IcebergErrorResponse", + '401': "IcebergErrorResponse", + '403': "IcebergErrorResponse", + '404': "ErrorModel", + '409': "ErrorModel", + '419': "IcebergErrorResponse", + '503': "IcebergErrorResponse", + '5XX': "IcebergErrorResponse", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + def create_view_with_http_info( + self, + prefix: Annotated[StrictStr, Field(description="An optional prefix in the path")], + namespace: Annotated[StrictStr, Field(description="A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte.")], + create_view_request: CreateViewRequest, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[LoadViewResult]: + """Create a view in the given namespace + + Create a view in the given namespace. + + :param prefix: An optional prefix in the path (required) + :type prefix: str + :param namespace: A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. (required) + :type namespace: str + :param create_view_request: (required) + :type create_view_request: CreateViewRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._create_view_serialize( + prefix=prefix, + namespace=namespace, + create_view_request=create_view_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "LoadViewResult", + '400': "IcebergErrorResponse", + '401': "IcebergErrorResponse", + '403': "IcebergErrorResponse", + '404': "ErrorModel", + '409': "ErrorModel", + '419': "IcebergErrorResponse", + '503': "IcebergErrorResponse", + '5XX': "IcebergErrorResponse", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + def create_view_without_preload_content( + self, + prefix: Annotated[StrictStr, Field(description="An optional prefix in the path")], + namespace: Annotated[StrictStr, Field(description="A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte.")], + create_view_request: CreateViewRequest, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """Create a view in the given namespace + + Create a view in the given namespace. + + :param prefix: An optional prefix in the path (required) + :type prefix: str + :param namespace: A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. (required) + :type namespace: str + :param create_view_request: (required) + :type create_view_request: CreateViewRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._create_view_serialize( + prefix=prefix, + namespace=namespace, + create_view_request=create_view_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "LoadViewResult", + '400': "IcebergErrorResponse", + '401': "IcebergErrorResponse", + '403': "IcebergErrorResponse", + '404': "ErrorModel", + '409': "ErrorModel", + '419': "IcebergErrorResponse", + '503': "IcebergErrorResponse", + '5XX': "IcebergErrorResponse", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _create_view_serialize( + self, + prefix, + namespace, + create_view_request, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[str, Union[str, bytes]] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + if prefix is not None: + _path_params['prefix'] = prefix + if namespace is not None: + _path_params['namespace'] = namespace + # process the query parameters + # process the header parameters + # process the form parameters + # process the body parameter + if create_view_request is not None: + _body_params = create_view_request + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + + # set the HTTP header `Content-Type` + if _content_type: + _header_params['Content-Type'] = _content_type + else: + _default_content_type = ( + self.api_client.select_header_content_type( + [ + 'application/json' + ] + ) + ) + if _default_content_type is not None: + _header_params['Content-Type'] = _default_content_type + + # authentication setting + _auth_settings: List[str] = [ + 'OAuth2', + 'BearerAuth' + ] + + return self.api_client.param_serialize( + method='POST', + resource_path='/v1/{prefix}/namespaces/{namespace}/views', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + + + + @validate_call + def drop_namespace( + self, + prefix: Annotated[StrictStr, Field(description="An optional prefix in the path")], + namespace: Annotated[StrictStr, Field(description="A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte.")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> None: + """Drop a namespace from the catalog. Namespace must be empty. + + + :param prefix: An optional prefix in the path (required) + :type prefix: str + :param namespace: A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. (required) + :type namespace: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._drop_namespace_serialize( + prefix=prefix, + namespace=namespace, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '204': None, + '400': "IcebergErrorResponse", + '401': "IcebergErrorResponse", + '403': "IcebergErrorResponse", + '404': "IcebergErrorResponse", + '419': "IcebergErrorResponse", + '503': "IcebergErrorResponse", + '5XX': "IcebergErrorResponse", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + def drop_namespace_with_http_info( + self, + prefix: Annotated[StrictStr, Field(description="An optional prefix in the path")], + namespace: Annotated[StrictStr, Field(description="A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte.")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[None]: + """Drop a namespace from the catalog. Namespace must be empty. + + + :param prefix: An optional prefix in the path (required) + :type prefix: str + :param namespace: A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. (required) + :type namespace: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._drop_namespace_serialize( + prefix=prefix, + namespace=namespace, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '204': None, + '400': "IcebergErrorResponse", + '401': "IcebergErrorResponse", + '403': "IcebergErrorResponse", + '404': "IcebergErrorResponse", + '419': "IcebergErrorResponse", + '503': "IcebergErrorResponse", + '5XX': "IcebergErrorResponse", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + def drop_namespace_without_preload_content( + self, + prefix: Annotated[StrictStr, Field(description="An optional prefix in the path")], + namespace: Annotated[StrictStr, Field(description="A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte.")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """Drop a namespace from the catalog. Namespace must be empty. + + + :param prefix: An optional prefix in the path (required) + :type prefix: str + :param namespace: A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. (required) + :type namespace: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._drop_namespace_serialize( + prefix=prefix, + namespace=namespace, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '204': None, + '400': "IcebergErrorResponse", + '401': "IcebergErrorResponse", + '403': "IcebergErrorResponse", + '404': "IcebergErrorResponse", + '419': "IcebergErrorResponse", + '503': "IcebergErrorResponse", + '5XX': "IcebergErrorResponse", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _drop_namespace_serialize( + self, + prefix, + namespace, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[str, Union[str, bytes]] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + if prefix is not None: + _path_params['prefix'] = prefix + if namespace is not None: + _path_params['namespace'] = namespace + # process the query parameters + # process the header parameters + # process the form parameters + # process the body parameter + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + + + # authentication setting + _auth_settings: List[str] = [ + 'OAuth2', + 'BearerAuth' + ] + + return self.api_client.param_serialize( + method='DELETE', + resource_path='/v1/{prefix}/namespaces/{namespace}', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + + + + @validate_call + def drop_table( + self, + prefix: Annotated[StrictStr, Field(description="An optional prefix in the path")], + namespace: Annotated[StrictStr, Field(description="A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte.")], + table: Annotated[StrictStr, Field(description="A table name")], + purge_requested: Annotated[Optional[StrictBool], Field(description="Whether the user requested to purge the underlying table's data and metadata")] = None, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> None: + """Drop a table from the catalog + + Remove a table from the catalog + + :param prefix: An optional prefix in the path (required) + :type prefix: str + :param namespace: A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. (required) + :type namespace: str + :param table: A table name (required) + :type table: str + :param purge_requested: Whether the user requested to purge the underlying table's data and metadata + :type purge_requested: bool + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._drop_table_serialize( + prefix=prefix, + namespace=namespace, + table=table, + purge_requested=purge_requested, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '204': None, + '400': "IcebergErrorResponse", + '401': "IcebergErrorResponse", + '403': "IcebergErrorResponse", + '404': "IcebergErrorResponse", + '419': "IcebergErrorResponse", + '503': "IcebergErrorResponse", + '5XX': "IcebergErrorResponse", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + def drop_table_with_http_info( + self, + prefix: Annotated[StrictStr, Field(description="An optional prefix in the path")], + namespace: Annotated[StrictStr, Field(description="A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte.")], + table: Annotated[StrictStr, Field(description="A table name")], + purge_requested: Annotated[Optional[StrictBool], Field(description="Whether the user requested to purge the underlying table's data and metadata")] = None, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[None]: + """Drop a table from the catalog + + Remove a table from the catalog + + :param prefix: An optional prefix in the path (required) + :type prefix: str + :param namespace: A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. (required) + :type namespace: str + :param table: A table name (required) + :type table: str + :param purge_requested: Whether the user requested to purge the underlying table's data and metadata + :type purge_requested: bool + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._drop_table_serialize( + prefix=prefix, + namespace=namespace, + table=table, + purge_requested=purge_requested, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '204': None, + '400': "IcebergErrorResponse", + '401': "IcebergErrorResponse", + '403': "IcebergErrorResponse", + '404': "IcebergErrorResponse", + '419': "IcebergErrorResponse", + '503': "IcebergErrorResponse", + '5XX': "IcebergErrorResponse", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + def drop_table_without_preload_content( + self, + prefix: Annotated[StrictStr, Field(description="An optional prefix in the path")], + namespace: Annotated[StrictStr, Field(description="A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte.")], + table: Annotated[StrictStr, Field(description="A table name")], + purge_requested: Annotated[Optional[StrictBool], Field(description="Whether the user requested to purge the underlying table's data and metadata")] = None, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """Drop a table from the catalog + + Remove a table from the catalog + + :param prefix: An optional prefix in the path (required) + :type prefix: str + :param namespace: A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. (required) + :type namespace: str + :param table: A table name (required) + :type table: str + :param purge_requested: Whether the user requested to purge the underlying table's data and metadata + :type purge_requested: bool + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._drop_table_serialize( + prefix=prefix, + namespace=namespace, + table=table, + purge_requested=purge_requested, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '204': None, + '400': "IcebergErrorResponse", + '401': "IcebergErrorResponse", + '403': "IcebergErrorResponse", + '404': "IcebergErrorResponse", + '419': "IcebergErrorResponse", + '503': "IcebergErrorResponse", + '5XX': "IcebergErrorResponse", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _drop_table_serialize( + self, + prefix, + namespace, + table, + purge_requested, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[str, Union[str, bytes]] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + if prefix is not None: + _path_params['prefix'] = prefix + if namespace is not None: + _path_params['namespace'] = namespace + if table is not None: + _path_params['table'] = table + # process the query parameters + if purge_requested is not None: + + _query_params.append(('purgeRequested', purge_requested)) + + # process the header parameters + # process the form parameters + # process the body parameter + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + + + # authentication setting + _auth_settings: List[str] = [ + 'OAuth2', + 'BearerAuth' + ] + + return self.api_client.param_serialize( + method='DELETE', + resource_path='/v1/{prefix}/namespaces/{namespace}/tables/{table}', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + + + + @validate_call + def drop_view( + self, + prefix: Annotated[StrictStr, Field(description="An optional prefix in the path")], + namespace: Annotated[StrictStr, Field(description="A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte.")], + view: Annotated[StrictStr, Field(description="A view name")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> None: + """Drop a view from the catalog + + Remove a view from the catalog + + :param prefix: An optional prefix in the path (required) + :type prefix: str + :param namespace: A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. (required) + :type namespace: str + :param view: A view name (required) + :type view: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._drop_view_serialize( + prefix=prefix, + namespace=namespace, + view=view, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '204': None, + '400': "IcebergErrorResponse", + '401': "IcebergErrorResponse", + '403': "IcebergErrorResponse", + '404': "ErrorModel", + '419': "IcebergErrorResponse", + '503': "IcebergErrorResponse", + '5XX': "IcebergErrorResponse", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + def drop_view_with_http_info( + self, + prefix: Annotated[StrictStr, Field(description="An optional prefix in the path")], + namespace: Annotated[StrictStr, Field(description="A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte.")], + view: Annotated[StrictStr, Field(description="A view name")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[None]: + """Drop a view from the catalog + + Remove a view from the catalog + + :param prefix: An optional prefix in the path (required) + :type prefix: str + :param namespace: A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. (required) + :type namespace: str + :param view: A view name (required) + :type view: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._drop_view_serialize( + prefix=prefix, + namespace=namespace, + view=view, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '204': None, + '400': "IcebergErrorResponse", + '401': "IcebergErrorResponse", + '403': "IcebergErrorResponse", + '404': "ErrorModel", + '419': "IcebergErrorResponse", + '503': "IcebergErrorResponse", + '5XX': "IcebergErrorResponse", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + def drop_view_without_preload_content( + self, + prefix: Annotated[StrictStr, Field(description="An optional prefix in the path")], + namespace: Annotated[StrictStr, Field(description="A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte.")], + view: Annotated[StrictStr, Field(description="A view name")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """Drop a view from the catalog + + Remove a view from the catalog + + :param prefix: An optional prefix in the path (required) + :type prefix: str + :param namespace: A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. (required) + :type namespace: str + :param view: A view name (required) + :type view: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._drop_view_serialize( + prefix=prefix, + namespace=namespace, + view=view, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '204': None, + '400': "IcebergErrorResponse", + '401': "IcebergErrorResponse", + '403': "IcebergErrorResponse", + '404': "ErrorModel", + '419': "IcebergErrorResponse", + '503': "IcebergErrorResponse", + '5XX': "IcebergErrorResponse", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _drop_view_serialize( + self, + prefix, + namespace, + view, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[str, Union[str, bytes]] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + if prefix is not None: + _path_params['prefix'] = prefix + if namespace is not None: + _path_params['namespace'] = namespace + if view is not None: + _path_params['view'] = view + # process the query parameters + # process the header parameters + # process the form parameters + # process the body parameter + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + + + # authentication setting + _auth_settings: List[str] = [ + 'OAuth2', + 'BearerAuth' + ] + + return self.api_client.param_serialize( + method='DELETE', + resource_path='/v1/{prefix}/namespaces/{namespace}/views/{view}', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + + + + @validate_call + def list_namespaces( + self, + prefix: Annotated[StrictStr, Field(description="An optional prefix in the path")], + page_token: Optional[StrictStr] = None, + page_size: Annotated[Optional[Annotated[int, Field(strict=True, ge=1)]], Field(description="For servers that support pagination, this signals an upper bound of the number of results that a client will receive. For servers that do not support pagination, clients may receive results larger than the indicated `pageSize`.")] = None, + parent: Annotated[Optional[StrictStr], Field(description="An optional namespace, underneath which to list namespaces. If not provided or empty, all top-level namespaces should be listed. If parent is a multipart namespace, the parts must be separated by the unit separator (`0x1F`) byte.")] = None, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ListNamespacesResponse: + """List namespaces, optionally providing a parent namespace to list underneath + + List all namespaces at a certain level, optionally starting from a given parent namespace. If table accounting.tax.paid.info exists, using 'SELECT NAMESPACE IN accounting' would translate into `GET /namespaces?parent=accounting` and must return a namespace, [\"accounting\", \"tax\"] only. Using 'SELECT NAMESPACE IN accounting.tax' would translate into `GET /namespaces?parent=accounting%1Ftax` and must return a namespace, [\"accounting\", \"tax\", \"paid\"]. If `parent` is not provided, all top-level namespaces should be listed. + + :param prefix: An optional prefix in the path (required) + :type prefix: str + :param page_token: + :type page_token: str + :param page_size: For servers that support pagination, this signals an upper bound of the number of results that a client will receive. For servers that do not support pagination, clients may receive results larger than the indicated `pageSize`. + :type page_size: int + :param parent: An optional namespace, underneath which to list namespaces. If not provided or empty, all top-level namespaces should be listed. If parent is a multipart namespace, the parts must be separated by the unit separator (`0x1F`) byte. + :type parent: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._list_namespaces_serialize( + prefix=prefix, + page_token=page_token, + page_size=page_size, + parent=parent, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "ListNamespacesResponse", + '400': "IcebergErrorResponse", + '401': "IcebergErrorResponse", + '403': "IcebergErrorResponse", + '404': "IcebergErrorResponse", + '419': "IcebergErrorResponse", + '503': "IcebergErrorResponse", + '5XX': "IcebergErrorResponse", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + def list_namespaces_with_http_info( + self, + prefix: Annotated[StrictStr, Field(description="An optional prefix in the path")], + page_token: Optional[StrictStr] = None, + page_size: Annotated[Optional[Annotated[int, Field(strict=True, ge=1)]], Field(description="For servers that support pagination, this signals an upper bound of the number of results that a client will receive. For servers that do not support pagination, clients may receive results larger than the indicated `pageSize`.")] = None, + parent: Annotated[Optional[StrictStr], Field(description="An optional namespace, underneath which to list namespaces. If not provided or empty, all top-level namespaces should be listed. If parent is a multipart namespace, the parts must be separated by the unit separator (`0x1F`) byte.")] = None, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[ListNamespacesResponse]: + """List namespaces, optionally providing a parent namespace to list underneath + + List all namespaces at a certain level, optionally starting from a given parent namespace. If table accounting.tax.paid.info exists, using 'SELECT NAMESPACE IN accounting' would translate into `GET /namespaces?parent=accounting` and must return a namespace, [\"accounting\", \"tax\"] only. Using 'SELECT NAMESPACE IN accounting.tax' would translate into `GET /namespaces?parent=accounting%1Ftax` and must return a namespace, [\"accounting\", \"tax\", \"paid\"]. If `parent` is not provided, all top-level namespaces should be listed. + + :param prefix: An optional prefix in the path (required) + :type prefix: str + :param page_token: + :type page_token: str + :param page_size: For servers that support pagination, this signals an upper bound of the number of results that a client will receive. For servers that do not support pagination, clients may receive results larger than the indicated `pageSize`. + :type page_size: int + :param parent: An optional namespace, underneath which to list namespaces. If not provided or empty, all top-level namespaces should be listed. If parent is a multipart namespace, the parts must be separated by the unit separator (`0x1F`) byte. + :type parent: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._list_namespaces_serialize( + prefix=prefix, + page_token=page_token, + page_size=page_size, + parent=parent, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "ListNamespacesResponse", + '400': "IcebergErrorResponse", + '401': "IcebergErrorResponse", + '403': "IcebergErrorResponse", + '404': "IcebergErrorResponse", + '419': "IcebergErrorResponse", + '503': "IcebergErrorResponse", + '5XX': "IcebergErrorResponse", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + def list_namespaces_without_preload_content( + self, + prefix: Annotated[StrictStr, Field(description="An optional prefix in the path")], + page_token: Optional[StrictStr] = None, + page_size: Annotated[Optional[Annotated[int, Field(strict=True, ge=1)]], Field(description="For servers that support pagination, this signals an upper bound of the number of results that a client will receive. For servers that do not support pagination, clients may receive results larger than the indicated `pageSize`.")] = None, + parent: Annotated[Optional[StrictStr], Field(description="An optional namespace, underneath which to list namespaces. If not provided or empty, all top-level namespaces should be listed. If parent is a multipart namespace, the parts must be separated by the unit separator (`0x1F`) byte.")] = None, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """List namespaces, optionally providing a parent namespace to list underneath + + List all namespaces at a certain level, optionally starting from a given parent namespace. If table accounting.tax.paid.info exists, using 'SELECT NAMESPACE IN accounting' would translate into `GET /namespaces?parent=accounting` and must return a namespace, [\"accounting\", \"tax\"] only. Using 'SELECT NAMESPACE IN accounting.tax' would translate into `GET /namespaces?parent=accounting%1Ftax` and must return a namespace, [\"accounting\", \"tax\", \"paid\"]. If `parent` is not provided, all top-level namespaces should be listed. + + :param prefix: An optional prefix in the path (required) + :type prefix: str + :param page_token: + :type page_token: str + :param page_size: For servers that support pagination, this signals an upper bound of the number of results that a client will receive. For servers that do not support pagination, clients may receive results larger than the indicated `pageSize`. + :type page_size: int + :param parent: An optional namespace, underneath which to list namespaces. If not provided or empty, all top-level namespaces should be listed. If parent is a multipart namespace, the parts must be separated by the unit separator (`0x1F`) byte. + :type parent: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._list_namespaces_serialize( + prefix=prefix, + page_token=page_token, + page_size=page_size, + parent=parent, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "ListNamespacesResponse", + '400': "IcebergErrorResponse", + '401': "IcebergErrorResponse", + '403': "IcebergErrorResponse", + '404': "IcebergErrorResponse", + '419': "IcebergErrorResponse", + '503': "IcebergErrorResponse", + '5XX': "IcebergErrorResponse", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _list_namespaces_serialize( + self, + prefix, + page_token, + page_size, + parent, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[str, Union[str, bytes]] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + if prefix is not None: + _path_params['prefix'] = prefix + # process the query parameters + if page_token is not None: + + _query_params.append(('pageToken', page_token)) + + if page_size is not None: + + _query_params.append(('pageSize', page_size)) + + if parent is not None: + + _query_params.append(('parent', parent)) + + # process the header parameters + # process the form parameters + # process the body parameter + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + + + # authentication setting + _auth_settings: List[str] = [ + 'OAuth2', + 'BearerAuth' + ] + + return self.api_client.param_serialize( + method='GET', + resource_path='/v1/{prefix}/namespaces', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + + + + @validate_call + def list_tables( + self, + prefix: Annotated[StrictStr, Field(description="An optional prefix in the path")], + namespace: Annotated[StrictStr, Field(description="A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte.")], + page_token: Optional[StrictStr] = None, + page_size: Annotated[Optional[Annotated[int, Field(strict=True, ge=1)]], Field(description="For servers that support pagination, this signals an upper bound of the number of results that a client will receive. For servers that do not support pagination, clients may receive results larger than the indicated `pageSize`.")] = None, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ListTablesResponse: + """List all table identifiers underneath a given namespace + + Return all table identifiers under this namespace + + :param prefix: An optional prefix in the path (required) + :type prefix: str + :param namespace: A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. (required) + :type namespace: str + :param page_token: + :type page_token: str + :param page_size: For servers that support pagination, this signals an upper bound of the number of results that a client will receive. For servers that do not support pagination, clients may receive results larger than the indicated `pageSize`. + :type page_size: int + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._list_tables_serialize( + prefix=prefix, + namespace=namespace, + page_token=page_token, + page_size=page_size, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "ListTablesResponse", + '400': "IcebergErrorResponse", + '401': "IcebergErrorResponse", + '403': "IcebergErrorResponse", + '404': "IcebergErrorResponse", + '419': "IcebergErrorResponse", + '503': "IcebergErrorResponse", + '5XX': "IcebergErrorResponse", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + def list_tables_with_http_info( + self, + prefix: Annotated[StrictStr, Field(description="An optional prefix in the path")], + namespace: Annotated[StrictStr, Field(description="A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte.")], + page_token: Optional[StrictStr] = None, + page_size: Annotated[Optional[Annotated[int, Field(strict=True, ge=1)]], Field(description="For servers that support pagination, this signals an upper bound of the number of results that a client will receive. For servers that do not support pagination, clients may receive results larger than the indicated `pageSize`.")] = None, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[ListTablesResponse]: + """List all table identifiers underneath a given namespace + + Return all table identifiers under this namespace + + :param prefix: An optional prefix in the path (required) + :type prefix: str + :param namespace: A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. (required) + :type namespace: str + :param page_token: + :type page_token: str + :param page_size: For servers that support pagination, this signals an upper bound of the number of results that a client will receive. For servers that do not support pagination, clients may receive results larger than the indicated `pageSize`. + :type page_size: int + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._list_tables_serialize( + prefix=prefix, + namespace=namespace, + page_token=page_token, + page_size=page_size, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "ListTablesResponse", + '400': "IcebergErrorResponse", + '401': "IcebergErrorResponse", + '403': "IcebergErrorResponse", + '404': "IcebergErrorResponse", + '419': "IcebergErrorResponse", + '503': "IcebergErrorResponse", + '5XX': "IcebergErrorResponse", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + def list_tables_without_preload_content( + self, + prefix: Annotated[StrictStr, Field(description="An optional prefix in the path")], + namespace: Annotated[StrictStr, Field(description="A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte.")], + page_token: Optional[StrictStr] = None, + page_size: Annotated[Optional[Annotated[int, Field(strict=True, ge=1)]], Field(description="For servers that support pagination, this signals an upper bound of the number of results that a client will receive. For servers that do not support pagination, clients may receive results larger than the indicated `pageSize`.")] = None, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """List all table identifiers underneath a given namespace + + Return all table identifiers under this namespace + + :param prefix: An optional prefix in the path (required) + :type prefix: str + :param namespace: A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. (required) + :type namespace: str + :param page_token: + :type page_token: str + :param page_size: For servers that support pagination, this signals an upper bound of the number of results that a client will receive. For servers that do not support pagination, clients may receive results larger than the indicated `pageSize`. + :type page_size: int + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._list_tables_serialize( + prefix=prefix, + namespace=namespace, + page_token=page_token, + page_size=page_size, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "ListTablesResponse", + '400': "IcebergErrorResponse", + '401': "IcebergErrorResponse", + '403': "IcebergErrorResponse", + '404': "IcebergErrorResponse", + '419': "IcebergErrorResponse", + '503': "IcebergErrorResponse", + '5XX': "IcebergErrorResponse", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _list_tables_serialize( + self, + prefix, + namespace, + page_token, + page_size, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[str, Union[str, bytes]] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + if prefix is not None: + _path_params['prefix'] = prefix + if namespace is not None: + _path_params['namespace'] = namespace + # process the query parameters + if page_token is not None: + + _query_params.append(('pageToken', page_token)) + + if page_size is not None: + + _query_params.append(('pageSize', page_size)) + + # process the header parameters + # process the form parameters + # process the body parameter + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + + + # authentication setting + _auth_settings: List[str] = [ + 'OAuth2', + 'BearerAuth' + ] + + return self.api_client.param_serialize( + method='GET', + resource_path='/v1/{prefix}/namespaces/{namespace}/tables', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + + + + @validate_call + def list_views( + self, + prefix: Annotated[StrictStr, Field(description="An optional prefix in the path")], + namespace: Annotated[StrictStr, Field(description="A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte.")], + page_token: Optional[StrictStr] = None, + page_size: Annotated[Optional[Annotated[int, Field(strict=True, ge=1)]], Field(description="For servers that support pagination, this signals an upper bound of the number of results that a client will receive. For servers that do not support pagination, clients may receive results larger than the indicated `pageSize`.")] = None, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ListTablesResponse: + """List all view identifiers underneath a given namespace + + Return all view identifiers under this namespace + + :param prefix: An optional prefix in the path (required) + :type prefix: str + :param namespace: A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. (required) + :type namespace: str + :param page_token: + :type page_token: str + :param page_size: For servers that support pagination, this signals an upper bound of the number of results that a client will receive. For servers that do not support pagination, clients may receive results larger than the indicated `pageSize`. + :type page_size: int + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._list_views_serialize( + prefix=prefix, + namespace=namespace, + page_token=page_token, + page_size=page_size, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "ListTablesResponse", + '400': "IcebergErrorResponse", + '401': "IcebergErrorResponse", + '403': "IcebergErrorResponse", + '404': "ErrorModel", + '419': "IcebergErrorResponse", + '503': "IcebergErrorResponse", + '5XX': "IcebergErrorResponse", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + def list_views_with_http_info( + self, + prefix: Annotated[StrictStr, Field(description="An optional prefix in the path")], + namespace: Annotated[StrictStr, Field(description="A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte.")], + page_token: Optional[StrictStr] = None, + page_size: Annotated[Optional[Annotated[int, Field(strict=True, ge=1)]], Field(description="For servers that support pagination, this signals an upper bound of the number of results that a client will receive. For servers that do not support pagination, clients may receive results larger than the indicated `pageSize`.")] = None, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[ListTablesResponse]: + """List all view identifiers underneath a given namespace + + Return all view identifiers under this namespace + + :param prefix: An optional prefix in the path (required) + :type prefix: str + :param namespace: A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. (required) + :type namespace: str + :param page_token: + :type page_token: str + :param page_size: For servers that support pagination, this signals an upper bound of the number of results that a client will receive. For servers that do not support pagination, clients may receive results larger than the indicated `pageSize`. + :type page_size: int + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._list_views_serialize( + prefix=prefix, + namespace=namespace, + page_token=page_token, + page_size=page_size, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "ListTablesResponse", + '400': "IcebergErrorResponse", + '401': "IcebergErrorResponse", + '403': "IcebergErrorResponse", + '404': "ErrorModel", + '419': "IcebergErrorResponse", + '503': "IcebergErrorResponse", + '5XX': "IcebergErrorResponse", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + def list_views_without_preload_content( + self, + prefix: Annotated[StrictStr, Field(description="An optional prefix in the path")], + namespace: Annotated[StrictStr, Field(description="A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte.")], + page_token: Optional[StrictStr] = None, + page_size: Annotated[Optional[Annotated[int, Field(strict=True, ge=1)]], Field(description="For servers that support pagination, this signals an upper bound of the number of results that a client will receive. For servers that do not support pagination, clients may receive results larger than the indicated `pageSize`.")] = None, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """List all view identifiers underneath a given namespace + + Return all view identifiers under this namespace + + :param prefix: An optional prefix in the path (required) + :type prefix: str + :param namespace: A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. (required) + :type namespace: str + :param page_token: + :type page_token: str + :param page_size: For servers that support pagination, this signals an upper bound of the number of results that a client will receive. For servers that do not support pagination, clients may receive results larger than the indicated `pageSize`. + :type page_size: int + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._list_views_serialize( + prefix=prefix, + namespace=namespace, + page_token=page_token, + page_size=page_size, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "ListTablesResponse", + '400': "IcebergErrorResponse", + '401': "IcebergErrorResponse", + '403': "IcebergErrorResponse", + '404': "ErrorModel", + '419': "IcebergErrorResponse", + '503': "IcebergErrorResponse", + '5XX': "IcebergErrorResponse", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _list_views_serialize( + self, + prefix, + namespace, + page_token, + page_size, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[str, Union[str, bytes]] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + if prefix is not None: + _path_params['prefix'] = prefix + if namespace is not None: + _path_params['namespace'] = namespace + # process the query parameters + if page_token is not None: + + _query_params.append(('pageToken', page_token)) + + if page_size is not None: + + _query_params.append(('pageSize', page_size)) + + # process the header parameters + # process the form parameters + # process the body parameter + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + + + # authentication setting + _auth_settings: List[str] = [ + 'OAuth2', + 'BearerAuth' + ] + + return self.api_client.param_serialize( + method='GET', + resource_path='/v1/{prefix}/namespaces/{namespace}/views', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + + + + @validate_call + def load_namespace_metadata( + self, + prefix: Annotated[StrictStr, Field(description="An optional prefix in the path")], + namespace: Annotated[StrictStr, Field(description="A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte.")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> GetNamespaceResponse: + """Load the metadata properties for a namespace + + Return all stored metadata properties for a given namespace + + :param prefix: An optional prefix in the path (required) + :type prefix: str + :param namespace: A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. (required) + :type namespace: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._load_namespace_metadata_serialize( + prefix=prefix, + namespace=namespace, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "GetNamespaceResponse", + '400': "IcebergErrorResponse", + '401': "IcebergErrorResponse", + '403': "IcebergErrorResponse", + '404': "IcebergErrorResponse", + '419': "IcebergErrorResponse", + '503': "IcebergErrorResponse", + '5XX': "IcebergErrorResponse", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + def load_namespace_metadata_with_http_info( + self, + prefix: Annotated[StrictStr, Field(description="An optional prefix in the path")], + namespace: Annotated[StrictStr, Field(description="A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte.")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[GetNamespaceResponse]: + """Load the metadata properties for a namespace + + Return all stored metadata properties for a given namespace + + :param prefix: An optional prefix in the path (required) + :type prefix: str + :param namespace: A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. (required) + :type namespace: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._load_namespace_metadata_serialize( + prefix=prefix, + namespace=namespace, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "GetNamespaceResponse", + '400': "IcebergErrorResponse", + '401': "IcebergErrorResponse", + '403': "IcebergErrorResponse", + '404': "IcebergErrorResponse", + '419': "IcebergErrorResponse", + '503': "IcebergErrorResponse", + '5XX': "IcebergErrorResponse", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + def load_namespace_metadata_without_preload_content( + self, + prefix: Annotated[StrictStr, Field(description="An optional prefix in the path")], + namespace: Annotated[StrictStr, Field(description="A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte.")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """Load the metadata properties for a namespace + + Return all stored metadata properties for a given namespace + + :param prefix: An optional prefix in the path (required) + :type prefix: str + :param namespace: A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. (required) + :type namespace: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._load_namespace_metadata_serialize( + prefix=prefix, + namespace=namespace, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "GetNamespaceResponse", + '400': "IcebergErrorResponse", + '401': "IcebergErrorResponse", + '403': "IcebergErrorResponse", + '404': "IcebergErrorResponse", + '419': "IcebergErrorResponse", + '503': "IcebergErrorResponse", + '5XX': "IcebergErrorResponse", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _load_namespace_metadata_serialize( + self, + prefix, + namespace, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[str, Union[str, bytes]] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + if prefix is not None: + _path_params['prefix'] = prefix + if namespace is not None: + _path_params['namespace'] = namespace + # process the query parameters + # process the header parameters + # process the form parameters + # process the body parameter + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + + + # authentication setting + _auth_settings: List[str] = [ + 'OAuth2', + 'BearerAuth' + ] + + return self.api_client.param_serialize( + method='GET', + resource_path='/v1/{prefix}/namespaces/{namespace}', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + + + + @validate_call + def load_table( + self, + prefix: Annotated[StrictStr, Field(description="An optional prefix in the path")], + namespace: Annotated[StrictStr, Field(description="A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte.")], + table: Annotated[StrictStr, Field(description="A table name")], + x_iceberg_access_delegation: Annotated[Optional[StrictStr], Field(description="Optional signal to the server that the client supports delegated access via a comma-separated list of access mechanisms. The server may choose to supply access via any or none of the requested mechanisms. Specific properties and handling for `vended-credentials` is documented in the `LoadTableResult` schema section of this spec document. The protocol and specification for `remote-signing` is documented in the `s3-signer-open-api.yaml` OpenApi spec in the `aws` module. ")] = None, + snapshots: Annotated[Optional[StrictStr], Field(description="The snapshots to return in the body of the metadata. Setting the value to `all` would return the full set of snapshots currently valid for the table. Setting the value to `refs` would load all snapshots referenced by branches or tags. Default if no param is provided is `all`.")] = None, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> LoadTableResult: + """Load a table from the catalog + + Load a table from the catalog. The response contains both configuration and table metadata. The configuration, if non-empty is used as additional configuration for the table that overrides catalog configuration. For example, this configuration may change the FileIO implementation to be used for the table. The response also contains the table's full metadata, matching the table metadata JSON file. The catalog configuration may contain credentials that should be used for subsequent requests for the table. The configuration key \"token\" is used to pass an access token to be used as a bearer token for table requests. Otherwise, a token may be passed using a RFC 8693 token type as a configuration key. For example, \"urn:ietf:params:oauth:token-type:jwt=\". + + :param prefix: An optional prefix in the path (required) + :type prefix: str + :param namespace: A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. (required) + :type namespace: str + :param table: A table name (required) + :type table: str + :param x_iceberg_access_delegation: Optional signal to the server that the client supports delegated access via a comma-separated list of access mechanisms. The server may choose to supply access via any or none of the requested mechanisms. Specific properties and handling for `vended-credentials` is documented in the `LoadTableResult` schema section of this spec document. The protocol and specification for `remote-signing` is documented in the `s3-signer-open-api.yaml` OpenApi spec in the `aws` module. + :type x_iceberg_access_delegation: str + :param snapshots: The snapshots to return in the body of the metadata. Setting the value to `all` would return the full set of snapshots currently valid for the table. Setting the value to `refs` would load all snapshots referenced by branches or tags. Default if no param is provided is `all`. + :type snapshots: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._load_table_serialize( + prefix=prefix, + namespace=namespace, + table=table, + x_iceberg_access_delegation=x_iceberg_access_delegation, + snapshots=snapshots, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "LoadTableResult", + '400': "IcebergErrorResponse", + '401': "IcebergErrorResponse", + '403': "IcebergErrorResponse", + '404': "IcebergErrorResponse", + '419': "IcebergErrorResponse", + '503': "IcebergErrorResponse", + '5XX': "IcebergErrorResponse", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + def load_table_with_http_info( + self, + prefix: Annotated[StrictStr, Field(description="An optional prefix in the path")], + namespace: Annotated[StrictStr, Field(description="A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte.")], + table: Annotated[StrictStr, Field(description="A table name")], + x_iceberg_access_delegation: Annotated[Optional[StrictStr], Field(description="Optional signal to the server that the client supports delegated access via a comma-separated list of access mechanisms. The server may choose to supply access via any or none of the requested mechanisms. Specific properties and handling for `vended-credentials` is documented in the `LoadTableResult` schema section of this spec document. The protocol and specification for `remote-signing` is documented in the `s3-signer-open-api.yaml` OpenApi spec in the `aws` module. ")] = None, + snapshots: Annotated[Optional[StrictStr], Field(description="The snapshots to return in the body of the metadata. Setting the value to `all` would return the full set of snapshots currently valid for the table. Setting the value to `refs` would load all snapshots referenced by branches or tags. Default if no param is provided is `all`.")] = None, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[LoadTableResult]: + """Load a table from the catalog + + Load a table from the catalog. The response contains both configuration and table metadata. The configuration, if non-empty is used as additional configuration for the table that overrides catalog configuration. For example, this configuration may change the FileIO implementation to be used for the table. The response also contains the table's full metadata, matching the table metadata JSON file. The catalog configuration may contain credentials that should be used for subsequent requests for the table. The configuration key \"token\" is used to pass an access token to be used as a bearer token for table requests. Otherwise, a token may be passed using a RFC 8693 token type as a configuration key. For example, \"urn:ietf:params:oauth:token-type:jwt=\". + + :param prefix: An optional prefix in the path (required) + :type prefix: str + :param namespace: A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. (required) + :type namespace: str + :param table: A table name (required) + :type table: str + :param x_iceberg_access_delegation: Optional signal to the server that the client supports delegated access via a comma-separated list of access mechanisms. The server may choose to supply access via any or none of the requested mechanisms. Specific properties and handling for `vended-credentials` is documented in the `LoadTableResult` schema section of this spec document. The protocol and specification for `remote-signing` is documented in the `s3-signer-open-api.yaml` OpenApi spec in the `aws` module. + :type x_iceberg_access_delegation: str + :param snapshots: The snapshots to return in the body of the metadata. Setting the value to `all` would return the full set of snapshots currently valid for the table. Setting the value to `refs` would load all snapshots referenced by branches or tags. Default if no param is provided is `all`. + :type snapshots: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._load_table_serialize( + prefix=prefix, + namespace=namespace, + table=table, + x_iceberg_access_delegation=x_iceberg_access_delegation, + snapshots=snapshots, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "LoadTableResult", + '400': "IcebergErrorResponse", + '401': "IcebergErrorResponse", + '403': "IcebergErrorResponse", + '404': "IcebergErrorResponse", + '419': "IcebergErrorResponse", + '503': "IcebergErrorResponse", + '5XX': "IcebergErrorResponse", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + def load_table_without_preload_content( + self, + prefix: Annotated[StrictStr, Field(description="An optional prefix in the path")], + namespace: Annotated[StrictStr, Field(description="A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte.")], + table: Annotated[StrictStr, Field(description="A table name")], + x_iceberg_access_delegation: Annotated[Optional[StrictStr], Field(description="Optional signal to the server that the client supports delegated access via a comma-separated list of access mechanisms. The server may choose to supply access via any or none of the requested mechanisms. Specific properties and handling for `vended-credentials` is documented in the `LoadTableResult` schema section of this spec document. The protocol and specification for `remote-signing` is documented in the `s3-signer-open-api.yaml` OpenApi spec in the `aws` module. ")] = None, + snapshots: Annotated[Optional[StrictStr], Field(description="The snapshots to return in the body of the metadata. Setting the value to `all` would return the full set of snapshots currently valid for the table. Setting the value to `refs` would load all snapshots referenced by branches or tags. Default if no param is provided is `all`.")] = None, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """Load a table from the catalog + + Load a table from the catalog. The response contains both configuration and table metadata. The configuration, if non-empty is used as additional configuration for the table that overrides catalog configuration. For example, this configuration may change the FileIO implementation to be used for the table. The response also contains the table's full metadata, matching the table metadata JSON file. The catalog configuration may contain credentials that should be used for subsequent requests for the table. The configuration key \"token\" is used to pass an access token to be used as a bearer token for table requests. Otherwise, a token may be passed using a RFC 8693 token type as a configuration key. For example, \"urn:ietf:params:oauth:token-type:jwt=\". + + :param prefix: An optional prefix in the path (required) + :type prefix: str + :param namespace: A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. (required) + :type namespace: str + :param table: A table name (required) + :type table: str + :param x_iceberg_access_delegation: Optional signal to the server that the client supports delegated access via a comma-separated list of access mechanisms. The server may choose to supply access via any or none of the requested mechanisms. Specific properties and handling for `vended-credentials` is documented in the `LoadTableResult` schema section of this spec document. The protocol and specification for `remote-signing` is documented in the `s3-signer-open-api.yaml` OpenApi spec in the `aws` module. + :type x_iceberg_access_delegation: str + :param snapshots: The snapshots to return in the body of the metadata. Setting the value to `all` would return the full set of snapshots currently valid for the table. Setting the value to `refs` would load all snapshots referenced by branches or tags. Default if no param is provided is `all`. + :type snapshots: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._load_table_serialize( + prefix=prefix, + namespace=namespace, + table=table, + x_iceberg_access_delegation=x_iceberg_access_delegation, + snapshots=snapshots, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "LoadTableResult", + '400': "IcebergErrorResponse", + '401': "IcebergErrorResponse", + '403': "IcebergErrorResponse", + '404': "IcebergErrorResponse", + '419': "IcebergErrorResponse", + '503': "IcebergErrorResponse", + '5XX': "IcebergErrorResponse", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _load_table_serialize( + self, + prefix, + namespace, + table, + x_iceberg_access_delegation, + snapshots, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[str, Union[str, bytes]] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + if prefix is not None: + _path_params['prefix'] = prefix + if namespace is not None: + _path_params['namespace'] = namespace + if table is not None: + _path_params['table'] = table + # process the query parameters + if snapshots is not None: + + _query_params.append(('snapshots', snapshots)) + + # process the header parameters + if x_iceberg_access_delegation is not None: + _header_params['X-Iceberg-Access-Delegation'] = x_iceberg_access_delegation + # process the form parameters + # process the body parameter + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + + + # authentication setting + _auth_settings: List[str] = [ + 'OAuth2', + 'BearerAuth' + ] + + return self.api_client.param_serialize( + method='GET', + resource_path='/v1/{prefix}/namespaces/{namespace}/tables/{table}', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + + + + @validate_call + def load_view( + self, + prefix: Annotated[StrictStr, Field(description="An optional prefix in the path")], + namespace: Annotated[StrictStr, Field(description="A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte.")], + view: Annotated[StrictStr, Field(description="A view name")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> LoadViewResult: + """Load a view from the catalog + + Load a view from the catalog. The response contains both configuration and view metadata. The configuration, if non-empty is used as additional configuration for the view that overrides catalog configuration. The response also contains the view's full metadata, matching the view metadata JSON file. The catalog configuration may contain credentials that should be used for subsequent requests for the view. The configuration key \"token\" is used to pass an access token to be used as a bearer token for view requests. Otherwise, a token may be passed using a RFC 8693 token type as a configuration key. For example, \"urn:ietf:params:oauth:token-type:jwt=\". + + :param prefix: An optional prefix in the path (required) + :type prefix: str + :param namespace: A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. (required) + :type namespace: str + :param view: A view name (required) + :type view: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._load_view_serialize( + prefix=prefix, + namespace=namespace, + view=view, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "LoadViewResult", + '400': "IcebergErrorResponse", + '401': "IcebergErrorResponse", + '403': "IcebergErrorResponse", + '404': "ErrorModel", + '419': "IcebergErrorResponse", + '503': "IcebergErrorResponse", + '5XX': "IcebergErrorResponse", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + def load_view_with_http_info( + self, + prefix: Annotated[StrictStr, Field(description="An optional prefix in the path")], + namespace: Annotated[StrictStr, Field(description="A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte.")], + view: Annotated[StrictStr, Field(description="A view name")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[LoadViewResult]: + """Load a view from the catalog + + Load a view from the catalog. The response contains both configuration and view metadata. The configuration, if non-empty is used as additional configuration for the view that overrides catalog configuration. The response also contains the view's full metadata, matching the view metadata JSON file. The catalog configuration may contain credentials that should be used for subsequent requests for the view. The configuration key \"token\" is used to pass an access token to be used as a bearer token for view requests. Otherwise, a token may be passed using a RFC 8693 token type as a configuration key. For example, \"urn:ietf:params:oauth:token-type:jwt=\". + + :param prefix: An optional prefix in the path (required) + :type prefix: str + :param namespace: A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. (required) + :type namespace: str + :param view: A view name (required) + :type view: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._load_view_serialize( + prefix=prefix, + namespace=namespace, + view=view, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "LoadViewResult", + '400': "IcebergErrorResponse", + '401': "IcebergErrorResponse", + '403': "IcebergErrorResponse", + '404': "ErrorModel", + '419': "IcebergErrorResponse", + '503': "IcebergErrorResponse", + '5XX': "IcebergErrorResponse", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + def load_view_without_preload_content( + self, + prefix: Annotated[StrictStr, Field(description="An optional prefix in the path")], + namespace: Annotated[StrictStr, Field(description="A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte.")], + view: Annotated[StrictStr, Field(description="A view name")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """Load a view from the catalog + + Load a view from the catalog. The response contains both configuration and view metadata. The configuration, if non-empty is used as additional configuration for the view that overrides catalog configuration. The response also contains the view's full metadata, matching the view metadata JSON file. The catalog configuration may contain credentials that should be used for subsequent requests for the view. The configuration key \"token\" is used to pass an access token to be used as a bearer token for view requests. Otherwise, a token may be passed using a RFC 8693 token type as a configuration key. For example, \"urn:ietf:params:oauth:token-type:jwt=\". + + :param prefix: An optional prefix in the path (required) + :type prefix: str + :param namespace: A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. (required) + :type namespace: str + :param view: A view name (required) + :type view: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._load_view_serialize( + prefix=prefix, + namespace=namespace, + view=view, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "LoadViewResult", + '400': "IcebergErrorResponse", + '401': "IcebergErrorResponse", + '403': "IcebergErrorResponse", + '404': "ErrorModel", + '419': "IcebergErrorResponse", + '503': "IcebergErrorResponse", + '5XX': "IcebergErrorResponse", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _load_view_serialize( + self, + prefix, + namespace, + view, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[str, Union[str, bytes]] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + if prefix is not None: + _path_params['prefix'] = prefix + if namespace is not None: + _path_params['namespace'] = namespace + if view is not None: + _path_params['view'] = view + # process the query parameters + # process the header parameters + # process the form parameters + # process the body parameter + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + + + # authentication setting + _auth_settings: List[str] = [ + 'OAuth2', + 'BearerAuth' + ] + + return self.api_client.param_serialize( + method='GET', + resource_path='/v1/{prefix}/namespaces/{namespace}/views/{view}', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + + + + @validate_call + def namespace_exists( + self, + prefix: Annotated[StrictStr, Field(description="An optional prefix in the path")], + namespace: Annotated[StrictStr, Field(description="A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte.")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> None: + """Check if a namespace exists + + Check if a namespace exists. The response does not contain a body. + + :param prefix: An optional prefix in the path (required) + :type prefix: str + :param namespace: A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. (required) + :type namespace: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._namespace_exists_serialize( + prefix=prefix, + namespace=namespace, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '204': None, + '400': "IcebergErrorResponse", + '401': "IcebergErrorResponse", + '403': "IcebergErrorResponse", + '404': "IcebergErrorResponse", + '419': "IcebergErrorResponse", + '503': "IcebergErrorResponse", + '5XX': "IcebergErrorResponse", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + def namespace_exists_with_http_info( + self, + prefix: Annotated[StrictStr, Field(description="An optional prefix in the path")], + namespace: Annotated[StrictStr, Field(description="A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte.")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[None]: + """Check if a namespace exists + + Check if a namespace exists. The response does not contain a body. + + :param prefix: An optional prefix in the path (required) + :type prefix: str + :param namespace: A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. (required) + :type namespace: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._namespace_exists_serialize( + prefix=prefix, + namespace=namespace, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '204': None, + '400': "IcebergErrorResponse", + '401': "IcebergErrorResponse", + '403': "IcebergErrorResponse", + '404': "IcebergErrorResponse", + '419': "IcebergErrorResponse", + '503': "IcebergErrorResponse", + '5XX': "IcebergErrorResponse", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + def namespace_exists_without_preload_content( + self, + prefix: Annotated[StrictStr, Field(description="An optional prefix in the path")], + namespace: Annotated[StrictStr, Field(description="A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte.")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """Check if a namespace exists + + Check if a namespace exists. The response does not contain a body. + + :param prefix: An optional prefix in the path (required) + :type prefix: str + :param namespace: A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. (required) + :type namespace: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._namespace_exists_serialize( + prefix=prefix, + namespace=namespace, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '204': None, + '400': "IcebergErrorResponse", + '401': "IcebergErrorResponse", + '403': "IcebergErrorResponse", + '404': "IcebergErrorResponse", + '419': "IcebergErrorResponse", + '503': "IcebergErrorResponse", + '5XX': "IcebergErrorResponse", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _namespace_exists_serialize( + self, + prefix, + namespace, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[str, Union[str, bytes]] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + if prefix is not None: + _path_params['prefix'] = prefix + if namespace is not None: + _path_params['namespace'] = namespace + # process the query parameters + # process the header parameters + # process the form parameters + # process the body parameter + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + + + # authentication setting + _auth_settings: List[str] = [ + 'OAuth2', + 'BearerAuth' + ] + + return self.api_client.param_serialize( + method='HEAD', + resource_path='/v1/{prefix}/namespaces/{namespace}', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + + + + @validate_call + def register_table( + self, + prefix: Annotated[StrictStr, Field(description="An optional prefix in the path")], + namespace: Annotated[StrictStr, Field(description="A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte.")], + register_table_request: RegisterTableRequest, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> LoadTableResult: + """Register a table in the given namespace using given metadata file location + + Register a table using given metadata file location. + + :param prefix: An optional prefix in the path (required) + :type prefix: str + :param namespace: A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. (required) + :type namespace: str + :param register_table_request: (required) + :type register_table_request: RegisterTableRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._register_table_serialize( + prefix=prefix, + namespace=namespace, + register_table_request=register_table_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "LoadTableResult", + '400': "IcebergErrorResponse", + '401': "IcebergErrorResponse", + '403': "IcebergErrorResponse", + '404': "IcebergErrorResponse", + '409': "IcebergErrorResponse", + '419': "IcebergErrorResponse", + '503': "IcebergErrorResponse", + '5XX': "IcebergErrorResponse", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + def register_table_with_http_info( + self, + prefix: Annotated[StrictStr, Field(description="An optional prefix in the path")], + namespace: Annotated[StrictStr, Field(description="A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte.")], + register_table_request: RegisterTableRequest, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[LoadTableResult]: + """Register a table in the given namespace using given metadata file location + + Register a table using given metadata file location. + + :param prefix: An optional prefix in the path (required) + :type prefix: str + :param namespace: A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. (required) + :type namespace: str + :param register_table_request: (required) + :type register_table_request: RegisterTableRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._register_table_serialize( + prefix=prefix, + namespace=namespace, + register_table_request=register_table_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "LoadTableResult", + '400': "IcebergErrorResponse", + '401': "IcebergErrorResponse", + '403': "IcebergErrorResponse", + '404': "IcebergErrorResponse", + '409': "IcebergErrorResponse", + '419': "IcebergErrorResponse", + '503': "IcebergErrorResponse", + '5XX': "IcebergErrorResponse", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + def register_table_without_preload_content( + self, + prefix: Annotated[StrictStr, Field(description="An optional prefix in the path")], + namespace: Annotated[StrictStr, Field(description="A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte.")], + register_table_request: RegisterTableRequest, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """Register a table in the given namespace using given metadata file location + + Register a table using given metadata file location. + + :param prefix: An optional prefix in the path (required) + :type prefix: str + :param namespace: A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. (required) + :type namespace: str + :param register_table_request: (required) + :type register_table_request: RegisterTableRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._register_table_serialize( + prefix=prefix, + namespace=namespace, + register_table_request=register_table_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "LoadTableResult", + '400': "IcebergErrorResponse", + '401': "IcebergErrorResponse", + '403': "IcebergErrorResponse", + '404': "IcebergErrorResponse", + '409': "IcebergErrorResponse", + '419': "IcebergErrorResponse", + '503': "IcebergErrorResponse", + '5XX': "IcebergErrorResponse", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _register_table_serialize( + self, + prefix, + namespace, + register_table_request, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[str, Union[str, bytes]] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + if prefix is not None: + _path_params['prefix'] = prefix + if namespace is not None: + _path_params['namespace'] = namespace + # process the query parameters + # process the header parameters + # process the form parameters + # process the body parameter + if register_table_request is not None: + _body_params = register_table_request + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + + # set the HTTP header `Content-Type` + if _content_type: + _header_params['Content-Type'] = _content_type + else: + _default_content_type = ( + self.api_client.select_header_content_type( + [ + 'application/json' + ] + ) + ) + if _default_content_type is not None: + _header_params['Content-Type'] = _default_content_type + + # authentication setting + _auth_settings: List[str] = [ + 'OAuth2', + 'BearerAuth' + ] + + return self.api_client.param_serialize( + method='POST', + resource_path='/v1/{prefix}/namespaces/{namespace}/register', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + + + + @validate_call + def rename_table( + self, + prefix: Annotated[StrictStr, Field(description="An optional prefix in the path")], + rename_table_request: Annotated[RenameTableRequest, Field(description="Current table identifier to rename and new table identifier to rename to")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> None: + """Rename a table from its current name to a new name + + Rename a table from one identifier to another. It's valid to move a table across namespaces, but the server implementation is not required to support it. + + :param prefix: An optional prefix in the path (required) + :type prefix: str + :param rename_table_request: Current table identifier to rename and new table identifier to rename to (required) + :type rename_table_request: RenameTableRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._rename_table_serialize( + prefix=prefix, + rename_table_request=rename_table_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '204': None, + '400': "IcebergErrorResponse", + '401': "IcebergErrorResponse", + '403': "IcebergErrorResponse", + '404': "IcebergErrorResponse", + '406': "ErrorModel", + '409': "IcebergErrorResponse", + '419': "IcebergErrorResponse", + '503': "IcebergErrorResponse", + '5XX': "IcebergErrorResponse", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + def rename_table_with_http_info( + self, + prefix: Annotated[StrictStr, Field(description="An optional prefix in the path")], + rename_table_request: Annotated[RenameTableRequest, Field(description="Current table identifier to rename and new table identifier to rename to")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[None]: + """Rename a table from its current name to a new name + + Rename a table from one identifier to another. It's valid to move a table across namespaces, but the server implementation is not required to support it. + + :param prefix: An optional prefix in the path (required) + :type prefix: str + :param rename_table_request: Current table identifier to rename and new table identifier to rename to (required) + :type rename_table_request: RenameTableRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._rename_table_serialize( + prefix=prefix, + rename_table_request=rename_table_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '204': None, + '400': "IcebergErrorResponse", + '401': "IcebergErrorResponse", + '403': "IcebergErrorResponse", + '404': "IcebergErrorResponse", + '406': "ErrorModel", + '409': "IcebergErrorResponse", + '419': "IcebergErrorResponse", + '503': "IcebergErrorResponse", + '5XX': "IcebergErrorResponse", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + def rename_table_without_preload_content( + self, + prefix: Annotated[StrictStr, Field(description="An optional prefix in the path")], + rename_table_request: Annotated[RenameTableRequest, Field(description="Current table identifier to rename and new table identifier to rename to")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """Rename a table from its current name to a new name + + Rename a table from one identifier to another. It's valid to move a table across namespaces, but the server implementation is not required to support it. + + :param prefix: An optional prefix in the path (required) + :type prefix: str + :param rename_table_request: Current table identifier to rename and new table identifier to rename to (required) + :type rename_table_request: RenameTableRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._rename_table_serialize( + prefix=prefix, + rename_table_request=rename_table_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '204': None, + '400': "IcebergErrorResponse", + '401': "IcebergErrorResponse", + '403': "IcebergErrorResponse", + '404': "IcebergErrorResponse", + '406': "ErrorModel", + '409': "IcebergErrorResponse", + '419': "IcebergErrorResponse", + '503': "IcebergErrorResponse", + '5XX': "IcebergErrorResponse", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _rename_table_serialize( + self, + prefix, + rename_table_request, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[str, Union[str, bytes]] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + if prefix is not None: + _path_params['prefix'] = prefix + # process the query parameters + # process the header parameters + # process the form parameters + # process the body parameter + if rename_table_request is not None: + _body_params = rename_table_request + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + + # set the HTTP header `Content-Type` + if _content_type: + _header_params['Content-Type'] = _content_type + else: + _default_content_type = ( + self.api_client.select_header_content_type( + [ + 'application/json' + ] + ) + ) + if _default_content_type is not None: + _header_params['Content-Type'] = _default_content_type + + # authentication setting + _auth_settings: List[str] = [ + 'OAuth2', + 'BearerAuth' + ] + + return self.api_client.param_serialize( + method='POST', + resource_path='/v1/{prefix}/tables/rename', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + + + + @validate_call + def rename_view( + self, + prefix: Annotated[StrictStr, Field(description="An optional prefix in the path")], + rename_table_request: Annotated[RenameTableRequest, Field(description="Current view identifier to rename and new view identifier to rename to")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> None: + """Rename a view from its current name to a new name + + Rename a view from one identifier to another. It's valid to move a view across namespaces, but the server implementation is not required to support it. + + :param prefix: An optional prefix in the path (required) + :type prefix: str + :param rename_table_request: Current view identifier to rename and new view identifier to rename to (required) + :type rename_table_request: RenameTableRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._rename_view_serialize( + prefix=prefix, + rename_table_request=rename_table_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '204': None, + '400': "IcebergErrorResponse", + '401': "IcebergErrorResponse", + '403': "IcebergErrorResponse", + '404': "ErrorModel", + '406': "ErrorModel", + '409': "ErrorModel", + '419': "IcebergErrorResponse", + '503': "IcebergErrorResponse", + '5XX': "IcebergErrorResponse", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + def rename_view_with_http_info( + self, + prefix: Annotated[StrictStr, Field(description="An optional prefix in the path")], + rename_table_request: Annotated[RenameTableRequest, Field(description="Current view identifier to rename and new view identifier to rename to")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[None]: + """Rename a view from its current name to a new name + + Rename a view from one identifier to another. It's valid to move a view across namespaces, but the server implementation is not required to support it. + + :param prefix: An optional prefix in the path (required) + :type prefix: str + :param rename_table_request: Current view identifier to rename and new view identifier to rename to (required) + :type rename_table_request: RenameTableRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._rename_view_serialize( + prefix=prefix, + rename_table_request=rename_table_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '204': None, + '400': "IcebergErrorResponse", + '401': "IcebergErrorResponse", + '403': "IcebergErrorResponse", + '404': "ErrorModel", + '406': "ErrorModel", + '409': "ErrorModel", + '419': "IcebergErrorResponse", + '503': "IcebergErrorResponse", + '5XX': "IcebergErrorResponse", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + def rename_view_without_preload_content( + self, + prefix: Annotated[StrictStr, Field(description="An optional prefix in the path")], + rename_table_request: Annotated[RenameTableRequest, Field(description="Current view identifier to rename and new view identifier to rename to")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """Rename a view from its current name to a new name + + Rename a view from one identifier to another. It's valid to move a view across namespaces, but the server implementation is not required to support it. + + :param prefix: An optional prefix in the path (required) + :type prefix: str + :param rename_table_request: Current view identifier to rename and new view identifier to rename to (required) + :type rename_table_request: RenameTableRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._rename_view_serialize( + prefix=prefix, + rename_table_request=rename_table_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '204': None, + '400': "IcebergErrorResponse", + '401': "IcebergErrorResponse", + '403': "IcebergErrorResponse", + '404': "ErrorModel", + '406': "ErrorModel", + '409': "ErrorModel", + '419': "IcebergErrorResponse", + '503': "IcebergErrorResponse", + '5XX': "IcebergErrorResponse", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _rename_view_serialize( + self, + prefix, + rename_table_request, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[str, Union[str, bytes]] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + if prefix is not None: + _path_params['prefix'] = prefix + # process the query parameters + # process the header parameters + # process the form parameters + # process the body parameter + if rename_table_request is not None: + _body_params = rename_table_request + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + + # set the HTTP header `Content-Type` + if _content_type: + _header_params['Content-Type'] = _content_type + else: + _default_content_type = ( + self.api_client.select_header_content_type( + [ + 'application/json' + ] + ) + ) + if _default_content_type is not None: + _header_params['Content-Type'] = _default_content_type + + # authentication setting + _auth_settings: List[str] = [ + 'OAuth2', + 'BearerAuth' + ] + + return self.api_client.param_serialize( + method='POST', + resource_path='/v1/{prefix}/views/rename', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + + + + @validate_call + def replace_view( + self, + prefix: Annotated[StrictStr, Field(description="An optional prefix in the path")], + namespace: Annotated[StrictStr, Field(description="A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte.")], + view: Annotated[StrictStr, Field(description="A view name")], + commit_view_request: CommitViewRequest, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> LoadViewResult: + """Replace a view + + Commit updates to a view. + + :param prefix: An optional prefix in the path (required) + :type prefix: str + :param namespace: A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. (required) + :type namespace: str + :param view: A view name (required) + :type view: str + :param commit_view_request: (required) + :type commit_view_request: CommitViewRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._replace_view_serialize( + prefix=prefix, + namespace=namespace, + view=view, + commit_view_request=commit_view_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "LoadViewResult", + '400': "IcebergErrorResponse", + '401': "IcebergErrorResponse", + '403': "IcebergErrorResponse", + '404': "ErrorModel", + '409': "ErrorModel", + '419': "IcebergErrorResponse", + '500': "ErrorModel", + '503': "IcebergErrorResponse", + '502': "ErrorModel", + '504': "ErrorModel", + '5XX': "ErrorModel", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + def replace_view_with_http_info( + self, + prefix: Annotated[StrictStr, Field(description="An optional prefix in the path")], + namespace: Annotated[StrictStr, Field(description="A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte.")], + view: Annotated[StrictStr, Field(description="A view name")], + commit_view_request: CommitViewRequest, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[LoadViewResult]: + """Replace a view + + Commit updates to a view. + + :param prefix: An optional prefix in the path (required) + :type prefix: str + :param namespace: A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. (required) + :type namespace: str + :param view: A view name (required) + :type view: str + :param commit_view_request: (required) + :type commit_view_request: CommitViewRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._replace_view_serialize( + prefix=prefix, + namespace=namespace, + view=view, + commit_view_request=commit_view_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "LoadViewResult", + '400': "IcebergErrorResponse", + '401': "IcebergErrorResponse", + '403': "IcebergErrorResponse", + '404': "ErrorModel", + '409': "ErrorModel", + '419': "IcebergErrorResponse", + '500': "ErrorModel", + '503': "IcebergErrorResponse", + '502': "ErrorModel", + '504': "ErrorModel", + '5XX': "ErrorModel", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + def replace_view_without_preload_content( + self, + prefix: Annotated[StrictStr, Field(description="An optional prefix in the path")], + namespace: Annotated[StrictStr, Field(description="A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte.")], + view: Annotated[StrictStr, Field(description="A view name")], + commit_view_request: CommitViewRequest, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """Replace a view + + Commit updates to a view. + + :param prefix: An optional prefix in the path (required) + :type prefix: str + :param namespace: A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. (required) + :type namespace: str + :param view: A view name (required) + :type view: str + :param commit_view_request: (required) + :type commit_view_request: CommitViewRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._replace_view_serialize( + prefix=prefix, + namespace=namespace, + view=view, + commit_view_request=commit_view_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "LoadViewResult", + '400': "IcebergErrorResponse", + '401': "IcebergErrorResponse", + '403': "IcebergErrorResponse", + '404': "ErrorModel", + '409': "ErrorModel", + '419': "IcebergErrorResponse", + '500': "ErrorModel", + '503': "IcebergErrorResponse", + '502': "ErrorModel", + '504': "ErrorModel", + '5XX': "ErrorModel", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _replace_view_serialize( + self, + prefix, + namespace, + view, + commit_view_request, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[str, Union[str, bytes]] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + if prefix is not None: + _path_params['prefix'] = prefix + if namespace is not None: + _path_params['namespace'] = namespace + if view is not None: + _path_params['view'] = view + # process the query parameters + # process the header parameters + # process the form parameters + # process the body parameter + if commit_view_request is not None: + _body_params = commit_view_request + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + + # set the HTTP header `Content-Type` + if _content_type: + _header_params['Content-Type'] = _content_type + else: + _default_content_type = ( + self.api_client.select_header_content_type( + [ + 'application/json' + ] + ) + ) + if _default_content_type is not None: + _header_params['Content-Type'] = _default_content_type + + # authentication setting + _auth_settings: List[str] = [ + 'OAuth2', + 'BearerAuth' + ] + + return self.api_client.param_serialize( + method='POST', + resource_path='/v1/{prefix}/namespaces/{namespace}/views/{view}', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + + + + @validate_call + def report_metrics( + self, + prefix: Annotated[StrictStr, Field(description="An optional prefix in the path")], + namespace: Annotated[StrictStr, Field(description="A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte.")], + table: Annotated[StrictStr, Field(description="A table name")], + report_metrics_request: Annotated[ReportMetricsRequest, Field(description="The request containing the metrics report to be sent")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> None: + """Send a metrics report to this endpoint to be processed by the backend + + + :param prefix: An optional prefix in the path (required) + :type prefix: str + :param namespace: A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. (required) + :type namespace: str + :param table: A table name (required) + :type table: str + :param report_metrics_request: The request containing the metrics report to be sent (required) + :type report_metrics_request: ReportMetricsRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._report_metrics_serialize( + prefix=prefix, + namespace=namespace, + table=table, + report_metrics_request=report_metrics_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '204': None, + '400': "IcebergErrorResponse", + '401': "IcebergErrorResponse", + '403': "IcebergErrorResponse", + '404': "IcebergErrorResponse", + '419': "IcebergErrorResponse", + '503': "IcebergErrorResponse", + '5XX': "IcebergErrorResponse", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + def report_metrics_with_http_info( + self, + prefix: Annotated[StrictStr, Field(description="An optional prefix in the path")], + namespace: Annotated[StrictStr, Field(description="A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte.")], + table: Annotated[StrictStr, Field(description="A table name")], + report_metrics_request: Annotated[ReportMetricsRequest, Field(description="The request containing the metrics report to be sent")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[None]: + """Send a metrics report to this endpoint to be processed by the backend + + + :param prefix: An optional prefix in the path (required) + :type prefix: str + :param namespace: A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. (required) + :type namespace: str + :param table: A table name (required) + :type table: str + :param report_metrics_request: The request containing the metrics report to be sent (required) + :type report_metrics_request: ReportMetricsRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._report_metrics_serialize( + prefix=prefix, + namespace=namespace, + table=table, + report_metrics_request=report_metrics_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '204': None, + '400': "IcebergErrorResponse", + '401': "IcebergErrorResponse", + '403': "IcebergErrorResponse", + '404': "IcebergErrorResponse", + '419': "IcebergErrorResponse", + '503': "IcebergErrorResponse", + '5XX': "IcebergErrorResponse", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + def report_metrics_without_preload_content( + self, + prefix: Annotated[StrictStr, Field(description="An optional prefix in the path")], + namespace: Annotated[StrictStr, Field(description="A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte.")], + table: Annotated[StrictStr, Field(description="A table name")], + report_metrics_request: Annotated[ReportMetricsRequest, Field(description="The request containing the metrics report to be sent")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """Send a metrics report to this endpoint to be processed by the backend + + + :param prefix: An optional prefix in the path (required) + :type prefix: str + :param namespace: A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. (required) + :type namespace: str + :param table: A table name (required) + :type table: str + :param report_metrics_request: The request containing the metrics report to be sent (required) + :type report_metrics_request: ReportMetricsRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._report_metrics_serialize( + prefix=prefix, + namespace=namespace, + table=table, + report_metrics_request=report_metrics_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '204': None, + '400': "IcebergErrorResponse", + '401': "IcebergErrorResponse", + '403': "IcebergErrorResponse", + '404': "IcebergErrorResponse", + '419': "IcebergErrorResponse", + '503': "IcebergErrorResponse", + '5XX': "IcebergErrorResponse", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _report_metrics_serialize( + self, + prefix, + namespace, + table, + report_metrics_request, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[str, Union[str, bytes]] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + if prefix is not None: + _path_params['prefix'] = prefix + if namespace is not None: + _path_params['namespace'] = namespace + if table is not None: + _path_params['table'] = table + # process the query parameters + # process the header parameters + # process the form parameters + # process the body parameter + if report_metrics_request is not None: + _body_params = report_metrics_request + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + + # set the HTTP header `Content-Type` + if _content_type: + _header_params['Content-Type'] = _content_type + else: + _default_content_type = ( + self.api_client.select_header_content_type( + [ + 'application/json' + ] + ) + ) + if _default_content_type is not None: + _header_params['Content-Type'] = _default_content_type + + # authentication setting + _auth_settings: List[str] = [ + 'OAuth2', + 'BearerAuth' + ] + + return self.api_client.param_serialize( + method='POST', + resource_path='/v1/{prefix}/namespaces/{namespace}/tables/{table}/metrics', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + + + + @validate_call + def send_notification( + self, + prefix: Annotated[StrictStr, Field(description="An optional prefix in the path")], + namespace: Annotated[StrictStr, Field(description="A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte.")], + table: Annotated[StrictStr, Field(description="A table name")], + notification_request: Annotated[NotificationRequest, Field(description="The request containing the notification to be sent")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> None: + """Sends a notification to the table + + + :param prefix: An optional prefix in the path (required) + :type prefix: str + :param namespace: A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. (required) + :type namespace: str + :param table: A table name (required) + :type table: str + :param notification_request: The request containing the notification to be sent (required) + :type notification_request: NotificationRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._send_notification_serialize( + prefix=prefix, + namespace=namespace, + table=table, + notification_request=notification_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '204': None, + '400': "IcebergErrorResponse", + '401': "IcebergErrorResponse", + '403': "IcebergErrorResponse", + '404': "IcebergErrorResponse", + '419': "IcebergErrorResponse", + '503': "IcebergErrorResponse", + '5XX': "IcebergErrorResponse", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + def send_notification_with_http_info( + self, + prefix: Annotated[StrictStr, Field(description="An optional prefix in the path")], + namespace: Annotated[StrictStr, Field(description="A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte.")], + table: Annotated[StrictStr, Field(description="A table name")], + notification_request: Annotated[NotificationRequest, Field(description="The request containing the notification to be sent")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[None]: + """Sends a notification to the table + + + :param prefix: An optional prefix in the path (required) + :type prefix: str + :param namespace: A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. (required) + :type namespace: str + :param table: A table name (required) + :type table: str + :param notification_request: The request containing the notification to be sent (required) + :type notification_request: NotificationRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._send_notification_serialize( + prefix=prefix, + namespace=namespace, + table=table, + notification_request=notification_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '204': None, + '400': "IcebergErrorResponse", + '401': "IcebergErrorResponse", + '403': "IcebergErrorResponse", + '404': "IcebergErrorResponse", + '419': "IcebergErrorResponse", + '503': "IcebergErrorResponse", + '5XX': "IcebergErrorResponse", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + def send_notification_without_preload_content( + self, + prefix: Annotated[StrictStr, Field(description="An optional prefix in the path")], + namespace: Annotated[StrictStr, Field(description="A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte.")], + table: Annotated[StrictStr, Field(description="A table name")], + notification_request: Annotated[NotificationRequest, Field(description="The request containing the notification to be sent")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """Sends a notification to the table + + + :param prefix: An optional prefix in the path (required) + :type prefix: str + :param namespace: A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. (required) + :type namespace: str + :param table: A table name (required) + :type table: str + :param notification_request: The request containing the notification to be sent (required) + :type notification_request: NotificationRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._send_notification_serialize( + prefix=prefix, + namespace=namespace, + table=table, + notification_request=notification_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '204': None, + '400': "IcebergErrorResponse", + '401': "IcebergErrorResponse", + '403': "IcebergErrorResponse", + '404': "IcebergErrorResponse", + '419': "IcebergErrorResponse", + '503': "IcebergErrorResponse", + '5XX': "IcebergErrorResponse", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _send_notification_serialize( + self, + prefix, + namespace, + table, + notification_request, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[str, Union[str, bytes]] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + if prefix is not None: + _path_params['prefix'] = prefix + if namespace is not None: + _path_params['namespace'] = namespace + if table is not None: + _path_params['table'] = table + # process the query parameters + # process the header parameters + # process the form parameters + # process the body parameter + if notification_request is not None: + _body_params = notification_request + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + + # set the HTTP header `Content-Type` + if _content_type: + _header_params['Content-Type'] = _content_type + else: + _default_content_type = ( + self.api_client.select_header_content_type( + [ + 'application/json' + ] + ) + ) + if _default_content_type is not None: + _header_params['Content-Type'] = _default_content_type + + # authentication setting + _auth_settings: List[str] = [ + 'OAuth2', + 'BearerAuth' + ] + + return self.api_client.param_serialize( + method='POST', + resource_path='/v1/{prefix}/namespaces/{namespace}/tables/{table}/notifications', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + + + + @validate_call + def table_exists( + self, + prefix: Annotated[StrictStr, Field(description="An optional prefix in the path")], + namespace: Annotated[StrictStr, Field(description="A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte.")], + table: Annotated[StrictStr, Field(description="A table name")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> None: + """Check if a table exists + + Check if a table exists within a given namespace. The response does not contain a body. + + :param prefix: An optional prefix in the path (required) + :type prefix: str + :param namespace: A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. (required) + :type namespace: str + :param table: A table name (required) + :type table: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._table_exists_serialize( + prefix=prefix, + namespace=namespace, + table=table, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '204': None, + '400': "IcebergErrorResponse", + '401': "IcebergErrorResponse", + '403': "IcebergErrorResponse", + '404': "IcebergErrorResponse", + '419': "IcebergErrorResponse", + '503': "IcebergErrorResponse", + '5XX': "IcebergErrorResponse", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + def table_exists_with_http_info( + self, + prefix: Annotated[StrictStr, Field(description="An optional prefix in the path")], + namespace: Annotated[StrictStr, Field(description="A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte.")], + table: Annotated[StrictStr, Field(description="A table name")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[None]: + """Check if a table exists + + Check if a table exists within a given namespace. The response does not contain a body. + + :param prefix: An optional prefix in the path (required) + :type prefix: str + :param namespace: A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. (required) + :type namespace: str + :param table: A table name (required) + :type table: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._table_exists_serialize( + prefix=prefix, + namespace=namespace, + table=table, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '204': None, + '400': "IcebergErrorResponse", + '401': "IcebergErrorResponse", + '403': "IcebergErrorResponse", + '404': "IcebergErrorResponse", + '419': "IcebergErrorResponse", + '503': "IcebergErrorResponse", + '5XX': "IcebergErrorResponse", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + def table_exists_without_preload_content( + self, + prefix: Annotated[StrictStr, Field(description="An optional prefix in the path")], + namespace: Annotated[StrictStr, Field(description="A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte.")], + table: Annotated[StrictStr, Field(description="A table name")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """Check if a table exists + + Check if a table exists within a given namespace. The response does not contain a body. + + :param prefix: An optional prefix in the path (required) + :type prefix: str + :param namespace: A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. (required) + :type namespace: str + :param table: A table name (required) + :type table: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._table_exists_serialize( + prefix=prefix, + namespace=namespace, + table=table, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '204': None, + '400': "IcebergErrorResponse", + '401': "IcebergErrorResponse", + '403': "IcebergErrorResponse", + '404': "IcebergErrorResponse", + '419': "IcebergErrorResponse", + '503': "IcebergErrorResponse", + '5XX': "IcebergErrorResponse", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _table_exists_serialize( + self, + prefix, + namespace, + table, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[str, Union[str, bytes]] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + if prefix is not None: + _path_params['prefix'] = prefix + if namespace is not None: + _path_params['namespace'] = namespace + if table is not None: + _path_params['table'] = table + # process the query parameters + # process the header parameters + # process the form parameters + # process the body parameter + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + + + # authentication setting + _auth_settings: List[str] = [ + 'OAuth2', + 'BearerAuth' + ] + + return self.api_client.param_serialize( + method='HEAD', + resource_path='/v1/{prefix}/namespaces/{namespace}/tables/{table}', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + + + + @validate_call + def update_properties( + self, + prefix: Annotated[StrictStr, Field(description="An optional prefix in the path")], + namespace: Annotated[StrictStr, Field(description="A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte.")], + update_namespace_properties_request: UpdateNamespacePropertiesRequest, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> UpdateNamespacePropertiesResponse: + """Set or remove properties on a namespace + + Set and/or remove properties on a namespace. The request body specifies a list of properties to remove and a map of key value pairs to update. Properties that are not in the request are not modified or removed by this call. Server implementations are not required to support namespace properties. + + :param prefix: An optional prefix in the path (required) + :type prefix: str + :param namespace: A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. (required) + :type namespace: str + :param update_namespace_properties_request: (required) + :type update_namespace_properties_request: UpdateNamespacePropertiesRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._update_properties_serialize( + prefix=prefix, + namespace=namespace, + update_namespace_properties_request=update_namespace_properties_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "UpdateNamespacePropertiesResponse", + '400': "IcebergErrorResponse", + '401': "IcebergErrorResponse", + '403': "IcebergErrorResponse", + '404': "IcebergErrorResponse", + '406': "ErrorModel", + '422': "IcebergErrorResponse", + '419': "IcebergErrorResponse", + '503': "IcebergErrorResponse", + '5XX': "IcebergErrorResponse", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + def update_properties_with_http_info( + self, + prefix: Annotated[StrictStr, Field(description="An optional prefix in the path")], + namespace: Annotated[StrictStr, Field(description="A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte.")], + update_namespace_properties_request: UpdateNamespacePropertiesRequest, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[UpdateNamespacePropertiesResponse]: + """Set or remove properties on a namespace + + Set and/or remove properties on a namespace. The request body specifies a list of properties to remove and a map of key value pairs to update. Properties that are not in the request are not modified or removed by this call. Server implementations are not required to support namespace properties. + + :param prefix: An optional prefix in the path (required) + :type prefix: str + :param namespace: A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. (required) + :type namespace: str + :param update_namespace_properties_request: (required) + :type update_namespace_properties_request: UpdateNamespacePropertiesRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._update_properties_serialize( + prefix=prefix, + namespace=namespace, + update_namespace_properties_request=update_namespace_properties_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "UpdateNamespacePropertiesResponse", + '400': "IcebergErrorResponse", + '401': "IcebergErrorResponse", + '403': "IcebergErrorResponse", + '404': "IcebergErrorResponse", + '406': "ErrorModel", + '422': "IcebergErrorResponse", + '419': "IcebergErrorResponse", + '503': "IcebergErrorResponse", + '5XX': "IcebergErrorResponse", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + def update_properties_without_preload_content( + self, + prefix: Annotated[StrictStr, Field(description="An optional prefix in the path")], + namespace: Annotated[StrictStr, Field(description="A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte.")], + update_namespace_properties_request: UpdateNamespacePropertiesRequest, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """Set or remove properties on a namespace + + Set and/or remove properties on a namespace. The request body specifies a list of properties to remove and a map of key value pairs to update. Properties that are not in the request are not modified or removed by this call. Server implementations are not required to support namespace properties. + + :param prefix: An optional prefix in the path (required) + :type prefix: str + :param namespace: A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. (required) + :type namespace: str + :param update_namespace_properties_request: (required) + :type update_namespace_properties_request: UpdateNamespacePropertiesRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._update_properties_serialize( + prefix=prefix, + namespace=namespace, + update_namespace_properties_request=update_namespace_properties_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "UpdateNamespacePropertiesResponse", + '400': "IcebergErrorResponse", + '401': "IcebergErrorResponse", + '403': "IcebergErrorResponse", + '404': "IcebergErrorResponse", + '406': "ErrorModel", + '422': "IcebergErrorResponse", + '419': "IcebergErrorResponse", + '503': "IcebergErrorResponse", + '5XX': "IcebergErrorResponse", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _update_properties_serialize( + self, + prefix, + namespace, + update_namespace_properties_request, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[str, Union[str, bytes]] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + if prefix is not None: + _path_params['prefix'] = prefix + if namespace is not None: + _path_params['namespace'] = namespace + # process the query parameters + # process the header parameters + # process the form parameters + # process the body parameter + if update_namespace_properties_request is not None: + _body_params = update_namespace_properties_request + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + + # set the HTTP header `Content-Type` + if _content_type: + _header_params['Content-Type'] = _content_type + else: + _default_content_type = ( + self.api_client.select_header_content_type( + [ + 'application/json' + ] + ) + ) + if _default_content_type is not None: + _header_params['Content-Type'] = _default_content_type + + # authentication setting + _auth_settings: List[str] = [ + 'OAuth2', + 'BearerAuth' + ] + + return self.api_client.param_serialize( + method='POST', + resource_path='/v1/{prefix}/namespaces/{namespace}/properties', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + + + + @validate_call + def update_table( + self, + prefix: Annotated[StrictStr, Field(description="An optional prefix in the path")], + namespace: Annotated[StrictStr, Field(description="A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte.")], + table: Annotated[StrictStr, Field(description="A table name")], + commit_table_request: CommitTableRequest, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> CommitTableResponse: + """Commit updates to a table + + Commit updates to a table. Commits have two parts, requirements and updates. Requirements are assertions that will be validated before attempting to make and commit changes. For example, `assert-ref-snapshot-id` will check that a named ref's snapshot ID has a certain value. Updates are changes to make to table metadata. For example, after asserting that the current main ref is at the expected snapshot, a commit may add a new child snapshot and set the ref to the new snapshot id. Create table transactions that are started by createTable with `stage-create` set to true are committed using this route. Transactions should include all changes to the table, including table initialization, like AddSchemaUpdate and SetCurrentSchemaUpdate. The `assert-create` requirement is used to ensure that the table was not created concurrently. + + :param prefix: An optional prefix in the path (required) + :type prefix: str + :param namespace: A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. (required) + :type namespace: str + :param table: A table name (required) + :type table: str + :param commit_table_request: (required) + :type commit_table_request: CommitTableRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._update_table_serialize( + prefix=prefix, + namespace=namespace, + table=table, + commit_table_request=commit_table_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "CommitTableResponse", + '400': "IcebergErrorResponse", + '401': "IcebergErrorResponse", + '403': "IcebergErrorResponse", + '404': "IcebergErrorResponse", + '409': "IcebergErrorResponse", + '419': "IcebergErrorResponse", + '500': "IcebergErrorResponse", + '503': "IcebergErrorResponse", + '502': "IcebergErrorResponse", + '504': "IcebergErrorResponse", + '5XX': "IcebergErrorResponse", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + def update_table_with_http_info( + self, + prefix: Annotated[StrictStr, Field(description="An optional prefix in the path")], + namespace: Annotated[StrictStr, Field(description="A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte.")], + table: Annotated[StrictStr, Field(description="A table name")], + commit_table_request: CommitTableRequest, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[CommitTableResponse]: + """Commit updates to a table + + Commit updates to a table. Commits have two parts, requirements and updates. Requirements are assertions that will be validated before attempting to make and commit changes. For example, `assert-ref-snapshot-id` will check that a named ref's snapshot ID has a certain value. Updates are changes to make to table metadata. For example, after asserting that the current main ref is at the expected snapshot, a commit may add a new child snapshot and set the ref to the new snapshot id. Create table transactions that are started by createTable with `stage-create` set to true are committed using this route. Transactions should include all changes to the table, including table initialization, like AddSchemaUpdate and SetCurrentSchemaUpdate. The `assert-create` requirement is used to ensure that the table was not created concurrently. + + :param prefix: An optional prefix in the path (required) + :type prefix: str + :param namespace: A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. (required) + :type namespace: str + :param table: A table name (required) + :type table: str + :param commit_table_request: (required) + :type commit_table_request: CommitTableRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._update_table_serialize( + prefix=prefix, + namespace=namespace, + table=table, + commit_table_request=commit_table_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "CommitTableResponse", + '400': "IcebergErrorResponse", + '401': "IcebergErrorResponse", + '403': "IcebergErrorResponse", + '404': "IcebergErrorResponse", + '409': "IcebergErrorResponse", + '419': "IcebergErrorResponse", + '500': "IcebergErrorResponse", + '503': "IcebergErrorResponse", + '502': "IcebergErrorResponse", + '504': "IcebergErrorResponse", + '5XX': "IcebergErrorResponse", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + def update_table_without_preload_content( + self, + prefix: Annotated[StrictStr, Field(description="An optional prefix in the path")], + namespace: Annotated[StrictStr, Field(description="A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte.")], + table: Annotated[StrictStr, Field(description="A table name")], + commit_table_request: CommitTableRequest, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """Commit updates to a table + + Commit updates to a table. Commits have two parts, requirements and updates. Requirements are assertions that will be validated before attempting to make and commit changes. For example, `assert-ref-snapshot-id` will check that a named ref's snapshot ID has a certain value. Updates are changes to make to table metadata. For example, after asserting that the current main ref is at the expected snapshot, a commit may add a new child snapshot and set the ref to the new snapshot id. Create table transactions that are started by createTable with `stage-create` set to true are committed using this route. Transactions should include all changes to the table, including table initialization, like AddSchemaUpdate and SetCurrentSchemaUpdate. The `assert-create` requirement is used to ensure that the table was not created concurrently. + + :param prefix: An optional prefix in the path (required) + :type prefix: str + :param namespace: A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. (required) + :type namespace: str + :param table: A table name (required) + :type table: str + :param commit_table_request: (required) + :type commit_table_request: CommitTableRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._update_table_serialize( + prefix=prefix, + namespace=namespace, + table=table, + commit_table_request=commit_table_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "CommitTableResponse", + '400': "IcebergErrorResponse", + '401': "IcebergErrorResponse", + '403': "IcebergErrorResponse", + '404': "IcebergErrorResponse", + '409': "IcebergErrorResponse", + '419': "IcebergErrorResponse", + '500': "IcebergErrorResponse", + '503': "IcebergErrorResponse", + '502': "IcebergErrorResponse", + '504': "IcebergErrorResponse", + '5XX': "IcebergErrorResponse", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _update_table_serialize( + self, + prefix, + namespace, + table, + commit_table_request, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[str, Union[str, bytes]] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + if prefix is not None: + _path_params['prefix'] = prefix + if namespace is not None: + _path_params['namespace'] = namespace + if table is not None: + _path_params['table'] = table + # process the query parameters + # process the header parameters + # process the form parameters + # process the body parameter + if commit_table_request is not None: + _body_params = commit_table_request + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + + # set the HTTP header `Content-Type` + if _content_type: + _header_params['Content-Type'] = _content_type + else: + _default_content_type = ( + self.api_client.select_header_content_type( + [ + 'application/json' + ] + ) + ) + if _default_content_type is not None: + _header_params['Content-Type'] = _default_content_type + + # authentication setting + _auth_settings: List[str] = [ + 'OAuth2', + 'BearerAuth' + ] + + return self.api_client.param_serialize( + method='POST', + resource_path='/v1/{prefix}/namespaces/{namespace}/tables/{table}', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + + + + @validate_call + def view_exists( + self, + prefix: Annotated[StrictStr, Field(description="An optional prefix in the path")], + namespace: Annotated[StrictStr, Field(description="A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte.")], + view: Annotated[StrictStr, Field(description="A view name")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> None: + """Check if a view exists + + Check if a view exists within a given namespace. This request does not return a response body. + + :param prefix: An optional prefix in the path (required) + :type prefix: str + :param namespace: A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. (required) + :type namespace: str + :param view: A view name (required) + :type view: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._view_exists_serialize( + prefix=prefix, + namespace=namespace, + view=view, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '204': None, + '400': None, + '401': None, + '404': None, + '419': "IcebergErrorResponse", + '503': "IcebergErrorResponse", + '5XX': "IcebergErrorResponse", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + def view_exists_with_http_info( + self, + prefix: Annotated[StrictStr, Field(description="An optional prefix in the path")], + namespace: Annotated[StrictStr, Field(description="A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte.")], + view: Annotated[StrictStr, Field(description="A view name")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[None]: + """Check if a view exists + + Check if a view exists within a given namespace. This request does not return a response body. + + :param prefix: An optional prefix in the path (required) + :type prefix: str + :param namespace: A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. (required) + :type namespace: str + :param view: A view name (required) + :type view: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._view_exists_serialize( + prefix=prefix, + namespace=namespace, + view=view, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '204': None, + '400': None, + '401': None, + '404': None, + '419': "IcebergErrorResponse", + '503': "IcebergErrorResponse", + '5XX': "IcebergErrorResponse", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + def view_exists_without_preload_content( + self, + prefix: Annotated[StrictStr, Field(description="An optional prefix in the path")], + namespace: Annotated[StrictStr, Field(description="A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte.")], + view: Annotated[StrictStr, Field(description="A view name")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """Check if a view exists + + Check if a view exists within a given namespace. This request does not return a response body. + + :param prefix: An optional prefix in the path (required) + :type prefix: str + :param namespace: A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. (required) + :type namespace: str + :param view: A view name (required) + :type view: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._view_exists_serialize( + prefix=prefix, + namespace=namespace, + view=view, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '204': None, + '400': None, + '401': None, + '404': None, + '419': "IcebergErrorResponse", + '503': "IcebergErrorResponse", + '5XX': "IcebergErrorResponse", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _view_exists_serialize( + self, + prefix, + namespace, + view, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[str, Union[str, bytes]] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + if prefix is not None: + _path_params['prefix'] = prefix + if namespace is not None: + _path_params['namespace'] = namespace + if view is not None: + _path_params['view'] = view + # process the query parameters + # process the header parameters + # process the form parameters + # process the body parameter + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + + + # authentication setting + _auth_settings: List[str] = [ + 'OAuth2', + 'BearerAuth' + ] + + return self.api_client.param_serialize( + method='HEAD', + resource_path='/v1/{prefix}/namespaces/{namespace}/views/{view}', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + diff --git a/regtests/client/python/polaris/catalog/api/iceberg_configuration_api.py b/regtests/client/python/polaris/catalog/api/iceberg_configuration_api.py new file mode 100644 index 0000000000..4d409d8e1b --- /dev/null +++ b/regtests/client/python/polaris/catalog/api/iceberg_configuration_api.py @@ -0,0 +1,319 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + +import warnings +from pydantic import validate_call, Field, StrictFloat, StrictStr, StrictInt +from typing import Any, Dict, List, Optional, Tuple, Union +from typing_extensions import Annotated + +from pydantic import Field, StrictStr +from typing import Optional +from typing_extensions import Annotated +from polaris.catalog.models.catalog_config import CatalogConfig + +from polaris.catalog.api_client import ApiClient, RequestSerialized +from polaris.catalog.api_response import ApiResponse +from polaris.catalog.rest import RESTResponseType + + +class IcebergConfigurationAPI: + """NOTE: This class is auto generated by OpenAPI Generator + Ref: https://openapi-generator.tech + + Do not edit the class manually. + """ + + def __init__(self, api_client=None) -> None: + if api_client is None: + api_client = ApiClient.get_default() + self.api_client = api_client + + + @validate_call + def get_config( + self, + warehouse: Annotated[Optional[StrictStr], Field(description="Warehouse location or identifier to request from the service")] = None, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> CatalogConfig: + """List all catalog configuration settings + + All REST clients should first call this route to get catalog configuration properties from the server to configure the catalog and its HTTP client. Configuration from the server consists of two sets of key/value pairs. - defaults - properties that should be used as default configuration; applied before client configuration - overrides - properties that should be used to override client configuration; applied after defaults and client configuration Catalog configuration is constructed by setting the defaults, then client- provided configuration, and finally overrides. The final property set is then used to configure the catalog. For example, a default configuration property might set the size of the client pool, which can be replaced with a client-specific setting. An override might be used to set the warehouse location, which is stored on the server rather than in client configuration. Common catalog configuration settings are documented at https://iceberg.apache.org/docs/latest/configuration/#catalog-properties + + :param warehouse: Warehouse location or identifier to request from the service + :type warehouse: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._get_config_serialize( + warehouse=warehouse, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "CatalogConfig", + '400': "IcebergErrorResponse", + '401': "IcebergErrorResponse", + '403': "IcebergErrorResponse", + '419': "IcebergErrorResponse", + '503': "IcebergErrorResponse", + '5XX': "IcebergErrorResponse", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + def get_config_with_http_info( + self, + warehouse: Annotated[Optional[StrictStr], Field(description="Warehouse location or identifier to request from the service")] = None, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[CatalogConfig]: + """List all catalog configuration settings + + All REST clients should first call this route to get catalog configuration properties from the server to configure the catalog and its HTTP client. Configuration from the server consists of two sets of key/value pairs. - defaults - properties that should be used as default configuration; applied before client configuration - overrides - properties that should be used to override client configuration; applied after defaults and client configuration Catalog configuration is constructed by setting the defaults, then client- provided configuration, and finally overrides. The final property set is then used to configure the catalog. For example, a default configuration property might set the size of the client pool, which can be replaced with a client-specific setting. An override might be used to set the warehouse location, which is stored on the server rather than in client configuration. Common catalog configuration settings are documented at https://iceberg.apache.org/docs/latest/configuration/#catalog-properties + + :param warehouse: Warehouse location or identifier to request from the service + :type warehouse: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._get_config_serialize( + warehouse=warehouse, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "CatalogConfig", + '400': "IcebergErrorResponse", + '401': "IcebergErrorResponse", + '403': "IcebergErrorResponse", + '419': "IcebergErrorResponse", + '503': "IcebergErrorResponse", + '5XX': "IcebergErrorResponse", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + def get_config_without_preload_content( + self, + warehouse: Annotated[Optional[StrictStr], Field(description="Warehouse location or identifier to request from the service")] = None, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """List all catalog configuration settings + + All REST clients should first call this route to get catalog configuration properties from the server to configure the catalog and its HTTP client. Configuration from the server consists of two sets of key/value pairs. - defaults - properties that should be used as default configuration; applied before client configuration - overrides - properties that should be used to override client configuration; applied after defaults and client configuration Catalog configuration is constructed by setting the defaults, then client- provided configuration, and finally overrides. The final property set is then used to configure the catalog. For example, a default configuration property might set the size of the client pool, which can be replaced with a client-specific setting. An override might be used to set the warehouse location, which is stored on the server rather than in client configuration. Common catalog configuration settings are documented at https://iceberg.apache.org/docs/latest/configuration/#catalog-properties + + :param warehouse: Warehouse location or identifier to request from the service + :type warehouse: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._get_config_serialize( + warehouse=warehouse, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "CatalogConfig", + '400': "IcebergErrorResponse", + '401': "IcebergErrorResponse", + '403': "IcebergErrorResponse", + '419': "IcebergErrorResponse", + '503': "IcebergErrorResponse", + '5XX': "IcebergErrorResponse", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _get_config_serialize( + self, + warehouse, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[str, Union[str, bytes]] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + # process the query parameters + if warehouse is not None: + + _query_params.append(('warehouse', warehouse)) + + # process the header parameters + # process the form parameters + # process the body parameter + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + + + # authentication setting + _auth_settings: List[str] = [ + 'OAuth2', + 'BearerAuth' + ] + + return self.api_client.param_serialize( + method='GET', + resource_path='/v1/config', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + diff --git a/regtests/client/python/polaris/catalog/api/iceberg_o_auth2_api.py b/regtests/client/python/polaris/catalog/api/iceberg_o_auth2_api.py new file mode 100644 index 0000000000..f1a163c9c8 --- /dev/null +++ b/regtests/client/python/polaris/catalog/api/iceberg_o_auth2_api.py @@ -0,0 +1,441 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + +import warnings +from pydantic import validate_call, Field, StrictFloat, StrictStr, StrictInt +from typing import Any, Dict, List, Optional, Tuple, Union +from typing_extensions import Annotated + +from pydantic import Field, StrictStr, field_validator +from typing import Optional +from typing_extensions import Annotated +from polaris.catalog.models.o_auth_token_response import OAuthTokenResponse +from polaris.catalog.models.token_type import TokenType + +from polaris.catalog.api_client import ApiClient, RequestSerialized +from polaris.catalog.api_response import ApiResponse +from polaris.catalog.rest import RESTResponseType + + +class IcebergOAuth2API: + """NOTE: This class is auto generated by OpenAPI Generator + Ref: https://openapi-generator.tech + + Do not edit the class manually. + """ + + def __init__(self, api_client=None) -> None: + if api_client is None: + api_client = ApiClient.get_default() + self.api_client = api_client + + + @validate_call + def get_token( + self, + grant_type: Optional[StrictStr] = None, + scope: Optional[StrictStr] = None, + client_id: Annotated[Optional[StrictStr], Field(description="Client ID This can be sent in the request body, but OAuth2 recommends sending it in a Basic Authorization header.")] = None, + client_secret: Annotated[Optional[StrictStr], Field(description="Client secret This can be sent in the request body, but OAuth2 recommends sending it in a Basic Authorization header.")] = None, + requested_token_type: Optional[TokenType] = None, + subject_token: Annotated[Optional[StrictStr], Field(description="Subject token for token exchange request")] = None, + subject_token_type: Optional[TokenType] = None, + actor_token: Annotated[Optional[StrictStr], Field(description="Actor token for token exchange request")] = None, + actor_token_type: Optional[TokenType] = None, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> OAuthTokenResponse: + """Get a token using an OAuth2 flow + + Exchange credentials for a token using the OAuth2 client credentials flow or token exchange. This endpoint is used for three purposes - 1. To exchange client credentials (client ID and secret) for an access token This uses the client credentials flow. 2. To exchange a client token and an identity token for a more specific access token This uses the token exchange flow. 3. To exchange an access token for one with the same claims and a refreshed expiration period This uses the token exchange flow. For example, a catalog client may be configured with client credentials from the OAuth2 Authorization flow. This client would exchange its client ID and secret for an access token using the client credentials request with this endpoint (1). Subsequent requests would then use that access token. Some clients may also handle sessions that have additional user context. These clients would use the token exchange flow to exchange a user token (the \"subject\" token) from the session for a more specific access token for that user, using the catalog's access token as the \"actor\" token (2). The user ID token is the \"subject\" token and can be any token type allowed by the OAuth2 token exchange flow, including a unsecured JWT token with a sub claim. This request should use the catalog's bearer token in the \"Authorization\" header. Clients may also use the token exchange flow to refresh a token that is about to expire by sending a token exchange request (3). The request's \"subject\" token should be the expiring token. This request should use the subject token in the \"Authorization\" header. + + :param grant_type: + :type grant_type: str + :param scope: + :type scope: str + :param client_id: Client ID This can be sent in the request body, but OAuth2 recommends sending it in a Basic Authorization header. + :type client_id: str + :param client_secret: Client secret This can be sent in the request body, but OAuth2 recommends sending it in a Basic Authorization header. + :type client_secret: str + :param requested_token_type: + :type requested_token_type: TokenType + :param subject_token: Subject token for token exchange request + :type subject_token: str + :param subject_token_type: + :type subject_token_type: TokenType + :param actor_token: Actor token for token exchange request + :type actor_token: str + :param actor_token_type: + :type actor_token_type: TokenType + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._get_token_serialize( + grant_type=grant_type, + scope=scope, + client_id=client_id, + client_secret=client_secret, + requested_token_type=requested_token_type, + subject_token=subject_token, + subject_token_type=subject_token_type, + actor_token=actor_token, + actor_token_type=actor_token_type, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "OAuthTokenResponse", + '400': "OAuthError", + '401': "OAuthError", + '5XX': "OAuthError", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + def get_token_with_http_info( + self, + grant_type: Optional[StrictStr] = None, + scope: Optional[StrictStr] = None, + client_id: Annotated[Optional[StrictStr], Field(description="Client ID This can be sent in the request body, but OAuth2 recommends sending it in a Basic Authorization header.")] = None, + client_secret: Annotated[Optional[StrictStr], Field(description="Client secret This can be sent in the request body, but OAuth2 recommends sending it in a Basic Authorization header.")] = None, + requested_token_type: Optional[TokenType] = None, + subject_token: Annotated[Optional[StrictStr], Field(description="Subject token for token exchange request")] = None, + subject_token_type: Optional[TokenType] = None, + actor_token: Annotated[Optional[StrictStr], Field(description="Actor token for token exchange request")] = None, + actor_token_type: Optional[TokenType] = None, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[OAuthTokenResponse]: + """Get a token using an OAuth2 flow + + Exchange credentials for a token using the OAuth2 client credentials flow or token exchange. This endpoint is used for three purposes - 1. To exchange client credentials (client ID and secret) for an access token This uses the client credentials flow. 2. To exchange a client token and an identity token for a more specific access token This uses the token exchange flow. 3. To exchange an access token for one with the same claims and a refreshed expiration period This uses the token exchange flow. For example, a catalog client may be configured with client credentials from the OAuth2 Authorization flow. This client would exchange its client ID and secret for an access token using the client credentials request with this endpoint (1). Subsequent requests would then use that access token. Some clients may also handle sessions that have additional user context. These clients would use the token exchange flow to exchange a user token (the \"subject\" token) from the session for a more specific access token for that user, using the catalog's access token as the \"actor\" token (2). The user ID token is the \"subject\" token and can be any token type allowed by the OAuth2 token exchange flow, including a unsecured JWT token with a sub claim. This request should use the catalog's bearer token in the \"Authorization\" header. Clients may also use the token exchange flow to refresh a token that is about to expire by sending a token exchange request (3). The request's \"subject\" token should be the expiring token. This request should use the subject token in the \"Authorization\" header. + + :param grant_type: + :type grant_type: str + :param scope: + :type scope: str + :param client_id: Client ID This can be sent in the request body, but OAuth2 recommends sending it in a Basic Authorization header. + :type client_id: str + :param client_secret: Client secret This can be sent in the request body, but OAuth2 recommends sending it in a Basic Authorization header. + :type client_secret: str + :param requested_token_type: + :type requested_token_type: TokenType + :param subject_token: Subject token for token exchange request + :type subject_token: str + :param subject_token_type: + :type subject_token_type: TokenType + :param actor_token: Actor token for token exchange request + :type actor_token: str + :param actor_token_type: + :type actor_token_type: TokenType + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._get_token_serialize( + grant_type=grant_type, + scope=scope, + client_id=client_id, + client_secret=client_secret, + requested_token_type=requested_token_type, + subject_token=subject_token, + subject_token_type=subject_token_type, + actor_token=actor_token, + actor_token_type=actor_token_type, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "OAuthTokenResponse", + '400': "OAuthError", + '401': "OAuthError", + '5XX': "OAuthError", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + def get_token_without_preload_content( + self, + grant_type: Optional[StrictStr] = None, + scope: Optional[StrictStr] = None, + client_id: Annotated[Optional[StrictStr], Field(description="Client ID This can be sent in the request body, but OAuth2 recommends sending it in a Basic Authorization header.")] = None, + client_secret: Annotated[Optional[StrictStr], Field(description="Client secret This can be sent in the request body, but OAuth2 recommends sending it in a Basic Authorization header.")] = None, + requested_token_type: Optional[TokenType] = None, + subject_token: Annotated[Optional[StrictStr], Field(description="Subject token for token exchange request")] = None, + subject_token_type: Optional[TokenType] = None, + actor_token: Annotated[Optional[StrictStr], Field(description="Actor token for token exchange request")] = None, + actor_token_type: Optional[TokenType] = None, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """Get a token using an OAuth2 flow + + Exchange credentials for a token using the OAuth2 client credentials flow or token exchange. This endpoint is used for three purposes - 1. To exchange client credentials (client ID and secret) for an access token This uses the client credentials flow. 2. To exchange a client token and an identity token for a more specific access token This uses the token exchange flow. 3. To exchange an access token for one with the same claims and a refreshed expiration period This uses the token exchange flow. For example, a catalog client may be configured with client credentials from the OAuth2 Authorization flow. This client would exchange its client ID and secret for an access token using the client credentials request with this endpoint (1). Subsequent requests would then use that access token. Some clients may also handle sessions that have additional user context. These clients would use the token exchange flow to exchange a user token (the \"subject\" token) from the session for a more specific access token for that user, using the catalog's access token as the \"actor\" token (2). The user ID token is the \"subject\" token and can be any token type allowed by the OAuth2 token exchange flow, including a unsecured JWT token with a sub claim. This request should use the catalog's bearer token in the \"Authorization\" header. Clients may also use the token exchange flow to refresh a token that is about to expire by sending a token exchange request (3). The request's \"subject\" token should be the expiring token. This request should use the subject token in the \"Authorization\" header. + + :param grant_type: + :type grant_type: str + :param scope: + :type scope: str + :param client_id: Client ID This can be sent in the request body, but OAuth2 recommends sending it in a Basic Authorization header. + :type client_id: str + :param client_secret: Client secret This can be sent in the request body, but OAuth2 recommends sending it in a Basic Authorization header. + :type client_secret: str + :param requested_token_type: + :type requested_token_type: TokenType + :param subject_token: Subject token for token exchange request + :type subject_token: str + :param subject_token_type: + :type subject_token_type: TokenType + :param actor_token: Actor token for token exchange request + :type actor_token: str + :param actor_token_type: + :type actor_token_type: TokenType + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._get_token_serialize( + grant_type=grant_type, + scope=scope, + client_id=client_id, + client_secret=client_secret, + requested_token_type=requested_token_type, + subject_token=subject_token, + subject_token_type=subject_token_type, + actor_token=actor_token, + actor_token_type=actor_token_type, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "OAuthTokenResponse", + '400': "OAuthError", + '401': "OAuthError", + '5XX': "OAuthError", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _get_token_serialize( + self, + grant_type, + scope, + client_id, + client_secret, + requested_token_type, + subject_token, + subject_token_type, + actor_token, + actor_token_type, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[str, Union[str, bytes]] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + # process the query parameters + # process the header parameters + # process the form parameters + if grant_type is not None: + _form_params.append(('grant_type', grant_type)) + if scope is not None: + _form_params.append(('scope', scope)) + if client_id is not None: + _form_params.append(('client_id', client_id)) + if client_secret is not None: + _form_params.append(('client_secret', client_secret)) + if requested_token_type is not None: + _form_params.append(('requested_token_type', requested_token_type)) + if subject_token is not None: + _form_params.append(('subject_token', subject_token)) + if subject_token_type is not None: + _form_params.append(('subject_token_type', subject_token_type)) + if actor_token is not None: + _form_params.append(('actor_token', actor_token)) + if actor_token_type is not None: + _form_params.append(('actor_token_type', actor_token_type)) + # process the body parameter + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + + # set the HTTP header `Content-Type` + if _content_type: + _header_params['Content-Type'] = _content_type + else: + _default_content_type = ( + self.api_client.select_header_content_type( + [ + 'application/x-www-form-urlencoded' + ] + ) + ) + if _default_content_type is not None: + _header_params['Content-Type'] = _default_content_type + + # authentication setting + _auth_settings: List[str] = [ + 'BearerAuth' + ] + + return self.api_client.param_serialize( + method='POST', + resource_path='/v1/oauth/tokens', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + diff --git a/regtests/client/python/polaris/catalog/api_client.py b/regtests/client/python/polaris/catalog/api_client.py new file mode 100644 index 0000000000..434429d84b --- /dev/null +++ b/regtests/client/python/polaris/catalog/api_client.py @@ -0,0 +1,788 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import datetime +from dateutil.parser import parse +from enum import Enum +import decimal +import json +import mimetypes +import os +import re +import tempfile + +from urllib.parse import quote +from typing import Tuple, Optional, List, Dict, Union +from pydantic import SecretStr + +from polaris.catalog.configuration import Configuration +from polaris.catalog.api_response import ApiResponse, T as ApiResponseT +import polaris.catalog.models +from polaris.catalog import rest +from polaris.catalog.exceptions import ( + ApiValueError, + ApiException, + BadRequestException, + UnauthorizedException, + ForbiddenException, + NotFoundException, + ServiceException +) + +RequestSerialized = Tuple[str, str, Dict[str, str], Optional[str], List[str]] + +class ApiClient: + """Generic API client for OpenAPI client library builds. + + OpenAPI generic API client. This client handles the client- + server communication, and is invariant across implementations. Specifics of + the methods and models for each application are generated from the OpenAPI + templates. + + :param configuration: .Configuration object for this client + :param header_name: a header to pass when making calls to the API. + :param header_value: a header value to pass when making calls to + the API. + :param cookie: a cookie to include in the header when making calls + to the API + """ + + PRIMITIVE_TYPES = (float, bool, bytes, str, int) + NATIVE_TYPES_MAPPING = { + 'int': int, + 'long': int, # TODO remove as only py3 is supported? + 'float': float, + 'str': str, + 'bool': bool, + 'date': datetime.date, + 'datetime': datetime.datetime, + 'decimal': decimal.Decimal, + 'object': object, + } + _pool = None + + def __init__( + self, + configuration=None, + header_name=None, + header_value=None, + cookie=None + ) -> None: + # use default configuration if none is provided + if configuration is None: + configuration = Configuration.get_default() + self.configuration = configuration + + self.rest_client = rest.RESTClientObject(configuration) + self.default_headers = {} + if header_name is not None: + self.default_headers[header_name] = header_value + self.cookie = cookie + # Set default User-Agent. + self.user_agent = 'OpenAPI-Generator/1.0.0/python' + self.client_side_validation = configuration.client_side_validation + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + pass + + @property + def user_agent(self): + """User agent for this API client""" + return self.default_headers['User-Agent'] + + @user_agent.setter + def user_agent(self, value): + self.default_headers['User-Agent'] = value + + def set_default_header(self, header_name, header_value): + self.default_headers[header_name] = header_value + + + _default = None + + @classmethod + def get_default(cls): + """Return new instance of ApiClient. + + This method returns newly created, based on default constructor, + object of ApiClient class or returns a copy of default + ApiClient. + + :return: The ApiClient object. + """ + if cls._default is None: + cls._default = ApiClient() + return cls._default + + @classmethod + def set_default(cls, default): + """Set default instance of ApiClient. + + It stores default ApiClient. + + :param default: object of ApiClient. + """ + cls._default = default + + def param_serialize( + self, + method, + resource_path, + path_params=None, + query_params=None, + header_params=None, + body=None, + post_params=None, + files=None, auth_settings=None, + collection_formats=None, + _host=None, + _request_auth=None + ) -> RequestSerialized: + + """Builds the HTTP request params needed by the request. + :param method: Method to call. + :param resource_path: Path to method endpoint. + :param path_params: Path parameters in the url. + :param query_params: Query parameters in the url. + :param header_params: Header parameters to be + placed in the request header. + :param body: Request body. + :param post_params dict: Request post form parameters, + for `application/x-www-form-urlencoded`, `multipart/form-data`. + :param auth_settings list: Auth Settings names for the request. + :param files dict: key -> filename, value -> filepath, + for `multipart/form-data`. + :param collection_formats: dict of collection formats for path, query, + header, and post parameters. + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the authentication + in the spec for a single request. + :return: tuple of form (path, http_method, query_params, header_params, + body, post_params, files) + """ + + config = self.configuration + + # header parameters + header_params = header_params or {} + header_params.update(self.default_headers) + if self.cookie: + header_params['Cookie'] = self.cookie + if header_params: + header_params = self.sanitize_for_serialization(header_params) + header_params = dict( + self.parameters_to_tuples(header_params,collection_formats) + ) + + # path parameters + if path_params: + path_params = self.sanitize_for_serialization(path_params) + path_params = self.parameters_to_tuples( + path_params, + collection_formats + ) + for k, v in path_params: + # specified safe chars, encode everything + resource_path = resource_path.replace( + '{%s}' % k, + quote(str(v), safe=config.safe_chars_for_path_param) + ) + + # post parameters + if post_params or files: + post_params = post_params if post_params else [] + post_params = self.sanitize_for_serialization(post_params) + post_params = self.parameters_to_tuples( + post_params, + collection_formats + ) + if files: + post_params.extend(self.files_parameters(files)) + + # auth setting + self.update_params_for_auth( + header_params, + query_params, + auth_settings, + resource_path, + method, + body, + request_auth=_request_auth + ) + + # body + if body: + body = self.sanitize_for_serialization(body) + + # request url + if _host is None or self.configuration.ignore_operation_servers: + url = self.configuration.host + resource_path + else: + # use server/host defined in path or operation instead + url = _host + resource_path + + # query parameters + if query_params: + query_params = self.sanitize_for_serialization(query_params) + url_query = self.parameters_to_url_query( + query_params, + collection_formats + ) + url += "?" + url_query + + return method, url, header_params, body, post_params + + + def call_api( + self, + method, + url, + header_params=None, + body=None, + post_params=None, + _request_timeout=None + ) -> rest.RESTResponse: + """Makes the HTTP request (synchronous) + :param method: Method to call. + :param url: Path to method endpoint. + :param header_params: Header parameters to be + placed in the request header. + :param body: Request body. + :param post_params dict: Request post form parameters, + for `application/x-www-form-urlencoded`, `multipart/form-data`. + :param _request_timeout: timeout setting for this request. + :return: RESTResponse + """ + + try: + # perform request and return response + response_data = self.rest_client.request( + method, url, + headers=header_params, + body=body, post_params=post_params, + _request_timeout=_request_timeout + ) + + except ApiException as e: + raise e + + return response_data + + def response_deserialize( + self, + response_data: rest.RESTResponse, + response_types_map: Optional[Dict[str, ApiResponseT]]=None + ) -> ApiResponse[ApiResponseT]: + """Deserializes response into an object. + :param response_data: RESTResponse object to be deserialized. + :param response_types_map: dict of response types. + :return: ApiResponse + """ + + msg = "RESTResponse.read() must be called before passing it to response_deserialize()" + assert response_data.data is not None, msg + + response_type = response_types_map.get(str(response_data.status), None) + if not response_type and isinstance(response_data.status, int) and 100 <= response_data.status <= 599: + # if not found, look for '1XX', '2XX', etc. + response_type = response_types_map.get(str(response_data.status)[0] + "XX", None) + + # deserialize response data + response_text = None + return_data = None + try: + if response_type == "bytearray": + return_data = response_data.data + elif response_type == "file": + return_data = self.__deserialize_file(response_data) + elif response_type is not None: + match = None + content_type = response_data.getheader('content-type') + if content_type is not None: + match = re.search(r"charset=([a-zA-Z\-\d]+)[\s;]?", content_type) + encoding = match.group(1) if match else "utf-8" + response_text = response_data.data.decode(encoding) + return_data = self.deserialize(response_text, response_type, content_type) + finally: + if not 200 <= response_data.status <= 299: + raise ApiException.from_response( + http_resp=response_data, + body=response_text, + data=return_data, + ) + + return ApiResponse( + status_code = response_data.status, + data = return_data, + headers = response_data.getheaders(), + raw_data = response_data.data + ) + + def sanitize_for_serialization(self, obj): + """Builds a JSON POST object. + + If obj is None, return None. + If obj is SecretStr, return obj.get_secret_value() + If obj is str, int, long, float, bool, return directly. + If obj is datetime.datetime, datetime.date + convert to string in iso8601 format. + If obj is decimal.Decimal return string representation. + If obj is list, sanitize each element in the list. + If obj is dict, return the dict. + If obj is OpenAPI model, return the properties dict. + + :param obj: The data to serialize. + :return: The serialized form of data. + """ + if obj is None: + return None + elif isinstance(obj, Enum): + return obj.value + elif isinstance(obj, SecretStr): + return obj.get_secret_value() + elif isinstance(obj, self.PRIMITIVE_TYPES): + return obj + elif isinstance(obj, list): + return [ + self.sanitize_for_serialization(sub_obj) for sub_obj in obj + ] + elif isinstance(obj, tuple): + return tuple( + self.sanitize_for_serialization(sub_obj) for sub_obj in obj + ) + elif isinstance(obj, (datetime.datetime, datetime.date)): + return obj.isoformat() + elif isinstance(obj, decimal.Decimal): + return str(obj) + + elif isinstance(obj, dict): + obj_dict = obj + else: + # Convert model obj to dict except + # attributes `openapi_types`, `attribute_map` + # and attributes which value is not None. + # Convert attribute name to json key in + # model definition for request. + if hasattr(obj, 'to_dict') and callable(getattr(obj, 'to_dict')): + obj_dict = obj.to_dict() + else: + obj_dict = obj.__dict__ + + return { + key: self.sanitize_for_serialization(val) + for key, val in obj_dict.items() + } + + def deserialize(self, response_text: str, response_type: str, content_type: Optional[str]): + """Deserializes response into an object. + + :param response: RESTResponse object to be deserialized. + :param response_type: class literal for + deserialized object, or string of class name. + :param content_type: content type of response. + + :return: deserialized object. + """ + + # fetch data from response object + if content_type is None: + try: + data = json.loads(response_text) + except ValueError: + data = response_text + elif content_type.startswith("application/json"): + if response_text == "": + data = "" + else: + data = json.loads(response_text) + elif content_type.startswith("text/plain"): + data = response_text + else: + raise ApiException( + status=0, + reason="Unsupported content type: {0}".format(content_type) + ) + + return self.__deserialize(data, response_type) + + def __deserialize(self, data, klass): + """Deserializes dict, list, str into an object. + + :param data: dict, list or str. + :param klass: class literal, or string of class name. + + :return: object. + """ + if data is None: + return None + + if isinstance(klass, str): + if klass.startswith('List['): + m = re.match(r'List\[(.*)]', klass) + assert m is not None, "Malformed List type definition" + sub_kls = m.group(1) + return [self.__deserialize(sub_data, sub_kls) + for sub_data in data] + + if klass.startswith('Dict['): + m = re.match(r'Dict\[([^,]*), (.*)]', klass) + assert m is not None, "Malformed Dict type definition" + sub_kls = m.group(2) + return {k: self.__deserialize(v, sub_kls) + for k, v in data.items()} + + # convert str to class + if klass in self.NATIVE_TYPES_MAPPING: + klass = self.NATIVE_TYPES_MAPPING[klass] + else: + klass = getattr(polaris.catalog.models, klass) + + if klass in self.PRIMITIVE_TYPES: + return self.__deserialize_primitive(data, klass) + elif klass == object: + return self.__deserialize_object(data) + elif klass == datetime.date: + return self.__deserialize_date(data) + elif klass == datetime.datetime: + return self.__deserialize_datetime(data) + elif klass == decimal.Decimal: + return decimal.Decimal(data) + elif issubclass(klass, Enum): + return self.__deserialize_enum(data, klass) + else: + return self.__deserialize_model(data, klass) + + def parameters_to_tuples(self, params, collection_formats): + """Get parameters as list of tuples, formatting collections. + + :param params: Parameters as dict or list of two-tuples + :param dict collection_formats: Parameter collection formats + :return: Parameters as list of tuples, collections formatted + """ + new_params: List[Tuple[str, str]] = [] + if collection_formats is None: + collection_formats = {} + for k, v in params.items() if isinstance(params, dict) else params: + if k in collection_formats: + collection_format = collection_formats[k] + if collection_format == 'multi': + new_params.extend((k, value) for value in v) + else: + if collection_format == 'ssv': + delimiter = ' ' + elif collection_format == 'tsv': + delimiter = '\t' + elif collection_format == 'pipes': + delimiter = '|' + else: # csv is the default + delimiter = ',' + new_params.append( + (k, delimiter.join(str(value) for value in v))) + else: + new_params.append((k, v)) + return new_params + + def parameters_to_url_query(self, params, collection_formats): + """Get parameters as list of tuples, formatting collections. + + :param params: Parameters as dict or list of two-tuples + :param dict collection_formats: Parameter collection formats + :return: URL query string (e.g. a=Hello%20World&b=123) + """ + new_params: List[Tuple[str, str]] = [] + if collection_formats is None: + collection_formats = {} + for k, v in params.items() if isinstance(params, dict) else params: + if isinstance(v, bool): + v = str(v).lower() + if isinstance(v, (int, float)): + v = str(v) + if isinstance(v, dict): + v = json.dumps(v) + + if k in collection_formats: + collection_format = collection_formats[k] + if collection_format == 'multi': + new_params.extend((k, str(value)) for value in v) + else: + if collection_format == 'ssv': + delimiter = ' ' + elif collection_format == 'tsv': + delimiter = '\t' + elif collection_format == 'pipes': + delimiter = '|' + else: # csv is the default + delimiter = ',' + new_params.append( + (k, delimiter.join(quote(str(value)) for value in v)) + ) + else: + new_params.append((k, quote(str(v)))) + + return "&".join(["=".join(map(str, item)) for item in new_params]) + + def files_parameters(self, files: Dict[str, Union[str, bytes]]): + """Builds form parameters. + + :param files: File parameters. + :return: Form parameters with files. + """ + params = [] + for k, v in files.items(): + if isinstance(v, str): + with open(v, 'rb') as f: + filename = os.path.basename(f.name) + filedata = f.read() + elif isinstance(v, bytes): + filename = k + filedata = v + else: + raise ValueError("Unsupported file value") + mimetype = ( + mimetypes.guess_type(filename)[0] + or 'application/octet-stream' + ) + params.append( + tuple([k, tuple([filename, filedata, mimetype])]) + ) + return params + + def select_header_accept(self, accepts: List[str]) -> Optional[str]: + """Returns `Accept` based on an array of accepts provided. + + :param accepts: List of headers. + :return: Accept (e.g. application/json). + """ + if not accepts: + return None + + for accept in accepts: + if re.search('json', accept, re.IGNORECASE): + return accept + + return accepts[0] + + def select_header_content_type(self, content_types): + """Returns `Content-Type` based on an array of content_types provided. + + :param content_types: List of content-types. + :return: Content-Type (e.g. application/json). + """ + if not content_types: + return None + + for content_type in content_types: + if re.search('json', content_type, re.IGNORECASE): + return content_type + + return content_types[0] + + def update_params_for_auth( + self, + headers, + queries, + auth_settings, + resource_path, + method, + body, + request_auth=None + ) -> None: + """Updates header and query params based on authentication setting. + + :param headers: Header parameters dict to be updated. + :param queries: Query parameters tuple list to be updated. + :param auth_settings: Authentication setting identifiers list. + :resource_path: A string representation of the HTTP request resource path. + :method: A string representation of the HTTP request method. + :body: A object representing the body of the HTTP request. + The object type is the return value of sanitize_for_serialization(). + :param request_auth: if set, the provided settings will + override the token in the configuration. + """ + if not auth_settings: + return + + if request_auth: + self._apply_auth_params( + headers, + queries, + resource_path, + method, + body, + request_auth + ) + else: + for auth in auth_settings: + auth_setting = self.configuration.auth_settings().get(auth) + if auth_setting: + self._apply_auth_params( + headers, + queries, + resource_path, + method, + body, + auth_setting + ) + + def _apply_auth_params( + self, + headers, + queries, + resource_path, + method, + body, + auth_setting + ) -> None: + """Updates the request parameters based on a single auth_setting + + :param headers: Header parameters dict to be updated. + :param queries: Query parameters tuple list to be updated. + :resource_path: A string representation of the HTTP request resource path. + :method: A string representation of the HTTP request method. + :body: A object representing the body of the HTTP request. + The object type is the return value of sanitize_for_serialization(). + :param auth_setting: auth settings for the endpoint + """ + if auth_setting['in'] == 'cookie': + headers['Cookie'] = auth_setting['value'] + elif auth_setting['in'] == 'header': + if auth_setting['type'] != 'http-signature': + headers[auth_setting['key']] = auth_setting['value'] + elif auth_setting['in'] == 'query': + queries.append((auth_setting['key'], auth_setting['value'])) + else: + raise ApiValueError( + 'Authentication token must be in `query` or `header`' + ) + + def __deserialize_file(self, response): + """Deserializes body to file + + Saves response body into a file in a temporary folder, + using the filename from the `Content-Disposition` header if provided. + + handle file downloading + save response body into a tmp file and return the instance + + :param response: RESTResponse. + :return: file path. + """ + fd, path = tempfile.mkstemp(dir=self.configuration.temp_folder_path) + os.close(fd) + os.remove(path) + + content_disposition = response.getheader("Content-Disposition") + if content_disposition: + m = re.search( + r'filename=[\'"]?([^\'"\s]+)[\'"]?', + content_disposition + ) + assert m is not None, "Unexpected 'content-disposition' header value" + filename = m.group(1) + path = os.path.join(os.path.dirname(path), filename) + + with open(path, "wb") as f: + f.write(response.data) + + return path + + def __deserialize_primitive(self, data, klass): + """Deserializes string to primitive type. + + :param data: str. + :param klass: class literal. + + :return: int, long, float, str, bool. + """ + try: + return klass(data) + except UnicodeEncodeError: + return str(data) + except TypeError: + return data + + def __deserialize_object(self, value): + """Return an original value. + + :return: object. + """ + return value + + def __deserialize_date(self, string): + """Deserializes string to date. + + :param string: str. + :return: date. + """ + try: + return parse(string).date() + except ImportError: + return string + except ValueError: + raise rest.ApiException( + status=0, + reason="Failed to parse `{0}` as date object".format(string) + ) + + def __deserialize_datetime(self, string): + """Deserializes string to datetime. + + The string should be in iso8601 datetime format. + + :param string: str. + :return: datetime. + """ + try: + return parse(string) + except ImportError: + return string + except ValueError: + raise rest.ApiException( + status=0, + reason=( + "Failed to parse `{0}` as datetime object" + .format(string) + ) + ) + + def __deserialize_enum(self, data, klass): + """Deserializes primitive type to enum. + + :param data: primitive type. + :param klass: class literal. + :return: enum value. + """ + try: + return klass(data) + except ValueError: + raise rest.ApiException( + status=0, + reason=( + "Failed to parse `{0}` as `{1}`" + .format(data, klass) + ) + ) + + def __deserialize_model(self, data, klass): + """Deserializes list or dict to model. + + :param data: dict, list. + :param klass: class literal. + :return: model object. + """ + + return klass.from_dict(data) diff --git a/regtests/client/python/polaris/catalog/api_response.py b/regtests/client/python/polaris/catalog/api_response.py new file mode 100644 index 0000000000..9bc7c11f6b --- /dev/null +++ b/regtests/client/python/polaris/catalog/api_response.py @@ -0,0 +1,21 @@ +"""API response object.""" + +from __future__ import annotations +from typing import Optional, Generic, Mapping, TypeVar +from pydantic import Field, StrictInt, StrictBytes, BaseModel + +T = TypeVar("T") + +class ApiResponse(BaseModel, Generic[T]): + """ + API response object + """ + + status_code: StrictInt = Field(description="HTTP status code") + headers: Optional[Mapping[str, str]] = Field(None, description="HTTP headers") + data: T = Field(description="Deserialized data given the data type") + raw_data: StrictBytes = Field(description="Raw data (HTTP response body)") + + model_config = { + "arbitrary_types_allowed": True + } diff --git a/regtests/client/python/polaris/catalog/configuration.py b/regtests/client/python/polaris/catalog/configuration.py new file mode 100644 index 0000000000..52dea68e3d --- /dev/null +++ b/regtests/client/python/polaris/catalog/configuration.py @@ -0,0 +1,501 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import copy +import logging +from logging import FileHandler +import multiprocessing +import sys +from typing import Optional +import urllib3 + +import http.client as httplib + +JSON_SCHEMA_VALIDATION_KEYWORDS = { + 'multipleOf', 'maximum', 'exclusiveMaximum', + 'minimum', 'exclusiveMinimum', 'maxLength', + 'minLength', 'pattern', 'maxItems', 'minItems' +} + +class Configuration: + """This class contains various settings of the API client. + + :param host: Base url. + :param ignore_operation_servers + Boolean to ignore operation servers for the API client. + Config will use `host` as the base url regardless of the operation servers. + :param api_key: Dict to store API key(s). + Each entry in the dict specifies an API key. + The dict key is the name of the security scheme in the OAS specification. + The dict value is the API key secret. + :param api_key_prefix: Dict to store API prefix (e.g. Bearer). + The dict key is the name of the security scheme in the OAS specification. + The dict value is an API key prefix when generating the auth data. + :param username: Username for HTTP basic authentication. + :param password: Password for HTTP basic authentication. + :param access_token: Access token. + :param server_index: Index to servers configuration. + :param server_variables: Mapping with string values to replace variables in + templated server configuration. The validation of enums is performed for + variables with defined enum values before. + :param server_operation_index: Mapping from operation ID to an index to server + configuration. + :param server_operation_variables: Mapping from operation ID to a mapping with + string values to replace variables in templated server configuration. + The validation of enums is performed for variables with defined enum + values before. + :param ssl_ca_cert: str - the path to a file of concatenated CA certificates + in PEM format. + :param retries: Number of retries for API requests. + + :Example: + """ + + _default = None + + def __init__(self, host=None, + api_key=None, api_key_prefix=None, + username=None, password=None, + access_token=None, + server_index=None, server_variables=None, + server_operation_index=None, server_operation_variables=None, + ignore_operation_servers=False, + ssl_ca_cert=None, + retries=None, + *, + debug: Optional[bool] = None + ) -> None: + """Constructor + """ + self._base_path = "https://localhost" if host is None else host + """Default Base url + """ + self.server_index = 0 if server_index is None and host is None else server_index + self.server_operation_index = server_operation_index or {} + """Default server index + """ + self.server_variables = server_variables or {} + self.server_operation_variables = server_operation_variables or {} + """Default server variables + """ + self.ignore_operation_servers = ignore_operation_servers + """Ignore operation servers + """ + self.temp_folder_path = None + """Temp file folder for downloading files + """ + # Authentication Settings + self.api_key = {} + if api_key: + self.api_key = api_key + """dict to store API key(s) + """ + self.api_key_prefix = {} + if api_key_prefix: + self.api_key_prefix = api_key_prefix + """dict to store API prefix (e.g. Bearer) + """ + self.refresh_api_key_hook = None + """function hook to refresh API key if expired + """ + self.username = username + """Username for HTTP basic authentication + """ + self.password = password + """Password for HTTP basic authentication + """ + self.access_token = access_token + """Access token + """ + self.logger = {} + """Logging Settings + """ + self.logger["package_logger"] = logging.getLogger("polaris.catalog") + self.logger["urllib3_logger"] = logging.getLogger("urllib3") + self.logger_format = '%(asctime)s %(levelname)s %(message)s' + """Log format + """ + self.logger_stream_handler = None + """Log stream handler + """ + self.logger_file_handler: Optional[FileHandler] = None + """Log file handler + """ + self.logger_file = None + """Debug file location + """ + if debug is not None: + self.debug = debug + else: + self.__debug = False + """Debug switch + """ + + self.verify_ssl = True + """SSL/TLS verification + Set this to false to skip verifying SSL certificate when calling API + from https server. + """ + self.ssl_ca_cert = ssl_ca_cert + """Set this to customize the certificate file to verify the peer. + """ + self.cert_file = None + """client certificate file + """ + self.key_file = None + """client key file + """ + self.assert_hostname = None + """Set this to True/False to enable/disable SSL hostname verification. + """ + self.tls_server_name = None + """SSL/TLS Server Name Indication (SNI) + Set this to the SNI value expected by the server. + """ + + self.connection_pool_maxsize = multiprocessing.cpu_count() * 5 + """urllib3 connection pool's maximum number of connections saved + per pool. urllib3 uses 1 connection as default value, but this is + not the best value when you are making a lot of possibly parallel + requests to the same host, which is often the case here. + cpu_count * 5 is used as default value to increase performance. + """ + + self.proxy: Optional[str] = None + """Proxy URL + """ + self.proxy_headers = None + """Proxy headers + """ + self.safe_chars_for_path_param = '' + """Safe chars for path_param + """ + self.retries = retries + """Adding retries to override urllib3 default value 3 + """ + # Enable client side validation + self.client_side_validation = True + + self.socket_options = None + """Options to pass down to the underlying urllib3 socket + """ + + self.datetime_format = "%Y-%m-%dT%H:%M:%S.%f%z" + """datetime format + """ + + self.date_format = "%Y-%m-%d" + """date format + """ + + def __deepcopy__(self, memo): + cls = self.__class__ + result = cls.__new__(cls) + memo[id(self)] = result + for k, v in self.__dict__.items(): + if k not in ('logger', 'logger_file_handler'): + setattr(result, k, copy.deepcopy(v, memo)) + # shallow copy of loggers + result.logger = copy.copy(self.logger) + # use setters to configure loggers + result.logger_file = self.logger_file + result.debug = self.debug + return result + + def __setattr__(self, name, value): + object.__setattr__(self, name, value) + + @classmethod + def set_default(cls, default): + """Set default instance of configuration. + + It stores default configuration, which can be + returned by get_default_copy method. + + :param default: object of Configuration + """ + cls._default = default + + @classmethod + def get_default_copy(cls): + """Deprecated. Please use `get_default` instead. + + Deprecated. Please use `get_default` instead. + + :return: The configuration object. + """ + return cls.get_default() + + @classmethod + def get_default(cls): + """Return the default configuration. + + This method returns newly created, based on default constructor, + object of Configuration class or returns a copy of default + configuration. + + :return: The configuration object. + """ + if cls._default is None: + cls._default = Configuration() + return cls._default + + @property + def logger_file(self): + """The logger file. + + If the logger_file is None, then add stream handler and remove file + handler. Otherwise, add file handler and remove stream handler. + + :param value: The logger_file path. + :type: str + """ + return self.__logger_file + + @logger_file.setter + def logger_file(self, value): + """The logger file. + + If the logger_file is None, then add stream handler and remove file + handler. Otherwise, add file handler and remove stream handler. + + :param value: The logger_file path. + :type: str + """ + self.__logger_file = value + if self.__logger_file: + # If set logging file, + # then add file handler and remove stream handler. + self.logger_file_handler = logging.FileHandler(self.__logger_file) + self.logger_file_handler.setFormatter(self.logger_formatter) + for _, logger in self.logger.items(): + logger.addHandler(self.logger_file_handler) + + @property + def debug(self): + """Debug status + + :param value: The debug status, True or False. + :type: bool + """ + return self.__debug + + @debug.setter + def debug(self, value): + """Debug status + + :param value: The debug status, True or False. + :type: bool + """ + self.__debug = value + if self.__debug: + # if debug status is True, turn on debug logging + for _, logger in self.logger.items(): + logger.setLevel(logging.DEBUG) + # turn on httplib debug + httplib.HTTPConnection.debuglevel = 1 + else: + # if debug status is False, turn off debug logging, + # setting log level to default `logging.WARNING` + for _, logger in self.logger.items(): + logger.setLevel(logging.WARNING) + # turn off httplib debug + httplib.HTTPConnection.debuglevel = 0 + + @property + def logger_format(self): + """The logger format. + + The logger_formatter will be updated when sets logger_format. + + :param value: The format string. + :type: str + """ + return self.__logger_format + + @logger_format.setter + def logger_format(self, value): + """The logger format. + + The logger_formatter will be updated when sets logger_format. + + :param value: The format string. + :type: str + """ + self.__logger_format = value + self.logger_formatter = logging.Formatter(self.__logger_format) + + def get_api_key_with_prefix(self, identifier, alias=None): + """Gets API key (with prefix if set). + + :param identifier: The identifier of apiKey. + :param alias: The alternative identifier of apiKey. + :return: The token for api key authentication. + """ + if self.refresh_api_key_hook is not None: + self.refresh_api_key_hook(self) + key = self.api_key.get(identifier, self.api_key.get(alias) if alias is not None else None) + if key: + prefix = self.api_key_prefix.get(identifier) + if prefix: + return "%s %s" % (prefix, key) + else: + return key + + def get_basic_auth_token(self): + """Gets HTTP basic authentication header (string). + + :return: The token for basic HTTP authentication. + """ + username = "" + if self.username is not None: + username = self.username + password = "" + if self.password is not None: + password = self.password + return urllib3.util.make_headers( + basic_auth=username + ':' + password + ).get('authorization') + + def auth_settings(self): + """Gets Auth Settings dict for api client. + + :return: The Auth Settings information dict. + """ + auth = {} + if self.access_token is not None: + auth['OAuth2'] = { + 'type': 'oauth2', + 'in': 'header', + 'key': 'Authorization', + 'value': 'Bearer ' + self.access_token + } + if self.access_token is not None: + auth['BearerAuth'] = { + 'type': 'bearer', + 'in': 'header', + 'key': 'Authorization', + 'value': 'Bearer ' + self.access_token + } + return auth + + def to_debug_report(self): + """Gets the essential information for debugging. + + :return: The report for debugging. + """ + return "Python SDK Debug Report:\n"\ + "OS: {env}\n"\ + "Python Version: {pyversion}\n"\ + "Version of the API: 0.0.1\n"\ + "SDK Package Version: 1.0.0".\ + format(env=sys.platform, pyversion=sys.version) + + def get_host_settings(self): + """Gets an array of host settings + + :return: An array of host settings + """ + return [ + { + 'url': "{scheme}://{host}/{basePath}", + 'description': "Server URL when the port can be inferred from the scheme", + 'variables': { + 'scheme': { + 'description': "The scheme of the URI, either http or https.", + 'default_value': "https", + }, + 'host': { + 'description': "The host address for the specified server", + 'default_value': "localhost", + }, + 'basePath': { + 'description': "Optional prefix to be appended to all routes", + 'default_value': "", + } + } + }, + { + 'url': "{scheme}://{host}:{port}/{basePath}", + 'description': "Generic base server URL, with all parts configurable", + 'variables': { + 'scheme': { + 'description': "The scheme of the URI, either http or https.", + 'default_value': "https", + }, + 'host': { + 'description': "The host address for the specified server", + 'default_value': "localhost", + }, + 'port': { + 'description': "The port used when addressing the host", + 'default_value': "443", + }, + 'basePath': { + 'description': "Optional prefix to be appended to all routes", + 'default_value': "", + } + } + } + ] + + def get_host_from_settings(self, index, variables=None, servers=None): + """Gets host URL based on the index and variables + :param index: array index of the host settings + :param variables: hash of variable and the corresponding value + :param servers: an array of host settings or None + :return: URL based on host settings + """ + if index is None: + return self._base_path + + variables = {} if variables is None else variables + servers = self.get_host_settings() if servers is None else servers + + try: + server = servers[index] + except IndexError: + raise ValueError( + "Invalid index {0} when selecting the host settings. " + "Must be less than {1}".format(index, len(servers))) + + url = server['url'] + + # go through variables and replace placeholders + for variable_name, variable in server.get('variables', {}).items(): + used_value = variables.get( + variable_name, variable['default_value']) + + if 'enum_values' in variable \ + and used_value not in variable['enum_values']: + raise ValueError( + "The variable `{0}` in the host URL has invalid value " + "{1}. Must be {2}.".format( + variable_name, variables[variable_name], + variable['enum_values'])) + + url = url.replace("{" + variable_name + "}", used_value) + + return url + + @property + def host(self): + """Return generated host.""" + return self.get_host_from_settings(self.server_index, variables=self.server_variables) + + @host.setter + def host(self, value): + """Fix base path.""" + self._base_path = value + self.server_index = None diff --git a/regtests/client/python/polaris/catalog/exceptions.py b/regtests/client/python/polaris/catalog/exceptions.py new file mode 100644 index 0000000000..cf15eaa377 --- /dev/null +++ b/regtests/client/python/polaris/catalog/exceptions.py @@ -0,0 +1,199 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + +from typing import Any, Optional +from typing_extensions import Self + +class OpenApiException(Exception): + """The base exception class for all OpenAPIExceptions""" + + +class ApiTypeError(OpenApiException, TypeError): + def __init__(self, msg, path_to_item=None, valid_classes=None, + key_type=None) -> None: + """ Raises an exception for TypeErrors + + Args: + msg (str): the exception message + + Keyword Args: + path_to_item (list): a list of keys an indices to get to the + current_item + None if unset + valid_classes (tuple): the primitive classes that current item + should be an instance of + None if unset + key_type (bool): False if our value is a value in a dict + True if it is a key in a dict + False if our item is an item in a list + None if unset + """ + self.path_to_item = path_to_item + self.valid_classes = valid_classes + self.key_type = key_type + full_msg = msg + if path_to_item: + full_msg = "{0} at {1}".format(msg, render_path(path_to_item)) + super(ApiTypeError, self).__init__(full_msg) + + +class ApiValueError(OpenApiException, ValueError): + def __init__(self, msg, path_to_item=None) -> None: + """ + Args: + msg (str): the exception message + + Keyword Args: + path_to_item (list) the path to the exception in the + received_data dict. None if unset + """ + + self.path_to_item = path_to_item + full_msg = msg + if path_to_item: + full_msg = "{0} at {1}".format(msg, render_path(path_to_item)) + super(ApiValueError, self).__init__(full_msg) + + +class ApiAttributeError(OpenApiException, AttributeError): + def __init__(self, msg, path_to_item=None) -> None: + """ + Raised when an attribute reference or assignment fails. + + Args: + msg (str): the exception message + + Keyword Args: + path_to_item (None/list) the path to the exception in the + received_data dict + """ + self.path_to_item = path_to_item + full_msg = msg + if path_to_item: + full_msg = "{0} at {1}".format(msg, render_path(path_to_item)) + super(ApiAttributeError, self).__init__(full_msg) + + +class ApiKeyError(OpenApiException, KeyError): + def __init__(self, msg, path_to_item=None) -> None: + """ + Args: + msg (str): the exception message + + Keyword Args: + path_to_item (None/list) the path to the exception in the + received_data dict + """ + self.path_to_item = path_to_item + full_msg = msg + if path_to_item: + full_msg = "{0} at {1}".format(msg, render_path(path_to_item)) + super(ApiKeyError, self).__init__(full_msg) + + +class ApiException(OpenApiException): + + def __init__( + self, + status=None, + reason=None, + http_resp=None, + *, + body: Optional[str] = None, + data: Optional[Any] = None, + ) -> None: + self.status = status + self.reason = reason + self.body = body + self.data = data + self.headers = None + + if http_resp: + if self.status is None: + self.status = http_resp.status + if self.reason is None: + self.reason = http_resp.reason + if self.body is None: + try: + self.body = http_resp.data.decode('utf-8') + except Exception: + pass + self.headers = http_resp.getheaders() + + @classmethod + def from_response( + cls, + *, + http_resp, + body: Optional[str], + data: Optional[Any], + ) -> Self: + if http_resp.status == 400: + raise BadRequestException(http_resp=http_resp, body=body, data=data) + + if http_resp.status == 401: + raise UnauthorizedException(http_resp=http_resp, body=body, data=data) + + if http_resp.status == 403: + raise ForbiddenException(http_resp=http_resp, body=body, data=data) + + if http_resp.status == 404: + raise NotFoundException(http_resp=http_resp, body=body, data=data) + + if 500 <= http_resp.status <= 599: + raise ServiceException(http_resp=http_resp, body=body, data=data) + raise ApiException(http_resp=http_resp, body=body, data=data) + + def __str__(self): + """Custom error messages for exception""" + error_message = "({0})\n"\ + "Reason: {1}\n".format(self.status, self.reason) + if self.headers: + error_message += "HTTP response headers: {0}\n".format( + self.headers) + + if self.data or self.body: + error_message += "HTTP response body: {0}\n".format(self.data or self.body) + + return error_message + + +class BadRequestException(ApiException): + pass + + +class NotFoundException(ApiException): + pass + + +class UnauthorizedException(ApiException): + pass + + +class ForbiddenException(ApiException): + pass + + +class ServiceException(ApiException): + pass + + +def render_path(path_to_item): + """Returns a string representation of a path""" + result = "" + for pth in path_to_item: + if isinstance(pth, int): + result += "[{0}]".format(pth) + else: + result += "['{0}']".format(pth) + return result diff --git a/regtests/client/python/polaris/catalog/models/__init__.py b/regtests/client/python/polaris/catalog/models/__init__.py new file mode 100644 index 0000000000..10a07c8f53 --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/__init__.py @@ -0,0 +1,126 @@ +# coding: utf-8 + +# flake8: noqa +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +# import models into model package +from polaris.catalog.models.add_partition_spec_update import AddPartitionSpecUpdate +from polaris.catalog.models.add_schema_update import AddSchemaUpdate +from polaris.catalog.models.add_snapshot_update import AddSnapshotUpdate +from polaris.catalog.models.add_sort_order_update import AddSortOrderUpdate +from polaris.catalog.models.add_view_version_update import AddViewVersionUpdate +from polaris.catalog.models.and_or_expression import AndOrExpression +from polaris.catalog.models.assert_create import AssertCreate +from polaris.catalog.models.assert_current_schema_id import AssertCurrentSchemaId +from polaris.catalog.models.assert_default_sort_order_id import AssertDefaultSortOrderId +from polaris.catalog.models.assert_default_spec_id import AssertDefaultSpecId +from polaris.catalog.models.assert_last_assigned_field_id import AssertLastAssignedFieldId +from polaris.catalog.models.assert_last_assigned_partition_id import AssertLastAssignedPartitionId +from polaris.catalog.models.assert_ref_snapshot_id import AssertRefSnapshotId +from polaris.catalog.models.assert_table_uuid import AssertTableUUID +from polaris.catalog.models.assert_view_uuid import AssertViewUUID +from polaris.catalog.models.assign_uuid_update import AssignUUIDUpdate +from polaris.catalog.models.base_update import BaseUpdate +from polaris.catalog.models.blob_metadata import BlobMetadata +from polaris.catalog.models.catalog_config import CatalogConfig +from polaris.catalog.models.commit_report import CommitReport +from polaris.catalog.models.commit_table_request import CommitTableRequest +from polaris.catalog.models.commit_table_response import CommitTableResponse +from polaris.catalog.models.commit_transaction_request import CommitTransactionRequest +from polaris.catalog.models.commit_view_request import CommitViewRequest +from polaris.catalog.models.content_file import ContentFile +from polaris.catalog.models.count_map import CountMap +from polaris.catalog.models.counter_result import CounterResult +from polaris.catalog.models.create_namespace_request import CreateNamespaceRequest +from polaris.catalog.models.create_namespace_response import CreateNamespaceResponse +from polaris.catalog.models.create_table_request import CreateTableRequest +from polaris.catalog.models.create_view_request import CreateViewRequest +from polaris.catalog.models.data_file import DataFile +from polaris.catalog.models.equality_delete_file import EqualityDeleteFile +from polaris.catalog.models.error_model import ErrorModel +from polaris.catalog.models.expression import Expression +from polaris.catalog.models.file_format import FileFormat +from polaris.catalog.models.get_namespace_response import GetNamespaceResponse +from polaris.catalog.models.iceberg_error_response import IcebergErrorResponse +from polaris.catalog.models.list_namespaces_response import ListNamespacesResponse +from polaris.catalog.models.list_tables_response import ListTablesResponse +from polaris.catalog.models.list_type import ListType +from polaris.catalog.models.literal_expression import LiteralExpression +from polaris.catalog.models.load_table_result import LoadTableResult +from polaris.catalog.models.load_view_result import LoadViewResult +from polaris.catalog.models.map_type import MapType +from polaris.catalog.models.metadata_log_inner import MetadataLogInner +from polaris.catalog.models.metric_result import MetricResult +from polaris.catalog.models.model_schema import ModelSchema +from polaris.catalog.models.not_expression import NotExpression +from polaris.catalog.models.notification_request import NotificationRequest +from polaris.catalog.models.notification_type import NotificationType +from polaris.catalog.models.null_order import NullOrder +from polaris.catalog.models.o_auth_error import OAuthError +from polaris.catalog.models.o_auth_token_response import OAuthTokenResponse +from polaris.catalog.models.partition_field import PartitionField +from polaris.catalog.models.partition_spec import PartitionSpec +from polaris.catalog.models.partition_statistics_file import PartitionStatisticsFile +from polaris.catalog.models.position_delete_file import PositionDeleteFile +from polaris.catalog.models.primitive_type_value import PrimitiveTypeValue +from polaris.catalog.models.register_table_request import RegisterTableRequest +from polaris.catalog.models.remove_partition_statistics_update import RemovePartitionStatisticsUpdate +from polaris.catalog.models.remove_properties_update import RemovePropertiesUpdate +from polaris.catalog.models.remove_snapshot_ref_update import RemoveSnapshotRefUpdate +from polaris.catalog.models.remove_snapshots_update import RemoveSnapshotsUpdate +from polaris.catalog.models.remove_statistics_update import RemoveStatisticsUpdate +from polaris.catalog.models.rename_table_request import RenameTableRequest +from polaris.catalog.models.report_metrics_request import ReportMetricsRequest +from polaris.catalog.models.sql_view_representation import SQLViewRepresentation +from polaris.catalog.models.scan_report import ScanReport +from polaris.catalog.models.set_current_schema_update import SetCurrentSchemaUpdate +from polaris.catalog.models.set_current_view_version_update import SetCurrentViewVersionUpdate +from polaris.catalog.models.set_default_sort_order_update import SetDefaultSortOrderUpdate +from polaris.catalog.models.set_default_spec_update import SetDefaultSpecUpdate +from polaris.catalog.models.set_expression import SetExpression +from polaris.catalog.models.set_location_update import SetLocationUpdate +from polaris.catalog.models.set_partition_statistics_update import SetPartitionStatisticsUpdate +from polaris.catalog.models.set_properties_update import SetPropertiesUpdate +from polaris.catalog.models.set_snapshot_ref_update import SetSnapshotRefUpdate +from polaris.catalog.models.set_statistics_update import SetStatisticsUpdate +from polaris.catalog.models.snapshot import Snapshot +from polaris.catalog.models.snapshot_log_inner import SnapshotLogInner +from polaris.catalog.models.snapshot_reference import SnapshotReference +from polaris.catalog.models.snapshot_summary import SnapshotSummary +from polaris.catalog.models.sort_direction import SortDirection +from polaris.catalog.models.sort_field import SortField +from polaris.catalog.models.sort_order import SortOrder +from polaris.catalog.models.statistics_file import StatisticsFile +from polaris.catalog.models.struct_field import StructField +from polaris.catalog.models.struct_type import StructType +from polaris.catalog.models.table_identifier import TableIdentifier +from polaris.catalog.models.table_metadata import TableMetadata +from polaris.catalog.models.table_requirement import TableRequirement +from polaris.catalog.models.table_update import TableUpdate +from polaris.catalog.models.table_update_notification import TableUpdateNotification +from polaris.catalog.models.term import Term +from polaris.catalog.models.timer_result import TimerResult +from polaris.catalog.models.token_type import TokenType +from polaris.catalog.models.transform_term import TransformTerm +from polaris.catalog.models.type import Type +from polaris.catalog.models.unary_expression import UnaryExpression +from polaris.catalog.models.update_namespace_properties_request import UpdateNamespacePropertiesRequest +from polaris.catalog.models.update_namespace_properties_response import UpdateNamespacePropertiesResponse +from polaris.catalog.models.upgrade_format_version_update import UpgradeFormatVersionUpdate +from polaris.catalog.models.value_map import ValueMap +from polaris.catalog.models.view_history_entry import ViewHistoryEntry +from polaris.catalog.models.view_metadata import ViewMetadata +from polaris.catalog.models.view_representation import ViewRepresentation +from polaris.catalog.models.view_requirement import ViewRequirement +from polaris.catalog.models.view_update import ViewUpdate +from polaris.catalog.models.view_version import ViewVersion diff --git a/regtests/client/python/polaris/catalog/models/add_partition_spec_update.py b/regtests/client/python/polaris/catalog/models/add_partition_spec_update.py new file mode 100644 index 0000000000..f163977cb9 --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/add_partition_spec_update.py @@ -0,0 +1,97 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import ConfigDict, StrictStr, field_validator +from typing import Any, ClassVar, Dict, List +from polaris.catalog.models.base_update import BaseUpdate +from polaris.catalog.models.partition_spec import PartitionSpec +from typing import Optional, Set +from typing_extensions import Self + +class AddPartitionSpecUpdate(BaseUpdate): + """ + AddPartitionSpecUpdate + """ # noqa: E501 + action: StrictStr + spec: PartitionSpec + __properties: ClassVar[List[str]] = ["action"] + + @field_validator('action') + def action_validate_enum(cls, value): + """Validates the enum""" + if value not in set(['add-spec']): + raise ValueError("must be one of enum values ('add-spec')") + return value + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of AddPartitionSpecUpdate from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of AddPartitionSpecUpdate from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "action": obj.get("action") + }) + return _obj + + diff --git a/regtests/client/python/polaris/catalog/models/add_schema_update.py b/regtests/client/python/polaris/catalog/models/add_schema_update.py new file mode 100644 index 0000000000..46a4999dff --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/add_schema_update.py @@ -0,0 +1,98 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import ConfigDict, Field, StrictInt, StrictStr, field_validator +from typing import Any, ClassVar, Dict, List, Optional +from polaris.catalog.models.base_update import BaseUpdate +from polaris.catalog.models.model_schema import ModelSchema +from typing import Optional, Set +from typing_extensions import Self + +class AddSchemaUpdate(BaseUpdate): + """ + AddSchemaUpdate + """ # noqa: E501 + action: StrictStr + var_schema: ModelSchema = Field(alias="schema") + last_column_id: Optional[StrictInt] = Field(default=None, description="The highest assigned column ID for the table. This is used to ensure columns are always assigned an unused ID when evolving schemas. When omitted, it will be computed on the server side.", alias="last-column-id") + __properties: ClassVar[List[str]] = ["action"] + + @field_validator('action') + def action_validate_enum(cls, value): + """Validates the enum""" + if value not in set(['add-schema']): + raise ValueError("must be one of enum values ('add-schema')") + return value + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of AddSchemaUpdate from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of AddSchemaUpdate from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "action": obj.get("action") + }) + return _obj + + diff --git a/regtests/client/python/polaris/catalog/models/add_snapshot_update.py b/regtests/client/python/polaris/catalog/models/add_snapshot_update.py new file mode 100644 index 0000000000..048a524739 --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/add_snapshot_update.py @@ -0,0 +1,97 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import ConfigDict, StrictStr, field_validator +from typing import Any, ClassVar, Dict, List +from polaris.catalog.models.base_update import BaseUpdate +from polaris.catalog.models.snapshot import Snapshot +from typing import Optional, Set +from typing_extensions import Self + +class AddSnapshotUpdate(BaseUpdate): + """ + AddSnapshotUpdate + """ # noqa: E501 + action: StrictStr + snapshot: Snapshot + __properties: ClassVar[List[str]] = ["action"] + + @field_validator('action') + def action_validate_enum(cls, value): + """Validates the enum""" + if value not in set(['add-snapshot']): + raise ValueError("must be one of enum values ('add-snapshot')") + return value + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of AddSnapshotUpdate from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of AddSnapshotUpdate from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "action": obj.get("action") + }) + return _obj + + diff --git a/regtests/client/python/polaris/catalog/models/add_sort_order_update.py b/regtests/client/python/polaris/catalog/models/add_sort_order_update.py new file mode 100644 index 0000000000..3e2145b319 --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/add_sort_order_update.py @@ -0,0 +1,97 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import ConfigDict, Field, StrictStr, field_validator +from typing import Any, ClassVar, Dict, List +from polaris.catalog.models.base_update import BaseUpdate +from polaris.catalog.models.sort_order import SortOrder +from typing import Optional, Set +from typing_extensions import Self + +class AddSortOrderUpdate(BaseUpdate): + """ + AddSortOrderUpdate + """ # noqa: E501 + action: StrictStr + sort_order: SortOrder = Field(alias="sort-order") + __properties: ClassVar[List[str]] = ["action"] + + @field_validator('action') + def action_validate_enum(cls, value): + """Validates the enum""" + if value not in set(['add-sort-order']): + raise ValueError("must be one of enum values ('add-sort-order')") + return value + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of AddSortOrderUpdate from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of AddSortOrderUpdate from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "action": obj.get("action") + }) + return _obj + + diff --git a/regtests/client/python/polaris/catalog/models/add_view_version_update.py b/regtests/client/python/polaris/catalog/models/add_view_version_update.py new file mode 100644 index 0000000000..b45c3cd5ac --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/add_view_version_update.py @@ -0,0 +1,97 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import ConfigDict, Field, StrictStr, field_validator +from typing import Any, ClassVar, Dict, List +from polaris.catalog.models.base_update import BaseUpdate +from polaris.catalog.models.view_version import ViewVersion +from typing import Optional, Set +from typing_extensions import Self + +class AddViewVersionUpdate(BaseUpdate): + """ + AddViewVersionUpdate + """ # noqa: E501 + action: StrictStr + view_version: ViewVersion = Field(alias="view-version") + __properties: ClassVar[List[str]] = ["action"] + + @field_validator('action') + def action_validate_enum(cls, value): + """Validates the enum""" + if value not in set(['add-view-version']): + raise ValueError("must be one of enum values ('add-view-version')") + return value + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of AddViewVersionUpdate from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of AddViewVersionUpdate from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "action": obj.get("action") + }) + return _obj + + diff --git a/regtests/client/python/polaris/catalog/models/and_or_expression.py b/regtests/client/python/polaris/catalog/models/and_or_expression.py new file mode 100644 index 0000000000..5688bd43d0 --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/and_or_expression.py @@ -0,0 +1,100 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, StrictStr +from typing import Any, ClassVar, Dict, List +from typing import Optional, Set +from typing_extensions import Self + +class AndOrExpression(BaseModel): + """ + AndOrExpression + """ # noqa: E501 + type: StrictStr + left: Expression + right: Expression + __properties: ClassVar[List[str]] = ["type", "left", "right"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of AndOrExpression from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of left + if self.left: + _dict['left'] = self.left.to_dict() + # override the default output from pydantic by calling `to_dict()` of right + if self.right: + _dict['right'] = self.right.to_dict() + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of AndOrExpression from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "type": obj.get("type"), + "left": Expression.from_dict(obj["left"]) if obj.get("left") is not None else None, + "right": Expression.from_dict(obj["right"]) if obj.get("right") is not None else None + }) + return _obj + +from polaris.catalog.models.expression import Expression +# TODO: Rewrite to not use raise_errors +AndOrExpression.model_rebuild(raise_errors=False) + diff --git a/regtests/client/python/polaris/catalog/models/assert_create.py b/regtests/client/python/polaris/catalog/models/assert_create.py new file mode 100644 index 0000000000..e281a37d22 --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/assert_create.py @@ -0,0 +1,95 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import ConfigDict, StrictStr, field_validator +from typing import Any, ClassVar, Dict, List +from polaris.catalog.models.table_requirement import TableRequirement +from typing import Optional, Set +from typing_extensions import Self + +class AssertCreate(TableRequirement): + """ + The table must not already exist; used for create transactions + """ # noqa: E501 + type: StrictStr + __properties: ClassVar[List[str]] = ["type"] + + @field_validator('type') + def type_validate_enum(cls, value): + """Validates the enum""" + if value not in set(['assert-create']): + raise ValueError("must be one of enum values ('assert-create')") + return value + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of AssertCreate from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of AssertCreate from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "type": obj.get("type") + }) + return _obj + + diff --git a/regtests/client/python/polaris/catalog/models/assert_current_schema_id.py b/regtests/client/python/polaris/catalog/models/assert_current_schema_id.py new file mode 100644 index 0000000000..6b2e75b318 --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/assert_current_schema_id.py @@ -0,0 +1,96 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import ConfigDict, Field, StrictInt, StrictStr, field_validator +from typing import Any, ClassVar, Dict, List +from polaris.catalog.models.table_requirement import TableRequirement +from typing import Optional, Set +from typing_extensions import Self + +class AssertCurrentSchemaId(TableRequirement): + """ + The table's current schema id must match the requirement's `current-schema-id` + """ # noqa: E501 + type: StrictStr + current_schema_id: StrictInt = Field(alias="current-schema-id") + __properties: ClassVar[List[str]] = ["type"] + + @field_validator('type') + def type_validate_enum(cls, value): + """Validates the enum""" + if value not in set(['assert-current-schema-id']): + raise ValueError("must be one of enum values ('assert-current-schema-id')") + return value + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of AssertCurrentSchemaId from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of AssertCurrentSchemaId from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "type": obj.get("type") + }) + return _obj + + diff --git a/regtests/client/python/polaris/catalog/models/assert_default_sort_order_id.py b/regtests/client/python/polaris/catalog/models/assert_default_sort_order_id.py new file mode 100644 index 0000000000..85ec938151 --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/assert_default_sort_order_id.py @@ -0,0 +1,96 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import ConfigDict, Field, StrictInt, StrictStr, field_validator +from typing import Any, ClassVar, Dict, List +from polaris.catalog.models.table_requirement import TableRequirement +from typing import Optional, Set +from typing_extensions import Self + +class AssertDefaultSortOrderId(TableRequirement): + """ + The table's default sort order id must match the requirement's `default-sort-order-id` + """ # noqa: E501 + type: StrictStr + default_sort_order_id: StrictInt = Field(alias="default-sort-order-id") + __properties: ClassVar[List[str]] = ["type"] + + @field_validator('type') + def type_validate_enum(cls, value): + """Validates the enum""" + if value not in set(['assert-default-sort-order-id']): + raise ValueError("must be one of enum values ('assert-default-sort-order-id')") + return value + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of AssertDefaultSortOrderId from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of AssertDefaultSortOrderId from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "type": obj.get("type") + }) + return _obj + + diff --git a/regtests/client/python/polaris/catalog/models/assert_default_spec_id.py b/regtests/client/python/polaris/catalog/models/assert_default_spec_id.py new file mode 100644 index 0000000000..764bb12c29 --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/assert_default_spec_id.py @@ -0,0 +1,96 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import ConfigDict, Field, StrictInt, StrictStr, field_validator +from typing import Any, ClassVar, Dict, List +from polaris.catalog.models.table_requirement import TableRequirement +from typing import Optional, Set +from typing_extensions import Self + +class AssertDefaultSpecId(TableRequirement): + """ + The table's default spec id must match the requirement's `default-spec-id` + """ # noqa: E501 + type: StrictStr + default_spec_id: StrictInt = Field(alias="default-spec-id") + __properties: ClassVar[List[str]] = ["type"] + + @field_validator('type') + def type_validate_enum(cls, value): + """Validates the enum""" + if value not in set(['assert-default-spec-id']): + raise ValueError("must be one of enum values ('assert-default-spec-id')") + return value + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of AssertDefaultSpecId from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of AssertDefaultSpecId from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "type": obj.get("type") + }) + return _obj + + diff --git a/regtests/client/python/polaris/catalog/models/assert_last_assigned_field_id.py b/regtests/client/python/polaris/catalog/models/assert_last_assigned_field_id.py new file mode 100644 index 0000000000..56c1beca23 --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/assert_last_assigned_field_id.py @@ -0,0 +1,96 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import ConfigDict, Field, StrictInt, StrictStr, field_validator +from typing import Any, ClassVar, Dict, List +from polaris.catalog.models.table_requirement import TableRequirement +from typing import Optional, Set +from typing_extensions import Self + +class AssertLastAssignedFieldId(TableRequirement): + """ + The table's last assigned column id must match the requirement's `last-assigned-field-id` + """ # noqa: E501 + type: StrictStr + last_assigned_field_id: StrictInt = Field(alias="last-assigned-field-id") + __properties: ClassVar[List[str]] = ["type"] + + @field_validator('type') + def type_validate_enum(cls, value): + """Validates the enum""" + if value not in set(['assert-last-assigned-field-id']): + raise ValueError("must be one of enum values ('assert-last-assigned-field-id')") + return value + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of AssertLastAssignedFieldId from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of AssertLastAssignedFieldId from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "type": obj.get("type") + }) + return _obj + + diff --git a/regtests/client/python/polaris/catalog/models/assert_last_assigned_partition_id.py b/regtests/client/python/polaris/catalog/models/assert_last_assigned_partition_id.py new file mode 100644 index 0000000000..7a6af9b9ad --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/assert_last_assigned_partition_id.py @@ -0,0 +1,96 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import ConfigDict, Field, StrictInt, StrictStr, field_validator +from typing import Any, ClassVar, Dict, List +from polaris.catalog.models.table_requirement import TableRequirement +from typing import Optional, Set +from typing_extensions import Self + +class AssertLastAssignedPartitionId(TableRequirement): + """ + The table's last assigned partition id must match the requirement's `last-assigned-partition-id` + """ # noqa: E501 + type: StrictStr + last_assigned_partition_id: StrictInt = Field(alias="last-assigned-partition-id") + __properties: ClassVar[List[str]] = ["type"] + + @field_validator('type') + def type_validate_enum(cls, value): + """Validates the enum""" + if value not in set(['assert-last-assigned-partition-id']): + raise ValueError("must be one of enum values ('assert-last-assigned-partition-id')") + return value + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of AssertLastAssignedPartitionId from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of AssertLastAssignedPartitionId from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "type": obj.get("type") + }) + return _obj + + diff --git a/regtests/client/python/polaris/catalog/models/assert_ref_snapshot_id.py b/regtests/client/python/polaris/catalog/models/assert_ref_snapshot_id.py new file mode 100644 index 0000000000..783dd49fef --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/assert_ref_snapshot_id.py @@ -0,0 +1,97 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import ConfigDict, Field, StrictInt, StrictStr, field_validator +from typing import Any, ClassVar, Dict, List +from polaris.catalog.models.table_requirement import TableRequirement +from typing import Optional, Set +from typing_extensions import Self + +class AssertRefSnapshotId(TableRequirement): + """ + The table branch or tag identified by the requirement's `ref` must reference the requirement's `snapshot-id`; if `snapshot-id` is `null` or missing, the ref must not already exist + """ # noqa: E501 + type: StrictStr + ref: StrictStr + snapshot_id: StrictInt = Field(alias="snapshot-id") + __properties: ClassVar[List[str]] = ["type"] + + @field_validator('type') + def type_validate_enum(cls, value): + """Validates the enum""" + if value not in set(['assert-ref-snapshot-id']): + raise ValueError("must be one of enum values ('assert-ref-snapshot-id')") + return value + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of AssertRefSnapshotId from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of AssertRefSnapshotId from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "type": obj.get("type") + }) + return _obj + + diff --git a/regtests/client/python/polaris/catalog/models/assert_table_uuid.py b/regtests/client/python/polaris/catalog/models/assert_table_uuid.py new file mode 100644 index 0000000000..a626003efa --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/assert_table_uuid.py @@ -0,0 +1,96 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import ConfigDict, StrictStr, field_validator +from typing import Any, ClassVar, Dict, List +from polaris.catalog.models.table_requirement import TableRequirement +from typing import Optional, Set +from typing_extensions import Self + +class AssertTableUUID(TableRequirement): + """ + The table UUID must match the requirement's `uuid` + """ # noqa: E501 + type: StrictStr + uuid: StrictStr + __properties: ClassVar[List[str]] = ["type"] + + @field_validator('type') + def type_validate_enum(cls, value): + """Validates the enum""" + if value not in set(['assert-table-uuid']): + raise ValueError("must be one of enum values ('assert-table-uuid')") + return value + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of AssertTableUUID from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of AssertTableUUID from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "type": obj.get("type") + }) + return _obj + + diff --git a/regtests/client/python/polaris/catalog/models/assert_view_uuid.py b/regtests/client/python/polaris/catalog/models/assert_view_uuid.py new file mode 100644 index 0000000000..b449ea7ba9 --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/assert_view_uuid.py @@ -0,0 +1,96 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import ConfigDict, StrictStr, field_validator +from typing import Any, ClassVar, Dict, List +from polaris.catalog.models.view_requirement import ViewRequirement +from typing import Optional, Set +from typing_extensions import Self + +class AssertViewUUID(ViewRequirement): + """ + The view UUID must match the requirement's `uuid` + """ # noqa: E501 + type: StrictStr + uuid: StrictStr + __properties: ClassVar[List[str]] = ["type"] + + @field_validator('type') + def type_validate_enum(cls, value): + """Validates the enum""" + if value not in set(['assert-view-uuid']): + raise ValueError("must be one of enum values ('assert-view-uuid')") + return value + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of AssertViewUUID from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of AssertViewUUID from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "type": obj.get("type") + }) + return _obj + + diff --git a/regtests/client/python/polaris/catalog/models/assign_uuid_update.py b/regtests/client/python/polaris/catalog/models/assign_uuid_update.py new file mode 100644 index 0000000000..6c6dc08430 --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/assign_uuid_update.py @@ -0,0 +1,96 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import ConfigDict, StrictStr, field_validator +from typing import Any, ClassVar, Dict, List +from polaris.catalog.models.base_update import BaseUpdate +from typing import Optional, Set +from typing_extensions import Self + +class AssignUUIDUpdate(BaseUpdate): + """ + Assigning a UUID to a table/view should only be done when creating the table/view. It is not safe to re-assign the UUID if a table/view already has a UUID assigned + """ # noqa: E501 + action: StrictStr + uuid: StrictStr + __properties: ClassVar[List[str]] = ["action"] + + @field_validator('action') + def action_validate_enum(cls, value): + """Validates the enum""" + if value not in set(['assign-uuid']): + raise ValueError("must be one of enum values ('assign-uuid')") + return value + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of AssignUUIDUpdate from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of AssignUUIDUpdate from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "action": obj.get("action") + }) + return _obj + + diff --git a/regtests/client/python/polaris/catalog/models/base_update.py b/regtests/client/python/polaris/catalog/models/base_update.py new file mode 100644 index 0000000000..b83a445c80 --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/base_update.py @@ -0,0 +1,167 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from importlib import import_module +from pydantic import BaseModel, ConfigDict, StrictStr +from typing import Any, ClassVar, Dict, List, Union +from typing import Optional, Set +from typing_extensions import Self + +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from polaris.catalog.models.add_schema_update import AddSchemaUpdate + from polaris.catalog.models.add_snapshot_update import AddSnapshotUpdate + from polaris.catalog.models.add_sort_order_update import AddSortOrderUpdate + from polaris.catalog.models.add_partition_spec_update import AddPartitionSpecUpdate + from polaris.catalog.models.add_view_version_update import AddViewVersionUpdate + from polaris.catalog.models.assign_uuid_update import AssignUUIDUpdate + from polaris.catalog.models.remove_partition_statistics_update import RemovePartitionStatisticsUpdate + from polaris.catalog.models.remove_properties_update import RemovePropertiesUpdate + from polaris.catalog.models.remove_snapshot_ref_update import RemoveSnapshotRefUpdate + from polaris.catalog.models.remove_snapshots_update import RemoveSnapshotsUpdate + from polaris.catalog.models.remove_statistics_update import RemoveStatisticsUpdate + from polaris.catalog.models.set_current_schema_update import SetCurrentSchemaUpdate + from polaris.catalog.models.set_current_view_version_update import SetCurrentViewVersionUpdate + from polaris.catalog.models.set_default_sort_order_update import SetDefaultSortOrderUpdate + from polaris.catalog.models.set_default_spec_update import SetDefaultSpecUpdate + from polaris.catalog.models.set_location_update import SetLocationUpdate + from polaris.catalog.models.set_partition_statistics_update import SetPartitionStatisticsUpdate + from polaris.catalog.models.set_properties_update import SetPropertiesUpdate + from polaris.catalog.models.set_snapshot_ref_update import SetSnapshotRefUpdate + from polaris.catalog.models.set_statistics_update import SetStatisticsUpdate + from polaris.catalog.models.upgrade_format_version_update import UpgradeFormatVersionUpdate + +class BaseUpdate(BaseModel): + """ + BaseUpdate + """ # noqa: E501 + action: StrictStr + __properties: ClassVar[List[str]] = ["action"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + # JSON field name that stores the object type + __discriminator_property_name: ClassVar[str] = 'action' + + # discriminator mappings + __discriminator_value_class_map: ClassVar[Dict[str, str]] = { + 'add-schema': 'AddSchemaUpdate','add-snapshot': 'AddSnapshotUpdate','add-sort-order': 'AddSortOrderUpdate','add-spec': 'AddPartitionSpecUpdate','add-view-version': 'AddViewVersionUpdate','assign-uuid': 'AssignUUIDUpdate','remove-partition-statistics': 'RemovePartitionStatisticsUpdate','remove-properties': 'RemovePropertiesUpdate','remove-snapshot-ref': 'RemoveSnapshotRefUpdate','remove-snapshots': 'RemoveSnapshotsUpdate','remove-statistics': 'RemoveStatisticsUpdate','set-current-schema': 'SetCurrentSchemaUpdate','set-current-view-version': 'SetCurrentViewVersionUpdate','set-default-sort-order': 'SetDefaultSortOrderUpdate','set-default-spec': 'SetDefaultSpecUpdate','set-location': 'SetLocationUpdate','set-partition-statistics': 'SetPartitionStatisticsUpdate','set-properties': 'SetPropertiesUpdate','set-snapshot-ref': 'SetSnapshotRefUpdate','set-statistics': 'SetStatisticsUpdate','upgrade-format-version': 'UpgradeFormatVersionUpdate' + } + + @classmethod + def get_discriminator_value(cls, obj: Dict[str, Any]) -> Optional[str]: + """Returns the discriminator value (object type) of the data""" + discriminator_value = obj[cls.__discriminator_property_name] + if discriminator_value: + return cls.__discriminator_value_class_map.get(discriminator_value) + else: + return None + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Union[AddSchemaUpdate, AddSnapshotUpdate, AddSortOrderUpdate, AddPartitionSpecUpdate, AddViewVersionUpdate, AssignUUIDUpdate, RemovePartitionStatisticsUpdate, RemovePropertiesUpdate, RemoveSnapshotRefUpdate, RemoveSnapshotsUpdate, RemoveStatisticsUpdate, SetCurrentSchemaUpdate, SetCurrentViewVersionUpdate, SetDefaultSortOrderUpdate, SetDefaultSpecUpdate, SetLocationUpdate, SetPartitionStatisticsUpdate, SetPropertiesUpdate, SetSnapshotRefUpdate, SetStatisticsUpdate, UpgradeFormatVersionUpdate]]: + """Create an instance of BaseUpdate from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Dict[str, Any]) -> Optional[Union[AddSchemaUpdate, AddSnapshotUpdate, AddSortOrderUpdate, AddPartitionSpecUpdate, AddViewVersionUpdate, AssignUUIDUpdate, RemovePartitionStatisticsUpdate, RemovePropertiesUpdate, RemoveSnapshotRefUpdate, RemoveSnapshotsUpdate, RemoveStatisticsUpdate, SetCurrentSchemaUpdate, SetCurrentViewVersionUpdate, SetDefaultSortOrderUpdate, SetDefaultSpecUpdate, SetLocationUpdate, SetPartitionStatisticsUpdate, SetPropertiesUpdate, SetSnapshotRefUpdate, SetStatisticsUpdate, UpgradeFormatVersionUpdate]]: + """Create an instance of BaseUpdate from a dict""" + # look up the object type based on discriminator mapping + object_type = cls.get_discriminator_value(obj) + if object_type == 'AddSchemaUpdate': + return import_module("polaris.catalog.models.add_schema_update").AddSchemaUpdate.from_dict(obj) + if object_type == 'AddSnapshotUpdate': + return import_module("polaris.catalog.models.add_snapshot_update").AddSnapshotUpdate.from_dict(obj) + if object_type == 'AddSortOrderUpdate': + return import_module("polaris.catalog.models.add_sort_order_update").AddSortOrderUpdate.from_dict(obj) + if object_type == 'AddPartitionSpecUpdate': + return import_module("polaris.catalog.models.add_partition_spec_update").AddPartitionSpecUpdate.from_dict(obj) + if object_type == 'AddViewVersionUpdate': + return import_module("polaris.catalog.models.add_view_version_update").AddViewVersionUpdate.from_dict(obj) + if object_type == 'AssignUUIDUpdate': + return import_module("polaris.catalog.models.assign_uuid_update").AssignUUIDUpdate.from_dict(obj) + if object_type == 'RemovePartitionStatisticsUpdate': + return import_module("polaris.catalog.models.remove_partition_statistics_update").RemovePartitionStatisticsUpdate.from_dict(obj) + if object_type == 'RemovePropertiesUpdate': + return import_module("polaris.catalog.models.remove_properties_update").RemovePropertiesUpdate.from_dict(obj) + if object_type == 'RemoveSnapshotRefUpdate': + return import_module("polaris.catalog.models.remove_snapshot_ref_update").RemoveSnapshotRefUpdate.from_dict(obj) + if object_type == 'RemoveSnapshotsUpdate': + return import_module("polaris.catalog.models.remove_snapshots_update").RemoveSnapshotsUpdate.from_dict(obj) + if object_type == 'RemoveStatisticsUpdate': + return import_module("polaris.catalog.models.remove_statistics_update").RemoveStatisticsUpdate.from_dict(obj) + if object_type == 'SetCurrentSchemaUpdate': + return import_module("polaris.catalog.models.set_current_schema_update").SetCurrentSchemaUpdate.from_dict(obj) + if object_type == 'SetCurrentViewVersionUpdate': + return import_module("polaris.catalog.models.set_current_view_version_update").SetCurrentViewVersionUpdate.from_dict(obj) + if object_type == 'SetDefaultSortOrderUpdate': + return import_module("polaris.catalog.models.set_default_sort_order_update").SetDefaultSortOrderUpdate.from_dict(obj) + if object_type == 'SetDefaultSpecUpdate': + return import_module("polaris.catalog.models.set_default_spec_update").SetDefaultSpecUpdate.from_dict(obj) + if object_type == 'SetLocationUpdate': + return import_module("polaris.catalog.models.set_location_update").SetLocationUpdate.from_dict(obj) + if object_type == 'SetPartitionStatisticsUpdate': + return import_module("polaris.catalog.models.set_partition_statistics_update").SetPartitionStatisticsUpdate.from_dict(obj) + if object_type == 'SetPropertiesUpdate': + return import_module("polaris.catalog.models.set_properties_update").SetPropertiesUpdate.from_dict(obj) + if object_type == 'SetSnapshotRefUpdate': + return import_module("polaris.catalog.models.set_snapshot_ref_update").SetSnapshotRefUpdate.from_dict(obj) + if object_type == 'SetStatisticsUpdate': + return import_module("polaris.catalog.models.set_statistics_update").SetStatisticsUpdate.from_dict(obj) + if object_type == 'UpgradeFormatVersionUpdate': + return import_module("polaris.catalog.models.upgrade_format_version_update").UpgradeFormatVersionUpdate.from_dict(obj) + + raise ValueError("BaseUpdate failed to lookup discriminator value from " + + json.dumps(obj) + ". Discriminator property name: " + cls.__discriminator_property_name + + ", mapping: " + json.dumps(cls.__discriminator_value_class_map)) + + diff --git a/regtests/client/python/polaris/catalog/models/blob_metadata.py b/regtests/client/python/polaris/catalog/models/blob_metadata.py new file mode 100644 index 0000000000..5c30309790 --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/blob_metadata.py @@ -0,0 +1,95 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictInt, StrictStr +from typing import Any, ClassVar, Dict, List, Optional +from typing import Optional, Set +from typing_extensions import Self + +class BlobMetadata(BaseModel): + """ + BlobMetadata + """ # noqa: E501 + type: StrictStr + snapshot_id: StrictInt = Field(alias="snapshot-id") + sequence_number: StrictInt = Field(alias="sequence-number") + fields: List[StrictInt] + properties: Optional[Dict[str, Any]] = None + __properties: ClassVar[List[str]] = ["type", "snapshot-id", "sequence-number", "fields", "properties"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of BlobMetadata from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of BlobMetadata from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "type": obj.get("type"), + "snapshot-id": obj.get("snapshot-id"), + "sequence-number": obj.get("sequence-number"), + "fields": obj.get("fields"), + "properties": obj.get("properties") + }) + return _obj + + diff --git a/regtests/client/python/polaris/catalog/models/catalog_config.py b/regtests/client/python/polaris/catalog/models/catalog_config.py new file mode 100644 index 0000000000..4e4950846c --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/catalog_config.py @@ -0,0 +1,89 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictStr +from typing import Any, ClassVar, Dict, List +from typing import Optional, Set +from typing_extensions import Self + +class CatalogConfig(BaseModel): + """ + Server-provided configuration for the catalog. + """ # noqa: E501 + overrides: Dict[str, StrictStr] = Field(description="Properties that should be used to override client configuration; applied after defaults and client configuration.") + defaults: Dict[str, StrictStr] = Field(description="Properties that should be used as default configuration; applied before client configuration.") + __properties: ClassVar[List[str]] = ["overrides", "defaults"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of CatalogConfig from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of CatalogConfig from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "overrides": obj.get("overrides"), + "defaults": obj.get("defaults") + }) + return _obj + + diff --git a/regtests/client/python/polaris/catalog/models/commit_report.py b/regtests/client/python/polaris/catalog/models/commit_report.py new file mode 100644 index 0000000000..f7998de0da --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/commit_report.py @@ -0,0 +1,110 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictInt, StrictStr +from typing import Any, ClassVar, Dict, List, Optional +from polaris.catalog.models.metric_result import MetricResult +from typing import Optional, Set +from typing_extensions import Self + +class CommitReport(BaseModel): + """ + CommitReport + """ # noqa: E501 + table_name: StrictStr = Field(alias="table-name") + snapshot_id: StrictInt = Field(alias="snapshot-id") + sequence_number: StrictInt = Field(alias="sequence-number") + operation: StrictStr + metrics: Dict[str, MetricResult] + metadata: Optional[Dict[str, StrictStr]] = None + __properties: ClassVar[List[str]] = ["table-name", "snapshot-id", "sequence-number", "operation", "metrics", "metadata"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of CommitReport from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of each value in metrics (dict) + _field_dict = {} + if self.metrics: + for _key in self.metrics: + if self.metrics[_key]: + _field_dict[_key] = self.metrics[_key].to_dict() + _dict['metrics'] = _field_dict + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of CommitReport from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "table-name": obj.get("table-name"), + "snapshot-id": obj.get("snapshot-id"), + "sequence-number": obj.get("sequence-number"), + "operation": obj.get("operation"), + "metrics": dict( + (_k, MetricResult.from_dict(_v)) + for _k, _v in obj["metrics"].items() + ) + if obj.get("metrics") is not None + else None, + "metadata": obj.get("metadata") + }) + return _obj + + diff --git a/regtests/client/python/polaris/catalog/models/commit_table_request.py b/regtests/client/python/polaris/catalog/models/commit_table_request.py new file mode 100644 index 0000000000..8ea8169d75 --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/commit_table_request.py @@ -0,0 +1,111 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict +from typing import Any, ClassVar, Dict, List, Optional +from polaris.catalog.models.table_identifier import TableIdentifier +from polaris.catalog.models.table_requirement import TableRequirement +from polaris.catalog.models.table_update import TableUpdate +from typing import Optional, Set +from typing_extensions import Self + +class CommitTableRequest(BaseModel): + """ + CommitTableRequest + """ # noqa: E501 + identifier: Optional[TableIdentifier] = None + requirements: List[TableRequirement] + updates: List[TableUpdate] + __properties: ClassVar[List[str]] = ["identifier", "requirements", "updates"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of CommitTableRequest from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of identifier + if self.identifier: + _dict['identifier'] = self.identifier.to_dict() + # override the default output from pydantic by calling `to_dict()` of each item in requirements (list) + _items = [] + if self.requirements: + for _item in self.requirements: + if _item: + _items.append(_item.to_dict()) + _dict['requirements'] = _items + # override the default output from pydantic by calling `to_dict()` of each item in updates (list) + _items = [] + if self.updates: + for _item in self.updates: + if _item: + _items.append(_item.to_dict()) + _dict['updates'] = _items + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of CommitTableRequest from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "identifier": TableIdentifier.from_dict(obj["identifier"]) if obj.get("identifier") is not None else None, + "requirements": [TableRequirement.from_dict(_item) for _item in obj["requirements"]] if obj.get("requirements") is not None else None, + "updates": [TableUpdate.from_dict(_item) for _item in obj["updates"]] if obj.get("updates") is not None else None + }) + return _obj + + diff --git a/regtests/client/python/polaris/catalog/models/commit_table_response.py b/regtests/client/python/polaris/catalog/models/commit_table_response.py new file mode 100644 index 0000000000..c55c836d4b --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/commit_table_response.py @@ -0,0 +1,93 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictStr +from typing import Any, ClassVar, Dict, List +from polaris.catalog.models.table_metadata import TableMetadata +from typing import Optional, Set +from typing_extensions import Self + +class CommitTableResponse(BaseModel): + """ + CommitTableResponse + """ # noqa: E501 + metadata_location: StrictStr = Field(alias="metadata-location") + metadata: TableMetadata + __properties: ClassVar[List[str]] = ["metadata-location", "metadata"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of CommitTableResponse from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of metadata + if self.metadata: + _dict['metadata'] = self.metadata.to_dict() + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of CommitTableResponse from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "metadata-location": obj.get("metadata-location"), + "metadata": TableMetadata.from_dict(obj["metadata"]) if obj.get("metadata") is not None else None + }) + return _obj + + diff --git a/regtests/client/python/polaris/catalog/models/commit_transaction_request.py b/regtests/client/python/polaris/catalog/models/commit_transaction_request.py new file mode 100644 index 0000000000..5c1feaeb1d --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/commit_transaction_request.py @@ -0,0 +1,95 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field +from typing import Any, ClassVar, Dict, List +from polaris.catalog.models.commit_table_request import CommitTableRequest +from typing import Optional, Set +from typing_extensions import Self + +class CommitTransactionRequest(BaseModel): + """ + CommitTransactionRequest + """ # noqa: E501 + table_changes: List[CommitTableRequest] = Field(alias="table-changes") + __properties: ClassVar[List[str]] = ["table-changes"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of CommitTransactionRequest from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of each item in table_changes (list) + _items = [] + if self.table_changes: + for _item in self.table_changes: + if _item: + _items.append(_item.to_dict()) + _dict['table-changes'] = _items + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of CommitTransactionRequest from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "table-changes": [CommitTableRequest.from_dict(_item) for _item in obj["table-changes"]] if obj.get("table-changes") is not None else None + }) + return _obj + + diff --git a/regtests/client/python/polaris/catalog/models/commit_view_request.py b/regtests/client/python/polaris/catalog/models/commit_view_request.py new file mode 100644 index 0000000000..a849135a88 --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/commit_view_request.py @@ -0,0 +1,111 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict +from typing import Any, ClassVar, Dict, List, Optional +from polaris.catalog.models.table_identifier import TableIdentifier +from polaris.catalog.models.view_requirement import ViewRequirement +from polaris.catalog.models.view_update import ViewUpdate +from typing import Optional, Set +from typing_extensions import Self + +class CommitViewRequest(BaseModel): + """ + CommitViewRequest + """ # noqa: E501 + identifier: Optional[TableIdentifier] = None + requirements: Optional[List[ViewRequirement]] = None + updates: List[ViewUpdate] + __properties: ClassVar[List[str]] = ["identifier", "requirements", "updates"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of CommitViewRequest from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of identifier + if self.identifier: + _dict['identifier'] = self.identifier.to_dict() + # override the default output from pydantic by calling `to_dict()` of each item in requirements (list) + _items = [] + if self.requirements: + for _item in self.requirements: + if _item: + _items.append(_item.to_dict()) + _dict['requirements'] = _items + # override the default output from pydantic by calling `to_dict()` of each item in updates (list) + _items = [] + if self.updates: + for _item in self.updates: + if _item: + _items.append(_item.to_dict()) + _dict['updates'] = _items + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of CommitViewRequest from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "identifier": TableIdentifier.from_dict(obj["identifier"]) if obj.get("identifier") is not None else None, + "requirements": [ViewRequirement.from_dict(_item) for _item in obj["requirements"]] if obj.get("requirements") is not None else None, + "updates": [ViewUpdate.from_dict(_item) for _item in obj["updates"]] if obj.get("updates") is not None else None + }) + return _obj + + diff --git a/regtests/client/python/polaris/catalog/models/content_file.py b/regtests/client/python/polaris/catalog/models/content_file.py new file mode 100644 index 0000000000..ede346069f --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/content_file.py @@ -0,0 +1,131 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from importlib import import_module +from pydantic import BaseModel, ConfigDict, Field, StrictInt, StrictStr +from typing import Any, ClassVar, Dict, List, Optional, Union +from polaris.catalog.models.file_format import FileFormat +from polaris.catalog.models.primitive_type_value import PrimitiveTypeValue +from typing import Optional, Set +from typing_extensions import Self + +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from polaris.catalog.models.data_file import DataFile + from polaris.catalog.models.equality_delete_file import EqualityDeleteFile + from polaris.catalog.models.position_delete_file import PositionDeleteFile + +class ContentFile(BaseModel): + """ + ContentFile + """ # noqa: E501 + content: StrictStr + file_path: StrictStr = Field(alias="file-path") + file_format: FileFormat = Field(alias="file-format") + spec_id: StrictInt = Field(alias="spec-id") + partition: Optional[List[PrimitiveTypeValue]] = Field(default=None, description="A list of partition field values ordered based on the fields of the partition spec specified by the `spec-id`") + file_size_in_bytes: StrictInt = Field(description="Total file size in bytes", alias="file-size-in-bytes") + record_count: StrictInt = Field(description="Number of records in the file", alias="record-count") + key_metadata: Optional[StrictStr] = Field(default=None, description="Encryption key metadata blob", alias="key-metadata") + split_offsets: Optional[List[StrictInt]] = Field(default=None, description="List of splittable offsets", alias="split-offsets") + sort_order_id: Optional[StrictInt] = Field(default=None, alias="sort-order-id") + __properties: ClassVar[List[str]] = ["content", "file-path", "file-format", "spec-id", "partition", "file-size-in-bytes", "record-count", "key-metadata", "split-offsets", "sort-order-id"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + # JSON field name that stores the object type + __discriminator_property_name: ClassVar[str] = 'content' + + # discriminator mappings + __discriminator_value_class_map: ClassVar[Dict[str, str]] = { + 'data': 'DataFile','equality-deletes': 'EqualityDeleteFile','position-deletes': 'PositionDeleteFile' + } + + @classmethod + def get_discriminator_value(cls, obj: Dict[str, Any]) -> Optional[str]: + """Returns the discriminator value (object type) of the data""" + discriminator_value = obj[cls.__discriminator_property_name] + if discriminator_value: + return cls.__discriminator_value_class_map.get(discriminator_value) + else: + return None + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Union[DataFile, EqualityDeleteFile, PositionDeleteFile]]: + """Create an instance of ContentFile from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of each item in partition (list) + _items = [] + if self.partition: + for _item in self.partition: + if _item: + _items.append(_item.to_dict()) + _dict['partition'] = _items + return _dict + + @classmethod + def from_dict(cls, obj: Dict[str, Any]) -> Optional[Union[DataFile, EqualityDeleteFile, PositionDeleteFile]]: + """Create an instance of ContentFile from a dict""" + # look up the object type based on discriminator mapping + object_type = cls.get_discriminator_value(obj) + if object_type == 'DataFile': + return import_module("polaris.catalog.models.data_file").DataFile.from_dict(obj) + if object_type == 'EqualityDeleteFile': + return import_module("polaris.catalog.models.equality_delete_file").EqualityDeleteFile.from_dict(obj) + if object_type == 'PositionDeleteFile': + return import_module("polaris.catalog.models.position_delete_file").PositionDeleteFile.from_dict(obj) + + raise ValueError("ContentFile failed to lookup discriminator value from " + + json.dumps(obj) + ". Discriminator property name: " + cls.__discriminator_property_name + + ", mapping: " + json.dumps(cls.__discriminator_value_class_map)) + + diff --git a/regtests/client/python/polaris/catalog/models/count_map.py b/regtests/client/python/polaris/catalog/models/count_map.py new file mode 100644 index 0000000000..a7c68df254 --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/count_map.py @@ -0,0 +1,89 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictInt +from typing import Any, ClassVar, Dict, List, Optional +from typing import Optional, Set +from typing_extensions import Self + +class CountMap(BaseModel): + """ + CountMap + """ # noqa: E501 + keys: Optional[List[StrictInt]] = Field(default=None, description="List of integer column ids for each corresponding value") + values: Optional[List[StrictInt]] = Field(default=None, description="List of Long values, matched to 'keys' by index") + __properties: ClassVar[List[str]] = ["keys", "values"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of CountMap from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of CountMap from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "keys": obj.get("keys"), + "values": obj.get("values") + }) + return _obj + + diff --git a/regtests/client/python/polaris/catalog/models/counter_result.py b/regtests/client/python/polaris/catalog/models/counter_result.py new file mode 100644 index 0000000000..852ddbd2e3 --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/counter_result.py @@ -0,0 +1,89 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, StrictInt, StrictStr +from typing import Any, ClassVar, Dict, List +from typing import Optional, Set +from typing_extensions import Self + +class CounterResult(BaseModel): + """ + CounterResult + """ # noqa: E501 + unit: StrictStr + value: StrictInt + __properties: ClassVar[List[str]] = ["unit", "value"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of CounterResult from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of CounterResult from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "unit": obj.get("unit"), + "value": obj.get("value") + }) + return _obj + + diff --git a/regtests/client/python/polaris/catalog/models/create_namespace_request.py b/regtests/client/python/polaris/catalog/models/create_namespace_request.py new file mode 100644 index 0000000000..68ff1570f2 --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/create_namespace_request.py @@ -0,0 +1,89 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictStr +from typing import Any, ClassVar, Dict, List, Optional +from typing import Optional, Set +from typing_extensions import Self + +class CreateNamespaceRequest(BaseModel): + """ + CreateNamespaceRequest + """ # noqa: E501 + namespace: List[StrictStr] = Field(description="Reference to one or more levels of a namespace") + properties: Optional[Dict[str, StrictStr]] = Field(default=None, description="Configured string to string map of properties for the namespace") + __properties: ClassVar[List[str]] = ["namespace", "properties"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of CreateNamespaceRequest from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of CreateNamespaceRequest from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "namespace": obj.get("namespace"), + "properties": obj.get("properties") + }) + return _obj + + diff --git a/regtests/client/python/polaris/catalog/models/create_namespace_response.py b/regtests/client/python/polaris/catalog/models/create_namespace_response.py new file mode 100644 index 0000000000..e202dcd5f7 --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/create_namespace_response.py @@ -0,0 +1,89 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictStr +from typing import Any, ClassVar, Dict, List, Optional +from typing import Optional, Set +from typing_extensions import Self + +class CreateNamespaceResponse(BaseModel): + """ + CreateNamespaceResponse + """ # noqa: E501 + namespace: List[StrictStr] = Field(description="Reference to one or more levels of a namespace") + properties: Optional[Dict[str, StrictStr]] = Field(default=None, description="Properties stored on the namespace, if supported by the server.") + __properties: ClassVar[List[str]] = ["namespace", "properties"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of CreateNamespaceResponse from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of CreateNamespaceResponse from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "namespace": obj.get("namespace"), + "properties": obj.get("properties") + }) + return _obj + + diff --git a/regtests/client/python/polaris/catalog/models/create_table_request.py b/regtests/client/python/polaris/catalog/models/create_table_request.py new file mode 100644 index 0000000000..fc864b30a2 --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/create_table_request.py @@ -0,0 +1,111 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictBool, StrictStr +from typing import Any, ClassVar, Dict, List, Optional +from polaris.catalog.models.model_schema import ModelSchema +from polaris.catalog.models.partition_spec import PartitionSpec +from polaris.catalog.models.sort_order import SortOrder +from typing import Optional, Set +from typing_extensions import Self + +class CreateTableRequest(BaseModel): + """ + CreateTableRequest + """ # noqa: E501 + name: StrictStr + location: Optional[StrictStr] = None + var_schema: ModelSchema = Field(alias="schema") + partition_spec: Optional[PartitionSpec] = Field(default=None, alias="partition-spec") + write_order: Optional[SortOrder] = Field(default=None, alias="write-order") + stage_create: Optional[StrictBool] = Field(default=None, alias="stage-create") + properties: Optional[Dict[str, StrictStr]] = None + __properties: ClassVar[List[str]] = ["name", "location", "schema", "partition-spec", "write-order", "stage-create", "properties"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of CreateTableRequest from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of var_schema + if self.var_schema: + _dict['schema'] = self.var_schema.to_dict() + # override the default output from pydantic by calling `to_dict()` of partition_spec + if self.partition_spec: + _dict['partition-spec'] = self.partition_spec.to_dict() + # override the default output from pydantic by calling `to_dict()` of write_order + if self.write_order: + _dict['write-order'] = self.write_order.to_dict() + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of CreateTableRequest from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "name": obj.get("name"), + "location": obj.get("location"), + "schema": ModelSchema.from_dict(obj["schema"]) if obj.get("schema") is not None else None, + "partition-spec": PartitionSpec.from_dict(obj["partition-spec"]) if obj.get("partition-spec") is not None else None, + "write-order": SortOrder.from_dict(obj["write-order"]) if obj.get("write-order") is not None else None, + "stage-create": obj.get("stage-create"), + "properties": obj.get("properties") + }) + return _obj + + diff --git a/regtests/client/python/polaris/catalog/models/create_view_request.py b/regtests/client/python/polaris/catalog/models/create_view_request.py new file mode 100644 index 0000000000..b5464a5bec --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/create_view_request.py @@ -0,0 +1,103 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictStr +from typing import Any, ClassVar, Dict, List, Optional +from polaris.catalog.models.model_schema import ModelSchema +from polaris.catalog.models.view_version import ViewVersion +from typing import Optional, Set +from typing_extensions import Self + +class CreateViewRequest(BaseModel): + """ + CreateViewRequest + """ # noqa: E501 + name: StrictStr + location: Optional[StrictStr] = None + var_schema: ModelSchema = Field(alias="schema") + view_version: ViewVersion = Field(alias="view-version") + properties: Dict[str, StrictStr] + __properties: ClassVar[List[str]] = ["name", "location", "schema", "view-version", "properties"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of CreateViewRequest from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of var_schema + if self.var_schema: + _dict['schema'] = self.var_schema.to_dict() + # override the default output from pydantic by calling `to_dict()` of view_version + if self.view_version: + _dict['view-version'] = self.view_version.to_dict() + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of CreateViewRequest from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "name": obj.get("name"), + "location": obj.get("location"), + "schema": ModelSchema.from_dict(obj["schema"]) if obj.get("schema") is not None else None, + "view-version": ViewVersion.from_dict(obj["view-version"]) if obj.get("view-version") is not None else None, + "properties": obj.get("properties") + }) + return _obj + + diff --git a/regtests/client/python/polaris/catalog/models/data_file.py b/regtests/client/python/polaris/catalog/models/data_file.py new file mode 100644 index 0000000000..0613e6a130 --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/data_file.py @@ -0,0 +1,121 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import ConfigDict, Field, StrictStr, field_validator +from typing import Any, ClassVar, Dict, List, Optional +from polaris.catalog.models.content_file import ContentFile +from polaris.catalog.models.count_map import CountMap +from polaris.catalog.models.file_format import FileFormat +from polaris.catalog.models.primitive_type_value import PrimitiveTypeValue +from polaris.catalog.models.value_map import ValueMap +from typing import Optional, Set +from typing_extensions import Self + +class DataFile(ContentFile): + """ + DataFile + """ # noqa: E501 + content: StrictStr + column_sizes: Optional[CountMap] = Field(default=None, description="Map of column id to total count, including null and NaN", alias="column-sizes") + value_counts: Optional[CountMap] = Field(default=None, description="Map of column id to null value count", alias="value-counts") + null_value_counts: Optional[CountMap] = Field(default=None, description="Map of column id to null value count", alias="null-value-counts") + nan_value_counts: Optional[CountMap] = Field(default=None, description="Map of column id to number of NaN values in the column", alias="nan-value-counts") + lower_bounds: Optional[ValueMap] = Field(default=None, description="Map of column id to lower bound primitive type values", alias="lower-bounds") + upper_bounds: Optional[ValueMap] = Field(default=None, description="Map of column id to upper bound primitive type values", alias="upper-bounds") + __properties: ClassVar[List[str]] = ["content", "file-path", "file-format", "spec-id", "partition", "file-size-in-bytes", "record-count", "key-metadata", "split-offsets", "sort-order-id"] + + @field_validator('content') + def content_validate_enum(cls, value): + """Validates the enum""" + if value not in set(['data']): + raise ValueError("must be one of enum values ('data')") + return value + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of DataFile from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of each item in partition (list) + _items = [] + if self.partition: + for _item in self.partition: + if _item: + _items.append(_item.to_dict()) + _dict['partition'] = _items + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of DataFile from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "content": obj.get("content"), + "file-path": obj.get("file-path"), + "file-format": obj.get("file-format"), + "spec-id": obj.get("spec-id"), + "partition": [PrimitiveTypeValue.from_dict(_item) for _item in obj["partition"]] if obj.get("partition") is not None else None, + "file-size-in-bytes": obj.get("file-size-in-bytes"), + "record-count": obj.get("record-count"), + "key-metadata": obj.get("key-metadata"), + "split-offsets": obj.get("split-offsets"), + "sort-order-id": obj.get("sort-order-id") + }) + return _obj + + diff --git a/regtests/client/python/polaris/catalog/models/equality_delete_file.py b/regtests/client/python/polaris/catalog/models/equality_delete_file.py new file mode 100644 index 0000000000..de4dbc2074 --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/equality_delete_file.py @@ -0,0 +1,114 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import ConfigDict, Field, StrictInt, StrictStr, field_validator +from typing import Any, ClassVar, Dict, List, Optional +from polaris.catalog.models.content_file import ContentFile +from polaris.catalog.models.file_format import FileFormat +from polaris.catalog.models.primitive_type_value import PrimitiveTypeValue +from typing import Optional, Set +from typing_extensions import Self + +class EqualityDeleteFile(ContentFile): + """ + EqualityDeleteFile + """ # noqa: E501 + content: StrictStr + equality_ids: Optional[List[StrictInt]] = Field(default=None, description="List of equality field IDs", alias="equality-ids") + __properties: ClassVar[List[str]] = ["content", "file-path", "file-format", "spec-id", "partition", "file-size-in-bytes", "record-count", "key-metadata", "split-offsets", "sort-order-id"] + + @field_validator('content') + def content_validate_enum(cls, value): + """Validates the enum""" + if value not in set(['equality-deletes']): + raise ValueError("must be one of enum values ('equality-deletes')") + return value + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of EqualityDeleteFile from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of each item in partition (list) + _items = [] + if self.partition: + for _item in self.partition: + if _item: + _items.append(_item.to_dict()) + _dict['partition'] = _items + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of EqualityDeleteFile from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "content": obj.get("content"), + "file-path": obj.get("file-path"), + "file-format": obj.get("file-format"), + "spec-id": obj.get("spec-id"), + "partition": [PrimitiveTypeValue.from_dict(_item) for _item in obj["partition"]] if obj.get("partition") is not None else None, + "file-size-in-bytes": obj.get("file-size-in-bytes"), + "record-count": obj.get("record-count"), + "key-metadata": obj.get("key-metadata"), + "split-offsets": obj.get("split-offsets"), + "sort-order-id": obj.get("sort-order-id") + }) + return _obj + + diff --git a/regtests/client/python/polaris/catalog/models/error_model.py b/regtests/client/python/polaris/catalog/models/error_model.py new file mode 100644 index 0000000000..f283d377e5 --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/error_model.py @@ -0,0 +1,94 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictStr +from typing import Any, ClassVar, Dict, List, Optional +from typing_extensions import Annotated +from typing import Optional, Set +from typing_extensions import Self + +class ErrorModel(BaseModel): + """ + JSON error payload returned in a response with further details on the error + """ # noqa: E501 + message: StrictStr = Field(description="Human-readable error message") + type: StrictStr = Field(description="Internal type definition of the error") + code: Annotated[int, Field(le=600, strict=True, ge=400)] = Field(description="HTTP response code") + stack: Optional[List[StrictStr]] = None + __properties: ClassVar[List[str]] = ["message", "type", "code", "stack"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of ErrorModel from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of ErrorModel from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "message": obj.get("message"), + "type": obj.get("type"), + "code": obj.get("code"), + "stack": obj.get("stack") + }) + return _obj + + diff --git a/regtests/client/python/polaris/catalog/models/expression.py b/regtests/client/python/polaris/catalog/models/expression.py new file mode 100644 index 0000000000..a22db3ef04 --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/expression.py @@ -0,0 +1,181 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import json +import pprint +from pydantic import BaseModel, ConfigDict, Field, StrictStr, ValidationError, field_validator +from typing import Any, List, Optional +from polaris.catalog.models.literal_expression import LiteralExpression +from polaris.catalog.models.set_expression import SetExpression +from polaris.catalog.models.unary_expression import UnaryExpression +from pydantic import StrictStr, Field +from typing import Union, List, Set, Optional, Dict +from typing_extensions import Literal, Self + +EXPRESSION_ONE_OF_SCHEMAS = ["AndOrExpression", "LiteralExpression", "NotExpression", "SetExpression", "UnaryExpression"] + +class Expression(BaseModel): + """ + Expression + """ + # data type: AndOrExpression + oneof_schema_1_validator: Optional[AndOrExpression] = None + # data type: NotExpression + oneof_schema_2_validator: Optional[NotExpression] = None + # data type: SetExpression + oneof_schema_3_validator: Optional[SetExpression] = None + # data type: LiteralExpression + oneof_schema_4_validator: Optional[LiteralExpression] = None + # data type: UnaryExpression + oneof_schema_5_validator: Optional[UnaryExpression] = None + actual_instance: Optional[Union[AndOrExpression, LiteralExpression, NotExpression, SetExpression, UnaryExpression]] = None + one_of_schemas: Set[str] = { "AndOrExpression", "LiteralExpression", "NotExpression", "SetExpression", "UnaryExpression" } + + model_config = ConfigDict( + validate_assignment=True, + protected_namespaces=(), + ) + + + def __init__(self, *args, **kwargs) -> None: + if args: + if len(args) > 1: + raise ValueError("If a position argument is used, only 1 is allowed to set `actual_instance`") + if kwargs: + raise ValueError("If a position argument is used, keyword arguments cannot be used.") + super().__init__(actual_instance=args[0]) + else: + super().__init__(**kwargs) + + @field_validator('actual_instance') + def actual_instance_must_validate_oneof(cls, v): + instance = Expression.model_construct() + error_messages = [] + match = 0 + # validate data type: AndOrExpression + if not isinstance(v, AndOrExpression): + error_messages.append(f"Error! Input type `{type(v)}` is not `AndOrExpression`") + else: + match += 1 + # validate data type: NotExpression + if not isinstance(v, NotExpression): + error_messages.append(f"Error! Input type `{type(v)}` is not `NotExpression`") + else: + match += 1 + # validate data type: SetExpression + if not isinstance(v, SetExpression): + error_messages.append(f"Error! Input type `{type(v)}` is not `SetExpression`") + else: + match += 1 + # validate data type: LiteralExpression + if not isinstance(v, LiteralExpression): + error_messages.append(f"Error! Input type `{type(v)}` is not `LiteralExpression`") + else: + match += 1 + # validate data type: UnaryExpression + if not isinstance(v, UnaryExpression): + error_messages.append(f"Error! Input type `{type(v)}` is not `UnaryExpression`") + else: + match += 1 + if match > 1: + # more than 1 match + raise ValueError("Multiple matches found when setting `actual_instance` in Expression with oneOf schemas: AndOrExpression, LiteralExpression, NotExpression, SetExpression, UnaryExpression. Details: " + ", ".join(error_messages)) + elif match == 0: + # no match + raise ValueError("No match found when setting `actual_instance` in Expression with oneOf schemas: AndOrExpression, LiteralExpression, NotExpression, SetExpression, UnaryExpression. Details: " + ", ".join(error_messages)) + else: + return v + + @classmethod + def from_dict(cls, obj: Union[str, Dict[str, Any]]) -> Self: + return cls.from_json(json.dumps(obj)) + + @classmethod + def from_json(cls, json_str: str) -> Self: + """Returns the object represented by the json string""" + instance = cls.model_construct() + error_messages = [] + match = 0 + + # deserialize data into AndOrExpression + try: + instance.actual_instance = AndOrExpression.from_json(json_str) + match += 1 + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) + # deserialize data into NotExpression + try: + instance.actual_instance = NotExpression.from_json(json_str) + match += 1 + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) + # deserialize data into SetExpression + try: + instance.actual_instance = SetExpression.from_json(json_str) + match += 1 + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) + # deserialize data into LiteralExpression + try: + instance.actual_instance = LiteralExpression.from_json(json_str) + match += 1 + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) + # deserialize data into UnaryExpression + try: + instance.actual_instance = UnaryExpression.from_json(json_str) + match += 1 + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) + + if match > 1: + # more than 1 match + raise ValueError("Multiple matches found when deserializing the JSON string into Expression with oneOf schemas: AndOrExpression, LiteralExpression, NotExpression, SetExpression, UnaryExpression. Details: " + ", ".join(error_messages)) + elif match == 0: + # no match + raise ValueError("No match found when deserializing the JSON string into Expression with oneOf schemas: AndOrExpression, LiteralExpression, NotExpression, SetExpression, UnaryExpression. Details: " + ", ".join(error_messages)) + else: + return instance + + def to_json(self) -> str: + """Returns the JSON representation of the actual instance""" + if self.actual_instance is None: + return "null" + + if hasattr(self.actual_instance, "to_json") and callable(self.actual_instance.to_json): + return self.actual_instance.to_json() + else: + return json.dumps(self.actual_instance) + + def to_dict(self) -> Optional[Union[Dict[str, Any], AndOrExpression, LiteralExpression, NotExpression, SetExpression, UnaryExpression]]: + """Returns the dict representation of the actual instance""" + if self.actual_instance is None: + return None + + if hasattr(self.actual_instance, "to_dict") and callable(self.actual_instance.to_dict): + return self.actual_instance.to_dict() + else: + # primitive type + return self.actual_instance + + def to_str(self) -> str: + """Returns the string representation of the actual instance""" + return pprint.pformat(self.model_dump()) + +from polaris.catalog.models.and_or_expression import AndOrExpression +from polaris.catalog.models.not_expression import NotExpression +# TODO: Rewrite to not use raise_errors +Expression.model_rebuild(raise_errors=False) + diff --git a/regtests/client/python/polaris/catalog/models/file_format.py b/regtests/client/python/polaris/catalog/models/file_format.py new file mode 100644 index 0000000000..f6c209036b --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/file_format.py @@ -0,0 +1,38 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import json +from enum import Enum +from typing_extensions import Self + + +class FileFormat(str, Enum): + """ + FileFormat + """ + + """ + allowed enum values + """ + AVRO = 'avro' + ORC = 'orc' + PARQUET = 'parquet' + + @classmethod + def from_json(cls, json_str: str) -> Self: + """Create an instance of FileFormat from a JSON string""" + return cls(json.loads(json_str)) + + diff --git a/regtests/client/python/polaris/catalog/models/get_namespace_response.py b/regtests/client/python/polaris/catalog/models/get_namespace_response.py new file mode 100644 index 0000000000..8e0d3629d1 --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/get_namespace_response.py @@ -0,0 +1,94 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictStr +from typing import Any, ClassVar, Dict, List, Optional +from typing import Optional, Set +from typing_extensions import Self + +class GetNamespaceResponse(BaseModel): + """ + GetNamespaceResponse + """ # noqa: E501 + namespace: List[StrictStr] = Field(description="Reference to one or more levels of a namespace") + properties: Optional[Dict[str, StrictStr]] = Field(default=None, description="Properties stored on the namespace, if supported by the server. If the server does not support namespace properties, it should return null for this field. If namespace properties are supported, but none are set, it should return an empty object.") + __properties: ClassVar[List[str]] = ["namespace", "properties"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of GetNamespaceResponse from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # set to None if properties (nullable) is None + # and model_fields_set contains the field + if self.properties is None and "properties" in self.model_fields_set: + _dict['properties'] = None + + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of GetNamespaceResponse from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "namespace": obj.get("namespace"), + "properties": obj.get("properties") + }) + return _obj + + diff --git a/regtests/client/python/polaris/catalog/models/iceberg_error_response.py b/regtests/client/python/polaris/catalog/models/iceberg_error_response.py new file mode 100644 index 0000000000..fce685d03e --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/iceberg_error_response.py @@ -0,0 +1,91 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict +from typing import Any, ClassVar, Dict, List +from polaris.catalog.models.error_model import ErrorModel +from typing import Optional, Set +from typing_extensions import Self + +class IcebergErrorResponse(BaseModel): + """ + JSON wrapper for all error responses (non-2xx) + """ # noqa: E501 + error: ErrorModel + __properties: ClassVar[List[str]] = ["error"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of IcebergErrorResponse from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of error + if self.error: + _dict['error'] = self.error.to_dict() + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of IcebergErrorResponse from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "error": ErrorModel.from_dict(obj["error"]) if obj.get("error") is not None else None + }) + return _obj + + diff --git a/regtests/client/python/polaris/catalog/models/list_namespaces_response.py b/regtests/client/python/polaris/catalog/models/list_namespaces_response.py new file mode 100644 index 0000000000..89ea46d218 --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/list_namespaces_response.py @@ -0,0 +1,94 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictStr +from typing import Any, ClassVar, Dict, List, Optional +from typing import Optional, Set +from typing_extensions import Self + +class ListNamespacesResponse(BaseModel): + """ + ListNamespacesResponse + """ # noqa: E501 + next_page_token: Optional[StrictStr] = Field(default=None, description="An opaque token that allows clients to make use of pagination for list APIs (e.g. ListTables). Clients may initiate the first paginated request by sending an empty query parameter `pageToken` to the server. Servers that support pagination should identify the `pageToken` parameter and return a `next-page-token` in the response if there are more results available. After the initial request, the value of `next-page-token` from each response must be used as the `pageToken` parameter value for the next request. The server must return `null` value for the `next-page-token` in the last response. Servers that support pagination must return all results in a single response with the value of `next-page-token` set to `null` if the query parameter `pageToken` is not set in the request. Servers that do not support pagination should ignore the `pageToken` parameter and return all results in a single response. The `next-page-token` must be omitted from the response. Clients must interpret either `null` or missing response value of `next-page-token` as the end of the listing results.", alias="next-page-token") + namespaces: Optional[List[List[StrictStr]]] = None + __properties: ClassVar[List[str]] = ["next-page-token", "namespaces"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of ListNamespacesResponse from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # set to None if next_page_token (nullable) is None + # and model_fields_set contains the field + if self.next_page_token is None and "next_page_token" in self.model_fields_set: + _dict['next-page-token'] = None + + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of ListNamespacesResponse from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "next-page-token": obj.get("next-page-token"), + "namespaces": obj.get("namespaces") + }) + return _obj + + diff --git a/regtests/client/python/polaris/catalog/models/list_tables_response.py b/regtests/client/python/polaris/catalog/models/list_tables_response.py new file mode 100644 index 0000000000..734e3bb5e4 --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/list_tables_response.py @@ -0,0 +1,102 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictStr +from typing import Any, ClassVar, Dict, List, Optional +from polaris.catalog.models.table_identifier import TableIdentifier +from typing import Optional, Set +from typing_extensions import Self + +class ListTablesResponse(BaseModel): + """ + ListTablesResponse + """ # noqa: E501 + next_page_token: Optional[StrictStr] = Field(default=None, description="An opaque token that allows clients to make use of pagination for list APIs (e.g. ListTables). Clients may initiate the first paginated request by sending an empty query parameter `pageToken` to the server. Servers that support pagination should identify the `pageToken` parameter and return a `next-page-token` in the response if there are more results available. After the initial request, the value of `next-page-token` from each response must be used as the `pageToken` parameter value for the next request. The server must return `null` value for the `next-page-token` in the last response. Servers that support pagination must return all results in a single response with the value of `next-page-token` set to `null` if the query parameter `pageToken` is not set in the request. Servers that do not support pagination should ignore the `pageToken` parameter and return all results in a single response. The `next-page-token` must be omitted from the response. Clients must interpret either `null` or missing response value of `next-page-token` as the end of the listing results.", alias="next-page-token") + identifiers: Optional[List[TableIdentifier]] = None + __properties: ClassVar[List[str]] = ["next-page-token", "identifiers"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of ListTablesResponse from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of each item in identifiers (list) + _items = [] + if self.identifiers: + for _item in self.identifiers: + if _item: + _items.append(_item.to_dict()) + _dict['identifiers'] = _items + # set to None if next_page_token (nullable) is None + # and model_fields_set contains the field + if self.next_page_token is None and "next_page_token" in self.model_fields_set: + _dict['next-page-token'] = None + + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of ListTablesResponse from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "next-page-token": obj.get("next-page-token"), + "identifiers": [TableIdentifier.from_dict(_item) for _item in obj["identifiers"]] if obj.get("identifiers") is not None else None + }) + return _obj + + diff --git a/regtests/client/python/polaris/catalog/models/list_type.py b/regtests/client/python/polaris/catalog/models/list_type.py new file mode 100644 index 0000000000..886b1ff0f8 --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/list_type.py @@ -0,0 +1,106 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictBool, StrictInt, StrictStr, field_validator +from typing import Any, ClassVar, Dict, List +from typing import Optional, Set +from typing_extensions import Self + +class ListType(BaseModel): + """ + ListType + """ # noqa: E501 + type: StrictStr + element_id: StrictInt = Field(alias="element-id") + element: Type + element_required: StrictBool = Field(alias="element-required") + __properties: ClassVar[List[str]] = ["type", "element-id", "element", "element-required"] + + @field_validator('type') + def type_validate_enum(cls, value): + """Validates the enum""" + if value not in set(['list']): + raise ValueError("must be one of enum values ('list')") + return value + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of ListType from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of element + if self.element: + _dict['element'] = self.element.to_dict() + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of ListType from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "type": obj.get("type"), + "element-id": obj.get("element-id"), + "element": Type.from_dict(obj["element"]) if obj.get("element") is not None else None, + "element-required": obj.get("element-required") + }) + return _obj + +from polaris.catalog.models.type import Type +# TODO: Rewrite to not use raise_errors +ListType.model_rebuild(raise_errors=False) + diff --git a/regtests/client/python/polaris/catalog/models/literal_expression.py b/regtests/client/python/polaris/catalog/models/literal_expression.py new file mode 100644 index 0000000000..cf5d8c5cc0 --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/literal_expression.py @@ -0,0 +1,95 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, StrictStr +from typing import Any, ClassVar, Dict, List +from polaris.catalog.models.term import Term +from typing import Optional, Set +from typing_extensions import Self + +class LiteralExpression(BaseModel): + """ + LiteralExpression + """ # noqa: E501 + type: StrictStr + term: Term + value: Dict[str, Any] + __properties: ClassVar[List[str]] = ["type", "term", "value"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of LiteralExpression from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of term + if self.term: + _dict['term'] = self.term.to_dict() + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of LiteralExpression from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "type": obj.get("type"), + "term": Term.from_dict(obj["term"]) if obj.get("term") is not None else None, + "value": obj.get("value") + }) + return _obj + + diff --git a/regtests/client/python/polaris/catalog/models/load_table_result.py b/regtests/client/python/polaris/catalog/models/load_table_result.py new file mode 100644 index 0000000000..21e17ca4d2 --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/load_table_result.py @@ -0,0 +1,95 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictStr +from typing import Any, ClassVar, Dict, List, Optional +from polaris.catalog.models.table_metadata import TableMetadata +from typing import Optional, Set +from typing_extensions import Self + +class LoadTableResult(BaseModel): + """ + Result used when a table is successfully loaded. The table metadata JSON is returned in the `metadata` field. The corresponding file location of table metadata should be returned in the `metadata-location` field, unless the metadata is not yet committed. For example, a create transaction may return metadata that is staged but not committed. Clients can check whether metadata has changed by comparing metadata locations after the table has been created. The `config` map returns table-specific configuration for the table's resources, including its HTTP client and FileIO. For example, config may contain a specific FileIO implementation class for the table depending on its underlying storage. The following configurations should be respected by clients: ## General Configurations - `token`: Authorization bearer token to use for table requests if OAuth2 security is enabled ## AWS Configurations The following configurations should be respected when working with tables stored in AWS S3 - `client.region`: region to configure client for making requests to AWS - `s3.access-key-id`: id for for credentials that provide access to the data in S3 - `s3.secret-access-key`: secret for credentials that provide access to data in S3 - `s3.session-token`: if present, this value should be used for as the session token - `s3.remote-signing-enabled`: if `true` remote signing should be performed as described in the `s3-signer-open-api.yaml` specification + """ # noqa: E501 + metadata_location: Optional[StrictStr] = Field(default=None, description="May be null if the table is staged as part of a transaction", alias="metadata-location") + metadata: TableMetadata + config: Optional[Dict[str, StrictStr]] = None + __properties: ClassVar[List[str]] = ["metadata-location", "metadata", "config"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of LoadTableResult from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of metadata + if self.metadata: + _dict['metadata'] = self.metadata.to_dict() + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of LoadTableResult from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "metadata-location": obj.get("metadata-location"), + "metadata": TableMetadata.from_dict(obj["metadata"]) if obj.get("metadata") is not None else None, + "config": obj.get("config") + }) + return _obj + + diff --git a/regtests/client/python/polaris/catalog/models/load_view_result.py b/regtests/client/python/polaris/catalog/models/load_view_result.py new file mode 100644 index 0000000000..7e87afbd57 --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/load_view_result.py @@ -0,0 +1,95 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictStr +from typing import Any, ClassVar, Dict, List, Optional +from polaris.catalog.models.view_metadata import ViewMetadata +from typing import Optional, Set +from typing_extensions import Self + +class LoadViewResult(BaseModel): + """ + Result used when a view is successfully loaded. The view metadata JSON is returned in the `metadata` field. The corresponding file location of view metadata is returned in the `metadata-location` field. Clients can check whether metadata has changed by comparing metadata locations after the view has been created. The `config` map returns view-specific configuration for the view's resources. The following configurations should be respected by clients: ## General Configurations - `token`: Authorization bearer token to use for view requests if OAuth2 security is enabled + """ # noqa: E501 + metadata_location: StrictStr = Field(alias="metadata-location") + metadata: ViewMetadata + config: Optional[Dict[str, StrictStr]] = None + __properties: ClassVar[List[str]] = ["metadata-location", "metadata", "config"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of LoadViewResult from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of metadata + if self.metadata: + _dict['metadata'] = self.metadata.to_dict() + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of LoadViewResult from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "metadata-location": obj.get("metadata-location"), + "metadata": ViewMetadata.from_dict(obj["metadata"]) if obj.get("metadata") is not None else None, + "config": obj.get("config") + }) + return _obj + + diff --git a/regtests/client/python/polaris/catalog/models/map_type.py b/regtests/client/python/polaris/catalog/models/map_type.py new file mode 100644 index 0000000000..92d8b76bed --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/map_type.py @@ -0,0 +1,113 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictBool, StrictInt, StrictStr, field_validator +from typing import Any, ClassVar, Dict, List +from typing import Optional, Set +from typing_extensions import Self + +class MapType(BaseModel): + """ + MapType + """ # noqa: E501 + type: StrictStr + key_id: StrictInt = Field(alias="key-id") + key: Type + value_id: StrictInt = Field(alias="value-id") + value: Type + value_required: StrictBool = Field(alias="value-required") + __properties: ClassVar[List[str]] = ["type", "key-id", "key", "value-id", "value", "value-required"] + + @field_validator('type') + def type_validate_enum(cls, value): + """Validates the enum""" + if value not in set(['map']): + raise ValueError("must be one of enum values ('map')") + return value + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of MapType from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of key + if self.key: + _dict['key'] = self.key.to_dict() + # override the default output from pydantic by calling `to_dict()` of value + if self.value: + _dict['value'] = self.value.to_dict() + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of MapType from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "type": obj.get("type"), + "key-id": obj.get("key-id"), + "key": Type.from_dict(obj["key"]) if obj.get("key") is not None else None, + "value-id": obj.get("value-id"), + "value": Type.from_dict(obj["value"]) if obj.get("value") is not None else None, + "value-required": obj.get("value-required") + }) + return _obj + +from polaris.catalog.models.type import Type +# TODO: Rewrite to not use raise_errors +MapType.model_rebuild(raise_errors=False) + diff --git a/regtests/client/python/polaris/catalog/models/metadata_log_inner.py b/regtests/client/python/polaris/catalog/models/metadata_log_inner.py new file mode 100644 index 0000000000..a2d4f0350a --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/metadata_log_inner.py @@ -0,0 +1,89 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictInt, StrictStr +from typing import Any, ClassVar, Dict, List +from typing import Optional, Set +from typing_extensions import Self + +class MetadataLogInner(BaseModel): + """ + MetadataLogInner + """ # noqa: E501 + metadata_file: StrictStr = Field(alias="metadata-file") + timestamp_ms: StrictInt = Field(alias="timestamp-ms") + __properties: ClassVar[List[str]] = ["metadata-file", "timestamp-ms"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of MetadataLogInner from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of MetadataLogInner from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "metadata-file": obj.get("metadata-file"), + "timestamp-ms": obj.get("timestamp-ms") + }) + return _obj + + diff --git a/regtests/client/python/polaris/catalog/models/metric_result.py b/regtests/client/python/polaris/catalog/models/metric_result.py new file mode 100644 index 0000000000..f6659adb3a --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/metric_result.py @@ -0,0 +1,134 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +from inspect import getfullargspec +import json +import pprint +import re # noqa: F401 +from pydantic import BaseModel, ConfigDict, Field, StrictStr, ValidationError, field_validator +from typing import Optional +from polaris.catalog.models.counter_result import CounterResult +from polaris.catalog.models.timer_result import TimerResult +from typing import Union, Any, List, Set, TYPE_CHECKING, Optional, Dict +from typing_extensions import Literal, Self +from pydantic import Field + +METRICRESULT_ANY_OF_SCHEMAS = ["CounterResult", "TimerResult"] + +class MetricResult(BaseModel): + """ + MetricResult + """ + + # data type: CounterResult + anyof_schema_1_validator: Optional[CounterResult] = None + # data type: TimerResult + anyof_schema_2_validator: Optional[TimerResult] = None + if TYPE_CHECKING: + actual_instance: Optional[Union[CounterResult, TimerResult]] = None + else: + actual_instance: Any = None + any_of_schemas: Set[str] = { "CounterResult", "TimerResult" } + + model_config = { + "validate_assignment": True, + "protected_namespaces": (), + } + + def __init__(self, *args, **kwargs) -> None: + if args: + if len(args) > 1: + raise ValueError("If a position argument is used, only 1 is allowed to set `actual_instance`") + if kwargs: + raise ValueError("If a position argument is used, keyword arguments cannot be used.") + super().__init__(actual_instance=args[0]) + else: + super().__init__(**kwargs) + + @field_validator('actual_instance') + def actual_instance_must_validate_anyof(cls, v): + instance = MetricResult.model_construct() + error_messages = [] + # validate data type: CounterResult + if not isinstance(v, CounterResult): + error_messages.append(f"Error! Input type `{type(v)}` is not `CounterResult`") + else: + return v + + # validate data type: TimerResult + if not isinstance(v, TimerResult): + error_messages.append(f"Error! Input type `{type(v)}` is not `TimerResult`") + else: + return v + + if error_messages: + # no match + raise ValueError("No match found when setting the actual_instance in MetricResult with anyOf schemas: CounterResult, TimerResult. Details: " + ", ".join(error_messages)) + else: + return v + + @classmethod + def from_dict(cls, obj: Dict[str, Any]) -> Self: + return cls.from_json(json.dumps(obj)) + + @classmethod + def from_json(cls, json_str: str) -> Self: + """Returns the object represented by the json string""" + instance = cls.model_construct() + error_messages = [] + # anyof_schema_1_validator: Optional[CounterResult] = None + try: + instance.actual_instance = CounterResult.from_json(json_str) + return instance + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) + # anyof_schema_2_validator: Optional[TimerResult] = None + try: + instance.actual_instance = TimerResult.from_json(json_str) + return instance + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) + + if error_messages: + # no match + raise ValueError("No match found when deserializing the JSON string into MetricResult with anyOf schemas: CounterResult, TimerResult. Details: " + ", ".join(error_messages)) + else: + return instance + + def to_json(self) -> str: + """Returns the JSON representation of the actual instance""" + if self.actual_instance is None: + return "null" + + if hasattr(self.actual_instance, "to_json") and callable(self.actual_instance.to_json): + return self.actual_instance.to_json() + else: + return json.dumps(self.actual_instance) + + def to_dict(self) -> Optional[Union[Dict[str, Any], CounterResult, TimerResult]]: + """Returns the dict representation of the actual instance""" + if self.actual_instance is None: + return None + + if hasattr(self.actual_instance, "to_dict") and callable(self.actual_instance.to_dict): + return self.actual_instance.to_dict() + else: + return self.actual_instance + + def to_str(self) -> str: + """Returns the string representation of the actual instance""" + return pprint.pformat(self.model_dump()) + + diff --git a/regtests/client/python/polaris/catalog/models/model_schema.py b/regtests/client/python/polaris/catalog/models/model_schema.py new file mode 100644 index 0000000000..15a4b1bec1 --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/model_schema.py @@ -0,0 +1,110 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictInt, StrictStr, field_validator +from typing import Any, ClassVar, Dict, List, Optional +from polaris.catalog.models.struct_field import StructField +from typing import Optional, Set +from typing_extensions import Self + +class ModelSchema(BaseModel): + """ + ModelSchema + """ # noqa: E501 + type: StrictStr + fields: List[StructField] + schema_id: Optional[StrictInt] = Field(default=None, alias="schema-id") + identifier_field_ids: Optional[List[StrictInt]] = Field(default=None, alias="identifier-field-ids") + __properties: ClassVar[List[str]] = ["type", "fields", "schema-id", "identifier-field-ids"] + + @field_validator('type') + def type_validate_enum(cls, value): + """Validates the enum""" + if value not in set(['struct']): + raise ValueError("must be one of enum values ('struct')") + return value + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of ModelSchema from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + * OpenAPI `readOnly` fields are excluded. + """ + excluded_fields: Set[str] = set([ + "schema_id", + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of each item in fields (list) + _items = [] + if self.fields: + for _item in self.fields: + if _item: + _items.append(_item.to_dict()) + _dict['fields'] = _items + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of ModelSchema from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "type": obj.get("type"), + "fields": [StructField.from_dict(_item) for _item in obj["fields"]] if obj.get("fields") is not None else None, + "schema-id": obj.get("schema-id"), + "identifier-field-ids": obj.get("identifier-field-ids") + }) + return _obj + + diff --git a/regtests/client/python/polaris/catalog/models/not_expression.py b/regtests/client/python/polaris/catalog/models/not_expression.py new file mode 100644 index 0000000000..5cfc26440b --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/not_expression.py @@ -0,0 +1,95 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, StrictStr +from typing import Any, ClassVar, Dict, List +from typing import Optional, Set +from typing_extensions import Self + +class NotExpression(BaseModel): + """ + NotExpression + """ # noqa: E501 + type: StrictStr + child: Expression + __properties: ClassVar[List[str]] = ["type", "child"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of NotExpression from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of child + if self.child: + _dict['child'] = self.child.to_dict() + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of NotExpression from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "type": obj.get("type"), + "child": Expression.from_dict(obj["child"]) if obj.get("child") is not None else None + }) + return _obj + +from polaris.catalog.models.expression import Expression +# TODO: Rewrite to not use raise_errors +NotExpression.model_rebuild(raise_errors=False) + diff --git a/regtests/client/python/polaris/catalog/models/notification_request.py b/regtests/client/python/polaris/catalog/models/notification_request.py new file mode 100644 index 0000000000..6add546be3 --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/notification_request.py @@ -0,0 +1,94 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field +from typing import Any, ClassVar, Dict, List, Optional +from polaris.catalog.models.notification_type import NotificationType +from polaris.catalog.models.table_update_notification import TableUpdateNotification +from typing import Optional, Set +from typing_extensions import Self + +class NotificationRequest(BaseModel): + """ + NotificationRequest + """ # noqa: E501 + notification_type: NotificationType = Field(alias="notification-type") + payload: Optional[TableUpdateNotification] = None + __properties: ClassVar[List[str]] = ["notification-type", "payload"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of NotificationRequest from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of payload + if self.payload: + _dict['payload'] = self.payload.to_dict() + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of NotificationRequest from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "notification-type": obj.get("notification-type"), + "payload": TableUpdateNotification.from_dict(obj["payload"]) if obj.get("payload") is not None else None + }) + return _obj + + diff --git a/regtests/client/python/polaris/catalog/models/notification_type.py b/regtests/client/python/polaris/catalog/models/notification_type.py new file mode 100644 index 0000000000..64b0d4b2af --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/notification_type.py @@ -0,0 +1,39 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import json +from enum import Enum +from typing_extensions import Self + + +class NotificationType(str, Enum): + """ + NotificationType + """ + + """ + allowed enum values + """ + UNKNOWN = 'UNKNOWN' + CREATE = 'CREATE' + UPDATE = 'UPDATE' + DROP = 'DROP' + + @classmethod + def from_json(cls, json_str: str) -> Self: + """Create an instance of NotificationType from a JSON string""" + return cls(json.loads(json_str)) + + diff --git a/regtests/client/python/polaris/catalog/models/null_order.py b/regtests/client/python/polaris/catalog/models/null_order.py new file mode 100644 index 0000000000..91ae75ebeb --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/null_order.py @@ -0,0 +1,37 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import json +from enum import Enum +from typing_extensions import Self + + +class NullOrder(str, Enum): + """ + NullOrder + """ + + """ + allowed enum values + """ + NULLS_MINUS_FIRST = 'nulls-first' + NULLS_MINUS_LAST = 'nulls-last' + + @classmethod + def from_json(cls, json_str: str) -> Self: + """Create an instance of NullOrder from a JSON string""" + return cls(json.loads(json_str)) + + diff --git a/regtests/client/python/polaris/catalog/models/o_auth_error.py b/regtests/client/python/polaris/catalog/models/o_auth_error.py new file mode 100644 index 0000000000..0399be5b0e --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/o_auth_error.py @@ -0,0 +1,98 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, StrictStr, field_validator +from typing import Any, ClassVar, Dict, List, Optional +from typing import Optional, Set +from typing_extensions import Self + +class OAuthError(BaseModel): + """ + OAuthError + """ # noqa: E501 + error: StrictStr + error_description: Optional[StrictStr] = None + error_uri: Optional[StrictStr] = None + __properties: ClassVar[List[str]] = ["error", "error_description", "error_uri"] + + @field_validator('error') + def error_validate_enum(cls, value): + """Validates the enum""" + if value not in set(['invalid_request', 'invalid_client', 'invalid_grant', 'unauthorized_client', 'unsupported_grant_type', 'invalid_scope']): + raise ValueError("must be one of enum values ('invalid_request', 'invalid_client', 'invalid_grant', 'unauthorized_client', 'unsupported_grant_type', 'invalid_scope')") + return value + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of OAuthError from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of OAuthError from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "error": obj.get("error"), + "error_description": obj.get("error_description"), + "error_uri": obj.get("error_uri") + }) + return _obj + + diff --git a/regtests/client/python/polaris/catalog/models/o_auth_token_response.py b/regtests/client/python/polaris/catalog/models/o_auth_token_response.py new file mode 100644 index 0000000000..c7786e539a --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/o_auth_token_response.py @@ -0,0 +1,105 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictInt, StrictStr, field_validator +from typing import Any, ClassVar, Dict, List, Optional +from polaris.catalog.models.token_type import TokenType +from typing import Optional, Set +from typing_extensions import Self + +class OAuthTokenResponse(BaseModel): + """ + OAuthTokenResponse + """ # noqa: E501 + access_token: StrictStr = Field(description="The access token, for client credentials or token exchange") + token_type: StrictStr = Field(description="Access token type for client credentials or token exchange See https://datatracker.ietf.org/doc/html/rfc6749#section-7.1") + expires_in: Optional[StrictInt] = Field(default=None, description="Lifetime of the access token in seconds for client credentials or token exchange") + issued_token_type: Optional[TokenType] = None + refresh_token: Optional[StrictStr] = Field(default=None, description="Refresh token for client credentials or token exchange") + scope: Optional[StrictStr] = Field(default=None, description="Authorization scope for client credentials or token exchange") + __properties: ClassVar[List[str]] = ["access_token", "token_type", "expires_in", "issued_token_type", "refresh_token", "scope"] + + @field_validator('token_type') + def token_type_validate_enum(cls, value): + """Validates the enum""" + if value not in set(['bearer', 'mac', 'N_A']): + raise ValueError("must be one of enum values ('bearer', 'mac', 'N_A')") + return value + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of OAuthTokenResponse from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of OAuthTokenResponse from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "access_token": obj.get("access_token"), + "token_type": obj.get("token_type"), + "expires_in": obj.get("expires_in"), + "issued_token_type": obj.get("issued_token_type"), + "refresh_token": obj.get("refresh_token"), + "scope": obj.get("scope") + }) + return _obj + + diff --git a/regtests/client/python/polaris/catalog/models/partition_field.py b/regtests/client/python/polaris/catalog/models/partition_field.py new file mode 100644 index 0000000000..b5d626ad95 --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/partition_field.py @@ -0,0 +1,93 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictInt, StrictStr +from typing import Any, ClassVar, Dict, List, Optional +from typing import Optional, Set +from typing_extensions import Self + +class PartitionField(BaseModel): + """ + PartitionField + """ # noqa: E501 + field_id: Optional[StrictInt] = Field(default=None, alias="field-id") + source_id: StrictInt = Field(alias="source-id") + name: StrictStr + transform: StrictStr + __properties: ClassVar[List[str]] = ["field-id", "source-id", "name", "transform"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of PartitionField from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of PartitionField from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "field-id": obj.get("field-id"), + "source-id": obj.get("source-id"), + "name": obj.get("name"), + "transform": obj.get("transform") + }) + return _obj + + diff --git a/regtests/client/python/polaris/catalog/models/partition_spec.py b/regtests/client/python/polaris/catalog/models/partition_spec.py new file mode 100644 index 0000000000..13d7bd2259 --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/partition_spec.py @@ -0,0 +1,99 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictInt +from typing import Any, ClassVar, Dict, List, Optional +from polaris.catalog.models.partition_field import PartitionField +from typing import Optional, Set +from typing_extensions import Self + +class PartitionSpec(BaseModel): + """ + PartitionSpec + """ # noqa: E501 + spec_id: Optional[StrictInt] = Field(default=None, alias="spec-id") + fields: List[PartitionField] + __properties: ClassVar[List[str]] = ["spec-id", "fields"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of PartitionSpec from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + * OpenAPI `readOnly` fields are excluded. + """ + excluded_fields: Set[str] = set([ + "spec_id", + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of each item in fields (list) + _items = [] + if self.fields: + for _item in self.fields: + if _item: + _items.append(_item.to_dict()) + _dict['fields'] = _items + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of PartitionSpec from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "spec-id": obj.get("spec-id"), + "fields": [PartitionField.from_dict(_item) for _item in obj["fields"]] if obj.get("fields") is not None else None + }) + return _obj + + diff --git a/regtests/client/python/polaris/catalog/models/partition_statistics_file.py b/regtests/client/python/polaris/catalog/models/partition_statistics_file.py new file mode 100644 index 0000000000..2a74c9e78d --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/partition_statistics_file.py @@ -0,0 +1,91 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictInt, StrictStr +from typing import Any, ClassVar, Dict, List +from typing import Optional, Set +from typing_extensions import Self + +class PartitionStatisticsFile(BaseModel): + """ + PartitionStatisticsFile + """ # noqa: E501 + snapshot_id: StrictInt = Field(alias="snapshot-id") + statistics_path: StrictStr = Field(alias="statistics-path") + file_size_in_bytes: StrictInt = Field(alias="file-size-in-bytes") + __properties: ClassVar[List[str]] = ["snapshot-id", "statistics-path", "file-size-in-bytes"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of PartitionStatisticsFile from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of PartitionStatisticsFile from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "snapshot-id": obj.get("snapshot-id"), + "statistics-path": obj.get("statistics-path"), + "file-size-in-bytes": obj.get("file-size-in-bytes") + }) + return _obj + + diff --git a/regtests/client/python/polaris/catalog/models/position_delete_file.py b/regtests/client/python/polaris/catalog/models/position_delete_file.py new file mode 100644 index 0000000000..8ea4417ec5 --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/position_delete_file.py @@ -0,0 +1,113 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import ConfigDict, StrictStr, field_validator +from typing import Any, ClassVar, Dict, List +from polaris.catalog.models.content_file import ContentFile +from polaris.catalog.models.file_format import FileFormat +from polaris.catalog.models.primitive_type_value import PrimitiveTypeValue +from typing import Optional, Set +from typing_extensions import Self + +class PositionDeleteFile(ContentFile): + """ + PositionDeleteFile + """ # noqa: E501 + content: StrictStr + __properties: ClassVar[List[str]] = ["content", "file-path", "file-format", "spec-id", "partition", "file-size-in-bytes", "record-count", "key-metadata", "split-offsets", "sort-order-id"] + + @field_validator('content') + def content_validate_enum(cls, value): + """Validates the enum""" + if value not in set(['position-deletes']): + raise ValueError("must be one of enum values ('position-deletes')") + return value + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of PositionDeleteFile from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of each item in partition (list) + _items = [] + if self.partition: + for _item in self.partition: + if _item: + _items.append(_item.to_dict()) + _dict['partition'] = _items + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of PositionDeleteFile from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "content": obj.get("content"), + "file-path": obj.get("file-path"), + "file-format": obj.get("file-format"), + "spec-id": obj.get("spec-id"), + "partition": [PrimitiveTypeValue.from_dict(_item) for _item in obj["partition"]] if obj.get("partition") is not None else None, + "file-size-in-bytes": obj.get("file-size-in-bytes"), + "record-count": obj.get("record-count"), + "key-metadata": obj.get("key-metadata"), + "split-offsets": obj.get("split-offsets"), + "sort-order-id": obj.get("sort-order-id") + }) + return _obj + + diff --git a/regtests/client/python/polaris/catalog/models/primitive_type_value.py b/regtests/client/python/polaris/catalog/models/primitive_type_value.py new file mode 100644 index 0000000000..36e79b0e0a --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/primitive_type_value.py @@ -0,0 +1,383 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import json +import pprint +from datetime import date +from pydantic import BaseModel, ConfigDict, Field, StrictBool, StrictFloat, StrictInt, StrictStr, ValidationError, field_validator +from typing import Any, List, Optional, Union +from typing_extensions import Annotated +from pydantic import StrictStr, Field +from typing import Union, List, Set, Optional, Dict +from typing_extensions import Literal, Self + +PRIMITIVETYPEVALUE_ONE_OF_SCHEMAS = ["bool", "date", "float", "int", "str"] + +class PrimitiveTypeValue(BaseModel): + """ + PrimitiveTypeValue + """ + # data type: bool + oneof_schema_1_validator: Optional[StrictBool] = None + # data type: int + oneof_schema_2_validator: Optional[StrictInt] = None + # data type: int + oneof_schema_3_validator: Optional[StrictInt] = None + # data type: float + oneof_schema_4_validator: Optional[Union[StrictFloat, StrictInt]] = None + # data type: float + oneof_schema_5_validator: Optional[Union[StrictFloat, StrictInt]] = None + # data type: str + oneof_schema_6_validator: Optional[StrictStr] = Field(default=None, description="Decimal type values are serialized as strings. Decimals with a positive scale serialize as numeric plain text, while decimals with a negative scale use scientific notation and the exponent will be equal to the negated scale. For instance, a decimal with a positive scale is '123.4500', with zero scale is '2', and with a negative scale is '2E+20'") + # data type: str + oneof_schema_7_validator: Optional[StrictStr] = None + # data type: str + oneof_schema_8_validator: Optional[Annotated[str, Field(min_length=36, strict=True, max_length=36)]] = Field(default=None, description="UUID type values are serialized as a 36-character lowercase string in standard UUID format as specified by RFC-4122") + # data type: date + oneof_schema_9_validator: Optional[date] = Field(default=None, description="Date type values follow the 'YYYY-MM-DD' ISO-8601 standard date format") + # data type: str + oneof_schema_10_validator: Optional[StrictStr] = Field(default=None, description="Time type values follow the 'HH:MM:SS.ssssss' ISO-8601 format with microsecond precision") + # data type: str + oneof_schema_11_validator: Optional[StrictStr] = Field(default=None, description="Timestamp type values follow the 'YYYY-MM-DDTHH:MM:SS.ssssss' ISO-8601 format with microsecond precision") + # data type: str + oneof_schema_12_validator: Optional[StrictStr] = Field(default=None, description="TimestampTz type values follow the 'YYYY-MM-DDTHH:MM:SS.ssssss+00:00' ISO-8601 format with microsecond precision, and a timezone offset (+00:00 for UTC)") + # data type: str + oneof_schema_13_validator: Optional[StrictStr] = Field(default=None, description="Timestamp_ns type values follow the 'YYYY-MM-DDTHH:MM:SS.sssssssss' ISO-8601 format with nanosecond precision") + # data type: str + oneof_schema_14_validator: Optional[StrictStr] = Field(default=None, description="Timestamp_ns type values follow the 'YYYY-MM-DDTHH:MM:SS.sssssssss+00:00' ISO-8601 format with nanosecond precision, and a timezone offset (+00:00 for UTC)") + # data type: str + oneof_schema_15_validator: Optional[StrictStr] = Field(default=None, description="Fixed length type values are stored and serialized as an uppercase hexadecimal string preserving the fixed length") + # data type: str + oneof_schema_16_validator: Optional[StrictStr] = Field(default=None, description="Binary type values are stored and serialized as an uppercase hexadecimal string") + actual_instance: Optional[Union[bool, date, float, int, str]] = None + one_of_schemas: Set[str] = { "bool", "date", "float", "int", "str" } + + model_config = ConfigDict( + validate_assignment=True, + protected_namespaces=(), + ) + + + def __init__(self, *args, **kwargs) -> None: + if args: + if len(args) > 1: + raise ValueError("If a position argument is used, only 1 is allowed to set `actual_instance`") + if kwargs: + raise ValueError("If a position argument is used, keyword arguments cannot be used.") + super().__init__(actual_instance=args[0]) + else: + super().__init__(**kwargs) + + @field_validator('actual_instance') + def actual_instance_must_validate_oneof(cls, v): + instance = PrimitiveTypeValue.model_construct() + error_messages = [] + match = 0 + # validate data type: bool + try: + instance.oneof_schema_1_validator = v + match += 1 + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) + # validate data type: int + try: + instance.oneof_schema_2_validator = v + match += 1 + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) + # validate data type: int + try: + instance.oneof_schema_3_validator = v + match += 1 + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) + # validate data type: float + try: + instance.oneof_schema_4_validator = v + match += 1 + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) + # validate data type: float + try: + instance.oneof_schema_5_validator = v + match += 1 + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) + # validate data type: str + try: + instance.oneof_schema_6_validator = v + match += 1 + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) + # validate data type: str + try: + instance.oneof_schema_7_validator = v + match += 1 + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) + # validate data type: str + try: + instance.oneof_schema_8_validator = v + match += 1 + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) + # validate data type: date + try: + instance.oneof_schema_9_validator = v + match += 1 + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) + # validate data type: str + try: + instance.oneof_schema_10_validator = v + match += 1 + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) + # validate data type: str + try: + instance.oneof_schema_11_validator = v + match += 1 + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) + # validate data type: str + try: + instance.oneof_schema_12_validator = v + match += 1 + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) + # validate data type: str + try: + instance.oneof_schema_13_validator = v + match += 1 + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) + # validate data type: str + try: + instance.oneof_schema_14_validator = v + match += 1 + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) + # validate data type: str + try: + instance.oneof_schema_15_validator = v + match += 1 + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) + # validate data type: str + try: + instance.oneof_schema_16_validator = v + match += 1 + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) + if match > 1: + # more than 1 match + raise ValueError("Multiple matches found when setting `actual_instance` in PrimitiveTypeValue with oneOf schemas: bool, date, float, int, str. Details: " + ", ".join(error_messages)) + elif match == 0: + # no match + raise ValueError("No match found when setting `actual_instance` in PrimitiveTypeValue with oneOf schemas: bool, date, float, int, str. Details: " + ", ".join(error_messages)) + else: + return v + + @classmethod + def from_dict(cls, obj: Union[str, Dict[str, Any]]) -> Self: + return cls.from_json(json.dumps(obj)) + + @classmethod + def from_json(cls, json_str: str) -> Self: + """Returns the object represented by the json string""" + instance = cls.model_construct() + error_messages = [] + match = 0 + + # deserialize data into bool + try: + # validation + instance.oneof_schema_1_validator = json.loads(json_str) + # assign value to actual_instance + instance.actual_instance = instance.oneof_schema_1_validator + match += 1 + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) + # deserialize data into int + try: + # validation + instance.oneof_schema_2_validator = json.loads(json_str) + # assign value to actual_instance + instance.actual_instance = instance.oneof_schema_2_validator + match += 1 + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) + # deserialize data into int + try: + # validation + instance.oneof_schema_3_validator = json.loads(json_str) + # assign value to actual_instance + instance.actual_instance = instance.oneof_schema_3_validator + match += 1 + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) + # deserialize data into float + try: + # validation + instance.oneof_schema_4_validator = json.loads(json_str) + # assign value to actual_instance + instance.actual_instance = instance.oneof_schema_4_validator + match += 1 + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) + # deserialize data into float + try: + # validation + instance.oneof_schema_5_validator = json.loads(json_str) + # assign value to actual_instance + instance.actual_instance = instance.oneof_schema_5_validator + match += 1 + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) + # deserialize data into str + try: + # validation + instance.oneof_schema_6_validator = json.loads(json_str) + # assign value to actual_instance + instance.actual_instance = instance.oneof_schema_6_validator + match += 1 + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) + # deserialize data into str + try: + # validation + instance.oneof_schema_7_validator = json.loads(json_str) + # assign value to actual_instance + instance.actual_instance = instance.oneof_schema_7_validator + match += 1 + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) + # deserialize data into str + try: + # validation + instance.oneof_schema_8_validator = json.loads(json_str) + # assign value to actual_instance + instance.actual_instance = instance.oneof_schema_8_validator + match += 1 + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) + # deserialize data into date + try: + # validation + instance.oneof_schema_9_validator = json.loads(json_str) + # assign value to actual_instance + instance.actual_instance = instance.oneof_schema_9_validator + match += 1 + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) + # deserialize data into str + try: + # validation + instance.oneof_schema_10_validator = json.loads(json_str) + # assign value to actual_instance + instance.actual_instance = instance.oneof_schema_10_validator + match += 1 + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) + # deserialize data into str + try: + # validation + instance.oneof_schema_11_validator = json.loads(json_str) + # assign value to actual_instance + instance.actual_instance = instance.oneof_schema_11_validator + match += 1 + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) + # deserialize data into str + try: + # validation + instance.oneof_schema_12_validator = json.loads(json_str) + # assign value to actual_instance + instance.actual_instance = instance.oneof_schema_12_validator + match += 1 + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) + # deserialize data into str + try: + # validation + instance.oneof_schema_13_validator = json.loads(json_str) + # assign value to actual_instance + instance.actual_instance = instance.oneof_schema_13_validator + match += 1 + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) + # deserialize data into str + try: + # validation + instance.oneof_schema_14_validator = json.loads(json_str) + # assign value to actual_instance + instance.actual_instance = instance.oneof_schema_14_validator + match += 1 + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) + # deserialize data into str + try: + # validation + instance.oneof_schema_15_validator = json.loads(json_str) + # assign value to actual_instance + instance.actual_instance = instance.oneof_schema_15_validator + match += 1 + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) + # deserialize data into str + try: + # validation + instance.oneof_schema_16_validator = json.loads(json_str) + # assign value to actual_instance + instance.actual_instance = instance.oneof_schema_16_validator + match += 1 + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) + + if match > 1: + # more than 1 match + raise ValueError("Multiple matches found when deserializing the JSON string into PrimitiveTypeValue with oneOf schemas: bool, date, float, int, str. Details: " + ", ".join(error_messages)) + elif match == 0: + # no match + raise ValueError("No match found when deserializing the JSON string into PrimitiveTypeValue with oneOf schemas: bool, date, float, int, str. Details: " + ", ".join(error_messages)) + else: + return instance + + def to_json(self) -> str: + """Returns the JSON representation of the actual instance""" + if self.actual_instance is None: + return "null" + + if hasattr(self.actual_instance, "to_json") and callable(self.actual_instance.to_json): + return self.actual_instance.to_json() + else: + return json.dumps(self.actual_instance) + + def to_dict(self) -> Optional[Union[Dict[str, Any], bool, date, float, int, str]]: + """Returns the dict representation of the actual instance""" + if self.actual_instance is None: + return None + + if hasattr(self.actual_instance, "to_dict") and callable(self.actual_instance.to_dict): + return self.actual_instance.to_dict() + else: + # primitive type + return self.actual_instance + + def to_str(self) -> str: + """Returns the string representation of the actual instance""" + return pprint.pformat(self.model_dump()) + + diff --git a/regtests/client/python/polaris/catalog/models/register_table_request.py b/regtests/client/python/polaris/catalog/models/register_table_request.py new file mode 100644 index 0000000000..39fe8057c6 --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/register_table_request.py @@ -0,0 +1,89 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictStr +from typing import Any, ClassVar, Dict, List +from typing import Optional, Set +from typing_extensions import Self + +class RegisterTableRequest(BaseModel): + """ + RegisterTableRequest + """ # noqa: E501 + name: StrictStr + metadata_location: StrictStr = Field(alias="metadata-location") + __properties: ClassVar[List[str]] = ["name", "metadata-location"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of RegisterTableRequest from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of RegisterTableRequest from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "name": obj.get("name"), + "metadata-location": obj.get("metadata-location") + }) + return _obj + + diff --git a/regtests/client/python/polaris/catalog/models/remove_partition_statistics_update.py b/regtests/client/python/polaris/catalog/models/remove_partition_statistics_update.py new file mode 100644 index 0000000000..6f8a3a4ea5 --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/remove_partition_statistics_update.py @@ -0,0 +1,96 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import ConfigDict, Field, StrictInt, StrictStr, field_validator +from typing import Any, ClassVar, Dict, List +from polaris.catalog.models.base_update import BaseUpdate +from typing import Optional, Set +from typing_extensions import Self + +class RemovePartitionStatisticsUpdate(BaseUpdate): + """ + RemovePartitionStatisticsUpdate + """ # noqa: E501 + action: StrictStr + snapshot_id: StrictInt = Field(alias="snapshot-id") + __properties: ClassVar[List[str]] = ["action"] + + @field_validator('action') + def action_validate_enum(cls, value): + """Validates the enum""" + if value not in set(['remove-partition-statistics']): + raise ValueError("must be one of enum values ('remove-partition-statistics')") + return value + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of RemovePartitionStatisticsUpdate from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of RemovePartitionStatisticsUpdate from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "action": obj.get("action") + }) + return _obj + + diff --git a/regtests/client/python/polaris/catalog/models/remove_properties_update.py b/regtests/client/python/polaris/catalog/models/remove_properties_update.py new file mode 100644 index 0000000000..4042243bb0 --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/remove_properties_update.py @@ -0,0 +1,96 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import ConfigDict, StrictStr, field_validator +from typing import Any, ClassVar, Dict, List +from polaris.catalog.models.base_update import BaseUpdate +from typing import Optional, Set +from typing_extensions import Self + +class RemovePropertiesUpdate(BaseUpdate): + """ + RemovePropertiesUpdate + """ # noqa: E501 + action: StrictStr + removals: List[StrictStr] + __properties: ClassVar[List[str]] = ["action"] + + @field_validator('action') + def action_validate_enum(cls, value): + """Validates the enum""" + if value not in set(['remove-properties']): + raise ValueError("must be one of enum values ('remove-properties')") + return value + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of RemovePropertiesUpdate from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of RemovePropertiesUpdate from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "action": obj.get("action") + }) + return _obj + + diff --git a/regtests/client/python/polaris/catalog/models/remove_snapshot_ref_update.py b/regtests/client/python/polaris/catalog/models/remove_snapshot_ref_update.py new file mode 100644 index 0000000000..1825964862 --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/remove_snapshot_ref_update.py @@ -0,0 +1,96 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import ConfigDict, Field, StrictStr, field_validator +from typing import Any, ClassVar, Dict, List +from polaris.catalog.models.base_update import BaseUpdate +from typing import Optional, Set +from typing_extensions import Self + +class RemoveSnapshotRefUpdate(BaseUpdate): + """ + RemoveSnapshotRefUpdate + """ # noqa: E501 + action: StrictStr + ref_name: StrictStr = Field(alias="ref-name") + __properties: ClassVar[List[str]] = ["action"] + + @field_validator('action') + def action_validate_enum(cls, value): + """Validates the enum""" + if value not in set(['remove-snapshot-ref']): + raise ValueError("must be one of enum values ('remove-snapshot-ref')") + return value + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of RemoveSnapshotRefUpdate from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of RemoveSnapshotRefUpdate from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "action": obj.get("action") + }) + return _obj + + diff --git a/regtests/client/python/polaris/catalog/models/remove_snapshots_update.py b/regtests/client/python/polaris/catalog/models/remove_snapshots_update.py new file mode 100644 index 0000000000..b1ec7c2ee7 --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/remove_snapshots_update.py @@ -0,0 +1,96 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import ConfigDict, Field, StrictInt, StrictStr, field_validator +from typing import Any, ClassVar, Dict, List +from polaris.catalog.models.base_update import BaseUpdate +from typing import Optional, Set +from typing_extensions import Self + +class RemoveSnapshotsUpdate(BaseUpdate): + """ + RemoveSnapshotsUpdate + """ # noqa: E501 + action: StrictStr + snapshot_ids: List[StrictInt] = Field(alias="snapshot-ids") + __properties: ClassVar[List[str]] = ["action"] + + @field_validator('action') + def action_validate_enum(cls, value): + """Validates the enum""" + if value not in set(['remove-snapshots']): + raise ValueError("must be one of enum values ('remove-snapshots')") + return value + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of RemoveSnapshotsUpdate from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of RemoveSnapshotsUpdate from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "action": obj.get("action") + }) + return _obj + + diff --git a/regtests/client/python/polaris/catalog/models/remove_statistics_update.py b/regtests/client/python/polaris/catalog/models/remove_statistics_update.py new file mode 100644 index 0000000000..48fd2549fb --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/remove_statistics_update.py @@ -0,0 +1,96 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import ConfigDict, Field, StrictInt, StrictStr, field_validator +from typing import Any, ClassVar, Dict, List +from polaris.catalog.models.base_update import BaseUpdate +from typing import Optional, Set +from typing_extensions import Self + +class RemoveStatisticsUpdate(BaseUpdate): + """ + RemoveStatisticsUpdate + """ # noqa: E501 + action: StrictStr + snapshot_id: StrictInt = Field(alias="snapshot-id") + __properties: ClassVar[List[str]] = ["action"] + + @field_validator('action') + def action_validate_enum(cls, value): + """Validates the enum""" + if value not in set(['remove-statistics']): + raise ValueError("must be one of enum values ('remove-statistics')") + return value + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of RemoveStatisticsUpdate from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of RemoveStatisticsUpdate from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "action": obj.get("action") + }) + return _obj + + diff --git a/regtests/client/python/polaris/catalog/models/rename_table_request.py b/regtests/client/python/polaris/catalog/models/rename_table_request.py new file mode 100644 index 0000000000..3a58b6157f --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/rename_table_request.py @@ -0,0 +1,96 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict +from typing import Any, ClassVar, Dict, List +from polaris.catalog.models.table_identifier import TableIdentifier +from typing import Optional, Set +from typing_extensions import Self + +class RenameTableRequest(BaseModel): + """ + RenameTableRequest + """ # noqa: E501 + source: TableIdentifier + destination: TableIdentifier + __properties: ClassVar[List[str]] = ["source", "destination"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of RenameTableRequest from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of source + if self.source: + _dict['source'] = self.source.to_dict() + # override the default output from pydantic by calling `to_dict()` of destination + if self.destination: + _dict['destination'] = self.destination.to_dict() + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of RenameTableRequest from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "source": TableIdentifier.from_dict(obj["source"]) if obj.get("source") is not None else None, + "destination": TableIdentifier.from_dict(obj["destination"]) if obj.get("destination") is not None else None + }) + return _obj + + diff --git a/regtests/client/python/polaris/catalog/models/report_metrics_request.py b/regtests/client/python/polaris/catalog/models/report_metrics_request.py new file mode 100644 index 0000000000..a9e4926c5f --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/report_metrics_request.py @@ -0,0 +1,134 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +from inspect import getfullargspec +import json +import pprint +import re # noqa: F401 +from pydantic import BaseModel, ConfigDict, Field, StrictStr, ValidationError, field_validator +from typing import Optional +from polaris.catalog.models.commit_report import CommitReport +from polaris.catalog.models.scan_report import ScanReport +from typing import Union, Any, List, Set, TYPE_CHECKING, Optional, Dict +from typing_extensions import Literal, Self +from pydantic import Field + +REPORTMETRICSREQUEST_ANY_OF_SCHEMAS = ["CommitReport", "ScanReport"] + +class ReportMetricsRequest(BaseModel): + """ + ReportMetricsRequest + """ + + # data type: ScanReport + anyof_schema_1_validator: Optional[ScanReport] = None + # data type: CommitReport + anyof_schema_2_validator: Optional[CommitReport] = None + if TYPE_CHECKING: + actual_instance: Optional[Union[CommitReport, ScanReport]] = None + else: + actual_instance: Any = None + any_of_schemas: Set[str] = { "CommitReport", "ScanReport" } + + model_config = { + "validate_assignment": True, + "protected_namespaces": (), + } + + def __init__(self, *args, **kwargs) -> None: + if args: + if len(args) > 1: + raise ValueError("If a position argument is used, only 1 is allowed to set `actual_instance`") + if kwargs: + raise ValueError("If a position argument is used, keyword arguments cannot be used.") + super().__init__(actual_instance=args[0]) + else: + super().__init__(**kwargs) + + @field_validator('actual_instance') + def actual_instance_must_validate_anyof(cls, v): + instance = ReportMetricsRequest.model_construct() + error_messages = [] + # validate data type: ScanReport + if not isinstance(v, ScanReport): + error_messages.append(f"Error! Input type `{type(v)}` is not `ScanReport`") + else: + return v + + # validate data type: CommitReport + if not isinstance(v, CommitReport): + error_messages.append(f"Error! Input type `{type(v)}` is not `CommitReport`") + else: + return v + + if error_messages: + # no match + raise ValueError("No match found when setting the actual_instance in ReportMetricsRequest with anyOf schemas: CommitReport, ScanReport. Details: " + ", ".join(error_messages)) + else: + return v + + @classmethod + def from_dict(cls, obj: Dict[str, Any]) -> Self: + return cls.from_json(json.dumps(obj)) + + @classmethod + def from_json(cls, json_str: str) -> Self: + """Returns the object represented by the json string""" + instance = cls.model_construct() + error_messages = [] + # anyof_schema_1_validator: Optional[ScanReport] = None + try: + instance.actual_instance = ScanReport.from_json(json_str) + return instance + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) + # anyof_schema_2_validator: Optional[CommitReport] = None + try: + instance.actual_instance = CommitReport.from_json(json_str) + return instance + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) + + if error_messages: + # no match + raise ValueError("No match found when deserializing the JSON string into ReportMetricsRequest with anyOf schemas: CommitReport, ScanReport. Details: " + ", ".join(error_messages)) + else: + return instance + + def to_json(self) -> str: + """Returns the JSON representation of the actual instance""" + if self.actual_instance is None: + return "null" + + if hasattr(self.actual_instance, "to_json") and callable(self.actual_instance.to_json): + return self.actual_instance.to_json() + else: + return json.dumps(self.actual_instance) + + def to_dict(self) -> Optional[Union[Dict[str, Any], CommitReport, ScanReport]]: + """Returns the dict representation of the actual instance""" + if self.actual_instance is None: + return None + + if hasattr(self.actual_instance, "to_dict") and callable(self.actual_instance.to_dict): + return self.actual_instance.to_dict() + else: + return self.actual_instance + + def to_str(self) -> str: + """Returns the string representation of the actual instance""" + return pprint.pformat(self.model_dump()) + + diff --git a/regtests/client/python/polaris/catalog/models/scan_report.py b/regtests/client/python/polaris/catalog/models/scan_report.py new file mode 100644 index 0000000000..6ab453199b --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/scan_report.py @@ -0,0 +1,118 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictInt, StrictStr +from typing import Any, ClassVar, Dict, List, Optional +from polaris.catalog.models.expression import Expression +from polaris.catalog.models.metric_result import MetricResult +from typing import Optional, Set +from typing_extensions import Self + +class ScanReport(BaseModel): + """ + ScanReport + """ # noqa: E501 + table_name: StrictStr = Field(alias="table-name") + snapshot_id: StrictInt = Field(alias="snapshot-id") + filter: Expression + schema_id: StrictInt = Field(alias="schema-id") + projected_field_ids: List[StrictInt] = Field(alias="projected-field-ids") + projected_field_names: List[StrictStr] = Field(alias="projected-field-names") + metrics: Dict[str, MetricResult] + metadata: Optional[Dict[str, StrictStr]] = None + __properties: ClassVar[List[str]] = ["table-name", "snapshot-id", "filter", "schema-id", "projected-field-ids", "projected-field-names", "metrics", "metadata"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of ScanReport from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of filter + if self.filter: + _dict['filter'] = self.filter.to_dict() + # override the default output from pydantic by calling `to_dict()` of each value in metrics (dict) + _field_dict = {} + if self.metrics: + for _key in self.metrics: + if self.metrics[_key]: + _field_dict[_key] = self.metrics[_key].to_dict() + _dict['metrics'] = _field_dict + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of ScanReport from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "table-name": obj.get("table-name"), + "snapshot-id": obj.get("snapshot-id"), + "filter": Expression.from_dict(obj["filter"]) if obj.get("filter") is not None else None, + "schema-id": obj.get("schema-id"), + "projected-field-ids": obj.get("projected-field-ids"), + "projected-field-names": obj.get("projected-field-names"), + "metrics": dict( + (_k, MetricResult.from_dict(_v)) + for _k, _v in obj["metrics"].items() + ) + if obj.get("metrics") is not None + else None, + "metadata": obj.get("metadata") + }) + return _obj + + diff --git a/regtests/client/python/polaris/catalog/models/set_current_schema_update.py b/regtests/client/python/polaris/catalog/models/set_current_schema_update.py new file mode 100644 index 0000000000..39891ca779 --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/set_current_schema_update.py @@ -0,0 +1,96 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import ConfigDict, Field, StrictInt, StrictStr, field_validator +from typing import Any, ClassVar, Dict, List +from polaris.catalog.models.base_update import BaseUpdate +from typing import Optional, Set +from typing_extensions import Self + +class SetCurrentSchemaUpdate(BaseUpdate): + """ + SetCurrentSchemaUpdate + """ # noqa: E501 + action: StrictStr + schema_id: StrictInt = Field(description="Schema ID to set as current, or -1 to set last added schema", alias="schema-id") + __properties: ClassVar[List[str]] = ["action"] + + @field_validator('action') + def action_validate_enum(cls, value): + """Validates the enum""" + if value not in set(['set-current-schema']): + raise ValueError("must be one of enum values ('set-current-schema')") + return value + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of SetCurrentSchemaUpdate from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of SetCurrentSchemaUpdate from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "action": obj.get("action") + }) + return _obj + + diff --git a/regtests/client/python/polaris/catalog/models/set_current_view_version_update.py b/regtests/client/python/polaris/catalog/models/set_current_view_version_update.py new file mode 100644 index 0000000000..e4623dff7e --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/set_current_view_version_update.py @@ -0,0 +1,96 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import ConfigDict, Field, StrictInt, StrictStr, field_validator +from typing import Any, ClassVar, Dict, List +from polaris.catalog.models.base_update import BaseUpdate +from typing import Optional, Set +from typing_extensions import Self + +class SetCurrentViewVersionUpdate(BaseUpdate): + """ + SetCurrentViewVersionUpdate + """ # noqa: E501 + action: StrictStr + view_version_id: StrictInt = Field(description="The view version id to set as current, or -1 to set last added view version id", alias="view-version-id") + __properties: ClassVar[List[str]] = ["action"] + + @field_validator('action') + def action_validate_enum(cls, value): + """Validates the enum""" + if value not in set(['set-current-view-version']): + raise ValueError("must be one of enum values ('set-current-view-version')") + return value + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of SetCurrentViewVersionUpdate from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of SetCurrentViewVersionUpdate from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "action": obj.get("action") + }) + return _obj + + diff --git a/regtests/client/python/polaris/catalog/models/set_default_sort_order_update.py b/regtests/client/python/polaris/catalog/models/set_default_sort_order_update.py new file mode 100644 index 0000000000..40f552cc98 --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/set_default_sort_order_update.py @@ -0,0 +1,96 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import ConfigDict, Field, StrictInt, StrictStr, field_validator +from typing import Any, ClassVar, Dict, List +from polaris.catalog.models.base_update import BaseUpdate +from typing import Optional, Set +from typing_extensions import Self + +class SetDefaultSortOrderUpdate(BaseUpdate): + """ + SetDefaultSortOrderUpdate + """ # noqa: E501 + action: StrictStr + sort_order_id: StrictInt = Field(description="Sort order ID to set as the default, or -1 to set last added sort order", alias="sort-order-id") + __properties: ClassVar[List[str]] = ["action"] + + @field_validator('action') + def action_validate_enum(cls, value): + """Validates the enum""" + if value not in set(['set-default-sort-order']): + raise ValueError("must be one of enum values ('set-default-sort-order')") + return value + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of SetDefaultSortOrderUpdate from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of SetDefaultSortOrderUpdate from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "action": obj.get("action") + }) + return _obj + + diff --git a/regtests/client/python/polaris/catalog/models/set_default_spec_update.py b/regtests/client/python/polaris/catalog/models/set_default_spec_update.py new file mode 100644 index 0000000000..da150c9a5b --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/set_default_spec_update.py @@ -0,0 +1,96 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import ConfigDict, Field, StrictInt, StrictStr, field_validator +from typing import Any, ClassVar, Dict, List +from polaris.catalog.models.base_update import BaseUpdate +from typing import Optional, Set +from typing_extensions import Self + +class SetDefaultSpecUpdate(BaseUpdate): + """ + SetDefaultSpecUpdate + """ # noqa: E501 + action: StrictStr + spec_id: StrictInt = Field(description="Partition spec ID to set as the default, or -1 to set last added spec", alias="spec-id") + __properties: ClassVar[List[str]] = ["action"] + + @field_validator('action') + def action_validate_enum(cls, value): + """Validates the enum""" + if value not in set(['set-default-spec']): + raise ValueError("must be one of enum values ('set-default-spec')") + return value + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of SetDefaultSpecUpdate from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of SetDefaultSpecUpdate from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "action": obj.get("action") + }) + return _obj + + diff --git a/regtests/client/python/polaris/catalog/models/set_expression.py b/regtests/client/python/polaris/catalog/models/set_expression.py new file mode 100644 index 0000000000..d2aa53a611 --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/set_expression.py @@ -0,0 +1,95 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, StrictStr +from typing import Any, ClassVar, Dict, List +from polaris.catalog.models.term import Term +from typing import Optional, Set +from typing_extensions import Self + +class SetExpression(BaseModel): + """ + SetExpression + """ # noqa: E501 + type: StrictStr + term: Term + values: List[Dict[str, Any]] + __properties: ClassVar[List[str]] = ["type", "term", "values"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of SetExpression from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of term + if self.term: + _dict['term'] = self.term.to_dict() + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of SetExpression from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "type": obj.get("type"), + "term": Term.from_dict(obj["term"]) if obj.get("term") is not None else None, + "values": obj.get("values") + }) + return _obj + + diff --git a/regtests/client/python/polaris/catalog/models/set_location_update.py b/regtests/client/python/polaris/catalog/models/set_location_update.py new file mode 100644 index 0000000000..d7e9c19708 --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/set_location_update.py @@ -0,0 +1,96 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import ConfigDict, StrictStr, field_validator +from typing import Any, ClassVar, Dict, List +from polaris.catalog.models.base_update import BaseUpdate +from typing import Optional, Set +from typing_extensions import Self + +class SetLocationUpdate(BaseUpdate): + """ + SetLocationUpdate + """ # noqa: E501 + action: StrictStr + location: StrictStr + __properties: ClassVar[List[str]] = ["action"] + + @field_validator('action') + def action_validate_enum(cls, value): + """Validates the enum""" + if value not in set(['set-location']): + raise ValueError("must be one of enum values ('set-location')") + return value + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of SetLocationUpdate from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of SetLocationUpdate from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "action": obj.get("action") + }) + return _obj + + diff --git a/regtests/client/python/polaris/catalog/models/set_partition_statistics_update.py b/regtests/client/python/polaris/catalog/models/set_partition_statistics_update.py new file mode 100644 index 0000000000..9c872dfc53 --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/set_partition_statistics_update.py @@ -0,0 +1,97 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import ConfigDict, Field, StrictStr, field_validator +from typing import Any, ClassVar, Dict, List +from polaris.catalog.models.base_update import BaseUpdate +from polaris.catalog.models.partition_statistics_file import PartitionStatisticsFile +from typing import Optional, Set +from typing_extensions import Self + +class SetPartitionStatisticsUpdate(BaseUpdate): + """ + SetPartitionStatisticsUpdate + """ # noqa: E501 + action: StrictStr + partition_statistics: PartitionStatisticsFile = Field(alias="partition-statistics") + __properties: ClassVar[List[str]] = ["action"] + + @field_validator('action') + def action_validate_enum(cls, value): + """Validates the enum""" + if value not in set(['set-partition-statistics']): + raise ValueError("must be one of enum values ('set-partition-statistics')") + return value + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of SetPartitionStatisticsUpdate from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of SetPartitionStatisticsUpdate from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "action": obj.get("action") + }) + return _obj + + diff --git a/regtests/client/python/polaris/catalog/models/set_properties_update.py b/regtests/client/python/polaris/catalog/models/set_properties_update.py new file mode 100644 index 0000000000..1f04486e1a --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/set_properties_update.py @@ -0,0 +1,96 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import ConfigDict, StrictStr, field_validator +from typing import Any, ClassVar, Dict, List +from polaris.catalog.models.base_update import BaseUpdate +from typing import Optional, Set +from typing_extensions import Self + +class SetPropertiesUpdate(BaseUpdate): + """ + SetPropertiesUpdate + """ # noqa: E501 + action: StrictStr + updates: Dict[str, StrictStr] + __properties: ClassVar[List[str]] = ["action"] + + @field_validator('action') + def action_validate_enum(cls, value): + """Validates the enum""" + if value not in set(['set-properties']): + raise ValueError("must be one of enum values ('set-properties')") + return value + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of SetPropertiesUpdate from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of SetPropertiesUpdate from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "action": obj.get("action") + }) + return _obj + + diff --git a/regtests/client/python/polaris/catalog/models/set_snapshot_ref_update.py b/regtests/client/python/polaris/catalog/models/set_snapshot_ref_update.py new file mode 100644 index 0000000000..fafee48029 --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/set_snapshot_ref_update.py @@ -0,0 +1,113 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import ConfigDict, Field, StrictInt, StrictStr, field_validator +from typing import Any, ClassVar, Dict, List, Optional +from polaris.catalog.models.base_update import BaseUpdate +from typing import Optional, Set +from typing_extensions import Self + +class SetSnapshotRefUpdate(BaseUpdate): + """ + SetSnapshotRefUpdate + """ # noqa: E501 + action: StrictStr + ref_name: StrictStr = Field(alias="ref-name") + type: StrictStr + snapshot_id: StrictInt = Field(alias="snapshot-id") + max_ref_age_ms: Optional[StrictInt] = Field(default=None, alias="max-ref-age-ms") + max_snapshot_age_ms: Optional[StrictInt] = Field(default=None, alias="max-snapshot-age-ms") + min_snapshots_to_keep: Optional[StrictInt] = Field(default=None, alias="min-snapshots-to-keep") + __properties: ClassVar[List[str]] = ["action", "type", "snapshot-id", "max-ref-age-ms", "max-snapshot-age-ms", "min-snapshots-to-keep"] + + @field_validator('action') + def action_validate_enum(cls, value): + """Validates the enum""" + if value not in set(['set-snapshot-ref']): + raise ValueError("must be one of enum values ('set-snapshot-ref')") + return value + + @field_validator('type') + def type_validate_enum(cls, value): + """Validates the enum""" + if value not in set(['tag', 'branch']): + raise ValueError("must be one of enum values ('tag', 'branch')") + return value + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of SetSnapshotRefUpdate from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of SetSnapshotRefUpdate from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "action": obj.get("action"), + "type": obj.get("type"), + "snapshot-id": obj.get("snapshot-id"), + "max-ref-age-ms": obj.get("max-ref-age-ms"), + "max-snapshot-age-ms": obj.get("max-snapshot-age-ms"), + "min-snapshots-to-keep": obj.get("min-snapshots-to-keep") + }) + return _obj + + diff --git a/regtests/client/python/polaris/catalog/models/set_statistics_update.py b/regtests/client/python/polaris/catalog/models/set_statistics_update.py new file mode 100644 index 0000000000..5ce65734d9 --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/set_statistics_update.py @@ -0,0 +1,98 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import ConfigDict, Field, StrictInt, StrictStr, field_validator +from typing import Any, ClassVar, Dict, List +from polaris.catalog.models.base_update import BaseUpdate +from polaris.catalog.models.statistics_file import StatisticsFile +from typing import Optional, Set +from typing_extensions import Self + +class SetStatisticsUpdate(BaseUpdate): + """ + SetStatisticsUpdate + """ # noqa: E501 + action: StrictStr + snapshot_id: StrictInt = Field(alias="snapshot-id") + statistics: StatisticsFile + __properties: ClassVar[List[str]] = ["action"] + + @field_validator('action') + def action_validate_enum(cls, value): + """Validates the enum""" + if value not in set(['set-statistics']): + raise ValueError("must be one of enum values ('set-statistics')") + return value + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of SetStatisticsUpdate from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of SetStatisticsUpdate from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "action": obj.get("action") + }) + return _obj + + diff --git a/regtests/client/python/polaris/catalog/models/snapshot.py b/regtests/client/python/polaris/catalog/models/snapshot.py new file mode 100644 index 0000000000..888ab24518 --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/snapshot.py @@ -0,0 +1,103 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictInt, StrictStr +from typing import Any, ClassVar, Dict, List, Optional +from polaris.catalog.models.snapshot_summary import SnapshotSummary +from typing import Optional, Set +from typing_extensions import Self + +class Snapshot(BaseModel): + """ + Snapshot + """ # noqa: E501 + snapshot_id: StrictInt = Field(alias="snapshot-id") + parent_snapshot_id: Optional[StrictInt] = Field(default=None, alias="parent-snapshot-id") + sequence_number: Optional[StrictInt] = Field(default=None, alias="sequence-number") + timestamp_ms: StrictInt = Field(alias="timestamp-ms") + manifest_list: StrictStr = Field(description="Location of the snapshot's manifest list file", alias="manifest-list") + summary: SnapshotSummary + schema_id: Optional[StrictInt] = Field(default=None, alias="schema-id") + __properties: ClassVar[List[str]] = ["snapshot-id", "parent-snapshot-id", "sequence-number", "timestamp-ms", "manifest-list", "summary", "schema-id"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of Snapshot from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of summary + if self.summary: + _dict['summary'] = self.summary.to_dict() + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of Snapshot from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "snapshot-id": obj.get("snapshot-id"), + "parent-snapshot-id": obj.get("parent-snapshot-id"), + "sequence-number": obj.get("sequence-number"), + "timestamp-ms": obj.get("timestamp-ms"), + "manifest-list": obj.get("manifest-list"), + "summary": SnapshotSummary.from_dict(obj["summary"]) if obj.get("summary") is not None else None, + "schema-id": obj.get("schema-id") + }) + return _obj + + diff --git a/regtests/client/python/polaris/catalog/models/snapshot_log_inner.py b/regtests/client/python/polaris/catalog/models/snapshot_log_inner.py new file mode 100644 index 0000000000..f61efbfcb4 --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/snapshot_log_inner.py @@ -0,0 +1,89 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictInt +from typing import Any, ClassVar, Dict, List +from typing import Optional, Set +from typing_extensions import Self + +class SnapshotLogInner(BaseModel): + """ + SnapshotLogInner + """ # noqa: E501 + snapshot_id: StrictInt = Field(alias="snapshot-id") + timestamp_ms: StrictInt = Field(alias="timestamp-ms") + __properties: ClassVar[List[str]] = ["snapshot-id", "timestamp-ms"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of SnapshotLogInner from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of SnapshotLogInner from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "snapshot-id": obj.get("snapshot-id"), + "timestamp-ms": obj.get("timestamp-ms") + }) + return _obj + + diff --git a/regtests/client/python/polaris/catalog/models/snapshot_reference.py b/regtests/client/python/polaris/catalog/models/snapshot_reference.py new file mode 100644 index 0000000000..b7e81a7b12 --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/snapshot_reference.py @@ -0,0 +1,102 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictInt, StrictStr, field_validator +from typing import Any, ClassVar, Dict, List, Optional +from typing import Optional, Set +from typing_extensions import Self + +class SnapshotReference(BaseModel): + """ + SnapshotReference + """ # noqa: E501 + type: StrictStr + snapshot_id: StrictInt = Field(alias="snapshot-id") + max_ref_age_ms: Optional[StrictInt] = Field(default=None, alias="max-ref-age-ms") + max_snapshot_age_ms: Optional[StrictInt] = Field(default=None, alias="max-snapshot-age-ms") + min_snapshots_to_keep: Optional[StrictInt] = Field(default=None, alias="min-snapshots-to-keep") + __properties: ClassVar[List[str]] = ["type", "snapshot-id", "max-ref-age-ms", "max-snapshot-age-ms", "min-snapshots-to-keep"] + + @field_validator('type') + def type_validate_enum(cls, value): + """Validates the enum""" + if value not in set(['tag', 'branch']): + raise ValueError("must be one of enum values ('tag', 'branch')") + return value + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of SnapshotReference from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of SnapshotReference from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "type": obj.get("type"), + "snapshot-id": obj.get("snapshot-id"), + "max-ref-age-ms": obj.get("max-ref-age-ms"), + "max-snapshot-age-ms": obj.get("max-snapshot-age-ms"), + "min-snapshots-to-keep": obj.get("min-snapshots-to-keep") + }) + return _obj + + diff --git a/regtests/client/python/polaris/catalog/models/snapshot_summary.py b/regtests/client/python/polaris/catalog/models/snapshot_summary.py new file mode 100644 index 0000000000..dcdc05f6bf --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/snapshot_summary.py @@ -0,0 +1,107 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, StrictStr, field_validator +from typing import Any, ClassVar, Dict, List +from typing import Optional, Set +from typing_extensions import Self + +class SnapshotSummary(BaseModel): + """ + SnapshotSummary + """ # noqa: E501 + operation: StrictStr + additional_properties: Dict[str, Any] = {} + __properties: ClassVar[List[str]] = ["operation"] + + @field_validator('operation') + def operation_validate_enum(cls, value): + """Validates the enum""" + if value not in set(['append', 'replace', 'overwrite', 'delete']): + raise ValueError("must be one of enum values ('append', 'replace', 'overwrite', 'delete')") + return value + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of SnapshotSummary from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + * Fields in `self.additional_properties` are added to the output dict. + """ + excluded_fields: Set[str] = set([ + "additional_properties", + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # puts key-value pairs in additional_properties in the top level + if self.additional_properties is not None: + for _key, _value in self.additional_properties.items(): + _dict[_key] = _value + + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of SnapshotSummary from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "operation": obj.get("operation") + }) + # store additional fields in additional_properties + for _key in obj.keys(): + if _key not in cls.__properties: + _obj.additional_properties[_key] = obj.get(_key) + + return _obj + + diff --git a/regtests/client/python/polaris/catalog/models/sort_direction.py b/regtests/client/python/polaris/catalog/models/sort_direction.py new file mode 100644 index 0000000000..799cdc96d3 --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/sort_direction.py @@ -0,0 +1,37 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import json +from enum import Enum +from typing_extensions import Self + + +class SortDirection(str, Enum): + """ + SortDirection + """ + + """ + allowed enum values + """ + ASC = 'asc' + DESC = 'desc' + + @classmethod + def from_json(cls, json_str: str) -> Self: + """Create an instance of SortDirection from a JSON string""" + return cls(json.loads(json_str)) + + diff --git a/regtests/client/python/polaris/catalog/models/sort_field.py b/regtests/client/python/polaris/catalog/models/sort_field.py new file mode 100644 index 0000000000..0fb3b98765 --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/sort_field.py @@ -0,0 +1,95 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictInt, StrictStr +from typing import Any, ClassVar, Dict, List +from polaris.catalog.models.null_order import NullOrder +from polaris.catalog.models.sort_direction import SortDirection +from typing import Optional, Set +from typing_extensions import Self + +class SortField(BaseModel): + """ + SortField + """ # noqa: E501 + source_id: StrictInt = Field(alias="source-id") + transform: StrictStr + direction: SortDirection + null_order: NullOrder = Field(alias="null-order") + __properties: ClassVar[List[str]] = ["source-id", "transform", "direction", "null-order"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of SortField from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of SortField from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "source-id": obj.get("source-id"), + "transform": obj.get("transform"), + "direction": obj.get("direction"), + "null-order": obj.get("null-order") + }) + return _obj + + diff --git a/regtests/client/python/polaris/catalog/models/sort_order.py b/regtests/client/python/polaris/catalog/models/sort_order.py new file mode 100644 index 0000000000..07a68ce734 --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/sort_order.py @@ -0,0 +1,99 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictInt +from typing import Any, ClassVar, Dict, List +from polaris.catalog.models.sort_field import SortField +from typing import Optional, Set +from typing_extensions import Self + +class SortOrder(BaseModel): + """ + SortOrder + """ # noqa: E501 + order_id: StrictInt = Field(alias="order-id") + fields: List[SortField] + __properties: ClassVar[List[str]] = ["order-id", "fields"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of SortOrder from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + * OpenAPI `readOnly` fields are excluded. + """ + excluded_fields: Set[str] = set([ + "order_id", + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of each item in fields (list) + _items = [] + if self.fields: + for _item in self.fields: + if _item: + _items.append(_item.to_dict()) + _dict['fields'] = _items + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of SortOrder from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "order-id": obj.get("order-id"), + "fields": [SortField.from_dict(_item) for _item in obj["fields"]] if obj.get("fields") is not None else None + }) + return _obj + + diff --git a/regtests/client/python/polaris/catalog/models/sql_view_representation.py b/regtests/client/python/polaris/catalog/models/sql_view_representation.py new file mode 100644 index 0000000000..0ad32c7f37 --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/sql_view_representation.py @@ -0,0 +1,91 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, StrictStr +from typing import Any, ClassVar, Dict, List +from typing import Optional, Set +from typing_extensions import Self + +class SQLViewRepresentation(BaseModel): + """ + SQLViewRepresentation + """ # noqa: E501 + type: StrictStr + sql: StrictStr + dialect: StrictStr + __properties: ClassVar[List[str]] = ["type", "sql", "dialect"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of SQLViewRepresentation from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of SQLViewRepresentation from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "type": obj.get("type"), + "sql": obj.get("sql"), + "dialect": obj.get("dialect") + }) + return _obj + + diff --git a/regtests/client/python/polaris/catalog/models/statistics_file.py b/regtests/client/python/polaris/catalog/models/statistics_file.py new file mode 100644 index 0000000000..cd07fb7ceb --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/statistics_file.py @@ -0,0 +1,103 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictInt, StrictStr +from typing import Any, ClassVar, Dict, List +from polaris.catalog.models.blob_metadata import BlobMetadata +from typing import Optional, Set +from typing_extensions import Self + +class StatisticsFile(BaseModel): + """ + StatisticsFile + """ # noqa: E501 + snapshot_id: StrictInt = Field(alias="snapshot-id") + statistics_path: StrictStr = Field(alias="statistics-path") + file_size_in_bytes: StrictInt = Field(alias="file-size-in-bytes") + file_footer_size_in_bytes: StrictInt = Field(alias="file-footer-size-in-bytes") + blob_metadata: List[BlobMetadata] = Field(alias="blob-metadata") + __properties: ClassVar[List[str]] = ["snapshot-id", "statistics-path", "file-size-in-bytes", "file-footer-size-in-bytes", "blob-metadata"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of StatisticsFile from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of each item in blob_metadata (list) + _items = [] + if self.blob_metadata: + for _item in self.blob_metadata: + if _item: + _items.append(_item.to_dict()) + _dict['blob-metadata'] = _items + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of StatisticsFile from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "snapshot-id": obj.get("snapshot-id"), + "statistics-path": obj.get("statistics-path"), + "file-size-in-bytes": obj.get("file-size-in-bytes"), + "file-footer-size-in-bytes": obj.get("file-footer-size-in-bytes"), + "blob-metadata": [BlobMetadata.from_dict(_item) for _item in obj["blob-metadata"]] if obj.get("blob-metadata") is not None else None + }) + return _obj + + diff --git a/regtests/client/python/polaris/catalog/models/struct_field.py b/regtests/client/python/polaris/catalog/models/struct_field.py new file mode 100644 index 0000000000..65ca86605c --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/struct_field.py @@ -0,0 +1,101 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, StrictBool, StrictInt, StrictStr +from typing import Any, ClassVar, Dict, List, Optional +from typing import Optional, Set +from typing_extensions import Self + +class StructField(BaseModel): + """ + StructField + """ # noqa: E501 + id: StrictInt + name: StrictStr + type: Type + required: StrictBool + doc: Optional[StrictStr] = None + __properties: ClassVar[List[str]] = ["id", "name", "type", "required", "doc"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of StructField from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of type + if self.type: + _dict['type'] = self.type.to_dict() + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of StructField from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "id": obj.get("id"), + "name": obj.get("name"), + "type": Type.from_dict(obj["type"]) if obj.get("type") is not None else None, + "required": obj.get("required"), + "doc": obj.get("doc") + }) + return _obj + +from polaris.catalog.models.type import Type +# TODO: Rewrite to not use raise_errors +StructField.model_rebuild(raise_errors=False) + diff --git a/regtests/client/python/polaris/catalog/models/struct_type.py b/regtests/client/python/polaris/catalog/models/struct_type.py new file mode 100644 index 0000000000..c65b50c9a0 --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/struct_type.py @@ -0,0 +1,106 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, StrictStr, field_validator +from typing import Any, ClassVar, Dict, List +from typing import Optional, Set +from typing_extensions import Self + +class StructType(BaseModel): + """ + StructType + """ # noqa: E501 + type: StrictStr + fields: List[StructField] + __properties: ClassVar[List[str]] = ["type", "fields"] + + @field_validator('type') + def type_validate_enum(cls, value): + """Validates the enum""" + if value not in set(['struct']): + raise ValueError("must be one of enum values ('struct')") + return value + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of StructType from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of each item in fields (list) + _items = [] + if self.fields: + for _item in self.fields: + if _item: + _items.append(_item.to_dict()) + _dict['fields'] = _items + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of StructType from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "type": obj.get("type"), + "fields": [StructField.from_dict(_item) for _item in obj["fields"]] if obj.get("fields") is not None else None + }) + return _obj + +from polaris.catalog.models.struct_field import StructField +# TODO: Rewrite to not use raise_errors +StructType.model_rebuild(raise_errors=False) + diff --git a/regtests/client/python/polaris/catalog/models/table_identifier.py b/regtests/client/python/polaris/catalog/models/table_identifier.py new file mode 100644 index 0000000000..d8df0b64e2 --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/table_identifier.py @@ -0,0 +1,89 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictStr +from typing import Any, ClassVar, Dict, List +from typing import Optional, Set +from typing_extensions import Self + +class TableIdentifier(BaseModel): + """ + TableIdentifier + """ # noqa: E501 + namespace: List[StrictStr] = Field(description="Reference to one or more levels of a namespace") + name: StrictStr + __properties: ClassVar[List[str]] = ["namespace", "name"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of TableIdentifier from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of TableIdentifier from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "namespace": obj.get("namespace"), + "name": obj.get("name") + }) + return _obj + + diff --git a/regtests/client/python/polaris/catalog/models/table_metadata.py b/regtests/client/python/polaris/catalog/models/table_metadata.py new file mode 100644 index 0000000000..ba92e3574d --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/table_metadata.py @@ -0,0 +1,205 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictInt, StrictStr +from typing import Any, ClassVar, Dict, List, Optional +from typing_extensions import Annotated +from polaris.catalog.models.metadata_log_inner import MetadataLogInner +from polaris.catalog.models.model_schema import ModelSchema +from polaris.catalog.models.partition_spec import PartitionSpec +from polaris.catalog.models.partition_statistics_file import PartitionStatisticsFile +from polaris.catalog.models.snapshot import Snapshot +from polaris.catalog.models.snapshot_log_inner import SnapshotLogInner +from polaris.catalog.models.snapshot_reference import SnapshotReference +from polaris.catalog.models.sort_order import SortOrder +from polaris.catalog.models.statistics_file import StatisticsFile +from typing import Optional, Set +from typing_extensions import Self + +class TableMetadata(BaseModel): + """ + TableMetadata + """ # noqa: E501 + format_version: Annotated[int, Field(le=2, strict=True, ge=1)] = Field(alias="format-version") + table_uuid: StrictStr = Field(alias="table-uuid") + location: Optional[StrictStr] = None + last_updated_ms: Optional[StrictInt] = Field(default=None, alias="last-updated-ms") + properties: Optional[Dict[str, StrictStr]] = None + schemas: Optional[List[ModelSchema]] = None + current_schema_id: Optional[StrictInt] = Field(default=None, alias="current-schema-id") + last_column_id: Optional[StrictInt] = Field(default=None, alias="last-column-id") + partition_specs: Optional[List[PartitionSpec]] = Field(default=None, alias="partition-specs") + default_spec_id: Optional[StrictInt] = Field(default=None, alias="default-spec-id") + last_partition_id: Optional[StrictInt] = Field(default=None, alias="last-partition-id") + sort_orders: Optional[List[SortOrder]] = Field(default=None, alias="sort-orders") + default_sort_order_id: Optional[StrictInt] = Field(default=None, alias="default-sort-order-id") + snapshots: Optional[List[Snapshot]] = None + refs: Optional[Dict[str, SnapshotReference]] = None + current_snapshot_id: Optional[StrictInt] = Field(default=None, alias="current-snapshot-id") + last_sequence_number: Optional[StrictInt] = Field(default=None, alias="last-sequence-number") + snapshot_log: Optional[List[SnapshotLogInner]] = Field(default=None, alias="snapshot-log") + metadata_log: Optional[List[MetadataLogInner]] = Field(default=None, alias="metadata-log") + statistics_files: Optional[List[StatisticsFile]] = Field(default=None, alias="statistics-files") + partition_statistics_files: Optional[List[PartitionStatisticsFile]] = Field(default=None, alias="partition-statistics-files") + __properties: ClassVar[List[str]] = ["format-version", "table-uuid", "location", "last-updated-ms", "properties", "schemas", "current-schema-id", "last-column-id", "partition-specs", "default-spec-id", "last-partition-id", "sort-orders", "default-sort-order-id", "snapshots", "refs", "current-snapshot-id", "last-sequence-number", "snapshot-log", "metadata-log", "statistics-files", "partition-statistics-files"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of TableMetadata from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of each item in schemas (list) + _items = [] + if self.schemas: + for _item in self.schemas: + if _item: + _items.append(_item.to_dict()) + _dict['schemas'] = _items + # override the default output from pydantic by calling `to_dict()` of each item in partition_specs (list) + _items = [] + if self.partition_specs: + for _item in self.partition_specs: + if _item: + _items.append(_item.to_dict()) + _dict['partition-specs'] = _items + # override the default output from pydantic by calling `to_dict()` of each item in sort_orders (list) + _items = [] + if self.sort_orders: + for _item in self.sort_orders: + if _item: + _items.append(_item.to_dict()) + _dict['sort-orders'] = _items + # override the default output from pydantic by calling `to_dict()` of each item in snapshots (list) + _items = [] + if self.snapshots: + for _item in self.snapshots: + if _item: + _items.append(_item.to_dict()) + _dict['snapshots'] = _items + # override the default output from pydantic by calling `to_dict()` of each value in refs (dict) + _field_dict = {} + if self.refs: + for _key in self.refs: + if self.refs[_key]: + _field_dict[_key] = self.refs[_key].to_dict() + _dict['refs'] = _field_dict + # override the default output from pydantic by calling `to_dict()` of each item in snapshot_log (list) + _items = [] + if self.snapshot_log: + for _item in self.snapshot_log: + if _item: + _items.append(_item.to_dict()) + _dict['snapshot-log'] = _items + # override the default output from pydantic by calling `to_dict()` of each item in metadata_log (list) + _items = [] + if self.metadata_log: + for _item in self.metadata_log: + if _item: + _items.append(_item.to_dict()) + _dict['metadata-log'] = _items + # override the default output from pydantic by calling `to_dict()` of each item in statistics_files (list) + _items = [] + if self.statistics_files: + for _item in self.statistics_files: + if _item: + _items.append(_item.to_dict()) + _dict['statistics-files'] = _items + # override the default output from pydantic by calling `to_dict()` of each item in partition_statistics_files (list) + _items = [] + if self.partition_statistics_files: + for _item in self.partition_statistics_files: + if _item: + _items.append(_item.to_dict()) + _dict['partition-statistics-files'] = _items + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of TableMetadata from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "format-version": obj.get("format-version"), + "table-uuid": obj.get("table-uuid"), + "location": obj.get("location"), + "last-updated-ms": obj.get("last-updated-ms"), + "properties": obj.get("properties"), + "schemas": [ModelSchema.from_dict(_item) for _item in obj["schemas"]] if obj.get("schemas") is not None else None, + "current-schema-id": obj.get("current-schema-id"), + "last-column-id": obj.get("last-column-id"), + "partition-specs": [PartitionSpec.from_dict(_item) for _item in obj["partition-specs"]] if obj.get("partition-specs") is not None else None, + "default-spec-id": obj.get("default-spec-id"), + "last-partition-id": obj.get("last-partition-id"), + "sort-orders": [SortOrder.from_dict(_item) for _item in obj["sort-orders"]] if obj.get("sort-orders") is not None else None, + "default-sort-order-id": obj.get("default-sort-order-id"), + "snapshots": [Snapshot.from_dict(_item) for _item in obj["snapshots"]] if obj.get("snapshots") is not None else None, + "refs": dict( + (_k, SnapshotReference.from_dict(_v)) + for _k, _v in obj["refs"].items() + ) + if obj.get("refs") is not None + else None, + "current-snapshot-id": obj.get("current-snapshot-id"), + "last-sequence-number": obj.get("last-sequence-number"), + "snapshot-log": [SnapshotLogInner.from_dict(_item) for _item in obj["snapshot-log"]] if obj.get("snapshot-log") is not None else None, + "metadata-log": [MetadataLogInner.from_dict(_item) for _item in obj["metadata-log"]] if obj.get("metadata-log") is not None else None, + "statistics-files": [StatisticsFile.from_dict(_item) for _item in obj["statistics-files"]] if obj.get("statistics-files") is not None else None, + "partition-statistics-files": [PartitionStatisticsFile.from_dict(_item) for _item in obj["partition-statistics-files"]] if obj.get("partition-statistics-files") is not None else None + }) + return _obj + + diff --git a/regtests/client/python/polaris/catalog/models/table_requirement.py b/regtests/client/python/polaris/catalog/models/table_requirement.py new file mode 100644 index 0000000000..994fa69aaa --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/table_requirement.py @@ -0,0 +1,128 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from importlib import import_module +from pydantic import BaseModel, ConfigDict, StrictStr +from typing import Any, ClassVar, Dict, List, Union +from typing import Optional, Set +from typing_extensions import Self + +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from polaris.catalog.models.assert_create import AssertCreate + from polaris.catalog.models.assert_current_schema_id import AssertCurrentSchemaId + from polaris.catalog.models.assert_default_sort_order_id import AssertDefaultSortOrderId + from polaris.catalog.models.assert_default_spec_id import AssertDefaultSpecId + from polaris.catalog.models.assert_last_assigned_field_id import AssertLastAssignedFieldId + from polaris.catalog.models.assert_last_assigned_partition_id import AssertLastAssignedPartitionId + from polaris.catalog.models.assert_ref_snapshot_id import AssertRefSnapshotId + from polaris.catalog.models.assert_table_uuid import AssertTableUUID + +class TableRequirement(BaseModel): + """ + TableRequirement + """ # noqa: E501 + type: StrictStr + __properties: ClassVar[List[str]] = ["type"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + # JSON field name that stores the object type + __discriminator_property_name: ClassVar[str] = 'type' + + # discriminator mappings + __discriminator_value_class_map: ClassVar[Dict[str, str]] = { + 'assert-create': 'AssertCreate','assert-current-schema-id': 'AssertCurrentSchemaId','assert-default-sort-order-id': 'AssertDefaultSortOrderId','assert-default-spec-id': 'AssertDefaultSpecId','assert-last-assigned-field-id': 'AssertLastAssignedFieldId','assert-last-assigned-partition-id': 'AssertLastAssignedPartitionId','assert-ref-snapshot-id': 'AssertRefSnapshotId','assert-table-uuid': 'AssertTableUUID' + } + + @classmethod + def get_discriminator_value(cls, obj: Dict[str, Any]) -> Optional[str]: + """Returns the discriminator value (object type) of the data""" + discriminator_value = obj[cls.__discriminator_property_name] + if discriminator_value: + return cls.__discriminator_value_class_map.get(discriminator_value) + else: + return None + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Union[AssertCreate, AssertCurrentSchemaId, AssertDefaultSortOrderId, AssertDefaultSpecId, AssertLastAssignedFieldId, AssertLastAssignedPartitionId, AssertRefSnapshotId, AssertTableUUID]]: + """Create an instance of TableRequirement from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Dict[str, Any]) -> Optional[Union[AssertCreate, AssertCurrentSchemaId, AssertDefaultSortOrderId, AssertDefaultSpecId, AssertLastAssignedFieldId, AssertLastAssignedPartitionId, AssertRefSnapshotId, AssertTableUUID]]: + """Create an instance of TableRequirement from a dict""" + # look up the object type based on discriminator mapping + object_type = cls.get_discriminator_value(obj) + if object_type == 'AssertCreate': + return import_module("polaris.catalog.models.assert_create").AssertCreate.from_dict(obj) + if object_type == 'AssertCurrentSchemaId': + return import_module("polaris.catalog.models.assert_current_schema_id").AssertCurrentSchemaId.from_dict(obj) + if object_type == 'AssertDefaultSortOrderId': + return import_module("polaris.catalog.models.assert_default_sort_order_id").AssertDefaultSortOrderId.from_dict(obj) + if object_type == 'AssertDefaultSpecId': + return import_module("polaris.catalog.models.assert_default_spec_id").AssertDefaultSpecId.from_dict(obj) + if object_type == 'AssertLastAssignedFieldId': + return import_module("polaris.catalog.models.assert_last_assigned_field_id").AssertLastAssignedFieldId.from_dict(obj) + if object_type == 'AssertLastAssignedPartitionId': + return import_module("polaris.catalog.models.assert_last_assigned_partition_id").AssertLastAssignedPartitionId.from_dict(obj) + if object_type == 'AssertRefSnapshotId': + return import_module("polaris.catalog.models.assert_ref_snapshot_id").AssertRefSnapshotId.from_dict(obj) + if object_type == 'AssertTableUUID': + return import_module("polaris.catalog.models.assert_table_uuid").AssertTableUUID.from_dict(obj) + + raise ValueError("TableRequirement failed to lookup discriminator value from " + + json.dumps(obj) + ". Discriminator property name: " + cls.__discriminator_property_name + + ", mapping: " + json.dumps(cls.__discriminator_value_class_map)) + + diff --git a/regtests/client/python/polaris/catalog/models/table_update.py b/regtests/client/python/polaris/catalog/models/table_update.py new file mode 100644 index 0000000000..b3d758fef6 --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/table_update.py @@ -0,0 +1,362 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +from inspect import getfullargspec +import json +import pprint +import re # noqa: F401 +from pydantic import BaseModel, ConfigDict, Field, StrictStr, ValidationError, field_validator +from typing import Optional +from polaris.catalog.models.add_partition_spec_update import AddPartitionSpecUpdate +from polaris.catalog.models.add_schema_update import AddSchemaUpdate +from polaris.catalog.models.add_snapshot_update import AddSnapshotUpdate +from polaris.catalog.models.add_sort_order_update import AddSortOrderUpdate +from polaris.catalog.models.assign_uuid_update import AssignUUIDUpdate +from polaris.catalog.models.remove_properties_update import RemovePropertiesUpdate +from polaris.catalog.models.remove_snapshot_ref_update import RemoveSnapshotRefUpdate +from polaris.catalog.models.remove_snapshots_update import RemoveSnapshotsUpdate +from polaris.catalog.models.remove_statistics_update import RemoveStatisticsUpdate +from polaris.catalog.models.set_current_schema_update import SetCurrentSchemaUpdate +from polaris.catalog.models.set_default_sort_order_update import SetDefaultSortOrderUpdate +from polaris.catalog.models.set_default_spec_update import SetDefaultSpecUpdate +from polaris.catalog.models.set_location_update import SetLocationUpdate +from polaris.catalog.models.set_properties_update import SetPropertiesUpdate +from polaris.catalog.models.set_snapshot_ref_update import SetSnapshotRefUpdate +from polaris.catalog.models.set_statistics_update import SetStatisticsUpdate +from polaris.catalog.models.upgrade_format_version_update import UpgradeFormatVersionUpdate +from typing import Union, Any, List, Set, TYPE_CHECKING, Optional, Dict +from typing_extensions import Literal, Self +from pydantic import Field + +TABLEUPDATE_ANY_OF_SCHEMAS = ["AddPartitionSpecUpdate", "AddSchemaUpdate", "AddSnapshotUpdate", "AddSortOrderUpdate", "AssignUUIDUpdate", "RemovePropertiesUpdate", "RemoveSnapshotRefUpdate", "RemoveSnapshotsUpdate", "RemoveStatisticsUpdate", "SetCurrentSchemaUpdate", "SetDefaultSortOrderUpdate", "SetDefaultSpecUpdate", "SetLocationUpdate", "SetPropertiesUpdate", "SetSnapshotRefUpdate", "SetStatisticsUpdate", "UpgradeFormatVersionUpdate"] + +class TableUpdate(BaseModel): + """ + TableUpdate + """ + + # data type: AssignUUIDUpdate + anyof_schema_1_validator: Optional[AssignUUIDUpdate] = None + # data type: UpgradeFormatVersionUpdate + anyof_schema_2_validator: Optional[UpgradeFormatVersionUpdate] = None + # data type: AddSchemaUpdate + anyof_schema_3_validator: Optional[AddSchemaUpdate] = None + # data type: SetCurrentSchemaUpdate + anyof_schema_4_validator: Optional[SetCurrentSchemaUpdate] = None + # data type: AddPartitionSpecUpdate + anyof_schema_5_validator: Optional[AddPartitionSpecUpdate] = None + # data type: SetDefaultSpecUpdate + anyof_schema_6_validator: Optional[SetDefaultSpecUpdate] = None + # data type: AddSortOrderUpdate + anyof_schema_7_validator: Optional[AddSortOrderUpdate] = None + # data type: SetDefaultSortOrderUpdate + anyof_schema_8_validator: Optional[SetDefaultSortOrderUpdate] = None + # data type: AddSnapshotUpdate + anyof_schema_9_validator: Optional[AddSnapshotUpdate] = None + # data type: SetSnapshotRefUpdate + anyof_schema_10_validator: Optional[SetSnapshotRefUpdate] = None + # data type: RemoveSnapshotsUpdate + anyof_schema_11_validator: Optional[RemoveSnapshotsUpdate] = None + # data type: RemoveSnapshotRefUpdate + anyof_schema_12_validator: Optional[RemoveSnapshotRefUpdate] = None + # data type: SetLocationUpdate + anyof_schema_13_validator: Optional[SetLocationUpdate] = None + # data type: SetPropertiesUpdate + anyof_schema_14_validator: Optional[SetPropertiesUpdate] = None + # data type: RemovePropertiesUpdate + anyof_schema_15_validator: Optional[RemovePropertiesUpdate] = None + # data type: SetStatisticsUpdate + anyof_schema_16_validator: Optional[SetStatisticsUpdate] = None + # data type: RemoveStatisticsUpdate + anyof_schema_17_validator: Optional[RemoveStatisticsUpdate] = None + if TYPE_CHECKING: + actual_instance: Optional[Union[AddPartitionSpecUpdate, AddSchemaUpdate, AddSnapshotUpdate, AddSortOrderUpdate, AssignUUIDUpdate, RemovePropertiesUpdate, RemoveSnapshotRefUpdate, RemoveSnapshotsUpdate, RemoveStatisticsUpdate, SetCurrentSchemaUpdate, SetDefaultSortOrderUpdate, SetDefaultSpecUpdate, SetLocationUpdate, SetPropertiesUpdate, SetSnapshotRefUpdate, SetStatisticsUpdate, UpgradeFormatVersionUpdate]] = None + else: + actual_instance: Any = None + any_of_schemas: Set[str] = { "AddPartitionSpecUpdate", "AddSchemaUpdate", "AddSnapshotUpdate", "AddSortOrderUpdate", "AssignUUIDUpdate", "RemovePropertiesUpdate", "RemoveSnapshotRefUpdate", "RemoveSnapshotsUpdate", "RemoveStatisticsUpdate", "SetCurrentSchemaUpdate", "SetDefaultSortOrderUpdate", "SetDefaultSpecUpdate", "SetLocationUpdate", "SetPropertiesUpdate", "SetSnapshotRefUpdate", "SetStatisticsUpdate", "UpgradeFormatVersionUpdate" } + + model_config = { + "validate_assignment": True, + "protected_namespaces": (), + } + + discriminator_value_class_map: Dict[str, str] = { + } + + def __init__(self, *args, **kwargs) -> None: + if args: + if len(args) > 1: + raise ValueError("If a position argument is used, only 1 is allowed to set `actual_instance`") + if kwargs: + raise ValueError("If a position argument is used, keyword arguments cannot be used.") + super().__init__(actual_instance=args[0]) + else: + super().__init__(**kwargs) + + @field_validator('actual_instance') + def actual_instance_must_validate_anyof(cls, v): + instance = TableUpdate.model_construct() + error_messages = [] + # validate data type: AssignUUIDUpdate + if not isinstance(v, AssignUUIDUpdate): + error_messages.append(f"Error! Input type `{type(v)}` is not `AssignUUIDUpdate`") + else: + return v + + # validate data type: UpgradeFormatVersionUpdate + if not isinstance(v, UpgradeFormatVersionUpdate): + error_messages.append(f"Error! Input type `{type(v)}` is not `UpgradeFormatVersionUpdate`") + else: + return v + + # validate data type: AddSchemaUpdate + if not isinstance(v, AddSchemaUpdate): + error_messages.append(f"Error! Input type `{type(v)}` is not `AddSchemaUpdate`") + else: + return v + + # validate data type: SetCurrentSchemaUpdate + if not isinstance(v, SetCurrentSchemaUpdate): + error_messages.append(f"Error! Input type `{type(v)}` is not `SetCurrentSchemaUpdate`") + else: + return v + + # validate data type: AddPartitionSpecUpdate + if not isinstance(v, AddPartitionSpecUpdate): + error_messages.append(f"Error! Input type `{type(v)}` is not `AddPartitionSpecUpdate`") + else: + return v + + # validate data type: SetDefaultSpecUpdate + if not isinstance(v, SetDefaultSpecUpdate): + error_messages.append(f"Error! Input type `{type(v)}` is not `SetDefaultSpecUpdate`") + else: + return v + + # validate data type: AddSortOrderUpdate + if not isinstance(v, AddSortOrderUpdate): + error_messages.append(f"Error! Input type `{type(v)}` is not `AddSortOrderUpdate`") + else: + return v + + # validate data type: SetDefaultSortOrderUpdate + if not isinstance(v, SetDefaultSortOrderUpdate): + error_messages.append(f"Error! Input type `{type(v)}` is not `SetDefaultSortOrderUpdate`") + else: + return v + + # validate data type: AddSnapshotUpdate + if not isinstance(v, AddSnapshotUpdate): + error_messages.append(f"Error! Input type `{type(v)}` is not `AddSnapshotUpdate`") + else: + return v + + # validate data type: SetSnapshotRefUpdate + if not isinstance(v, SetSnapshotRefUpdate): + error_messages.append(f"Error! Input type `{type(v)}` is not `SetSnapshotRefUpdate`") + else: + return v + + # validate data type: RemoveSnapshotsUpdate + if not isinstance(v, RemoveSnapshotsUpdate): + error_messages.append(f"Error! Input type `{type(v)}` is not `RemoveSnapshotsUpdate`") + else: + return v + + # validate data type: RemoveSnapshotRefUpdate + if not isinstance(v, RemoveSnapshotRefUpdate): + error_messages.append(f"Error! Input type `{type(v)}` is not `RemoveSnapshotRefUpdate`") + else: + return v + + # validate data type: SetLocationUpdate + if not isinstance(v, SetLocationUpdate): + error_messages.append(f"Error! Input type `{type(v)}` is not `SetLocationUpdate`") + else: + return v + + # validate data type: SetPropertiesUpdate + if not isinstance(v, SetPropertiesUpdate): + error_messages.append(f"Error! Input type `{type(v)}` is not `SetPropertiesUpdate`") + else: + return v + + # validate data type: RemovePropertiesUpdate + if not isinstance(v, RemovePropertiesUpdate): + error_messages.append(f"Error! Input type `{type(v)}` is not `RemovePropertiesUpdate`") + else: + return v + + # validate data type: SetStatisticsUpdate + if not isinstance(v, SetStatisticsUpdate): + error_messages.append(f"Error! Input type `{type(v)}` is not `SetStatisticsUpdate`") + else: + return v + + # validate data type: RemoveStatisticsUpdate + if not isinstance(v, RemoveStatisticsUpdate): + error_messages.append(f"Error! Input type `{type(v)}` is not `RemoveStatisticsUpdate`") + else: + return v + + if error_messages: + # no match + raise ValueError("No match found when setting the actual_instance in TableUpdate with anyOf schemas: AddPartitionSpecUpdate, AddSchemaUpdate, AddSnapshotUpdate, AddSortOrderUpdate, AssignUUIDUpdate, RemovePropertiesUpdate, RemoveSnapshotRefUpdate, RemoveSnapshotsUpdate, RemoveStatisticsUpdate, SetCurrentSchemaUpdate, SetDefaultSortOrderUpdate, SetDefaultSpecUpdate, SetLocationUpdate, SetPropertiesUpdate, SetSnapshotRefUpdate, SetStatisticsUpdate, UpgradeFormatVersionUpdate. Details: " + ", ".join(error_messages)) + else: + return v + + @classmethod + def from_dict(cls, obj: Dict[str, Any]) -> Self: + return cls.from_json(json.dumps(obj)) + + @classmethod + def from_json(cls, json_str: str) -> Self: + """Returns the object represented by the json string""" + instance = cls.model_construct() + error_messages = [] + # anyof_schema_1_validator: Optional[AssignUUIDUpdate] = None + try: + instance.actual_instance = AssignUUIDUpdate.from_json(json_str) + return instance + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) + # anyof_schema_2_validator: Optional[UpgradeFormatVersionUpdate] = None + try: + instance.actual_instance = UpgradeFormatVersionUpdate.from_json(json_str) + return instance + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) + # anyof_schema_3_validator: Optional[AddSchemaUpdate] = None + try: + instance.actual_instance = AddSchemaUpdate.from_json(json_str) + return instance + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) + # anyof_schema_4_validator: Optional[SetCurrentSchemaUpdate] = None + try: + instance.actual_instance = SetCurrentSchemaUpdate.from_json(json_str) + return instance + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) + # anyof_schema_5_validator: Optional[AddPartitionSpecUpdate] = None + try: + instance.actual_instance = AddPartitionSpecUpdate.from_json(json_str) + return instance + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) + # anyof_schema_6_validator: Optional[SetDefaultSpecUpdate] = None + try: + instance.actual_instance = SetDefaultSpecUpdate.from_json(json_str) + return instance + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) + # anyof_schema_7_validator: Optional[AddSortOrderUpdate] = None + try: + instance.actual_instance = AddSortOrderUpdate.from_json(json_str) + return instance + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) + # anyof_schema_8_validator: Optional[SetDefaultSortOrderUpdate] = None + try: + instance.actual_instance = SetDefaultSortOrderUpdate.from_json(json_str) + return instance + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) + # anyof_schema_9_validator: Optional[AddSnapshotUpdate] = None + try: + instance.actual_instance = AddSnapshotUpdate.from_json(json_str) + return instance + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) + # anyof_schema_10_validator: Optional[SetSnapshotRefUpdate] = None + try: + instance.actual_instance = SetSnapshotRefUpdate.from_json(json_str) + return instance + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) + # anyof_schema_11_validator: Optional[RemoveSnapshotsUpdate] = None + try: + instance.actual_instance = RemoveSnapshotsUpdate.from_json(json_str) + return instance + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) + # anyof_schema_12_validator: Optional[RemoveSnapshotRefUpdate] = None + try: + instance.actual_instance = RemoveSnapshotRefUpdate.from_json(json_str) + return instance + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) + # anyof_schema_13_validator: Optional[SetLocationUpdate] = None + try: + instance.actual_instance = SetLocationUpdate.from_json(json_str) + return instance + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) + # anyof_schema_14_validator: Optional[SetPropertiesUpdate] = None + try: + instance.actual_instance = SetPropertiesUpdate.from_json(json_str) + return instance + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) + # anyof_schema_15_validator: Optional[RemovePropertiesUpdate] = None + try: + instance.actual_instance = RemovePropertiesUpdate.from_json(json_str) + return instance + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) + # anyof_schema_16_validator: Optional[SetStatisticsUpdate] = None + try: + instance.actual_instance = SetStatisticsUpdate.from_json(json_str) + return instance + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) + # anyof_schema_17_validator: Optional[RemoveStatisticsUpdate] = None + try: + instance.actual_instance = RemoveStatisticsUpdate.from_json(json_str) + return instance + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) + + if error_messages: + # no match + raise ValueError("No match found when deserializing the JSON string into TableUpdate with anyOf schemas: AddPartitionSpecUpdate, AddSchemaUpdate, AddSnapshotUpdate, AddSortOrderUpdate, AssignUUIDUpdate, RemovePropertiesUpdate, RemoveSnapshotRefUpdate, RemoveSnapshotsUpdate, RemoveStatisticsUpdate, SetCurrentSchemaUpdate, SetDefaultSortOrderUpdate, SetDefaultSpecUpdate, SetLocationUpdate, SetPropertiesUpdate, SetSnapshotRefUpdate, SetStatisticsUpdate, UpgradeFormatVersionUpdate. Details: " + ", ".join(error_messages)) + else: + return instance + + def to_json(self) -> str: + """Returns the JSON representation of the actual instance""" + if self.actual_instance is None: + return "null" + + if hasattr(self.actual_instance, "to_json") and callable(self.actual_instance.to_json): + return self.actual_instance.to_json() + else: + return json.dumps(self.actual_instance) + + def to_dict(self) -> Optional[Union[Dict[str, Any], AddPartitionSpecUpdate, AddSchemaUpdate, AddSnapshotUpdate, AddSortOrderUpdate, AssignUUIDUpdate, RemovePropertiesUpdate, RemoveSnapshotRefUpdate, RemoveSnapshotsUpdate, RemoveStatisticsUpdate, SetCurrentSchemaUpdate, SetDefaultSortOrderUpdate, SetDefaultSpecUpdate, SetLocationUpdate, SetPropertiesUpdate, SetSnapshotRefUpdate, SetStatisticsUpdate, UpgradeFormatVersionUpdate]]: + """Returns the dict representation of the actual instance""" + if self.actual_instance is None: + return None + + if hasattr(self.actual_instance, "to_dict") and callable(self.actual_instance.to_dict): + return self.actual_instance.to_dict() + else: + return self.actual_instance + + def to_str(self) -> str: + """Returns the string representation of the actual instance""" + return pprint.pformat(self.model_dump()) + + diff --git a/regtests/client/python/polaris/catalog/models/table_update_notification.py b/regtests/client/python/polaris/catalog/models/table_update_notification.py new file mode 100644 index 0000000000..dbd7687485 --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/table_update_notification.py @@ -0,0 +1,99 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictInt, StrictStr +from typing import Any, ClassVar, Dict, List, Optional +from polaris.catalog.models.table_metadata import TableMetadata +from typing import Optional, Set +from typing_extensions import Self + +class TableUpdateNotification(BaseModel): + """ + TableUpdateNotification + """ # noqa: E501 + table_name: StrictStr = Field(alias="table-name") + timestamp: StrictInt + table_uuid: StrictStr = Field(alias="table-uuid") + metadata_location: StrictStr = Field(alias="metadata-location") + metadata: Optional[TableMetadata] = None + __properties: ClassVar[List[str]] = ["table-name", "timestamp", "table-uuid", "metadata-location", "metadata"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of TableUpdateNotification from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of metadata + if self.metadata: + _dict['metadata'] = self.metadata.to_dict() + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of TableUpdateNotification from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "table-name": obj.get("table-name"), + "timestamp": obj.get("timestamp"), + "table-uuid": obj.get("table-uuid"), + "metadata-location": obj.get("metadata-location"), + "metadata": TableMetadata.from_dict(obj["metadata"]) if obj.get("metadata") is not None else None + }) + return _obj + + diff --git a/regtests/client/python/polaris/catalog/models/term.py b/regtests/client/python/polaris/catalog/models/term.py new file mode 100644 index 0000000000..52e23ca9e7 --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/term.py @@ -0,0 +1,140 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import json +import pprint +from pydantic import BaseModel, ConfigDict, Field, StrictStr, ValidationError, field_validator +from typing import Any, List, Optional +from polaris.catalog.models.transform_term import TransformTerm +from pydantic import StrictStr, Field +from typing import Union, List, Set, Optional, Dict +from typing_extensions import Literal, Self + +TERM_ONE_OF_SCHEMAS = ["TransformTerm", "str"] + +class Term(BaseModel): + """ + Term + """ + # data type: str + oneof_schema_1_validator: Optional[StrictStr] = None + # data type: TransformTerm + oneof_schema_2_validator: Optional[TransformTerm] = None + actual_instance: Optional[Union[TransformTerm, str]] = None + one_of_schemas: Set[str] = { "TransformTerm", "str" } + + model_config = ConfigDict( + validate_assignment=True, + protected_namespaces=(), + ) + + + def __init__(self, *args, **kwargs) -> None: + if args: + if len(args) > 1: + raise ValueError("If a position argument is used, only 1 is allowed to set `actual_instance`") + if kwargs: + raise ValueError("If a position argument is used, keyword arguments cannot be used.") + super().__init__(actual_instance=args[0]) + else: + super().__init__(**kwargs) + + @field_validator('actual_instance') + def actual_instance_must_validate_oneof(cls, v): + instance = Term.model_construct() + error_messages = [] + match = 0 + # validate data type: str + try: + instance.oneof_schema_1_validator = v + match += 1 + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) + # validate data type: TransformTerm + if not isinstance(v, TransformTerm): + error_messages.append(f"Error! Input type `{type(v)}` is not `TransformTerm`") + else: + match += 1 + if match > 1: + # more than 1 match + raise ValueError("Multiple matches found when setting `actual_instance` in Term with oneOf schemas: TransformTerm, str. Details: " + ", ".join(error_messages)) + elif match == 0: + # no match + raise ValueError("No match found when setting `actual_instance` in Term with oneOf schemas: TransformTerm, str. Details: " + ", ".join(error_messages)) + else: + return v + + @classmethod + def from_dict(cls, obj: Union[str, Dict[str, Any]]) -> Self: + return cls.from_json(json.dumps(obj)) + + @classmethod + def from_json(cls, json_str: str) -> Self: + """Returns the object represented by the json string""" + instance = cls.model_construct() + error_messages = [] + match = 0 + + # deserialize data into str + try: + # validation + instance.oneof_schema_1_validator = json.loads(json_str) + # assign value to actual_instance + instance.actual_instance = instance.oneof_schema_1_validator + match += 1 + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) + # deserialize data into TransformTerm + try: + instance.actual_instance = TransformTerm.from_json(json_str) + match += 1 + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) + + if match > 1: + # more than 1 match + raise ValueError("Multiple matches found when deserializing the JSON string into Term with oneOf schemas: TransformTerm, str. Details: " + ", ".join(error_messages)) + elif match == 0: + # no match + raise ValueError("No match found when deserializing the JSON string into Term with oneOf schemas: TransformTerm, str. Details: " + ", ".join(error_messages)) + else: + return instance + + def to_json(self) -> str: + """Returns the JSON representation of the actual instance""" + if self.actual_instance is None: + return "null" + + if hasattr(self.actual_instance, "to_json") and callable(self.actual_instance.to_json): + return self.actual_instance.to_json() + else: + return json.dumps(self.actual_instance) + + def to_dict(self) -> Optional[Union[Dict[str, Any], TransformTerm, str]]: + """Returns the dict representation of the actual instance""" + if self.actual_instance is None: + return None + + if hasattr(self.actual_instance, "to_dict") and callable(self.actual_instance.to_dict): + return self.actual_instance.to_dict() + else: + # primitive type + return self.actual_instance + + def to_str(self) -> str: + """Returns the string representation of the actual instance""" + return pprint.pformat(self.model_dump()) + + diff --git a/regtests/client/python/polaris/catalog/models/timer_result.py b/regtests/client/python/polaris/catalog/models/timer_result.py new file mode 100644 index 0000000000..1de1deb866 --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/timer_result.py @@ -0,0 +1,91 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictInt, StrictStr +from typing import Any, ClassVar, Dict, List +from typing import Optional, Set +from typing_extensions import Self + +class TimerResult(BaseModel): + """ + TimerResult + """ # noqa: E501 + time_unit: StrictStr = Field(alias="time-unit") + count: StrictInt + total_duration: StrictInt = Field(alias="total-duration") + __properties: ClassVar[List[str]] = ["time-unit", "count", "total-duration"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of TimerResult from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of TimerResult from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "time-unit": obj.get("time-unit"), + "count": obj.get("count"), + "total-duration": obj.get("total-duration") + }) + return _obj + + diff --git a/regtests/client/python/polaris/catalog/models/token_type.py b/regtests/client/python/polaris/catalog/models/token_type.py new file mode 100644 index 0000000000..68f5ef272f --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/token_type.py @@ -0,0 +1,41 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import json +from enum import Enum +from typing_extensions import Self + + +class TokenType(str, Enum): + """ + Token type identifier, from RFC 8693 Section 3 See https://datatracker.ietf.org/doc/html/rfc8693#section-3 + """ + + """ + allowed enum values + """ + URN_COLON_IETF_COLON_PARAMS_COLON_OAUTH_COLON_TOKEN_MINUS_TYPE_COLON_ACCESS_TOKEN = 'urn:ietf:params:oauth:token-type:access_token' + URN_COLON_IETF_COLON_PARAMS_COLON_OAUTH_COLON_TOKEN_MINUS_TYPE_COLON_REFRESH_TOKEN = 'urn:ietf:params:oauth:token-type:refresh_token' + URN_COLON_IETF_COLON_PARAMS_COLON_OAUTH_COLON_TOKEN_MINUS_TYPE_COLON_ID_TOKEN = 'urn:ietf:params:oauth:token-type:id_token' + URN_COLON_IETF_COLON_PARAMS_COLON_OAUTH_COLON_TOKEN_MINUS_TYPE_COLON_SAML1 = 'urn:ietf:params:oauth:token-type:saml1' + URN_COLON_IETF_COLON_PARAMS_COLON_OAUTH_COLON_TOKEN_MINUS_TYPE_COLON_SAML2 = 'urn:ietf:params:oauth:token-type:saml2' + URN_COLON_IETF_COLON_PARAMS_COLON_OAUTH_COLON_TOKEN_MINUS_TYPE_COLON_JWT = 'urn:ietf:params:oauth:token-type:jwt' + + @classmethod + def from_json(cls, json_str: str) -> Self: + """Create an instance of TokenType from a JSON string""" + return cls(json.loads(json_str)) + + diff --git a/regtests/client/python/polaris/catalog/models/transform_term.py b/regtests/client/python/polaris/catalog/models/transform_term.py new file mode 100644 index 0000000000..b5b70e8922 --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/transform_term.py @@ -0,0 +1,98 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, StrictStr, field_validator +from typing import Any, ClassVar, Dict, List +from typing import Optional, Set +from typing_extensions import Self + +class TransformTerm(BaseModel): + """ + TransformTerm + """ # noqa: E501 + type: StrictStr + transform: StrictStr + term: StrictStr + __properties: ClassVar[List[str]] = ["type", "transform", "term"] + + @field_validator('type') + def type_validate_enum(cls, value): + """Validates the enum""" + if value not in set(['transform']): + raise ValueError("must be one of enum values ('transform')") + return value + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of TransformTerm from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of TransformTerm from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "type": obj.get("type"), + "transform": obj.get("transform"), + "term": obj.get("term") + }) + return _obj + + diff --git a/regtests/client/python/polaris/catalog/models/type.py b/regtests/client/python/polaris/catalog/models/type.py new file mode 100644 index 0000000000..06d416ce54 --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/type.py @@ -0,0 +1,170 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import json +import pprint +from pydantic import BaseModel, ConfigDict, Field, StrictStr, ValidationError, field_validator +from typing import Any, List, Optional +from pydantic import StrictStr, Field +from typing import Union, List, Set, Optional, Dict +from typing_extensions import Literal, Self + +TYPE_ONE_OF_SCHEMAS = ["ListType", "MapType", "StructType", "str"] + +class Type(BaseModel): + """ + Type + """ + # data type: str + oneof_schema_1_validator: Optional[StrictStr] = None + # data type: StructType + oneof_schema_2_validator: Optional[StructType] = None + # data type: ListType + oneof_schema_3_validator: Optional[ListType] = None + # data type: MapType + oneof_schema_4_validator: Optional[MapType] = None + actual_instance: Optional[Union[ListType, MapType, StructType, str]] = None + one_of_schemas: Set[str] = { "ListType", "MapType", "StructType", "str" } + + model_config = ConfigDict( + validate_assignment=True, + protected_namespaces=(), + ) + + + def __init__(self, *args, **kwargs) -> None: + if args: + if len(args) > 1: + raise ValueError("If a position argument is used, only 1 is allowed to set `actual_instance`") + if kwargs: + raise ValueError("If a position argument is used, keyword arguments cannot be used.") + super().__init__(actual_instance=args[0]) + else: + super().__init__(**kwargs) + + @field_validator('actual_instance') + def actual_instance_must_validate_oneof(cls, v): + instance = Type.model_construct() + error_messages = [] + match = 0 + # validate data type: str + try: + instance.oneof_schema_1_validator = v + match += 1 + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) + # validate data type: StructType + if not isinstance(v, StructType): + error_messages.append(f"Error! Input type `{type(v)}` is not `StructType`") + else: + match += 1 + # validate data type: ListType + if not isinstance(v, ListType): + error_messages.append(f"Error! Input type `{type(v)}` is not `ListType`") + else: + match += 1 + # validate data type: MapType + if not isinstance(v, MapType): + error_messages.append(f"Error! Input type `{type(v)}` is not `MapType`") + else: + match += 1 + if match > 1: + # more than 1 match + raise ValueError("Multiple matches found when setting `actual_instance` in Type with oneOf schemas: ListType, MapType, StructType, str. Details: " + ", ".join(error_messages)) + elif match == 0: + # no match + raise ValueError("No match found when setting `actual_instance` in Type with oneOf schemas: ListType, MapType, StructType, str. Details: " + ", ".join(error_messages)) + else: + return v + + @classmethod + def from_dict(cls, obj: Union[str, Dict[str, Any]]) -> Self: + return cls.from_json(json.dumps(obj)) + + @classmethod + def from_json(cls, json_str: str) -> Self: + """Returns the object represented by the json string""" + instance = cls.model_construct() + error_messages = [] + match = 0 + + # deserialize data into str + try: + # validation + instance.oneof_schema_1_validator = json.loads(json_str) + # assign value to actual_instance + instance.actual_instance = instance.oneof_schema_1_validator + match += 1 + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) + # deserialize data into StructType + try: + instance.actual_instance = StructType.from_json(json_str) + match += 1 + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) + # deserialize data into ListType + try: + instance.actual_instance = ListType.from_json(json_str) + match += 1 + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) + # deserialize data into MapType + try: + instance.actual_instance = MapType.from_json(json_str) + match += 1 + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) + + if match > 1: + # more than 1 match + raise ValueError("Multiple matches found when deserializing the JSON string into Type with oneOf schemas: ListType, MapType, StructType, str. Details: " + ", ".join(error_messages)) + elif match == 0: + # no match + raise ValueError("No match found when deserializing the JSON string into Type with oneOf schemas: ListType, MapType, StructType, str. Details: " + ", ".join(error_messages)) + else: + return instance + + def to_json(self) -> str: + """Returns the JSON representation of the actual instance""" + if self.actual_instance is None: + return "null" + + if hasattr(self.actual_instance, "to_json") and callable(self.actual_instance.to_json): + return self.actual_instance.to_json() + else: + return json.dumps(self.actual_instance) + + def to_dict(self) -> Optional[Union[Dict[str, Any], ListType, MapType, StructType, str]]: + """Returns the dict representation of the actual instance""" + if self.actual_instance is None: + return None + + if hasattr(self.actual_instance, "to_dict") and callable(self.actual_instance.to_dict): + return self.actual_instance.to_dict() + else: + # primitive type + return self.actual_instance + + def to_str(self) -> str: + """Returns the string representation of the actual instance""" + return pprint.pformat(self.model_dump()) + +from polaris.catalog.models.list_type import ListType +from polaris.catalog.models.map_type import MapType +from polaris.catalog.models.struct_type import StructType +# TODO: Rewrite to not use raise_errors +Type.model_rebuild(raise_errors=False) + diff --git a/regtests/client/python/polaris/catalog/models/unary_expression.py b/regtests/client/python/polaris/catalog/models/unary_expression.py new file mode 100644 index 0000000000..f91f5ae41d --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/unary_expression.py @@ -0,0 +1,95 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, StrictStr +from typing import Any, ClassVar, Dict, List +from polaris.catalog.models.term import Term +from typing import Optional, Set +from typing_extensions import Self + +class UnaryExpression(BaseModel): + """ + UnaryExpression + """ # noqa: E501 + type: StrictStr + term: Term + value: Dict[str, Any] + __properties: ClassVar[List[str]] = ["type", "term", "value"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of UnaryExpression from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of term + if self.term: + _dict['term'] = self.term.to_dict() + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of UnaryExpression from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "type": obj.get("type"), + "term": Term.from_dict(obj["term"]) if obj.get("term") is not None else None, + "value": obj.get("value") + }) + return _obj + + diff --git a/regtests/client/python/polaris/catalog/models/update_namespace_properties_request.py b/regtests/client/python/polaris/catalog/models/update_namespace_properties_request.py new file mode 100644 index 0000000000..79d6164ebb --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/update_namespace_properties_request.py @@ -0,0 +1,89 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, StrictStr +from typing import Any, ClassVar, Dict, List, Optional +from typing import Optional, Set +from typing_extensions import Self + +class UpdateNamespacePropertiesRequest(BaseModel): + """ + UpdateNamespacePropertiesRequest + """ # noqa: E501 + removals: Optional[List[StrictStr]] = None + updates: Optional[Dict[str, StrictStr]] = None + __properties: ClassVar[List[str]] = ["removals", "updates"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of UpdateNamespacePropertiesRequest from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of UpdateNamespacePropertiesRequest from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "removals": obj.get("removals"), + "updates": obj.get("updates") + }) + return _obj + + diff --git a/regtests/client/python/polaris/catalog/models/update_namespace_properties_response.py b/regtests/client/python/polaris/catalog/models/update_namespace_properties_response.py new file mode 100644 index 0000000000..5758b03ddd --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/update_namespace_properties_response.py @@ -0,0 +1,96 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictStr +from typing import Any, ClassVar, Dict, List, Optional +from typing import Optional, Set +from typing_extensions import Self + +class UpdateNamespacePropertiesResponse(BaseModel): + """ + UpdateNamespacePropertiesResponse + """ # noqa: E501 + updated: List[StrictStr] = Field(description="List of property keys that were added or updated") + removed: List[StrictStr] = Field(description="List of properties that were removed") + missing: Optional[List[StrictStr]] = Field(default=None, description="List of properties requested for removal that were not found in the namespace's properties. Represents a partial success response. Server's do not need to implement this.") + __properties: ClassVar[List[str]] = ["updated", "removed", "missing"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of UpdateNamespacePropertiesResponse from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # set to None if missing (nullable) is None + # and model_fields_set contains the field + if self.missing is None and "missing" in self.model_fields_set: + _dict['missing'] = None + + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of UpdateNamespacePropertiesResponse from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "updated": obj.get("updated"), + "removed": obj.get("removed"), + "missing": obj.get("missing") + }) + return _obj + + diff --git a/regtests/client/python/polaris/catalog/models/upgrade_format_version_update.py b/regtests/client/python/polaris/catalog/models/upgrade_format_version_update.py new file mode 100644 index 0000000000..03f4607c94 --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/upgrade_format_version_update.py @@ -0,0 +1,96 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import ConfigDict, Field, StrictInt, StrictStr, field_validator +from typing import Any, ClassVar, Dict, List +from polaris.catalog.models.base_update import BaseUpdate +from typing import Optional, Set +from typing_extensions import Self + +class UpgradeFormatVersionUpdate(BaseUpdate): + """ + UpgradeFormatVersionUpdate + """ # noqa: E501 + action: StrictStr + format_version: StrictInt = Field(alias="format-version") + __properties: ClassVar[List[str]] = ["action"] + + @field_validator('action') + def action_validate_enum(cls, value): + """Validates the enum""" + if value not in set(['upgrade-format-version']): + raise ValueError("must be one of enum values ('upgrade-format-version')") + return value + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of UpgradeFormatVersionUpdate from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of UpgradeFormatVersionUpdate from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "action": obj.get("action") + }) + return _obj + + diff --git a/regtests/client/python/polaris/catalog/models/value_map.py b/regtests/client/python/polaris/catalog/models/value_map.py new file mode 100644 index 0000000000..e3c478e86a --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/value_map.py @@ -0,0 +1,97 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictInt +from typing import Any, ClassVar, Dict, List, Optional +from polaris.catalog.models.primitive_type_value import PrimitiveTypeValue +from typing import Optional, Set +from typing_extensions import Self + +class ValueMap(BaseModel): + """ + ValueMap + """ # noqa: E501 + keys: Optional[List[StrictInt]] = Field(default=None, description="List of integer column ids for each corresponding value") + values: Optional[List[PrimitiveTypeValue]] = Field(default=None, description="List of primitive type values, matched to 'keys' by index") + __properties: ClassVar[List[str]] = ["keys", "values"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of ValueMap from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of each item in values (list) + _items = [] + if self.values: + for _item in self.values: + if _item: + _items.append(_item.to_dict()) + _dict['values'] = _items + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of ValueMap from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "keys": obj.get("keys"), + "values": [PrimitiveTypeValue.from_dict(_item) for _item in obj["values"]] if obj.get("values") is not None else None + }) + return _obj + + diff --git a/regtests/client/python/polaris/catalog/models/view_history_entry.py b/regtests/client/python/polaris/catalog/models/view_history_entry.py new file mode 100644 index 0000000000..f8b61abefe --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/view_history_entry.py @@ -0,0 +1,89 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictInt +from typing import Any, ClassVar, Dict, List +from typing import Optional, Set +from typing_extensions import Self + +class ViewHistoryEntry(BaseModel): + """ + ViewHistoryEntry + """ # noqa: E501 + version_id: StrictInt = Field(alias="version-id") + timestamp_ms: StrictInt = Field(alias="timestamp-ms") + __properties: ClassVar[List[str]] = ["version-id", "timestamp-ms"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of ViewHistoryEntry from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of ViewHistoryEntry from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "version-id": obj.get("version-id"), + "timestamp-ms": obj.get("timestamp-ms") + }) + return _obj + + diff --git a/regtests/client/python/polaris/catalog/models/view_metadata.py b/regtests/client/python/polaris/catalog/models/view_metadata.py new file mode 100644 index 0000000000..01e95c9874 --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/view_metadata.py @@ -0,0 +1,126 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictInt, StrictStr +from typing import Any, ClassVar, Dict, List, Optional +from typing_extensions import Annotated +from polaris.catalog.models.model_schema import ModelSchema +from polaris.catalog.models.view_history_entry import ViewHistoryEntry +from polaris.catalog.models.view_version import ViewVersion +from typing import Optional, Set +from typing_extensions import Self + +class ViewMetadata(BaseModel): + """ + ViewMetadata + """ # noqa: E501 + view_uuid: StrictStr = Field(alias="view-uuid") + format_version: Annotated[int, Field(le=1, strict=True, ge=1)] = Field(alias="format-version") + location: StrictStr + current_version_id: StrictInt = Field(alias="current-version-id") + versions: List[ViewVersion] + version_log: List[ViewHistoryEntry] = Field(alias="version-log") + schemas: List[ModelSchema] + properties: Optional[Dict[str, StrictStr]] = None + __properties: ClassVar[List[str]] = ["view-uuid", "format-version", "location", "current-version-id", "versions", "version-log", "schemas", "properties"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of ViewMetadata from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of each item in versions (list) + _items = [] + if self.versions: + for _item in self.versions: + if _item: + _items.append(_item.to_dict()) + _dict['versions'] = _items + # override the default output from pydantic by calling `to_dict()` of each item in version_log (list) + _items = [] + if self.version_log: + for _item in self.version_log: + if _item: + _items.append(_item.to_dict()) + _dict['version-log'] = _items + # override the default output from pydantic by calling `to_dict()` of each item in schemas (list) + _items = [] + if self.schemas: + for _item in self.schemas: + if _item: + _items.append(_item.to_dict()) + _dict['schemas'] = _items + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of ViewMetadata from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "view-uuid": obj.get("view-uuid"), + "format-version": obj.get("format-version"), + "location": obj.get("location"), + "current-version-id": obj.get("current-version-id"), + "versions": [ViewVersion.from_dict(_item) for _item in obj["versions"]] if obj.get("versions") is not None else None, + "version-log": [ViewHistoryEntry.from_dict(_item) for _item in obj["version-log"]] if obj.get("version-log") is not None else None, + "schemas": [ModelSchema.from_dict(_item) for _item in obj["schemas"]] if obj.get("schemas") is not None else None, + "properties": obj.get("properties") + }) + return _obj + + diff --git a/regtests/client/python/polaris/catalog/models/view_representation.py b/regtests/client/python/polaris/catalog/models/view_representation.py new file mode 100644 index 0000000000..4dd8bac363 --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/view_representation.py @@ -0,0 +1,123 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import json +import pprint +from pydantic import BaseModel, ConfigDict, Field, StrictStr, ValidationError, field_validator +from typing import Any, List, Optional +from polaris.catalog.models.sql_view_representation import SQLViewRepresentation +from pydantic import StrictStr, Field +from typing import Union, List, Set, Optional, Dict +from typing_extensions import Literal, Self + +VIEWREPRESENTATION_ONE_OF_SCHEMAS = ["SQLViewRepresentation"] + +class ViewRepresentation(BaseModel): + """ + ViewRepresentation + """ + # data type: SQLViewRepresentation + oneof_schema_1_validator: Optional[SQLViewRepresentation] = None + actual_instance: Optional[Union[SQLViewRepresentation]] = None + one_of_schemas: Set[str] = { "SQLViewRepresentation" } + + model_config = ConfigDict( + validate_assignment=True, + protected_namespaces=(), + ) + + + def __init__(self, *args, **kwargs) -> None: + if args: + if len(args) > 1: + raise ValueError("If a position argument is used, only 1 is allowed to set `actual_instance`") + if kwargs: + raise ValueError("If a position argument is used, keyword arguments cannot be used.") + super().__init__(actual_instance=args[0]) + else: + super().__init__(**kwargs) + + @field_validator('actual_instance') + def actual_instance_must_validate_oneof(cls, v): + instance = ViewRepresentation.model_construct() + error_messages = [] + match = 0 + # validate data type: SQLViewRepresentation + if not isinstance(v, SQLViewRepresentation): + error_messages.append(f"Error! Input type `{type(v)}` is not `SQLViewRepresentation`") + else: + match += 1 + if match > 1: + # more than 1 match + raise ValueError("Multiple matches found when setting `actual_instance` in ViewRepresentation with oneOf schemas: SQLViewRepresentation. Details: " + ", ".join(error_messages)) + elif match == 0: + # no match + raise ValueError("No match found when setting `actual_instance` in ViewRepresentation with oneOf schemas: SQLViewRepresentation. Details: " + ", ".join(error_messages)) + else: + return v + + @classmethod + def from_dict(cls, obj: Union[str, Dict[str, Any]]) -> Self: + return cls.from_json(json.dumps(obj)) + + @classmethod + def from_json(cls, json_str: str) -> Self: + """Returns the object represented by the json string""" + instance = cls.model_construct() + error_messages = [] + match = 0 + + # deserialize data into SQLViewRepresentation + try: + instance.actual_instance = SQLViewRepresentation.from_json(json_str) + match += 1 + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) + + if match > 1: + # more than 1 match + raise ValueError("Multiple matches found when deserializing the JSON string into ViewRepresentation with oneOf schemas: SQLViewRepresentation. Details: " + ", ".join(error_messages)) + elif match == 0: + # no match + raise ValueError("No match found when deserializing the JSON string into ViewRepresentation with oneOf schemas: SQLViewRepresentation. Details: " + ", ".join(error_messages)) + else: + return instance + + def to_json(self) -> str: + """Returns the JSON representation of the actual instance""" + if self.actual_instance is None: + return "null" + + if hasattr(self.actual_instance, "to_json") and callable(self.actual_instance.to_json): + return self.actual_instance.to_json() + else: + return json.dumps(self.actual_instance) + + def to_dict(self) -> Optional[Union[Dict[str, Any], SQLViewRepresentation]]: + """Returns the dict representation of the actual instance""" + if self.actual_instance is None: + return None + + if hasattr(self.actual_instance, "to_dict") and callable(self.actual_instance.to_dict): + return self.actual_instance.to_dict() + else: + # primitive type + return self.actual_instance + + def to_str(self) -> str: + """Returns the string representation of the actual instance""" + return pprint.pformat(self.model_dump()) + + diff --git a/regtests/client/python/polaris/catalog/models/view_requirement.py b/regtests/client/python/polaris/catalog/models/view_requirement.py new file mode 100644 index 0000000000..6b3ac6ec62 --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/view_requirement.py @@ -0,0 +1,107 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from importlib import import_module +from pydantic import BaseModel, ConfigDict, StrictStr +from typing import Any, ClassVar, Dict, List, Union +from typing import Optional, Set +from typing_extensions import Self + +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from polaris.catalog.models.assert_view_uuid import AssertViewUUID + +class ViewRequirement(BaseModel): + """ + ViewRequirement + """ # noqa: E501 + type: StrictStr + __properties: ClassVar[List[str]] = ["type"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + # JSON field name that stores the object type + __discriminator_property_name: ClassVar[str] = 'type' + + # discriminator mappings + __discriminator_value_class_map: ClassVar[Dict[str, str]] = { + 'assert-view-uuid': 'AssertViewUUID' + } + + @classmethod + def get_discriminator_value(cls, obj: Dict[str, Any]) -> Optional[str]: + """Returns the discriminator value (object type) of the data""" + discriminator_value = obj[cls.__discriminator_property_name] + if discriminator_value: + return cls.__discriminator_value_class_map.get(discriminator_value) + else: + return None + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Union[AssertViewUUID]]: + """Create an instance of ViewRequirement from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Dict[str, Any]) -> Optional[Union[AssertViewUUID]]: + """Create an instance of ViewRequirement from a dict""" + # look up the object type based on discriminator mapping + object_type = cls.get_discriminator_value(obj) + if object_type == 'AssertViewUUID': + return import_module("polaris.catalog.models.assert_view_uuid").AssertViewUUID.from_dict(obj) + + raise ValueError("ViewRequirement failed to lookup discriminator value from " + + json.dumps(obj) + ". Discriminator property name: " + cls.__discriminator_property_name + + ", mapping: " + json.dumps(cls.__discriminator_value_class_map)) + + diff --git a/regtests/client/python/polaris/catalog/models/view_update.py b/regtests/client/python/polaris/catalog/models/view_update.py new file mode 100644 index 0000000000..0924487823 --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/view_update.py @@ -0,0 +1,227 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +from inspect import getfullargspec +import json +import pprint +import re # noqa: F401 +from pydantic import BaseModel, ConfigDict, Field, StrictStr, ValidationError, field_validator +from typing import Optional +from polaris.catalog.models.add_schema_update import AddSchemaUpdate +from polaris.catalog.models.add_view_version_update import AddViewVersionUpdate +from polaris.catalog.models.assign_uuid_update import AssignUUIDUpdate +from polaris.catalog.models.remove_properties_update import RemovePropertiesUpdate +from polaris.catalog.models.set_current_view_version_update import SetCurrentViewVersionUpdate +from polaris.catalog.models.set_location_update import SetLocationUpdate +from polaris.catalog.models.set_properties_update import SetPropertiesUpdate +from polaris.catalog.models.upgrade_format_version_update import UpgradeFormatVersionUpdate +from typing import Union, Any, List, Set, TYPE_CHECKING, Optional, Dict +from typing_extensions import Literal, Self +from pydantic import Field + +VIEWUPDATE_ANY_OF_SCHEMAS = ["AddSchemaUpdate", "AddViewVersionUpdate", "AssignUUIDUpdate", "RemovePropertiesUpdate", "SetCurrentViewVersionUpdate", "SetLocationUpdate", "SetPropertiesUpdate", "UpgradeFormatVersionUpdate"] + +class ViewUpdate(BaseModel): + """ + ViewUpdate + """ + + # data type: AssignUUIDUpdate + anyof_schema_1_validator: Optional[AssignUUIDUpdate] = None + # data type: UpgradeFormatVersionUpdate + anyof_schema_2_validator: Optional[UpgradeFormatVersionUpdate] = None + # data type: AddSchemaUpdate + anyof_schema_3_validator: Optional[AddSchemaUpdate] = None + # data type: SetLocationUpdate + anyof_schema_4_validator: Optional[SetLocationUpdate] = None + # data type: SetPropertiesUpdate + anyof_schema_5_validator: Optional[SetPropertiesUpdate] = None + # data type: RemovePropertiesUpdate + anyof_schema_6_validator: Optional[RemovePropertiesUpdate] = None + # data type: AddViewVersionUpdate + anyof_schema_7_validator: Optional[AddViewVersionUpdate] = None + # data type: SetCurrentViewVersionUpdate + anyof_schema_8_validator: Optional[SetCurrentViewVersionUpdate] = None + if TYPE_CHECKING: + actual_instance: Optional[Union[AddSchemaUpdate, AddViewVersionUpdate, AssignUUIDUpdate, RemovePropertiesUpdate, SetCurrentViewVersionUpdate, SetLocationUpdate, SetPropertiesUpdate, UpgradeFormatVersionUpdate]] = None + else: + actual_instance: Any = None + any_of_schemas: Set[str] = { "AddSchemaUpdate", "AddViewVersionUpdate", "AssignUUIDUpdate", "RemovePropertiesUpdate", "SetCurrentViewVersionUpdate", "SetLocationUpdate", "SetPropertiesUpdate", "UpgradeFormatVersionUpdate" } + + model_config = { + "validate_assignment": True, + "protected_namespaces": (), + } + + discriminator_value_class_map: Dict[str, str] = { + } + + def __init__(self, *args, **kwargs) -> None: + if args: + if len(args) > 1: + raise ValueError("If a position argument is used, only 1 is allowed to set `actual_instance`") + if kwargs: + raise ValueError("If a position argument is used, keyword arguments cannot be used.") + super().__init__(actual_instance=args[0]) + else: + super().__init__(**kwargs) + + @field_validator('actual_instance') + def actual_instance_must_validate_anyof(cls, v): + instance = ViewUpdate.model_construct() + error_messages = [] + # validate data type: AssignUUIDUpdate + if not isinstance(v, AssignUUIDUpdate): + error_messages.append(f"Error! Input type `{type(v)}` is not `AssignUUIDUpdate`") + else: + return v + + # validate data type: UpgradeFormatVersionUpdate + if not isinstance(v, UpgradeFormatVersionUpdate): + error_messages.append(f"Error! Input type `{type(v)}` is not `UpgradeFormatVersionUpdate`") + else: + return v + + # validate data type: AddSchemaUpdate + if not isinstance(v, AddSchemaUpdate): + error_messages.append(f"Error! Input type `{type(v)}` is not `AddSchemaUpdate`") + else: + return v + + # validate data type: SetLocationUpdate + if not isinstance(v, SetLocationUpdate): + error_messages.append(f"Error! Input type `{type(v)}` is not `SetLocationUpdate`") + else: + return v + + # validate data type: SetPropertiesUpdate + if not isinstance(v, SetPropertiesUpdate): + error_messages.append(f"Error! Input type `{type(v)}` is not `SetPropertiesUpdate`") + else: + return v + + # validate data type: RemovePropertiesUpdate + if not isinstance(v, RemovePropertiesUpdate): + error_messages.append(f"Error! Input type `{type(v)}` is not `RemovePropertiesUpdate`") + else: + return v + + # validate data type: AddViewVersionUpdate + if not isinstance(v, AddViewVersionUpdate): + error_messages.append(f"Error! Input type `{type(v)}` is not `AddViewVersionUpdate`") + else: + return v + + # validate data type: SetCurrentViewVersionUpdate + if not isinstance(v, SetCurrentViewVersionUpdate): + error_messages.append(f"Error! Input type `{type(v)}` is not `SetCurrentViewVersionUpdate`") + else: + return v + + if error_messages: + # no match + raise ValueError("No match found when setting the actual_instance in ViewUpdate with anyOf schemas: AddSchemaUpdate, AddViewVersionUpdate, AssignUUIDUpdate, RemovePropertiesUpdate, SetCurrentViewVersionUpdate, SetLocationUpdate, SetPropertiesUpdate, UpgradeFormatVersionUpdate. Details: " + ", ".join(error_messages)) + else: + return v + + @classmethod + def from_dict(cls, obj: Dict[str, Any]) -> Self: + return cls.from_json(json.dumps(obj)) + + @classmethod + def from_json(cls, json_str: str) -> Self: + """Returns the object represented by the json string""" + instance = cls.model_construct() + error_messages = [] + # anyof_schema_1_validator: Optional[AssignUUIDUpdate] = None + try: + instance.actual_instance = AssignUUIDUpdate.from_json(json_str) + return instance + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) + # anyof_schema_2_validator: Optional[UpgradeFormatVersionUpdate] = None + try: + instance.actual_instance = UpgradeFormatVersionUpdate.from_json(json_str) + return instance + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) + # anyof_schema_3_validator: Optional[AddSchemaUpdate] = None + try: + instance.actual_instance = AddSchemaUpdate.from_json(json_str) + return instance + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) + # anyof_schema_4_validator: Optional[SetLocationUpdate] = None + try: + instance.actual_instance = SetLocationUpdate.from_json(json_str) + return instance + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) + # anyof_schema_5_validator: Optional[SetPropertiesUpdate] = None + try: + instance.actual_instance = SetPropertiesUpdate.from_json(json_str) + return instance + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) + # anyof_schema_6_validator: Optional[RemovePropertiesUpdate] = None + try: + instance.actual_instance = RemovePropertiesUpdate.from_json(json_str) + return instance + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) + # anyof_schema_7_validator: Optional[AddViewVersionUpdate] = None + try: + instance.actual_instance = AddViewVersionUpdate.from_json(json_str) + return instance + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) + # anyof_schema_8_validator: Optional[SetCurrentViewVersionUpdate] = None + try: + instance.actual_instance = SetCurrentViewVersionUpdate.from_json(json_str) + return instance + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) + + if error_messages: + # no match + raise ValueError("No match found when deserializing the JSON string into ViewUpdate with anyOf schemas: AddSchemaUpdate, AddViewVersionUpdate, AssignUUIDUpdate, RemovePropertiesUpdate, SetCurrentViewVersionUpdate, SetLocationUpdate, SetPropertiesUpdate, UpgradeFormatVersionUpdate. Details: " + ", ".join(error_messages)) + else: + return instance + + def to_json(self) -> str: + """Returns the JSON representation of the actual instance""" + if self.actual_instance is None: + return "null" + + if hasattr(self.actual_instance, "to_json") and callable(self.actual_instance.to_json): + return self.actual_instance.to_json() + else: + return json.dumps(self.actual_instance) + + def to_dict(self) -> Optional[Union[Dict[str, Any], AddSchemaUpdate, AddViewVersionUpdate, AssignUUIDUpdate, RemovePropertiesUpdate, SetCurrentViewVersionUpdate, SetLocationUpdate, SetPropertiesUpdate, UpgradeFormatVersionUpdate]]: + """Returns the dict representation of the actual instance""" + if self.actual_instance is None: + return None + + if hasattr(self.actual_instance, "to_dict") and callable(self.actual_instance.to_dict): + return self.actual_instance.to_dict() + else: + return self.actual_instance + + def to_str(self) -> str: + """Returns the string representation of the actual instance""" + return pprint.pformat(self.model_dump()) + + diff --git a/regtests/client/python/polaris/catalog/models/view_version.py b/regtests/client/python/polaris/catalog/models/view_version.py new file mode 100644 index 0000000000..5f61d23e1e --- /dev/null +++ b/regtests/client/python/polaris/catalog/models/view_version.py @@ -0,0 +1,107 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictInt, StrictStr +from typing import Any, ClassVar, Dict, List, Optional +from polaris.catalog.models.view_representation import ViewRepresentation +from typing import Optional, Set +from typing_extensions import Self + +class ViewVersion(BaseModel): + """ + ViewVersion + """ # noqa: E501 + version_id: StrictInt = Field(alias="version-id") + timestamp_ms: StrictInt = Field(alias="timestamp-ms") + schema_id: StrictInt = Field(description="Schema ID to set as current, or -1 to set last added schema", alias="schema-id") + summary: Dict[str, StrictStr] + representations: List[ViewRepresentation] + default_catalog: Optional[StrictStr] = Field(default=None, alias="default-catalog") + default_namespace: List[StrictStr] = Field(description="Reference to one or more levels of a namespace", alias="default-namespace") + __properties: ClassVar[List[str]] = ["version-id", "timestamp-ms", "schema-id", "summary", "representations", "default-catalog", "default-namespace"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of ViewVersion from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of each item in representations (list) + _items = [] + if self.representations: + for _item in self.representations: + if _item: + _items.append(_item.to_dict()) + _dict['representations'] = _items + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of ViewVersion from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "version-id": obj.get("version-id"), + "timestamp-ms": obj.get("timestamp-ms"), + "schema-id": obj.get("schema-id"), + "summary": obj.get("summary"), + "representations": [ViewRepresentation.from_dict(_item) for _item in obj["representations"]] if obj.get("representations") is not None else None, + "default-catalog": obj.get("default-catalog"), + "default-namespace": obj.get("default-namespace") + }) + return _obj + + diff --git a/regtests/client/python/polaris/catalog/py.typed b/regtests/client/python/polaris/catalog/py.typed new file mode 100644 index 0000000000..e69de29bb2 diff --git a/regtests/client/python/polaris/catalog/rest.py b/regtests/client/python/polaris/catalog/rest.py new file mode 100644 index 0000000000..31d6a92dd7 --- /dev/null +++ b/regtests/client/python/polaris/catalog/rest.py @@ -0,0 +1,257 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import io +import json +import re +import ssl + +import urllib3 + +from polaris.catalog.exceptions import ApiException, ApiValueError + +SUPPORTED_SOCKS_PROXIES = {"socks5", "socks5h", "socks4", "socks4a"} +RESTResponseType = urllib3.HTTPResponse + + +def is_socks_proxy_url(url): + if url is None: + return False + split_section = url.split("://") + if len(split_section) < 2: + return False + else: + return split_section[0].lower() in SUPPORTED_SOCKS_PROXIES + + +class RESTResponse(io.IOBase): + + def __init__(self, resp) -> None: + self.response = resp + self.status = resp.status + self.reason = resp.reason + self.data = None + + def read(self): + if self.data is None: + self.data = self.response.data + return self.data + + def getheaders(self): + """Returns a dictionary of the response headers.""" + return self.response.headers + + def getheader(self, name, default=None): + """Returns a given response header.""" + return self.response.headers.get(name, default) + + +class RESTClientObject: + + def __init__(self, configuration) -> None: + # urllib3.PoolManager will pass all kw parameters to connectionpool + # https://github.com/shazow/urllib3/blob/f9409436f83aeb79fbaf090181cd81b784f1b8ce/urllib3/poolmanager.py#L75 # noqa: E501 + # https://github.com/shazow/urllib3/blob/f9409436f83aeb79fbaf090181cd81b784f1b8ce/urllib3/connectionpool.py#L680 # noqa: E501 + # Custom SSL certificates and client certificates: http://urllib3.readthedocs.io/en/latest/advanced-usage.html # noqa: E501 + + # cert_reqs + if configuration.verify_ssl: + cert_reqs = ssl.CERT_REQUIRED + else: + cert_reqs = ssl.CERT_NONE + + pool_args = { + "cert_reqs": cert_reqs, + "ca_certs": configuration.ssl_ca_cert, + "cert_file": configuration.cert_file, + "key_file": configuration.key_file, + } + if configuration.assert_hostname is not None: + pool_args['assert_hostname'] = ( + configuration.assert_hostname + ) + + if configuration.retries is not None: + pool_args['retries'] = configuration.retries + + if configuration.tls_server_name: + pool_args['server_hostname'] = configuration.tls_server_name + + + if configuration.socket_options is not None: + pool_args['socket_options'] = configuration.socket_options + + if configuration.connection_pool_maxsize is not None: + pool_args['maxsize'] = configuration.connection_pool_maxsize + + # https pool manager + self.pool_manager: urllib3.PoolManager + + if configuration.proxy: + if is_socks_proxy_url(configuration.proxy): + from urllib3.contrib.socks import SOCKSProxyManager + pool_args["proxy_url"] = configuration.proxy + pool_args["headers"] = configuration.proxy_headers + self.pool_manager = SOCKSProxyManager(**pool_args) + else: + pool_args["proxy_url"] = configuration.proxy + pool_args["proxy_headers"] = configuration.proxy_headers + self.pool_manager = urllib3.ProxyManager(**pool_args) + else: + self.pool_manager = urllib3.PoolManager(**pool_args) + + def request( + self, + method, + url, + headers=None, + body=None, + post_params=None, + _request_timeout=None + ): + """Perform requests. + + :param method: http request method + :param url: http request url + :param headers: http request headers + :param body: request json body, for `application/json` + :param post_params: request post parameters, + `application/x-www-form-urlencoded` + and `multipart/form-data` + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + """ + method = method.upper() + assert method in [ + 'GET', + 'HEAD', + 'DELETE', + 'POST', + 'PUT', + 'PATCH', + 'OPTIONS' + ] + + if post_params and body: + raise ApiValueError( + "body parameter cannot be used with post_params parameter." + ) + + post_params = post_params or {} + headers = headers or {} + + timeout = None + if _request_timeout: + if isinstance(_request_timeout, (int, float)): + timeout = urllib3.Timeout(total=_request_timeout) + elif ( + isinstance(_request_timeout, tuple) + and len(_request_timeout) == 2 + ): + timeout = urllib3.Timeout( + connect=_request_timeout[0], + read=_request_timeout[1] + ) + + try: + # For `POST`, `PUT`, `PATCH`, `OPTIONS`, `DELETE` + if method in ['POST', 'PUT', 'PATCH', 'OPTIONS', 'DELETE']: + + # no content type provided or payload is json + content_type = headers.get('Content-Type') + if ( + not content_type + or re.search('json', content_type, re.IGNORECASE) + ): + request_body = None + if body is not None: + request_body = json.dumps(body) + r = self.pool_manager.request( + method, + url, + body=request_body, + timeout=timeout, + headers=headers, + preload_content=False + ) + elif content_type == 'application/x-www-form-urlencoded': + r = self.pool_manager.request( + method, + url, + fields=post_params, + encode_multipart=False, + timeout=timeout, + headers=headers, + preload_content=False + ) + elif content_type == 'multipart/form-data': + # must del headers['Content-Type'], or the correct + # Content-Type which generated by urllib3 will be + # overwritten. + del headers['Content-Type'] + # Ensures that dict objects are serialized + post_params = [(a, json.dumps(b)) if isinstance(b, dict) else (a,b) for a, b in post_params] + r = self.pool_manager.request( + method, + url, + fields=post_params, + encode_multipart=True, + timeout=timeout, + headers=headers, + preload_content=False + ) + # Pass a `string` parameter directly in the body to support + # other content types than JSON when `body` argument is + # provided in serialized form. + elif isinstance(body, str) or isinstance(body, bytes): + r = self.pool_manager.request( + method, + url, + body=body, + timeout=timeout, + headers=headers, + preload_content=False + ) + elif headers['Content-Type'] == 'text/plain' and isinstance(body, bool): + request_body = "true" if body else "false" + r = self.pool_manager.request( + method, + url, + body=request_body, + preload_content=False, + timeout=timeout, + headers=headers) + else: + # Cannot generate the request from given parameters + msg = """Cannot prepare a request message for provided + arguments. Please check that your arguments match + declared content type.""" + raise ApiException(status=0, reason=msg) + # For `GET`, `HEAD` + else: + r = self.pool_manager.request( + method, + url, + fields={}, + timeout=timeout, + headers=headers, + preload_content=False + ) + except urllib3.exceptions.SSLError as e: + msg = "\n".join([type(e).__name__, str(e)]) + raise ApiException(status=0, reason=msg) + + return RESTResponse(r) diff --git a/regtests/client/python/polaris/management/__init__.py b/regtests/client/python/polaris/management/__init__.py new file mode 100644 index 0000000000..400517acd7 --- /dev/null +++ b/regtests/client/python/polaris/management/__init__.py @@ -0,0 +1,73 @@ +# coding: utf-8 + +# flake8: noqa + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +__version__ = "1.0.0" + +# import apis into sdk package +from polaris.management.api.polaris_default_api import PolarisDefaultApi + +# import ApiClient +from polaris.management.api_response import ApiResponse +from polaris.management.api_client import ApiClient +from polaris.management.configuration import Configuration +from polaris.management.exceptions import OpenApiException +from polaris.management.exceptions import ApiTypeError +from polaris.management.exceptions import ApiValueError +from polaris.management.exceptions import ApiKeyError +from polaris.management.exceptions import ApiAttributeError +from polaris.management.exceptions import ApiException + +# import models into sdk package +from polaris.management.models.add_grant_request import AddGrantRequest +from polaris.management.models.aws_storage_config_info import AwsStorageConfigInfo +from polaris.management.models.azure_storage_config_info import AzureStorageConfigInfo +from polaris.management.models.catalog import Catalog +from polaris.management.models.catalog_grant import CatalogGrant +from polaris.management.models.catalog_privilege import CatalogPrivilege +from polaris.management.models.catalog_properties import CatalogProperties +from polaris.management.models.catalog_role import CatalogRole +from polaris.management.models.catalog_roles import CatalogRoles +from polaris.management.models.catalogs import Catalogs +from polaris.management.models.create_catalog_request import CreateCatalogRequest +from polaris.management.models.create_catalog_role_request import CreateCatalogRoleRequest +from polaris.management.models.create_principal_request import CreatePrincipalRequest +from polaris.management.models.create_principal_role_request import CreatePrincipalRoleRequest +from polaris.management.models.external_catalog import ExternalCatalog +from polaris.management.models.file_storage_config_info import FileStorageConfigInfo +from polaris.management.models.gcp_storage_config_info import GcpStorageConfigInfo +from polaris.management.models.grant_catalog_role_request import GrantCatalogRoleRequest +from polaris.management.models.grant_principal_role_request import GrantPrincipalRoleRequest +from polaris.management.models.grant_resource import GrantResource +from polaris.management.models.grant_resources import GrantResources +from polaris.management.models.namespace_grant import NamespaceGrant +from polaris.management.models.namespace_privilege import NamespacePrivilege +from polaris.management.models.polaris_catalog import PolarisCatalog +from polaris.management.models.principal import Principal +from polaris.management.models.principal_role import PrincipalRole +from polaris.management.models.principal_roles import PrincipalRoles +from polaris.management.models.principal_with_credentials import PrincipalWithCredentials +from polaris.management.models.principal_with_credentials_credentials import PrincipalWithCredentialsCredentials +from polaris.management.models.principals import Principals +from polaris.management.models.revoke_grant_request import RevokeGrantRequest +from polaris.management.models.storage_config_info import StorageConfigInfo +from polaris.management.models.table_grant import TableGrant +from polaris.management.models.table_privilege import TablePrivilege +from polaris.management.models.update_catalog_request import UpdateCatalogRequest +from polaris.management.models.update_catalog_role_request import UpdateCatalogRoleRequest +from polaris.management.models.update_principal_request import UpdatePrincipalRequest +from polaris.management.models.update_principal_role_request import UpdatePrincipalRoleRequest +from polaris.management.models.view_grant import ViewGrant +from polaris.management.models.view_privilege import ViewPrivilege diff --git a/regtests/client/python/polaris/management/api/__init__.py b/regtests/client/python/polaris/management/api/__init__.py new file mode 100644 index 0000000000..53bae63ba1 --- /dev/null +++ b/regtests/client/python/polaris/management/api/__init__.py @@ -0,0 +1,5 @@ +# flake8: noqa + +# import apis into api package +from polaris.management.api.polaris_default_api import PolarisDefaultApi + diff --git a/regtests/client/python/polaris/management/api/polaris_default_api.py b/regtests/client/python/polaris/management/api/polaris_default_api.py new file mode 100644 index 0000000000..fdf69e3ff9 --- /dev/null +++ b/regtests/client/python/polaris/management/api/polaris_default_api.py @@ -0,0 +1,8883 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + +import warnings +from pydantic import validate_call, Field, StrictFloat, StrictStr, StrictInt +from typing import Any, Dict, List, Optional, Tuple, Union +from typing_extensions import Annotated + +from pydantic import Field, StrictBool, StrictStr +from typing import Optional +from typing_extensions import Annotated +from polaris.management.models.add_grant_request import AddGrantRequest +from polaris.management.models.catalog import Catalog +from polaris.management.models.catalog_role import CatalogRole +from polaris.management.models.catalog_roles import CatalogRoles +from polaris.management.models.catalogs import Catalogs +from polaris.management.models.create_catalog_request import CreateCatalogRequest +from polaris.management.models.create_catalog_role_request import CreateCatalogRoleRequest +from polaris.management.models.create_principal_request import CreatePrincipalRequest +from polaris.management.models.create_principal_role_request import CreatePrincipalRoleRequest +from polaris.management.models.grant_catalog_role_request import GrantCatalogRoleRequest +from polaris.management.models.grant_principal_role_request import GrantPrincipalRoleRequest +from polaris.management.models.grant_resources import GrantResources +from polaris.management.models.principal import Principal +from polaris.management.models.principal_role import PrincipalRole +from polaris.management.models.principal_roles import PrincipalRoles +from polaris.management.models.principal_with_credentials import PrincipalWithCredentials +from polaris.management.models.principals import Principals +from polaris.management.models.revoke_grant_request import RevokeGrantRequest +from polaris.management.models.update_catalog_request import UpdateCatalogRequest +from polaris.management.models.update_catalog_role_request import UpdateCatalogRoleRequest +from polaris.management.models.update_principal_request import UpdatePrincipalRequest +from polaris.management.models.update_principal_role_request import UpdatePrincipalRoleRequest + +from polaris.management.api_client import ApiClient, RequestSerialized +from polaris.management.api_response import ApiResponse +from polaris.management.rest import RESTResponseType + + +class PolarisDefaultApi: + """NOTE: This class is auto generated by OpenAPI Generator + Ref: https://openapi-generator.tech + + Do not edit the class manually. + """ + + def __init__(self, api_client=None) -> None: + if api_client is None: + api_client = ApiClient.get_default() + self.api_client = api_client + + + @validate_call + def add_grant_to_catalog_role( + self, + catalog_name: Annotated[StrictStr, Field(description="The name of the catalog where the role will receive the grant")], + catalog_role_name: Annotated[StrictStr, Field(description="The name of the role receiving the grant (must exist)")], + add_grant_request: Optional[AddGrantRequest] = None, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> None: + """add_grant_to_catalog_role + + Add a new grant to the catalog role + + :param catalog_name: The name of the catalog where the role will receive the grant (required) + :type catalog_name: str + :param catalog_role_name: The name of the role receiving the grant (must exist) (required) + :type catalog_role_name: str + :param add_grant_request: + :type add_grant_request: AddGrantRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._add_grant_to_catalog_role_serialize( + catalog_name=catalog_name, + catalog_role_name=catalog_role_name, + add_grant_request=add_grant_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '201': None, + '403': None, + '404': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + def add_grant_to_catalog_role_with_http_info( + self, + catalog_name: Annotated[StrictStr, Field(description="The name of the catalog where the role will receive the grant")], + catalog_role_name: Annotated[StrictStr, Field(description="The name of the role receiving the grant (must exist)")], + add_grant_request: Optional[AddGrantRequest] = None, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[None]: + """add_grant_to_catalog_role + + Add a new grant to the catalog role + + :param catalog_name: The name of the catalog where the role will receive the grant (required) + :type catalog_name: str + :param catalog_role_name: The name of the role receiving the grant (must exist) (required) + :type catalog_role_name: str + :param add_grant_request: + :type add_grant_request: AddGrantRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._add_grant_to_catalog_role_serialize( + catalog_name=catalog_name, + catalog_role_name=catalog_role_name, + add_grant_request=add_grant_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '201': None, + '403': None, + '404': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + def add_grant_to_catalog_role_without_preload_content( + self, + catalog_name: Annotated[StrictStr, Field(description="The name of the catalog where the role will receive the grant")], + catalog_role_name: Annotated[StrictStr, Field(description="The name of the role receiving the grant (must exist)")], + add_grant_request: Optional[AddGrantRequest] = None, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """add_grant_to_catalog_role + + Add a new grant to the catalog role + + :param catalog_name: The name of the catalog where the role will receive the grant (required) + :type catalog_name: str + :param catalog_role_name: The name of the role receiving the grant (must exist) (required) + :type catalog_role_name: str + :param add_grant_request: + :type add_grant_request: AddGrantRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._add_grant_to_catalog_role_serialize( + catalog_name=catalog_name, + catalog_role_name=catalog_role_name, + add_grant_request=add_grant_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '201': None, + '403': None, + '404': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _add_grant_to_catalog_role_serialize( + self, + catalog_name, + catalog_role_name, + add_grant_request, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[str, Union[str, bytes]] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + if catalog_name is not None: + _path_params['catalogName'] = catalog_name + if catalog_role_name is not None: + _path_params['catalogRoleName'] = catalog_role_name + # process the query parameters + # process the header parameters + # process the form parameters + # process the body parameter + if add_grant_request is not None: + _body_params = add_grant_request + + + + # set the HTTP header `Content-Type` + if _content_type: + _header_params['Content-Type'] = _content_type + else: + _default_content_type = ( + self.api_client.select_header_content_type( + [ + 'application/json' + ] + ) + ) + if _default_content_type is not None: + _header_params['Content-Type'] = _default_content_type + + # authentication setting + _auth_settings: List[str] = [ + 'OAuth2' + ] + + return self.api_client.param_serialize( + method='PUT', + resource_path='/catalogs/{catalogName}/catalog-roles/{catalogRoleName}/grants', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + + + + @validate_call + def assign_catalog_role_to_principal_role( + self, + principal_role_name: Annotated[StrictStr, Field(description="The principal role name")], + catalog_name: Annotated[StrictStr, Field(description="The name of the catalog where the catalogRoles reside")], + grant_catalog_role_request: Annotated[GrantCatalogRoleRequest, Field(description="The principal to create")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> None: + """assign_catalog_role_to_principal_role + + Assign a catalog role to a principal role + + :param principal_role_name: The principal role name (required) + :type principal_role_name: str + :param catalog_name: The name of the catalog where the catalogRoles reside (required) + :type catalog_name: str + :param grant_catalog_role_request: The principal to create (required) + :type grant_catalog_role_request: GrantCatalogRoleRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._assign_catalog_role_to_principal_role_serialize( + principal_role_name=principal_role_name, + catalog_name=catalog_name, + grant_catalog_role_request=grant_catalog_role_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '201': None, + '403': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + def assign_catalog_role_to_principal_role_with_http_info( + self, + principal_role_name: Annotated[StrictStr, Field(description="The principal role name")], + catalog_name: Annotated[StrictStr, Field(description="The name of the catalog where the catalogRoles reside")], + grant_catalog_role_request: Annotated[GrantCatalogRoleRequest, Field(description="The principal to create")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[None]: + """assign_catalog_role_to_principal_role + + Assign a catalog role to a principal role + + :param principal_role_name: The principal role name (required) + :type principal_role_name: str + :param catalog_name: The name of the catalog where the catalogRoles reside (required) + :type catalog_name: str + :param grant_catalog_role_request: The principal to create (required) + :type grant_catalog_role_request: GrantCatalogRoleRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._assign_catalog_role_to_principal_role_serialize( + principal_role_name=principal_role_name, + catalog_name=catalog_name, + grant_catalog_role_request=grant_catalog_role_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '201': None, + '403': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + def assign_catalog_role_to_principal_role_without_preload_content( + self, + principal_role_name: Annotated[StrictStr, Field(description="The principal role name")], + catalog_name: Annotated[StrictStr, Field(description="The name of the catalog where the catalogRoles reside")], + grant_catalog_role_request: Annotated[GrantCatalogRoleRequest, Field(description="The principal to create")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """assign_catalog_role_to_principal_role + + Assign a catalog role to a principal role + + :param principal_role_name: The principal role name (required) + :type principal_role_name: str + :param catalog_name: The name of the catalog where the catalogRoles reside (required) + :type catalog_name: str + :param grant_catalog_role_request: The principal to create (required) + :type grant_catalog_role_request: GrantCatalogRoleRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._assign_catalog_role_to_principal_role_serialize( + principal_role_name=principal_role_name, + catalog_name=catalog_name, + grant_catalog_role_request=grant_catalog_role_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '201': None, + '403': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _assign_catalog_role_to_principal_role_serialize( + self, + principal_role_name, + catalog_name, + grant_catalog_role_request, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[str, Union[str, bytes]] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + if principal_role_name is not None: + _path_params['principalRoleName'] = principal_role_name + if catalog_name is not None: + _path_params['catalogName'] = catalog_name + # process the query parameters + # process the header parameters + # process the form parameters + # process the body parameter + if grant_catalog_role_request is not None: + _body_params = grant_catalog_role_request + + + + # set the HTTP header `Content-Type` + if _content_type: + _header_params['Content-Type'] = _content_type + else: + _default_content_type = ( + self.api_client.select_header_content_type( + [ + 'application/json' + ] + ) + ) + if _default_content_type is not None: + _header_params['Content-Type'] = _default_content_type + + # authentication setting + _auth_settings: List[str] = [ + 'OAuth2' + ] + + return self.api_client.param_serialize( + method='PUT', + resource_path='/principal-roles/{principalRoleName}/catalog-roles/{catalogName}', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + + + + @validate_call + def assign_principal_role( + self, + principal_name: Annotated[StrictStr, Field(description="The name of the target principal")], + grant_principal_role_request: Annotated[GrantPrincipalRoleRequest, Field(description="The principal role to assign")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> None: + """assign_principal_role + + Add a role to the principal + + :param principal_name: The name of the target principal (required) + :type principal_name: str + :param grant_principal_role_request: The principal role to assign (required) + :type grant_principal_role_request: GrantPrincipalRoleRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._assign_principal_role_serialize( + principal_name=principal_name, + grant_principal_role_request=grant_principal_role_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '201': None, + '403': None, + '404': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + def assign_principal_role_with_http_info( + self, + principal_name: Annotated[StrictStr, Field(description="The name of the target principal")], + grant_principal_role_request: Annotated[GrantPrincipalRoleRequest, Field(description="The principal role to assign")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[None]: + """assign_principal_role + + Add a role to the principal + + :param principal_name: The name of the target principal (required) + :type principal_name: str + :param grant_principal_role_request: The principal role to assign (required) + :type grant_principal_role_request: GrantPrincipalRoleRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._assign_principal_role_serialize( + principal_name=principal_name, + grant_principal_role_request=grant_principal_role_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '201': None, + '403': None, + '404': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + def assign_principal_role_without_preload_content( + self, + principal_name: Annotated[StrictStr, Field(description="The name of the target principal")], + grant_principal_role_request: Annotated[GrantPrincipalRoleRequest, Field(description="The principal role to assign")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """assign_principal_role + + Add a role to the principal + + :param principal_name: The name of the target principal (required) + :type principal_name: str + :param grant_principal_role_request: The principal role to assign (required) + :type grant_principal_role_request: GrantPrincipalRoleRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._assign_principal_role_serialize( + principal_name=principal_name, + grant_principal_role_request=grant_principal_role_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '201': None, + '403': None, + '404': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _assign_principal_role_serialize( + self, + principal_name, + grant_principal_role_request, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[str, Union[str, bytes]] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + if principal_name is not None: + _path_params['principalName'] = principal_name + # process the query parameters + # process the header parameters + # process the form parameters + # process the body parameter + if grant_principal_role_request is not None: + _body_params = grant_principal_role_request + + + + # set the HTTP header `Content-Type` + if _content_type: + _header_params['Content-Type'] = _content_type + else: + _default_content_type = ( + self.api_client.select_header_content_type( + [ + 'application/json' + ] + ) + ) + if _default_content_type is not None: + _header_params['Content-Type'] = _default_content_type + + # authentication setting + _auth_settings: List[str] = [ + 'OAuth2' + ] + + return self.api_client.param_serialize( + method='PUT', + resource_path='/principals/{principalName}/principal-roles', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + + + + @validate_call + def create_catalog( + self, + create_catalog_request: Annotated[CreateCatalogRequest, Field(description="The Catalog to create")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> None: + """create_catalog + + Add a new Catalog + + :param create_catalog_request: The Catalog to create (required) + :type create_catalog_request: CreateCatalogRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._create_catalog_serialize( + create_catalog_request=create_catalog_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '201': None, + '403': None, + '404': None, + '409': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + def create_catalog_with_http_info( + self, + create_catalog_request: Annotated[CreateCatalogRequest, Field(description="The Catalog to create")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[None]: + """create_catalog + + Add a new Catalog + + :param create_catalog_request: The Catalog to create (required) + :type create_catalog_request: CreateCatalogRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._create_catalog_serialize( + create_catalog_request=create_catalog_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '201': None, + '403': None, + '404': None, + '409': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + def create_catalog_without_preload_content( + self, + create_catalog_request: Annotated[CreateCatalogRequest, Field(description="The Catalog to create")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """create_catalog + + Add a new Catalog + + :param create_catalog_request: The Catalog to create (required) + :type create_catalog_request: CreateCatalogRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._create_catalog_serialize( + create_catalog_request=create_catalog_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '201': None, + '403': None, + '404': None, + '409': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _create_catalog_serialize( + self, + create_catalog_request, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[str, Union[str, bytes]] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + # process the query parameters + # process the header parameters + # process the form parameters + # process the body parameter + if create_catalog_request is not None: + _body_params = create_catalog_request + + + + # set the HTTP header `Content-Type` + if _content_type: + _header_params['Content-Type'] = _content_type + else: + _default_content_type = ( + self.api_client.select_header_content_type( + [ + 'application/json' + ] + ) + ) + if _default_content_type is not None: + _header_params['Content-Type'] = _default_content_type + + # authentication setting + _auth_settings: List[str] = [ + 'OAuth2' + ] + + return self.api_client.param_serialize( + method='POST', + resource_path='/catalogs', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + + + + @validate_call + def create_catalog_role( + self, + catalog_name: Annotated[StrictStr, Field(description="The catalog for which we are reading/updating roles")], + create_catalog_role_request: Optional[CreateCatalogRoleRequest] = None, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> None: + """create_catalog_role + + Create a new role in the catalog + + :param catalog_name: The catalog for which we are reading/updating roles (required) + :type catalog_name: str + :param create_catalog_role_request: + :type create_catalog_role_request: CreateCatalogRoleRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._create_catalog_role_serialize( + catalog_name=catalog_name, + create_catalog_role_request=create_catalog_role_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '201': None, + '403': None, + '404': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + def create_catalog_role_with_http_info( + self, + catalog_name: Annotated[StrictStr, Field(description="The catalog for which we are reading/updating roles")], + create_catalog_role_request: Optional[CreateCatalogRoleRequest] = None, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[None]: + """create_catalog_role + + Create a new role in the catalog + + :param catalog_name: The catalog for which we are reading/updating roles (required) + :type catalog_name: str + :param create_catalog_role_request: + :type create_catalog_role_request: CreateCatalogRoleRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._create_catalog_role_serialize( + catalog_name=catalog_name, + create_catalog_role_request=create_catalog_role_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '201': None, + '403': None, + '404': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + def create_catalog_role_without_preload_content( + self, + catalog_name: Annotated[StrictStr, Field(description="The catalog for which we are reading/updating roles")], + create_catalog_role_request: Optional[CreateCatalogRoleRequest] = None, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """create_catalog_role + + Create a new role in the catalog + + :param catalog_name: The catalog for which we are reading/updating roles (required) + :type catalog_name: str + :param create_catalog_role_request: + :type create_catalog_role_request: CreateCatalogRoleRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._create_catalog_role_serialize( + catalog_name=catalog_name, + create_catalog_role_request=create_catalog_role_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '201': None, + '403': None, + '404': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _create_catalog_role_serialize( + self, + catalog_name, + create_catalog_role_request, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[str, Union[str, bytes]] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + if catalog_name is not None: + _path_params['catalogName'] = catalog_name + # process the query parameters + # process the header parameters + # process the form parameters + # process the body parameter + if create_catalog_role_request is not None: + _body_params = create_catalog_role_request + + + + # set the HTTP header `Content-Type` + if _content_type: + _header_params['Content-Type'] = _content_type + else: + _default_content_type = ( + self.api_client.select_header_content_type( + [ + 'application/json' + ] + ) + ) + if _default_content_type is not None: + _header_params['Content-Type'] = _default_content_type + + # authentication setting + _auth_settings: List[str] = [ + 'OAuth2' + ] + + return self.api_client.param_serialize( + method='POST', + resource_path='/catalogs/{catalogName}/catalog-roles', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + + + + @validate_call + def create_principal( + self, + create_principal_request: Annotated[CreatePrincipalRequest, Field(description="The principal to create")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> PrincipalWithCredentials: + """create_principal + + Create a principal + + :param create_principal_request: The principal to create (required) + :type create_principal_request: CreatePrincipalRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._create_principal_serialize( + create_principal_request=create_principal_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '201': "PrincipalWithCredentials", + '403': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + def create_principal_with_http_info( + self, + create_principal_request: Annotated[CreatePrincipalRequest, Field(description="The principal to create")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[PrincipalWithCredentials]: + """create_principal + + Create a principal + + :param create_principal_request: The principal to create (required) + :type create_principal_request: CreatePrincipalRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._create_principal_serialize( + create_principal_request=create_principal_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '201': "PrincipalWithCredentials", + '403': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + def create_principal_without_preload_content( + self, + create_principal_request: Annotated[CreatePrincipalRequest, Field(description="The principal to create")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """create_principal + + Create a principal + + :param create_principal_request: The principal to create (required) + :type create_principal_request: CreatePrincipalRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._create_principal_serialize( + create_principal_request=create_principal_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '201': "PrincipalWithCredentials", + '403': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _create_principal_serialize( + self, + create_principal_request, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[str, Union[str, bytes]] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + # process the query parameters + # process the header parameters + # process the form parameters + # process the body parameter + if create_principal_request is not None: + _body_params = create_principal_request + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + + # set the HTTP header `Content-Type` + if _content_type: + _header_params['Content-Type'] = _content_type + else: + _default_content_type = ( + self.api_client.select_header_content_type( + [ + 'application/json' + ] + ) + ) + if _default_content_type is not None: + _header_params['Content-Type'] = _default_content_type + + # authentication setting + _auth_settings: List[str] = [ + 'OAuth2' + ] + + return self.api_client.param_serialize( + method='POST', + resource_path='/principals', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + + + + @validate_call + def create_principal_role( + self, + create_principal_role_request: Annotated[CreatePrincipalRoleRequest, Field(description="The principal to create")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> None: + """create_principal_role + + Create a principal role + + :param create_principal_role_request: The principal to create (required) + :type create_principal_role_request: CreatePrincipalRoleRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._create_principal_role_serialize( + create_principal_role_request=create_principal_role_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '201': None, + '403': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + def create_principal_role_with_http_info( + self, + create_principal_role_request: Annotated[CreatePrincipalRoleRequest, Field(description="The principal to create")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[None]: + """create_principal_role + + Create a principal role + + :param create_principal_role_request: The principal to create (required) + :type create_principal_role_request: CreatePrincipalRoleRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._create_principal_role_serialize( + create_principal_role_request=create_principal_role_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '201': None, + '403': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + def create_principal_role_without_preload_content( + self, + create_principal_role_request: Annotated[CreatePrincipalRoleRequest, Field(description="The principal to create")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """create_principal_role + + Create a principal role + + :param create_principal_role_request: The principal to create (required) + :type create_principal_role_request: CreatePrincipalRoleRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._create_principal_role_serialize( + create_principal_role_request=create_principal_role_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '201': None, + '403': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _create_principal_role_serialize( + self, + create_principal_role_request, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[str, Union[str, bytes]] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + # process the query parameters + # process the header parameters + # process the form parameters + # process the body parameter + if create_principal_role_request is not None: + _body_params = create_principal_role_request + + + + # set the HTTP header `Content-Type` + if _content_type: + _header_params['Content-Type'] = _content_type + else: + _default_content_type = ( + self.api_client.select_header_content_type( + [ + 'application/json' + ] + ) + ) + if _default_content_type is not None: + _header_params['Content-Type'] = _default_content_type + + # authentication setting + _auth_settings: List[str] = [ + 'OAuth2' + ] + + return self.api_client.param_serialize( + method='POST', + resource_path='/principal-roles', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + + + + @validate_call + def delete_catalog( + self, + catalog_name: Annotated[StrictStr, Field(description="The name of the catalog")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> None: + """delete_catalog + + Delete an existing catalog. This is a cascading operation that deletes all metadata, including principals, roles and grants. If the catalog is an internal catalog, all tables and namespaces are dropped without purge. + + :param catalog_name: The name of the catalog (required) + :type catalog_name: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._delete_catalog_serialize( + catalog_name=catalog_name, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '204': None, + '403': None, + '404': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + def delete_catalog_with_http_info( + self, + catalog_name: Annotated[StrictStr, Field(description="The name of the catalog")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[None]: + """delete_catalog + + Delete an existing catalog. This is a cascading operation that deletes all metadata, including principals, roles and grants. If the catalog is an internal catalog, all tables and namespaces are dropped without purge. + + :param catalog_name: The name of the catalog (required) + :type catalog_name: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._delete_catalog_serialize( + catalog_name=catalog_name, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '204': None, + '403': None, + '404': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + def delete_catalog_without_preload_content( + self, + catalog_name: Annotated[StrictStr, Field(description="The name of the catalog")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """delete_catalog + + Delete an existing catalog. This is a cascading operation that deletes all metadata, including principals, roles and grants. If the catalog is an internal catalog, all tables and namespaces are dropped without purge. + + :param catalog_name: The name of the catalog (required) + :type catalog_name: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._delete_catalog_serialize( + catalog_name=catalog_name, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '204': None, + '403': None, + '404': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _delete_catalog_serialize( + self, + catalog_name, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[str, Union[str, bytes]] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + if catalog_name is not None: + _path_params['catalogName'] = catalog_name + # process the query parameters + # process the header parameters + # process the form parameters + # process the body parameter + + + + + # authentication setting + _auth_settings: List[str] = [ + 'OAuth2' + ] + + return self.api_client.param_serialize( + method='DELETE', + resource_path='/catalogs/{catalogName}', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + + + + @validate_call + def delete_catalog_role( + self, + catalog_name: Annotated[StrictStr, Field(description="The catalog for which we are retrieving roles")], + catalog_role_name: Annotated[StrictStr, Field(description="The name of the role")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> None: + """delete_catalog_role + + Delete an existing role from the catalog. All associated grants will also be deleted + + :param catalog_name: The catalog for which we are retrieving roles (required) + :type catalog_name: str + :param catalog_role_name: The name of the role (required) + :type catalog_role_name: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._delete_catalog_role_serialize( + catalog_name=catalog_name, + catalog_role_name=catalog_role_name, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '204': None, + '403': None, + '404': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + def delete_catalog_role_with_http_info( + self, + catalog_name: Annotated[StrictStr, Field(description="The catalog for which we are retrieving roles")], + catalog_role_name: Annotated[StrictStr, Field(description="The name of the role")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[None]: + """delete_catalog_role + + Delete an existing role from the catalog. All associated grants will also be deleted + + :param catalog_name: The catalog for which we are retrieving roles (required) + :type catalog_name: str + :param catalog_role_name: The name of the role (required) + :type catalog_role_name: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._delete_catalog_role_serialize( + catalog_name=catalog_name, + catalog_role_name=catalog_role_name, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '204': None, + '403': None, + '404': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + def delete_catalog_role_without_preload_content( + self, + catalog_name: Annotated[StrictStr, Field(description="The catalog for which we are retrieving roles")], + catalog_role_name: Annotated[StrictStr, Field(description="The name of the role")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """delete_catalog_role + + Delete an existing role from the catalog. All associated grants will also be deleted + + :param catalog_name: The catalog for which we are retrieving roles (required) + :type catalog_name: str + :param catalog_role_name: The name of the role (required) + :type catalog_role_name: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._delete_catalog_role_serialize( + catalog_name=catalog_name, + catalog_role_name=catalog_role_name, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '204': None, + '403': None, + '404': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _delete_catalog_role_serialize( + self, + catalog_name, + catalog_role_name, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[str, Union[str, bytes]] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + if catalog_name is not None: + _path_params['catalogName'] = catalog_name + if catalog_role_name is not None: + _path_params['catalogRoleName'] = catalog_role_name + # process the query parameters + # process the header parameters + # process the form parameters + # process the body parameter + + + + + # authentication setting + _auth_settings: List[str] = [ + 'OAuth2' + ] + + return self.api_client.param_serialize( + method='DELETE', + resource_path='/catalogs/{catalogName}/catalog-roles/{catalogRoleName}', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + + + + @validate_call + def delete_principal( + self, + principal_name: Annotated[StrictStr, Field(description="The principal name")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> None: + """delete_principal + + Remove a principal from polaris + + :param principal_name: The principal name (required) + :type principal_name: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._delete_principal_serialize( + principal_name=principal_name, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '204': None, + '403': None, + '404': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + def delete_principal_with_http_info( + self, + principal_name: Annotated[StrictStr, Field(description="The principal name")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[None]: + """delete_principal + + Remove a principal from polaris + + :param principal_name: The principal name (required) + :type principal_name: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._delete_principal_serialize( + principal_name=principal_name, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '204': None, + '403': None, + '404': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + def delete_principal_without_preload_content( + self, + principal_name: Annotated[StrictStr, Field(description="The principal name")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """delete_principal + + Remove a principal from polaris + + :param principal_name: The principal name (required) + :type principal_name: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._delete_principal_serialize( + principal_name=principal_name, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '204': None, + '403': None, + '404': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _delete_principal_serialize( + self, + principal_name, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[str, Union[str, bytes]] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + if principal_name is not None: + _path_params['principalName'] = principal_name + # process the query parameters + # process the header parameters + # process the form parameters + # process the body parameter + + + + + # authentication setting + _auth_settings: List[str] = [ + 'OAuth2' + ] + + return self.api_client.param_serialize( + method='DELETE', + resource_path='/principals/{principalName}', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + + + + @validate_call + def delete_principal_role( + self, + principal_role_name: Annotated[StrictStr, Field(description="The principal role name")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> None: + """delete_principal_role + + Remove a principal role from polaris + + :param principal_role_name: The principal role name (required) + :type principal_role_name: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._delete_principal_role_serialize( + principal_role_name=principal_role_name, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '204': None, + '403': None, + '404': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + def delete_principal_role_with_http_info( + self, + principal_role_name: Annotated[StrictStr, Field(description="The principal role name")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[None]: + """delete_principal_role + + Remove a principal role from polaris + + :param principal_role_name: The principal role name (required) + :type principal_role_name: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._delete_principal_role_serialize( + principal_role_name=principal_role_name, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '204': None, + '403': None, + '404': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + def delete_principal_role_without_preload_content( + self, + principal_role_name: Annotated[StrictStr, Field(description="The principal role name")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """delete_principal_role + + Remove a principal role from polaris + + :param principal_role_name: The principal role name (required) + :type principal_role_name: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._delete_principal_role_serialize( + principal_role_name=principal_role_name, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '204': None, + '403': None, + '404': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _delete_principal_role_serialize( + self, + principal_role_name, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[str, Union[str, bytes]] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + if principal_role_name is not None: + _path_params['principalRoleName'] = principal_role_name + # process the query parameters + # process the header parameters + # process the form parameters + # process the body parameter + + + + + # authentication setting + _auth_settings: List[str] = [ + 'OAuth2' + ] + + return self.api_client.param_serialize( + method='DELETE', + resource_path='/principal-roles/{principalRoleName}', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + + + + @validate_call + def get_catalog( + self, + catalog_name: Annotated[StrictStr, Field(description="The name of the catalog")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> Catalog: + """get_catalog + + Get the details of a catalog + + :param catalog_name: The name of the catalog (required) + :type catalog_name: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._get_catalog_serialize( + catalog_name=catalog_name, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "Catalog", + '403': None, + '404': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + def get_catalog_with_http_info( + self, + catalog_name: Annotated[StrictStr, Field(description="The name of the catalog")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[Catalog]: + """get_catalog + + Get the details of a catalog + + :param catalog_name: The name of the catalog (required) + :type catalog_name: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._get_catalog_serialize( + catalog_name=catalog_name, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "Catalog", + '403': None, + '404': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + def get_catalog_without_preload_content( + self, + catalog_name: Annotated[StrictStr, Field(description="The name of the catalog")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """get_catalog + + Get the details of a catalog + + :param catalog_name: The name of the catalog (required) + :type catalog_name: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._get_catalog_serialize( + catalog_name=catalog_name, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "Catalog", + '403': None, + '404': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _get_catalog_serialize( + self, + catalog_name, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[str, Union[str, bytes]] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + if catalog_name is not None: + _path_params['catalogName'] = catalog_name + # process the query parameters + # process the header parameters + # process the form parameters + # process the body parameter + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + + + # authentication setting + _auth_settings: List[str] = [ + 'OAuth2' + ] + + return self.api_client.param_serialize( + method='GET', + resource_path='/catalogs/{catalogName}', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + + + + @validate_call + def get_catalog_role( + self, + catalog_name: Annotated[StrictStr, Field(description="The catalog for which we are retrieving roles")], + catalog_role_name: Annotated[StrictStr, Field(description="The name of the role")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> CatalogRole: + """get_catalog_role + + Get the details of an existing role + + :param catalog_name: The catalog for which we are retrieving roles (required) + :type catalog_name: str + :param catalog_role_name: The name of the role (required) + :type catalog_role_name: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._get_catalog_role_serialize( + catalog_name=catalog_name, + catalog_role_name=catalog_role_name, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "CatalogRole", + '403': None, + '404': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + def get_catalog_role_with_http_info( + self, + catalog_name: Annotated[StrictStr, Field(description="The catalog for which we are retrieving roles")], + catalog_role_name: Annotated[StrictStr, Field(description="The name of the role")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[CatalogRole]: + """get_catalog_role + + Get the details of an existing role + + :param catalog_name: The catalog for which we are retrieving roles (required) + :type catalog_name: str + :param catalog_role_name: The name of the role (required) + :type catalog_role_name: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._get_catalog_role_serialize( + catalog_name=catalog_name, + catalog_role_name=catalog_role_name, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "CatalogRole", + '403': None, + '404': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + def get_catalog_role_without_preload_content( + self, + catalog_name: Annotated[StrictStr, Field(description="The catalog for which we are retrieving roles")], + catalog_role_name: Annotated[StrictStr, Field(description="The name of the role")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """get_catalog_role + + Get the details of an existing role + + :param catalog_name: The catalog for which we are retrieving roles (required) + :type catalog_name: str + :param catalog_role_name: The name of the role (required) + :type catalog_role_name: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._get_catalog_role_serialize( + catalog_name=catalog_name, + catalog_role_name=catalog_role_name, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "CatalogRole", + '403': None, + '404': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _get_catalog_role_serialize( + self, + catalog_name, + catalog_role_name, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[str, Union[str, bytes]] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + if catalog_name is not None: + _path_params['catalogName'] = catalog_name + if catalog_role_name is not None: + _path_params['catalogRoleName'] = catalog_role_name + # process the query parameters + # process the header parameters + # process the form parameters + # process the body parameter + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + + + # authentication setting + _auth_settings: List[str] = [ + 'OAuth2' + ] + + return self.api_client.param_serialize( + method='GET', + resource_path='/catalogs/{catalogName}/catalog-roles/{catalogRoleName}', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + + + + @validate_call + def get_principal( + self, + principal_name: Annotated[StrictStr, Field(description="The principal name")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> Principal: + """get_principal + + Get the principal details + + :param principal_name: The principal name (required) + :type principal_name: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._get_principal_serialize( + principal_name=principal_name, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "Principal", + '403': None, + '404': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + def get_principal_with_http_info( + self, + principal_name: Annotated[StrictStr, Field(description="The principal name")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[Principal]: + """get_principal + + Get the principal details + + :param principal_name: The principal name (required) + :type principal_name: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._get_principal_serialize( + principal_name=principal_name, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "Principal", + '403': None, + '404': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + def get_principal_without_preload_content( + self, + principal_name: Annotated[StrictStr, Field(description="The principal name")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """get_principal + + Get the principal details + + :param principal_name: The principal name (required) + :type principal_name: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._get_principal_serialize( + principal_name=principal_name, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "Principal", + '403': None, + '404': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _get_principal_serialize( + self, + principal_name, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[str, Union[str, bytes]] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + if principal_name is not None: + _path_params['principalName'] = principal_name + # process the query parameters + # process the header parameters + # process the form parameters + # process the body parameter + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + + + # authentication setting + _auth_settings: List[str] = [ + 'OAuth2' + ] + + return self.api_client.param_serialize( + method='GET', + resource_path='/principals/{principalName}', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + + + + @validate_call + def get_principal_role( + self, + principal_role_name: Annotated[StrictStr, Field(description="The principal role name")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> PrincipalRole: + """get_principal_role + + Get the principal role details + + :param principal_role_name: The principal role name (required) + :type principal_role_name: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._get_principal_role_serialize( + principal_role_name=principal_role_name, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "PrincipalRole", + '403': None, + '404': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + def get_principal_role_with_http_info( + self, + principal_role_name: Annotated[StrictStr, Field(description="The principal role name")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[PrincipalRole]: + """get_principal_role + + Get the principal role details + + :param principal_role_name: The principal role name (required) + :type principal_role_name: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._get_principal_role_serialize( + principal_role_name=principal_role_name, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "PrincipalRole", + '403': None, + '404': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + def get_principal_role_without_preload_content( + self, + principal_role_name: Annotated[StrictStr, Field(description="The principal role name")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """get_principal_role + + Get the principal role details + + :param principal_role_name: The principal role name (required) + :type principal_role_name: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._get_principal_role_serialize( + principal_role_name=principal_role_name, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "PrincipalRole", + '403': None, + '404': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _get_principal_role_serialize( + self, + principal_role_name, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[str, Union[str, bytes]] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + if principal_role_name is not None: + _path_params['principalRoleName'] = principal_role_name + # process the query parameters + # process the header parameters + # process the form parameters + # process the body parameter + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + + + # authentication setting + _auth_settings: List[str] = [ + 'OAuth2' + ] + + return self.api_client.param_serialize( + method='GET', + resource_path='/principal-roles/{principalRoleName}', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + + + + @validate_call + def list_assignee_principal_roles_for_catalog_role( + self, + catalog_name: Annotated[StrictStr, Field(description="The name of the catalog where the catalog role resides")], + catalog_role_name: Annotated[StrictStr, Field(description="The name of the catalog role")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> PrincipalRoles: + """list_assignee_principal_roles_for_catalog_role + + List the PrincipalRoles to whome the tagetcatalog role has been assigned + + :param catalog_name: The name of the catalog where the catalog role resides (required) + :type catalog_name: str + :param catalog_role_name: The name of the catalog role (required) + :type catalog_role_name: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._list_assignee_principal_roles_for_catalog_role_serialize( + catalog_name=catalog_name, + catalog_role_name=catalog_role_name, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "PrincipalRoles", + '403': None, + '404': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + def list_assignee_principal_roles_for_catalog_role_with_http_info( + self, + catalog_name: Annotated[StrictStr, Field(description="The name of the catalog where the catalog role resides")], + catalog_role_name: Annotated[StrictStr, Field(description="The name of the catalog role")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[PrincipalRoles]: + """list_assignee_principal_roles_for_catalog_role + + List the PrincipalRoles to whome the tagetcatalog role has been assigned + + :param catalog_name: The name of the catalog where the catalog role resides (required) + :type catalog_name: str + :param catalog_role_name: The name of the catalog role (required) + :type catalog_role_name: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._list_assignee_principal_roles_for_catalog_role_serialize( + catalog_name=catalog_name, + catalog_role_name=catalog_role_name, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "PrincipalRoles", + '403': None, + '404': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + def list_assignee_principal_roles_for_catalog_role_without_preload_content( + self, + catalog_name: Annotated[StrictStr, Field(description="The name of the catalog where the catalog role resides")], + catalog_role_name: Annotated[StrictStr, Field(description="The name of the catalog role")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """list_assignee_principal_roles_for_catalog_role + + List the PrincipalRoles to whome the tagetcatalog role has been assigned + + :param catalog_name: The name of the catalog where the catalog role resides (required) + :type catalog_name: str + :param catalog_role_name: The name of the catalog role (required) + :type catalog_role_name: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._list_assignee_principal_roles_for_catalog_role_serialize( + catalog_name=catalog_name, + catalog_role_name=catalog_role_name, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "PrincipalRoles", + '403': None, + '404': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _list_assignee_principal_roles_for_catalog_role_serialize( + self, + catalog_name, + catalog_role_name, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[str, Union[str, bytes]] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + if catalog_name is not None: + _path_params['catalogName'] = catalog_name + if catalog_role_name is not None: + _path_params['catalogRoleName'] = catalog_role_name + # process the query parameters + # process the header parameters + # process the form parameters + # process the body parameter + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + + + # authentication setting + _auth_settings: List[str] = [ + 'OAuth2' + ] + + return self.api_client.param_serialize( + method='GET', + resource_path='/catalogs/{catalogName}/catalog-roles/{catalogRoleName}/principal-roles', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + + + + @validate_call + def list_assignee_principals_for_principal_role( + self, + principal_role_name: Annotated[StrictStr, Field(description="The principal role name")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> Principals: + """list_assignee_principals_for_principal_role + + List the Principals to whom the target principal role has been assigned + + :param principal_role_name: The principal role name (required) + :type principal_role_name: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._list_assignee_principals_for_principal_role_serialize( + principal_role_name=principal_role_name, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "Principals", + '403': None, + '404': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + def list_assignee_principals_for_principal_role_with_http_info( + self, + principal_role_name: Annotated[StrictStr, Field(description="The principal role name")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[Principals]: + """list_assignee_principals_for_principal_role + + List the Principals to whom the target principal role has been assigned + + :param principal_role_name: The principal role name (required) + :type principal_role_name: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._list_assignee_principals_for_principal_role_serialize( + principal_role_name=principal_role_name, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "Principals", + '403': None, + '404': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + def list_assignee_principals_for_principal_role_without_preload_content( + self, + principal_role_name: Annotated[StrictStr, Field(description="The principal role name")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """list_assignee_principals_for_principal_role + + List the Principals to whom the target principal role has been assigned + + :param principal_role_name: The principal role name (required) + :type principal_role_name: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._list_assignee_principals_for_principal_role_serialize( + principal_role_name=principal_role_name, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "Principals", + '403': None, + '404': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _list_assignee_principals_for_principal_role_serialize( + self, + principal_role_name, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[str, Union[str, bytes]] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + if principal_role_name is not None: + _path_params['principalRoleName'] = principal_role_name + # process the query parameters + # process the header parameters + # process the form parameters + # process the body parameter + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + + + # authentication setting + _auth_settings: List[str] = [ + 'OAuth2' + ] + + return self.api_client.param_serialize( + method='GET', + resource_path='/principal-roles/{principalRoleName}/principals', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + + + + @validate_call + def list_catalog_roles( + self, + catalog_name: Annotated[StrictStr, Field(description="The catalog for which we are reading/updating roles")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> CatalogRoles: + """list_catalog_roles + + List existing roles in the catalog + + :param catalog_name: The catalog for which we are reading/updating roles (required) + :type catalog_name: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._list_catalog_roles_serialize( + catalog_name=catalog_name, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "CatalogRoles", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + def list_catalog_roles_with_http_info( + self, + catalog_name: Annotated[StrictStr, Field(description="The catalog for which we are reading/updating roles")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[CatalogRoles]: + """list_catalog_roles + + List existing roles in the catalog + + :param catalog_name: The catalog for which we are reading/updating roles (required) + :type catalog_name: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._list_catalog_roles_serialize( + catalog_name=catalog_name, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "CatalogRoles", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + def list_catalog_roles_without_preload_content( + self, + catalog_name: Annotated[StrictStr, Field(description="The catalog for which we are reading/updating roles")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """list_catalog_roles + + List existing roles in the catalog + + :param catalog_name: The catalog for which we are reading/updating roles (required) + :type catalog_name: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._list_catalog_roles_serialize( + catalog_name=catalog_name, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "CatalogRoles", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _list_catalog_roles_serialize( + self, + catalog_name, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[str, Union[str, bytes]] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + if catalog_name is not None: + _path_params['catalogName'] = catalog_name + # process the query parameters + # process the header parameters + # process the form parameters + # process the body parameter + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + + + # authentication setting + _auth_settings: List[str] = [ + 'OAuth2' + ] + + return self.api_client.param_serialize( + method='GET', + resource_path='/catalogs/{catalogName}/catalog-roles', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + + + + @validate_call + def list_catalog_roles_for_principal_role( + self, + principal_role_name: Annotated[StrictStr, Field(description="The principal role name")], + catalog_name: Annotated[StrictStr, Field(description="The name of the catalog where the catalogRoles reside")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> CatalogRoles: + """list_catalog_roles_for_principal_role + + Get the catalog roles mapped to the principal role + + :param principal_role_name: The principal role name (required) + :type principal_role_name: str + :param catalog_name: The name of the catalog where the catalogRoles reside (required) + :type catalog_name: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._list_catalog_roles_for_principal_role_serialize( + principal_role_name=principal_role_name, + catalog_name=catalog_name, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "CatalogRoles", + '403': None, + '404': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + def list_catalog_roles_for_principal_role_with_http_info( + self, + principal_role_name: Annotated[StrictStr, Field(description="The principal role name")], + catalog_name: Annotated[StrictStr, Field(description="The name of the catalog where the catalogRoles reside")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[CatalogRoles]: + """list_catalog_roles_for_principal_role + + Get the catalog roles mapped to the principal role + + :param principal_role_name: The principal role name (required) + :type principal_role_name: str + :param catalog_name: The name of the catalog where the catalogRoles reside (required) + :type catalog_name: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._list_catalog_roles_for_principal_role_serialize( + principal_role_name=principal_role_name, + catalog_name=catalog_name, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "CatalogRoles", + '403': None, + '404': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + def list_catalog_roles_for_principal_role_without_preload_content( + self, + principal_role_name: Annotated[StrictStr, Field(description="The principal role name")], + catalog_name: Annotated[StrictStr, Field(description="The name of the catalog where the catalogRoles reside")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """list_catalog_roles_for_principal_role + + Get the catalog roles mapped to the principal role + + :param principal_role_name: The principal role name (required) + :type principal_role_name: str + :param catalog_name: The name of the catalog where the catalogRoles reside (required) + :type catalog_name: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._list_catalog_roles_for_principal_role_serialize( + principal_role_name=principal_role_name, + catalog_name=catalog_name, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "CatalogRoles", + '403': None, + '404': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _list_catalog_roles_for_principal_role_serialize( + self, + principal_role_name, + catalog_name, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[str, Union[str, bytes]] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + if principal_role_name is not None: + _path_params['principalRoleName'] = principal_role_name + if catalog_name is not None: + _path_params['catalogName'] = catalog_name + # process the query parameters + # process the header parameters + # process the form parameters + # process the body parameter + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + + + # authentication setting + _auth_settings: List[str] = [ + 'OAuth2' + ] + + return self.api_client.param_serialize( + method='GET', + resource_path='/principal-roles/{principalRoleName}/catalog-roles/{catalogName}', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + + + + @validate_call + def list_catalogs( + self, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> Catalogs: + """list_catalogs + + List all catalogs in this polaris service + + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._list_catalogs_serialize( + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "Catalogs", + '403': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + def list_catalogs_with_http_info( + self, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[Catalogs]: + """list_catalogs + + List all catalogs in this polaris service + + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._list_catalogs_serialize( + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "Catalogs", + '403': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + def list_catalogs_without_preload_content( + self, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """list_catalogs + + List all catalogs in this polaris service + + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._list_catalogs_serialize( + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "Catalogs", + '403': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _list_catalogs_serialize( + self, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[str, Union[str, bytes]] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + # process the query parameters + # process the header parameters + # process the form parameters + # process the body parameter + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + + + # authentication setting + _auth_settings: List[str] = [ + 'OAuth2' + ] + + return self.api_client.param_serialize( + method='GET', + resource_path='/catalogs', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + + + + @validate_call + def list_grants_for_catalog_role( + self, + catalog_name: Annotated[StrictStr, Field(description="The name of the catalog where the role will receive the grant")], + catalog_role_name: Annotated[StrictStr, Field(description="The name of the role receiving the grant (must exist)")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> GrantResources: + """list_grants_for_catalog_role + + List the grants the catalog role holds + + :param catalog_name: The name of the catalog where the role will receive the grant (required) + :type catalog_name: str + :param catalog_role_name: The name of the role receiving the grant (must exist) (required) + :type catalog_role_name: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._list_grants_for_catalog_role_serialize( + catalog_name=catalog_name, + catalog_role_name=catalog_role_name, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "GrantResources", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + def list_grants_for_catalog_role_with_http_info( + self, + catalog_name: Annotated[StrictStr, Field(description="The name of the catalog where the role will receive the grant")], + catalog_role_name: Annotated[StrictStr, Field(description="The name of the role receiving the grant (must exist)")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[GrantResources]: + """list_grants_for_catalog_role + + List the grants the catalog role holds + + :param catalog_name: The name of the catalog where the role will receive the grant (required) + :type catalog_name: str + :param catalog_role_name: The name of the role receiving the grant (must exist) (required) + :type catalog_role_name: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._list_grants_for_catalog_role_serialize( + catalog_name=catalog_name, + catalog_role_name=catalog_role_name, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "GrantResources", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + def list_grants_for_catalog_role_without_preload_content( + self, + catalog_name: Annotated[StrictStr, Field(description="The name of the catalog where the role will receive the grant")], + catalog_role_name: Annotated[StrictStr, Field(description="The name of the role receiving the grant (must exist)")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """list_grants_for_catalog_role + + List the grants the catalog role holds + + :param catalog_name: The name of the catalog where the role will receive the grant (required) + :type catalog_name: str + :param catalog_role_name: The name of the role receiving the grant (must exist) (required) + :type catalog_role_name: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._list_grants_for_catalog_role_serialize( + catalog_name=catalog_name, + catalog_role_name=catalog_role_name, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "GrantResources", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _list_grants_for_catalog_role_serialize( + self, + catalog_name, + catalog_role_name, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[str, Union[str, bytes]] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + if catalog_name is not None: + _path_params['catalogName'] = catalog_name + if catalog_role_name is not None: + _path_params['catalogRoleName'] = catalog_role_name + # process the query parameters + # process the header parameters + # process the form parameters + # process the body parameter + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + + + # authentication setting + _auth_settings: List[str] = [ + 'OAuth2' + ] + + return self.api_client.param_serialize( + method='GET', + resource_path='/catalogs/{catalogName}/catalog-roles/{catalogRoleName}/grants', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + + + + @validate_call + def list_principal_roles( + self, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> PrincipalRoles: + """list_principal_roles + + List the principal roles + + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._list_principal_roles_serialize( + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "PrincipalRoles", + '403': None, + '404': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + def list_principal_roles_with_http_info( + self, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[PrincipalRoles]: + """list_principal_roles + + List the principal roles + + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._list_principal_roles_serialize( + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "PrincipalRoles", + '403': None, + '404': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + def list_principal_roles_without_preload_content( + self, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """list_principal_roles + + List the principal roles + + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._list_principal_roles_serialize( + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "PrincipalRoles", + '403': None, + '404': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _list_principal_roles_serialize( + self, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[str, Union[str, bytes]] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + # process the query parameters + # process the header parameters + # process the form parameters + # process the body parameter + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + + + # authentication setting + _auth_settings: List[str] = [ + 'OAuth2' + ] + + return self.api_client.param_serialize( + method='GET', + resource_path='/principal-roles', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + + + + @validate_call + def list_principal_roles_assigned( + self, + principal_name: Annotated[StrictStr, Field(description="The name of the target principal")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> PrincipalRoles: + """list_principal_roles_assigned + + List the roles assigned to the principal + + :param principal_name: The name of the target principal (required) + :type principal_name: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._list_principal_roles_assigned_serialize( + principal_name=principal_name, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "PrincipalRoles", + '403': None, + '404': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + def list_principal_roles_assigned_with_http_info( + self, + principal_name: Annotated[StrictStr, Field(description="The name of the target principal")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[PrincipalRoles]: + """list_principal_roles_assigned + + List the roles assigned to the principal + + :param principal_name: The name of the target principal (required) + :type principal_name: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._list_principal_roles_assigned_serialize( + principal_name=principal_name, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "PrincipalRoles", + '403': None, + '404': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + def list_principal_roles_assigned_without_preload_content( + self, + principal_name: Annotated[StrictStr, Field(description="The name of the target principal")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """list_principal_roles_assigned + + List the roles assigned to the principal + + :param principal_name: The name of the target principal (required) + :type principal_name: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._list_principal_roles_assigned_serialize( + principal_name=principal_name, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "PrincipalRoles", + '403': None, + '404': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _list_principal_roles_assigned_serialize( + self, + principal_name, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[str, Union[str, bytes]] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + if principal_name is not None: + _path_params['principalName'] = principal_name + # process the query parameters + # process the header parameters + # process the form parameters + # process the body parameter + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + + + # authentication setting + _auth_settings: List[str] = [ + 'OAuth2' + ] + + return self.api_client.param_serialize( + method='GET', + resource_path='/principals/{principalName}/principal-roles', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + + + + @validate_call + def list_principals( + self, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> Principals: + """list_principals + + List the principals for the current catalog + + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._list_principals_serialize( + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "Principals", + '403': None, + '404': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + def list_principals_with_http_info( + self, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[Principals]: + """list_principals + + List the principals for the current catalog + + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._list_principals_serialize( + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "Principals", + '403': None, + '404': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + def list_principals_without_preload_content( + self, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """list_principals + + List the principals for the current catalog + + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._list_principals_serialize( + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "Principals", + '403': None, + '404': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _list_principals_serialize( + self, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[str, Union[str, bytes]] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + # process the query parameters + # process the header parameters + # process the form parameters + # process the body parameter + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + + + # authentication setting + _auth_settings: List[str] = [ + 'OAuth2' + ] + + return self.api_client.param_serialize( + method='GET', + resource_path='/principals', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + + + + @validate_call + def revoke_catalog_role_from_principal_role( + self, + principal_role_name: Annotated[StrictStr, Field(description="The principal role name")], + catalog_name: Annotated[StrictStr, Field(description="The name of the catalog that contains the role to revoke")], + catalog_role_name: Annotated[StrictStr, Field(description="The name of the catalog role that should be revoked")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> None: + """revoke_catalog_role_from_principal_role + + Remove a catalog role from a principal role + + :param principal_role_name: The principal role name (required) + :type principal_role_name: str + :param catalog_name: The name of the catalog that contains the role to revoke (required) + :type catalog_name: str + :param catalog_role_name: The name of the catalog role that should be revoked (required) + :type catalog_role_name: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._revoke_catalog_role_from_principal_role_serialize( + principal_role_name=principal_role_name, + catalog_name=catalog_name, + catalog_role_name=catalog_role_name, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '204': None, + '403': None, + '404': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + def revoke_catalog_role_from_principal_role_with_http_info( + self, + principal_role_name: Annotated[StrictStr, Field(description="The principal role name")], + catalog_name: Annotated[StrictStr, Field(description="The name of the catalog that contains the role to revoke")], + catalog_role_name: Annotated[StrictStr, Field(description="The name of the catalog role that should be revoked")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[None]: + """revoke_catalog_role_from_principal_role + + Remove a catalog role from a principal role + + :param principal_role_name: The principal role name (required) + :type principal_role_name: str + :param catalog_name: The name of the catalog that contains the role to revoke (required) + :type catalog_name: str + :param catalog_role_name: The name of the catalog role that should be revoked (required) + :type catalog_role_name: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._revoke_catalog_role_from_principal_role_serialize( + principal_role_name=principal_role_name, + catalog_name=catalog_name, + catalog_role_name=catalog_role_name, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '204': None, + '403': None, + '404': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + def revoke_catalog_role_from_principal_role_without_preload_content( + self, + principal_role_name: Annotated[StrictStr, Field(description="The principal role name")], + catalog_name: Annotated[StrictStr, Field(description="The name of the catalog that contains the role to revoke")], + catalog_role_name: Annotated[StrictStr, Field(description="The name of the catalog role that should be revoked")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """revoke_catalog_role_from_principal_role + + Remove a catalog role from a principal role + + :param principal_role_name: The principal role name (required) + :type principal_role_name: str + :param catalog_name: The name of the catalog that contains the role to revoke (required) + :type catalog_name: str + :param catalog_role_name: The name of the catalog role that should be revoked (required) + :type catalog_role_name: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._revoke_catalog_role_from_principal_role_serialize( + principal_role_name=principal_role_name, + catalog_name=catalog_name, + catalog_role_name=catalog_role_name, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '204': None, + '403': None, + '404': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _revoke_catalog_role_from_principal_role_serialize( + self, + principal_role_name, + catalog_name, + catalog_role_name, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[str, Union[str, bytes]] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + if principal_role_name is not None: + _path_params['principalRoleName'] = principal_role_name + if catalog_name is not None: + _path_params['catalogName'] = catalog_name + if catalog_role_name is not None: + _path_params['catalogRoleName'] = catalog_role_name + # process the query parameters + # process the header parameters + # process the form parameters + # process the body parameter + + + + + # authentication setting + _auth_settings: List[str] = [ + 'OAuth2' + ] + + return self.api_client.param_serialize( + method='DELETE', + resource_path='/principal-roles/{principalRoleName}/catalog-roles/{catalogName}/{catalogRoleName}', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + + + + @validate_call + def revoke_grant_from_catalog_role( + self, + catalog_name: Annotated[StrictStr, Field(description="The name of the catalog where the role will receive the grant")], + catalog_role_name: Annotated[StrictStr, Field(description="The name of the role receiving the grant (must exist)")], + cascade: Annotated[Optional[StrictBool], Field(description="If true, the grant revocation cascades to all subresources.")] = None, + revoke_grant_request: Optional[RevokeGrantRequest] = None, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> None: + """revoke_grant_from_catalog_role + + Delete a specific grant from the role. This may be a subset or a superset of the grants the role has. In case of a subset, the role will retain the grants not specified. If the `cascade` parameter is true, grant revocation will have a cascading effect - that is, if a principal has specific grants on a subresource, and grants are revoked on a parent resource, the grants present on the subresource will be revoked as well. By default, this behavior is disabled and grant revocation only affects the specified resource. + + :param catalog_name: The name of the catalog where the role will receive the grant (required) + :type catalog_name: str + :param catalog_role_name: The name of the role receiving the grant (must exist) (required) + :type catalog_role_name: str + :param cascade: If true, the grant revocation cascades to all subresources. + :type cascade: bool + :param revoke_grant_request: + :type revoke_grant_request: RevokeGrantRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._revoke_grant_from_catalog_role_serialize( + catalog_name=catalog_name, + catalog_role_name=catalog_role_name, + cascade=cascade, + revoke_grant_request=revoke_grant_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '201': None, + '403': None, + '404': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + def revoke_grant_from_catalog_role_with_http_info( + self, + catalog_name: Annotated[StrictStr, Field(description="The name of the catalog where the role will receive the grant")], + catalog_role_name: Annotated[StrictStr, Field(description="The name of the role receiving the grant (must exist)")], + cascade: Annotated[Optional[StrictBool], Field(description="If true, the grant revocation cascades to all subresources.")] = None, + revoke_grant_request: Optional[RevokeGrantRequest] = None, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[None]: + """revoke_grant_from_catalog_role + + Delete a specific grant from the role. This may be a subset or a superset of the grants the role has. In case of a subset, the role will retain the grants not specified. If the `cascade` parameter is true, grant revocation will have a cascading effect - that is, if a principal has specific grants on a subresource, and grants are revoked on a parent resource, the grants present on the subresource will be revoked as well. By default, this behavior is disabled and grant revocation only affects the specified resource. + + :param catalog_name: The name of the catalog where the role will receive the grant (required) + :type catalog_name: str + :param catalog_role_name: The name of the role receiving the grant (must exist) (required) + :type catalog_role_name: str + :param cascade: If true, the grant revocation cascades to all subresources. + :type cascade: bool + :param revoke_grant_request: + :type revoke_grant_request: RevokeGrantRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._revoke_grant_from_catalog_role_serialize( + catalog_name=catalog_name, + catalog_role_name=catalog_role_name, + cascade=cascade, + revoke_grant_request=revoke_grant_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '201': None, + '403': None, + '404': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + def revoke_grant_from_catalog_role_without_preload_content( + self, + catalog_name: Annotated[StrictStr, Field(description="The name of the catalog where the role will receive the grant")], + catalog_role_name: Annotated[StrictStr, Field(description="The name of the role receiving the grant (must exist)")], + cascade: Annotated[Optional[StrictBool], Field(description="If true, the grant revocation cascades to all subresources.")] = None, + revoke_grant_request: Optional[RevokeGrantRequest] = None, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """revoke_grant_from_catalog_role + + Delete a specific grant from the role. This may be a subset or a superset of the grants the role has. In case of a subset, the role will retain the grants not specified. If the `cascade` parameter is true, grant revocation will have a cascading effect - that is, if a principal has specific grants on a subresource, and grants are revoked on a parent resource, the grants present on the subresource will be revoked as well. By default, this behavior is disabled and grant revocation only affects the specified resource. + + :param catalog_name: The name of the catalog where the role will receive the grant (required) + :type catalog_name: str + :param catalog_role_name: The name of the role receiving the grant (must exist) (required) + :type catalog_role_name: str + :param cascade: If true, the grant revocation cascades to all subresources. + :type cascade: bool + :param revoke_grant_request: + :type revoke_grant_request: RevokeGrantRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._revoke_grant_from_catalog_role_serialize( + catalog_name=catalog_name, + catalog_role_name=catalog_role_name, + cascade=cascade, + revoke_grant_request=revoke_grant_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '201': None, + '403': None, + '404': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _revoke_grant_from_catalog_role_serialize( + self, + catalog_name, + catalog_role_name, + cascade, + revoke_grant_request, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[str, Union[str, bytes]] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + if catalog_name is not None: + _path_params['catalogName'] = catalog_name + if catalog_role_name is not None: + _path_params['catalogRoleName'] = catalog_role_name + # process the query parameters + if cascade is not None: + + _query_params.append(('cascade', cascade)) + + # process the header parameters + # process the form parameters + # process the body parameter + if revoke_grant_request is not None: + _body_params = revoke_grant_request + + + + # set the HTTP header `Content-Type` + if _content_type: + _header_params['Content-Type'] = _content_type + else: + _default_content_type = ( + self.api_client.select_header_content_type( + [ + 'application/json' + ] + ) + ) + if _default_content_type is not None: + _header_params['Content-Type'] = _default_content_type + + # authentication setting + _auth_settings: List[str] = [ + 'OAuth2' + ] + + return self.api_client.param_serialize( + method='POST', + resource_path='/catalogs/{catalogName}/catalog-roles/{catalogRoleName}/grants', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + + + + @validate_call + def revoke_principal_role( + self, + principal_name: Annotated[StrictStr, Field(description="The name of the target principal")], + principal_role_name: Annotated[StrictStr, Field(description="The name of the role")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> None: + """revoke_principal_role + + Remove a role from a catalog principal + + :param principal_name: The name of the target principal (required) + :type principal_name: str + :param principal_role_name: The name of the role (required) + :type principal_role_name: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._revoke_principal_role_serialize( + principal_name=principal_name, + principal_role_name=principal_role_name, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '204': None, + '403': None, + '404': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + def revoke_principal_role_with_http_info( + self, + principal_name: Annotated[StrictStr, Field(description="The name of the target principal")], + principal_role_name: Annotated[StrictStr, Field(description="The name of the role")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[None]: + """revoke_principal_role + + Remove a role from a catalog principal + + :param principal_name: The name of the target principal (required) + :type principal_name: str + :param principal_role_name: The name of the role (required) + :type principal_role_name: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._revoke_principal_role_serialize( + principal_name=principal_name, + principal_role_name=principal_role_name, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '204': None, + '403': None, + '404': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + def revoke_principal_role_without_preload_content( + self, + principal_name: Annotated[StrictStr, Field(description="The name of the target principal")], + principal_role_name: Annotated[StrictStr, Field(description="The name of the role")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """revoke_principal_role + + Remove a role from a catalog principal + + :param principal_name: The name of the target principal (required) + :type principal_name: str + :param principal_role_name: The name of the role (required) + :type principal_role_name: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._revoke_principal_role_serialize( + principal_name=principal_name, + principal_role_name=principal_role_name, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '204': None, + '403': None, + '404': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _revoke_principal_role_serialize( + self, + principal_name, + principal_role_name, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[str, Union[str, bytes]] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + if principal_name is not None: + _path_params['principalName'] = principal_name + if principal_role_name is not None: + _path_params['principalRoleName'] = principal_role_name + # process the query parameters + # process the header parameters + # process the form parameters + # process the body parameter + + + + + # authentication setting + _auth_settings: List[str] = [ + 'OAuth2' + ] + + return self.api_client.param_serialize( + method='DELETE', + resource_path='/principals/{principalName}/principal-roles/{principalRoleName}', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + + + + @validate_call + def rotate_credentials( + self, + principal_name: Annotated[StrictStr, Field(description="The user name")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> PrincipalWithCredentials: + """rotate_credentials + + Rotate a principal's credentials. The new credentials will be returned in the response. This is the only API, aside from createPrincipal, that returns the user's credentials. This API is *not* idempotent. + + :param principal_name: The user name (required) + :type principal_name: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._rotate_credentials_serialize( + principal_name=principal_name, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "PrincipalWithCredentials", + '403': None, + '404': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + def rotate_credentials_with_http_info( + self, + principal_name: Annotated[StrictStr, Field(description="The user name")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[PrincipalWithCredentials]: + """rotate_credentials + + Rotate a principal's credentials. The new credentials will be returned in the response. This is the only API, aside from createPrincipal, that returns the user's credentials. This API is *not* idempotent. + + :param principal_name: The user name (required) + :type principal_name: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._rotate_credentials_serialize( + principal_name=principal_name, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "PrincipalWithCredentials", + '403': None, + '404': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + def rotate_credentials_without_preload_content( + self, + principal_name: Annotated[StrictStr, Field(description="The user name")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """rotate_credentials + + Rotate a principal's credentials. The new credentials will be returned in the response. This is the only API, aside from createPrincipal, that returns the user's credentials. This API is *not* idempotent. + + :param principal_name: The user name (required) + :type principal_name: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._rotate_credentials_serialize( + principal_name=principal_name, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "PrincipalWithCredentials", + '403': None, + '404': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _rotate_credentials_serialize( + self, + principal_name, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[str, Union[str, bytes]] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + if principal_name is not None: + _path_params['principalName'] = principal_name + # process the query parameters + # process the header parameters + # process the form parameters + # process the body parameter + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + + + # authentication setting + _auth_settings: List[str] = [ + 'OAuth2' + ] + + return self.api_client.param_serialize( + method='POST', + resource_path='/principals/{principalName}/rotate', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + + + + @validate_call + def update_catalog( + self, + catalog_name: Annotated[StrictStr, Field(description="The name of the catalog")], + update_catalog_request: Annotated[UpdateCatalogRequest, Field(description="The catalog details to use in the update")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> Catalog: + """update_catalog + + Update an existing catalog + + :param catalog_name: The name of the catalog (required) + :type catalog_name: str + :param update_catalog_request: The catalog details to use in the update (required) + :type update_catalog_request: UpdateCatalogRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._update_catalog_serialize( + catalog_name=catalog_name, + update_catalog_request=update_catalog_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "Catalog", + '403': None, + '404': None, + '409': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + def update_catalog_with_http_info( + self, + catalog_name: Annotated[StrictStr, Field(description="The name of the catalog")], + update_catalog_request: Annotated[UpdateCatalogRequest, Field(description="The catalog details to use in the update")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[Catalog]: + """update_catalog + + Update an existing catalog + + :param catalog_name: The name of the catalog (required) + :type catalog_name: str + :param update_catalog_request: The catalog details to use in the update (required) + :type update_catalog_request: UpdateCatalogRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._update_catalog_serialize( + catalog_name=catalog_name, + update_catalog_request=update_catalog_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "Catalog", + '403': None, + '404': None, + '409': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + def update_catalog_without_preload_content( + self, + catalog_name: Annotated[StrictStr, Field(description="The name of the catalog")], + update_catalog_request: Annotated[UpdateCatalogRequest, Field(description="The catalog details to use in the update")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """update_catalog + + Update an existing catalog + + :param catalog_name: The name of the catalog (required) + :type catalog_name: str + :param update_catalog_request: The catalog details to use in the update (required) + :type update_catalog_request: UpdateCatalogRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._update_catalog_serialize( + catalog_name=catalog_name, + update_catalog_request=update_catalog_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "Catalog", + '403': None, + '404': None, + '409': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _update_catalog_serialize( + self, + catalog_name, + update_catalog_request, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[str, Union[str, bytes]] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + if catalog_name is not None: + _path_params['catalogName'] = catalog_name + # process the query parameters + # process the header parameters + # process the form parameters + # process the body parameter + if update_catalog_request is not None: + _body_params = update_catalog_request + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + + # set the HTTP header `Content-Type` + if _content_type: + _header_params['Content-Type'] = _content_type + else: + _default_content_type = ( + self.api_client.select_header_content_type( + [ + 'application/json' + ] + ) + ) + if _default_content_type is not None: + _header_params['Content-Type'] = _default_content_type + + # authentication setting + _auth_settings: List[str] = [ + 'OAuth2' + ] + + return self.api_client.param_serialize( + method='PUT', + resource_path='/catalogs/{catalogName}', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + + + + @validate_call + def update_catalog_role( + self, + catalog_name: Annotated[StrictStr, Field(description="The catalog for which we are retrieving roles")], + catalog_role_name: Annotated[StrictStr, Field(description="The name of the role")], + update_catalog_role_request: Optional[UpdateCatalogRoleRequest] = None, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> CatalogRole: + """update_catalog_role + + Update an existing role in the catalog + + :param catalog_name: The catalog for which we are retrieving roles (required) + :type catalog_name: str + :param catalog_role_name: The name of the role (required) + :type catalog_role_name: str + :param update_catalog_role_request: + :type update_catalog_role_request: UpdateCatalogRoleRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._update_catalog_role_serialize( + catalog_name=catalog_name, + catalog_role_name=catalog_role_name, + update_catalog_role_request=update_catalog_role_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "CatalogRole", + '403': None, + '404': None, + '409': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + def update_catalog_role_with_http_info( + self, + catalog_name: Annotated[StrictStr, Field(description="The catalog for which we are retrieving roles")], + catalog_role_name: Annotated[StrictStr, Field(description="The name of the role")], + update_catalog_role_request: Optional[UpdateCatalogRoleRequest] = None, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[CatalogRole]: + """update_catalog_role + + Update an existing role in the catalog + + :param catalog_name: The catalog for which we are retrieving roles (required) + :type catalog_name: str + :param catalog_role_name: The name of the role (required) + :type catalog_role_name: str + :param update_catalog_role_request: + :type update_catalog_role_request: UpdateCatalogRoleRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._update_catalog_role_serialize( + catalog_name=catalog_name, + catalog_role_name=catalog_role_name, + update_catalog_role_request=update_catalog_role_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "CatalogRole", + '403': None, + '404': None, + '409': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + def update_catalog_role_without_preload_content( + self, + catalog_name: Annotated[StrictStr, Field(description="The catalog for which we are retrieving roles")], + catalog_role_name: Annotated[StrictStr, Field(description="The name of the role")], + update_catalog_role_request: Optional[UpdateCatalogRoleRequest] = None, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """update_catalog_role + + Update an existing role in the catalog + + :param catalog_name: The catalog for which we are retrieving roles (required) + :type catalog_name: str + :param catalog_role_name: The name of the role (required) + :type catalog_role_name: str + :param update_catalog_role_request: + :type update_catalog_role_request: UpdateCatalogRoleRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._update_catalog_role_serialize( + catalog_name=catalog_name, + catalog_role_name=catalog_role_name, + update_catalog_role_request=update_catalog_role_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "CatalogRole", + '403': None, + '404': None, + '409': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _update_catalog_role_serialize( + self, + catalog_name, + catalog_role_name, + update_catalog_role_request, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[str, Union[str, bytes]] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + if catalog_name is not None: + _path_params['catalogName'] = catalog_name + if catalog_role_name is not None: + _path_params['catalogRoleName'] = catalog_role_name + # process the query parameters + # process the header parameters + # process the form parameters + # process the body parameter + if update_catalog_role_request is not None: + _body_params = update_catalog_role_request + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + + # set the HTTP header `Content-Type` + if _content_type: + _header_params['Content-Type'] = _content_type + else: + _default_content_type = ( + self.api_client.select_header_content_type( + [ + 'application/json' + ] + ) + ) + if _default_content_type is not None: + _header_params['Content-Type'] = _default_content_type + + # authentication setting + _auth_settings: List[str] = [ + 'OAuth2' + ] + + return self.api_client.param_serialize( + method='PUT', + resource_path='/catalogs/{catalogName}/catalog-roles/{catalogRoleName}', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + + + + @validate_call + def update_principal( + self, + principal_name: Annotated[StrictStr, Field(description="The principal name")], + update_principal_request: Annotated[UpdatePrincipalRequest, Field(description="The principal details to use in the update")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> Principal: + """update_principal + + Update an existing principal + + :param principal_name: The principal name (required) + :type principal_name: str + :param update_principal_request: The principal details to use in the update (required) + :type update_principal_request: UpdatePrincipalRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._update_principal_serialize( + principal_name=principal_name, + update_principal_request=update_principal_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "Principal", + '403': None, + '404': None, + '409': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + def update_principal_with_http_info( + self, + principal_name: Annotated[StrictStr, Field(description="The principal name")], + update_principal_request: Annotated[UpdatePrincipalRequest, Field(description="The principal details to use in the update")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[Principal]: + """update_principal + + Update an existing principal + + :param principal_name: The principal name (required) + :type principal_name: str + :param update_principal_request: The principal details to use in the update (required) + :type update_principal_request: UpdatePrincipalRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._update_principal_serialize( + principal_name=principal_name, + update_principal_request=update_principal_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "Principal", + '403': None, + '404': None, + '409': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + def update_principal_without_preload_content( + self, + principal_name: Annotated[StrictStr, Field(description="The principal name")], + update_principal_request: Annotated[UpdatePrincipalRequest, Field(description="The principal details to use in the update")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """update_principal + + Update an existing principal + + :param principal_name: The principal name (required) + :type principal_name: str + :param update_principal_request: The principal details to use in the update (required) + :type update_principal_request: UpdatePrincipalRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._update_principal_serialize( + principal_name=principal_name, + update_principal_request=update_principal_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "Principal", + '403': None, + '404': None, + '409': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _update_principal_serialize( + self, + principal_name, + update_principal_request, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[str, Union[str, bytes]] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + if principal_name is not None: + _path_params['principalName'] = principal_name + # process the query parameters + # process the header parameters + # process the form parameters + # process the body parameter + if update_principal_request is not None: + _body_params = update_principal_request + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + + # set the HTTP header `Content-Type` + if _content_type: + _header_params['Content-Type'] = _content_type + else: + _default_content_type = ( + self.api_client.select_header_content_type( + [ + 'application/json' + ] + ) + ) + if _default_content_type is not None: + _header_params['Content-Type'] = _default_content_type + + # authentication setting + _auth_settings: List[str] = [ + 'OAuth2' + ] + + return self.api_client.param_serialize( + method='PUT', + resource_path='/principals/{principalName}', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + + + + @validate_call + def update_principal_role( + self, + principal_role_name: Annotated[StrictStr, Field(description="The principal role name")], + update_principal_role_request: Annotated[UpdatePrincipalRoleRequest, Field(description="The principalRole details to use in the update")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> PrincipalRole: + """update_principal_role + + Update an existing principalRole + + :param principal_role_name: The principal role name (required) + :type principal_role_name: str + :param update_principal_role_request: The principalRole details to use in the update (required) + :type update_principal_role_request: UpdatePrincipalRoleRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._update_principal_role_serialize( + principal_role_name=principal_role_name, + update_principal_role_request=update_principal_role_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "PrincipalRole", + '403': None, + '404': None, + '409': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + def update_principal_role_with_http_info( + self, + principal_role_name: Annotated[StrictStr, Field(description="The principal role name")], + update_principal_role_request: Annotated[UpdatePrincipalRoleRequest, Field(description="The principalRole details to use in the update")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[PrincipalRole]: + """update_principal_role + + Update an existing principalRole + + :param principal_role_name: The principal role name (required) + :type principal_role_name: str + :param update_principal_role_request: The principalRole details to use in the update (required) + :type update_principal_role_request: UpdatePrincipalRoleRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._update_principal_role_serialize( + principal_role_name=principal_role_name, + update_principal_role_request=update_principal_role_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "PrincipalRole", + '403': None, + '404': None, + '409': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + def update_principal_role_without_preload_content( + self, + principal_role_name: Annotated[StrictStr, Field(description="The principal role name")], + update_principal_role_request: Annotated[UpdatePrincipalRoleRequest, Field(description="The principalRole details to use in the update")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """update_principal_role + + Update an existing principalRole + + :param principal_role_name: The principal role name (required) + :type principal_role_name: str + :param update_principal_role_request: The principalRole details to use in the update (required) + :type update_principal_role_request: UpdatePrincipalRoleRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._update_principal_role_serialize( + principal_role_name=principal_role_name, + update_principal_role_request=update_principal_role_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "PrincipalRole", + '403': None, + '404': None, + '409': None, + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _update_principal_role_serialize( + self, + principal_role_name, + update_principal_role_request, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[str, Union[str, bytes]] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + if principal_role_name is not None: + _path_params['principalRoleName'] = principal_role_name + # process the query parameters + # process the header parameters + # process the form parameters + # process the body parameter + if update_principal_role_request is not None: + _body_params = update_principal_role_request + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + + # set the HTTP header `Content-Type` + if _content_type: + _header_params['Content-Type'] = _content_type + else: + _default_content_type = ( + self.api_client.select_header_content_type( + [ + 'application/json' + ] + ) + ) + if _default_content_type is not None: + _header_params['Content-Type'] = _default_content_type + + # authentication setting + _auth_settings: List[str] = [ + 'OAuth2' + ] + + return self.api_client.param_serialize( + method='PUT', + resource_path='/principal-roles/{principalRoleName}', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + diff --git a/regtests/client/python/polaris/management/api_client.py b/regtests/client/python/polaris/management/api_client.py new file mode 100644 index 0000000000..d558f9c8dc --- /dev/null +++ b/regtests/client/python/polaris/management/api_client.py @@ -0,0 +1,788 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import datetime +from dateutil.parser import parse +from enum import Enum +import decimal +import json +import mimetypes +import os +import re +import tempfile + +from urllib.parse import quote +from typing import Tuple, Optional, List, Dict, Union +from pydantic import SecretStr + +from polaris.management.configuration import Configuration +from polaris.management.api_response import ApiResponse, T as ApiResponseT +import polaris.management.models +from polaris.management import rest +from polaris.management.exceptions import ( + ApiValueError, + ApiException, + BadRequestException, + UnauthorizedException, + ForbiddenException, + NotFoundException, + ServiceException +) + +RequestSerialized = Tuple[str, str, Dict[str, str], Optional[str], List[str]] + +class ApiClient: + """Generic API client for OpenAPI client library builds. + + OpenAPI generic API client. This client handles the client- + server communication, and is invariant across implementations. Specifics of + the methods and models for each application are generated from the OpenAPI + templates. + + :param configuration: .Configuration object for this client + :param header_name: a header to pass when making calls to the API. + :param header_value: a header value to pass when making calls to + the API. + :param cookie: a cookie to include in the header when making calls + to the API + """ + + PRIMITIVE_TYPES = (float, bool, bytes, str, int) + NATIVE_TYPES_MAPPING = { + 'int': int, + 'long': int, # TODO remove as only py3 is supported? + 'float': float, + 'str': str, + 'bool': bool, + 'date': datetime.date, + 'datetime': datetime.datetime, + 'decimal': decimal.Decimal, + 'object': object, + } + _pool = None + + def __init__( + self, + configuration=None, + header_name=None, + header_value=None, + cookie=None + ) -> None: + # use default configuration if none is provided + if configuration is None: + configuration = Configuration.get_default() + self.configuration = configuration + + self.rest_client = rest.RESTClientObject(configuration) + self.default_headers = {} + if header_name is not None: + self.default_headers[header_name] = header_value + self.cookie = cookie + # Set default User-Agent. + self.user_agent = 'OpenAPI-Generator/1.0.0/python' + self.client_side_validation = configuration.client_side_validation + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + pass + + @property + def user_agent(self): + """User agent for this API client""" + return self.default_headers['User-Agent'] + + @user_agent.setter + def user_agent(self, value): + self.default_headers['User-Agent'] = value + + def set_default_header(self, header_name, header_value): + self.default_headers[header_name] = header_value + + + _default = None + + @classmethod + def get_default(cls): + """Return new instance of ApiClient. + + This method returns newly created, based on default constructor, + object of ApiClient class or returns a copy of default + ApiClient. + + :return: The ApiClient object. + """ + if cls._default is None: + cls._default = ApiClient() + return cls._default + + @classmethod + def set_default(cls, default): + """Set default instance of ApiClient. + + It stores default ApiClient. + + :param default: object of ApiClient. + """ + cls._default = default + + def param_serialize( + self, + method, + resource_path, + path_params=None, + query_params=None, + header_params=None, + body=None, + post_params=None, + files=None, auth_settings=None, + collection_formats=None, + _host=None, + _request_auth=None + ) -> RequestSerialized: + + """Builds the HTTP request params needed by the request. + :param method: Method to call. + :param resource_path: Path to method endpoint. + :param path_params: Path parameters in the url. + :param query_params: Query parameters in the url. + :param header_params: Header parameters to be + placed in the request header. + :param body: Request body. + :param post_params dict: Request post form parameters, + for `application/x-www-form-urlencoded`, `multipart/form-data`. + :param auth_settings list: Auth Settings names for the request. + :param files dict: key -> filename, value -> filepath, + for `multipart/form-data`. + :param collection_formats: dict of collection formats for path, query, + header, and post parameters. + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the authentication + in the spec for a single request. + :return: tuple of form (path, http_method, query_params, header_params, + body, post_params, files) + """ + + config = self.configuration + + # header parameters + header_params = header_params or {} + header_params.update(self.default_headers) + if self.cookie: + header_params['Cookie'] = self.cookie + if header_params: + header_params = self.sanitize_for_serialization(header_params) + header_params = dict( + self.parameters_to_tuples(header_params,collection_formats) + ) + + # path parameters + if path_params: + path_params = self.sanitize_for_serialization(path_params) + path_params = self.parameters_to_tuples( + path_params, + collection_formats + ) + for k, v in path_params: + # specified safe chars, encode everything + resource_path = resource_path.replace( + '{%s}' % k, + quote(str(v), safe=config.safe_chars_for_path_param) + ) + + # post parameters + if post_params or files: + post_params = post_params if post_params else [] + post_params = self.sanitize_for_serialization(post_params) + post_params = self.parameters_to_tuples( + post_params, + collection_formats + ) + if files: + post_params.extend(self.files_parameters(files)) + + # auth setting + self.update_params_for_auth( + header_params, + query_params, + auth_settings, + resource_path, + method, + body, + request_auth=_request_auth + ) + + # body + if body: + body = self.sanitize_for_serialization(body) + + # request url + if _host is None or self.configuration.ignore_operation_servers: + url = self.configuration.host + resource_path + else: + # use server/host defined in path or operation instead + url = _host + resource_path + + # query parameters + if query_params: + query_params = self.sanitize_for_serialization(query_params) + url_query = self.parameters_to_url_query( + query_params, + collection_formats + ) + url += "?" + url_query + + return method, url, header_params, body, post_params + + + def call_api( + self, + method, + url, + header_params=None, + body=None, + post_params=None, + _request_timeout=None + ) -> rest.RESTResponse: + """Makes the HTTP request (synchronous) + :param method: Method to call. + :param url: Path to method endpoint. + :param header_params: Header parameters to be + placed in the request header. + :param body: Request body. + :param post_params dict: Request post form parameters, + for `application/x-www-form-urlencoded`, `multipart/form-data`. + :param _request_timeout: timeout setting for this request. + :return: RESTResponse + """ + + try: + # perform request and return response + response_data = self.rest_client.request( + method, url, + headers=header_params, + body=body, post_params=post_params, + _request_timeout=_request_timeout + ) + + except ApiException as e: + raise e + + return response_data + + def response_deserialize( + self, + response_data: rest.RESTResponse, + response_types_map: Optional[Dict[str, ApiResponseT]]=None + ) -> ApiResponse[ApiResponseT]: + """Deserializes response into an object. + :param response_data: RESTResponse object to be deserialized. + :param response_types_map: dict of response types. + :return: ApiResponse + """ + + msg = "RESTResponse.read() must be called before passing it to response_deserialize()" + assert response_data.data is not None, msg + + response_type = response_types_map.get(str(response_data.status), None) + if not response_type and isinstance(response_data.status, int) and 100 <= response_data.status <= 599: + # if not found, look for '1XX', '2XX', etc. + response_type = response_types_map.get(str(response_data.status)[0] + "XX", None) + + # deserialize response data + response_text = None + return_data = None + try: + if response_type == "bytearray": + return_data = response_data.data + elif response_type == "file": + return_data = self.__deserialize_file(response_data) + elif response_type is not None: + match = None + content_type = response_data.getheader('content-type') + if content_type is not None: + match = re.search(r"charset=([a-zA-Z\-\d]+)[\s;]?", content_type) + encoding = match.group(1) if match else "utf-8" + response_text = response_data.data.decode(encoding) + return_data = self.deserialize(response_text, response_type, content_type) + finally: + if not 200 <= response_data.status <= 299: + raise ApiException.from_response( + http_resp=response_data, + body=response_text, + data=return_data, + ) + + return ApiResponse( + status_code = response_data.status, + data = return_data, + headers = response_data.getheaders(), + raw_data = response_data.data + ) + + def sanitize_for_serialization(self, obj): + """Builds a JSON POST object. + + If obj is None, return None. + If obj is SecretStr, return obj.get_secret_value() + If obj is str, int, long, float, bool, return directly. + If obj is datetime.datetime, datetime.date + convert to string in iso8601 format. + If obj is decimal.Decimal return string representation. + If obj is list, sanitize each element in the list. + If obj is dict, return the dict. + If obj is OpenAPI model, return the properties dict. + + :param obj: The data to serialize. + :return: The serialized form of data. + """ + if obj is None: + return None + elif isinstance(obj, Enum): + return obj.value + elif isinstance(obj, SecretStr): + return obj.get_secret_value() + elif isinstance(obj, self.PRIMITIVE_TYPES): + return obj + elif isinstance(obj, list): + return [ + self.sanitize_for_serialization(sub_obj) for sub_obj in obj + ] + elif isinstance(obj, tuple): + return tuple( + self.sanitize_for_serialization(sub_obj) for sub_obj in obj + ) + elif isinstance(obj, (datetime.datetime, datetime.date)): + return obj.isoformat() + elif isinstance(obj, decimal.Decimal): + return str(obj) + + elif isinstance(obj, dict): + obj_dict = obj + else: + # Convert model obj to dict except + # attributes `openapi_types`, `attribute_map` + # and attributes which value is not None. + # Convert attribute name to json key in + # model definition for request. + if hasattr(obj, 'to_dict') and callable(getattr(obj, 'to_dict')): + obj_dict = obj.to_dict() + else: + obj_dict = obj.__dict__ + + return { + key: self.sanitize_for_serialization(val) + for key, val in obj_dict.items() + } + + def deserialize(self, response_text: str, response_type: str, content_type: Optional[str]): + """Deserializes response into an object. + + :param response: RESTResponse object to be deserialized. + :param response_type: class literal for + deserialized object, or string of class name. + :param content_type: content type of response. + + :return: deserialized object. + """ + + # fetch data from response object + if content_type is None: + try: + data = json.loads(response_text) + except ValueError: + data = response_text + elif content_type.startswith("application/json"): + if response_text == "": + data = "" + else: + data = json.loads(response_text) + elif content_type.startswith("text/plain"): + data = response_text + else: + raise ApiException( + status=0, + reason="Unsupported content type: {0}".format(content_type) + ) + + return self.__deserialize(data, response_type) + + def __deserialize(self, data, klass): + """Deserializes dict, list, str into an object. + + :param data: dict, list or str. + :param klass: class literal, or string of class name. + + :return: object. + """ + if data is None: + return None + + if isinstance(klass, str): + if klass.startswith('List['): + m = re.match(r'List\[(.*)]', klass) + assert m is not None, "Malformed List type definition" + sub_kls = m.group(1) + return [self.__deserialize(sub_data, sub_kls) + for sub_data in data] + + if klass.startswith('Dict['): + m = re.match(r'Dict\[([^,]*), (.*)]', klass) + assert m is not None, "Malformed Dict type definition" + sub_kls = m.group(2) + return {k: self.__deserialize(v, sub_kls) + for k, v in data.items()} + + # convert str to class + if klass in self.NATIVE_TYPES_MAPPING: + klass = self.NATIVE_TYPES_MAPPING[klass] + else: + klass = getattr(polaris.management.models, klass) + + if klass in self.PRIMITIVE_TYPES: + return self.__deserialize_primitive(data, klass) + elif klass == object: + return self.__deserialize_object(data) + elif klass == datetime.date: + return self.__deserialize_date(data) + elif klass == datetime.datetime: + return self.__deserialize_datetime(data) + elif klass == decimal.Decimal: + return decimal.Decimal(data) + elif issubclass(klass, Enum): + return self.__deserialize_enum(data, klass) + else: + return self.__deserialize_model(data, klass) + + def parameters_to_tuples(self, params, collection_formats): + """Get parameters as list of tuples, formatting collections. + + :param params: Parameters as dict or list of two-tuples + :param dict collection_formats: Parameter collection formats + :return: Parameters as list of tuples, collections formatted + """ + new_params: List[Tuple[str, str]] = [] + if collection_formats is None: + collection_formats = {} + for k, v in params.items() if isinstance(params, dict) else params: + if k in collection_formats: + collection_format = collection_formats[k] + if collection_format == 'multi': + new_params.extend((k, value) for value in v) + else: + if collection_format == 'ssv': + delimiter = ' ' + elif collection_format == 'tsv': + delimiter = '\t' + elif collection_format == 'pipes': + delimiter = '|' + else: # csv is the default + delimiter = ',' + new_params.append( + (k, delimiter.join(str(value) for value in v))) + else: + new_params.append((k, v)) + return new_params + + def parameters_to_url_query(self, params, collection_formats): + """Get parameters as list of tuples, formatting collections. + + :param params: Parameters as dict or list of two-tuples + :param dict collection_formats: Parameter collection formats + :return: URL query string (e.g. a=Hello%20World&b=123) + """ + new_params: List[Tuple[str, str]] = [] + if collection_formats is None: + collection_formats = {} + for k, v in params.items() if isinstance(params, dict) else params: + if isinstance(v, bool): + v = str(v).lower() + if isinstance(v, (int, float)): + v = str(v) + if isinstance(v, dict): + v = json.dumps(v) + + if k in collection_formats: + collection_format = collection_formats[k] + if collection_format == 'multi': + new_params.extend((k, str(value)) for value in v) + else: + if collection_format == 'ssv': + delimiter = ' ' + elif collection_format == 'tsv': + delimiter = '\t' + elif collection_format == 'pipes': + delimiter = '|' + else: # csv is the default + delimiter = ',' + new_params.append( + (k, delimiter.join(quote(str(value)) for value in v)) + ) + else: + new_params.append((k, quote(str(v)))) + + return "&".join(["=".join(map(str, item)) for item in new_params]) + + def files_parameters(self, files: Dict[str, Union[str, bytes]]): + """Builds form parameters. + + :param files: File parameters. + :return: Form parameters with files. + """ + params = [] + for k, v in files.items(): + if isinstance(v, str): + with open(v, 'rb') as f: + filename = os.path.basename(f.name) + filedata = f.read() + elif isinstance(v, bytes): + filename = k + filedata = v + else: + raise ValueError("Unsupported file value") + mimetype = ( + mimetypes.guess_type(filename)[0] + or 'application/octet-stream' + ) + params.append( + tuple([k, tuple([filename, filedata, mimetype])]) + ) + return params + + def select_header_accept(self, accepts: List[str]) -> Optional[str]: + """Returns `Accept` based on an array of accepts provided. + + :param accepts: List of headers. + :return: Accept (e.g. application/json). + """ + if not accepts: + return None + + for accept in accepts: + if re.search('json', accept, re.IGNORECASE): + return accept + + return accepts[0] + + def select_header_content_type(self, content_types): + """Returns `Content-Type` based on an array of content_types provided. + + :param content_types: List of content-types. + :return: Content-Type (e.g. application/json). + """ + if not content_types: + return None + + for content_type in content_types: + if re.search('json', content_type, re.IGNORECASE): + return content_type + + return content_types[0] + + def update_params_for_auth( + self, + headers, + queries, + auth_settings, + resource_path, + method, + body, + request_auth=None + ) -> None: + """Updates header and query params based on authentication setting. + + :param headers: Header parameters dict to be updated. + :param queries: Query parameters tuple list to be updated. + :param auth_settings: Authentication setting identifiers list. + :resource_path: A string representation of the HTTP request resource path. + :method: A string representation of the HTTP request method. + :body: A object representing the body of the HTTP request. + The object type is the return value of sanitize_for_serialization(). + :param request_auth: if set, the provided settings will + override the token in the configuration. + """ + if not auth_settings: + return + + if request_auth: + self._apply_auth_params( + headers, + queries, + resource_path, + method, + body, + request_auth + ) + else: + for auth in auth_settings: + auth_setting = self.configuration.auth_settings().get(auth) + if auth_setting: + self._apply_auth_params( + headers, + queries, + resource_path, + method, + body, + auth_setting + ) + + def _apply_auth_params( + self, + headers, + queries, + resource_path, + method, + body, + auth_setting + ) -> None: + """Updates the request parameters based on a single auth_setting + + :param headers: Header parameters dict to be updated. + :param queries: Query parameters tuple list to be updated. + :resource_path: A string representation of the HTTP request resource path. + :method: A string representation of the HTTP request method. + :body: A object representing the body of the HTTP request. + The object type is the return value of sanitize_for_serialization(). + :param auth_setting: auth settings for the endpoint + """ + if auth_setting['in'] == 'cookie': + headers['Cookie'] = auth_setting['value'] + elif auth_setting['in'] == 'header': + if auth_setting['type'] != 'http-signature': + headers[auth_setting['key']] = auth_setting['value'] + elif auth_setting['in'] == 'query': + queries.append((auth_setting['key'], auth_setting['value'])) + else: + raise ApiValueError( + 'Authentication token must be in `query` or `header`' + ) + + def __deserialize_file(self, response): + """Deserializes body to file + + Saves response body into a file in a temporary folder, + using the filename from the `Content-Disposition` header if provided. + + handle file downloading + save response body into a tmp file and return the instance + + :param response: RESTResponse. + :return: file path. + """ + fd, path = tempfile.mkstemp(dir=self.configuration.temp_folder_path) + os.close(fd) + os.remove(path) + + content_disposition = response.getheader("Content-Disposition") + if content_disposition: + m = re.search( + r'filename=[\'"]?([^\'"\s]+)[\'"]?', + content_disposition + ) + assert m is not None, "Unexpected 'content-disposition' header value" + filename = m.group(1) + path = os.path.join(os.path.dirname(path), filename) + + with open(path, "wb") as f: + f.write(response.data) + + return path + + def __deserialize_primitive(self, data, klass): + """Deserializes string to primitive type. + + :param data: str. + :param klass: class literal. + + :return: int, long, float, str, bool. + """ + try: + return klass(data) + except UnicodeEncodeError: + return str(data) + except TypeError: + return data + + def __deserialize_object(self, value): + """Return an original value. + + :return: object. + """ + return value + + def __deserialize_date(self, string): + """Deserializes string to date. + + :param string: str. + :return: date. + """ + try: + return parse(string).date() + except ImportError: + return string + except ValueError: + raise rest.ApiException( + status=0, + reason="Failed to parse `{0}` as date object".format(string) + ) + + def __deserialize_datetime(self, string): + """Deserializes string to datetime. + + The string should be in iso8601 datetime format. + + :param string: str. + :return: datetime. + """ + try: + return parse(string) + except ImportError: + return string + except ValueError: + raise rest.ApiException( + status=0, + reason=( + "Failed to parse `{0}` as datetime object" + .format(string) + ) + ) + + def __deserialize_enum(self, data, klass): + """Deserializes primitive type to enum. + + :param data: primitive type. + :param klass: class literal. + :return: enum value. + """ + try: + return klass(data) + except ValueError: + raise rest.ApiException( + status=0, + reason=( + "Failed to parse `{0}` as `{1}`" + .format(data, klass) + ) + ) + + def __deserialize_model(self, data, klass): + """Deserializes list or dict to model. + + :param data: dict, list. + :param klass: class literal. + :return: model object. + """ + + return klass.from_dict(data) diff --git a/regtests/client/python/polaris/management/api_response.py b/regtests/client/python/polaris/management/api_response.py new file mode 100644 index 0000000000..9bc7c11f6b --- /dev/null +++ b/regtests/client/python/polaris/management/api_response.py @@ -0,0 +1,21 @@ +"""API response object.""" + +from __future__ import annotations +from typing import Optional, Generic, Mapping, TypeVar +from pydantic import Field, StrictInt, StrictBytes, BaseModel + +T = TypeVar("T") + +class ApiResponse(BaseModel, Generic[T]): + """ + API response object + """ + + status_code: StrictInt = Field(description="HTTP status code") + headers: Optional[Mapping[str, str]] = Field(None, description="HTTP headers") + data: T = Field(description="Deserialized data given the data type") + raw_data: StrictBytes = Field(description="Raw data (HTTP response body)") + + model_config = { + "arbitrary_types_allowed": True + } diff --git a/regtests/client/python/polaris/management/configuration.py b/regtests/client/python/polaris/management/configuration.py new file mode 100644 index 0000000000..a2c20d54ce --- /dev/null +++ b/regtests/client/python/polaris/management/configuration.py @@ -0,0 +1,468 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import copy +import logging +from logging import FileHandler +import multiprocessing +import sys +from typing import Optional +import urllib3 + +import http.client as httplib + +JSON_SCHEMA_VALIDATION_KEYWORDS = { + 'multipleOf', 'maximum', 'exclusiveMaximum', + 'minimum', 'exclusiveMinimum', 'maxLength', + 'minLength', 'pattern', 'maxItems', 'minItems' +} + +class Configuration: + """This class contains various settings of the API client. + + :param host: Base url. + :param ignore_operation_servers + Boolean to ignore operation servers for the API client. + Config will use `host` as the base url regardless of the operation servers. + :param api_key: Dict to store API key(s). + Each entry in the dict specifies an API key. + The dict key is the name of the security scheme in the OAS specification. + The dict value is the API key secret. + :param api_key_prefix: Dict to store API prefix (e.g. Bearer). + The dict key is the name of the security scheme in the OAS specification. + The dict value is an API key prefix when generating the auth data. + :param username: Username for HTTP basic authentication. + :param password: Password for HTTP basic authentication. + :param access_token: Access token. + :param server_index: Index to servers configuration. + :param server_variables: Mapping with string values to replace variables in + templated server configuration. The validation of enums is performed for + variables with defined enum values before. + :param server_operation_index: Mapping from operation ID to an index to server + configuration. + :param server_operation_variables: Mapping from operation ID to a mapping with + string values to replace variables in templated server configuration. + The validation of enums is performed for variables with defined enum + values before. + :param ssl_ca_cert: str - the path to a file of concatenated CA certificates + in PEM format. + :param retries: Number of retries for API requests. + + :Example: + """ + + _default = None + + def __init__(self, host=None, + api_key=None, api_key_prefix=None, + username=None, password=None, + access_token=None, + server_index=None, server_variables=None, + server_operation_index=None, server_operation_variables=None, + ignore_operation_servers=False, + ssl_ca_cert=None, + retries=None, + *, + debug: Optional[bool] = None + ) -> None: + """Constructor + """ + self._base_path = "https://localhost/api/management/v1" if host is None else host + """Default Base url + """ + self.server_index = 0 if server_index is None and host is None else server_index + self.server_operation_index = server_operation_index or {} + """Default server index + """ + self.server_variables = server_variables or {} + self.server_operation_variables = server_operation_variables or {} + """Default server variables + """ + self.ignore_operation_servers = ignore_operation_servers + """Ignore operation servers + """ + self.temp_folder_path = None + """Temp file folder for downloading files + """ + # Authentication Settings + self.api_key = {} + if api_key: + self.api_key = api_key + """dict to store API key(s) + """ + self.api_key_prefix = {} + if api_key_prefix: + self.api_key_prefix = api_key_prefix + """dict to store API prefix (e.g. Bearer) + """ + self.refresh_api_key_hook = None + """function hook to refresh API key if expired + """ + self.username = username + """Username for HTTP basic authentication + """ + self.password = password + """Password for HTTP basic authentication + """ + self.access_token = access_token + """Access token + """ + self.logger = {} + """Logging Settings + """ + self.logger["package_logger"] = logging.getLogger("polaris.management") + self.logger["urllib3_logger"] = logging.getLogger("urllib3") + self.logger_format = '%(asctime)s %(levelname)s %(message)s' + """Log format + """ + self.logger_stream_handler = None + """Log stream handler + """ + self.logger_file_handler: Optional[FileHandler] = None + """Log file handler + """ + self.logger_file = None + """Debug file location + """ + if debug is not None: + self.debug = debug + else: + self.__debug = False + """Debug switch + """ + + self.verify_ssl = True + """SSL/TLS verification + Set this to false to skip verifying SSL certificate when calling API + from https server. + """ + self.ssl_ca_cert = ssl_ca_cert + """Set this to customize the certificate file to verify the peer. + """ + self.cert_file = None + """client certificate file + """ + self.key_file = None + """client key file + """ + self.assert_hostname = None + """Set this to True/False to enable/disable SSL hostname verification. + """ + self.tls_server_name = None + """SSL/TLS Server Name Indication (SNI) + Set this to the SNI value expected by the server. + """ + + self.connection_pool_maxsize = multiprocessing.cpu_count() * 5 + """urllib3 connection pool's maximum number of connections saved + per pool. urllib3 uses 1 connection as default value, but this is + not the best value when you are making a lot of possibly parallel + requests to the same host, which is often the case here. + cpu_count * 5 is used as default value to increase performance. + """ + + self.proxy: Optional[str] = None + """Proxy URL + """ + self.proxy_headers = None + """Proxy headers + """ + self.safe_chars_for_path_param = '' + """Safe chars for path_param + """ + self.retries = retries + """Adding retries to override urllib3 default value 3 + """ + # Enable client side validation + self.client_side_validation = True + + self.socket_options = None + """Options to pass down to the underlying urllib3 socket + """ + + self.datetime_format = "%Y-%m-%dT%H:%M:%S.%f%z" + """datetime format + """ + + self.date_format = "%Y-%m-%d" + """date format + """ + + def __deepcopy__(self, memo): + cls = self.__class__ + result = cls.__new__(cls) + memo[id(self)] = result + for k, v in self.__dict__.items(): + if k not in ('logger', 'logger_file_handler'): + setattr(result, k, copy.deepcopy(v, memo)) + # shallow copy of loggers + result.logger = copy.copy(self.logger) + # use setters to configure loggers + result.logger_file = self.logger_file + result.debug = self.debug + return result + + def __setattr__(self, name, value): + object.__setattr__(self, name, value) + + @classmethod + def set_default(cls, default): + """Set default instance of configuration. + + It stores default configuration, which can be + returned by get_default_copy method. + + :param default: object of Configuration + """ + cls._default = default + + @classmethod + def get_default_copy(cls): + """Deprecated. Please use `get_default` instead. + + Deprecated. Please use `get_default` instead. + + :return: The configuration object. + """ + return cls.get_default() + + @classmethod + def get_default(cls): + """Return the default configuration. + + This method returns newly created, based on default constructor, + object of Configuration class or returns a copy of default + configuration. + + :return: The configuration object. + """ + if cls._default is None: + cls._default = Configuration() + return cls._default + + @property + def logger_file(self): + """The logger file. + + If the logger_file is None, then add stream handler and remove file + handler. Otherwise, add file handler and remove stream handler. + + :param value: The logger_file path. + :type: str + """ + return self.__logger_file + + @logger_file.setter + def logger_file(self, value): + """The logger file. + + If the logger_file is None, then add stream handler and remove file + handler. Otherwise, add file handler and remove stream handler. + + :param value: The logger_file path. + :type: str + """ + self.__logger_file = value + if self.__logger_file: + # If set logging file, + # then add file handler and remove stream handler. + self.logger_file_handler = logging.FileHandler(self.__logger_file) + self.logger_file_handler.setFormatter(self.logger_formatter) + for _, logger in self.logger.items(): + logger.addHandler(self.logger_file_handler) + + @property + def debug(self): + """Debug status + + :param value: The debug status, True or False. + :type: bool + """ + return self.__debug + + @debug.setter + def debug(self, value): + """Debug status + + :param value: The debug status, True or False. + :type: bool + """ + self.__debug = value + if self.__debug: + # if debug status is True, turn on debug logging + for _, logger in self.logger.items(): + logger.setLevel(logging.DEBUG) + # turn on httplib debug + httplib.HTTPConnection.debuglevel = 1 + else: + # if debug status is False, turn off debug logging, + # setting log level to default `logging.WARNING` + for _, logger in self.logger.items(): + logger.setLevel(logging.WARNING) + # turn off httplib debug + httplib.HTTPConnection.debuglevel = 0 + + @property + def logger_format(self): + """The logger format. + + The logger_formatter will be updated when sets logger_format. + + :param value: The format string. + :type: str + """ + return self.__logger_format + + @logger_format.setter + def logger_format(self, value): + """The logger format. + + The logger_formatter will be updated when sets logger_format. + + :param value: The format string. + :type: str + """ + self.__logger_format = value + self.logger_formatter = logging.Formatter(self.__logger_format) + + def get_api_key_with_prefix(self, identifier, alias=None): + """Gets API key (with prefix if set). + + :param identifier: The identifier of apiKey. + :param alias: The alternative identifier of apiKey. + :return: The token for api key authentication. + """ + if self.refresh_api_key_hook is not None: + self.refresh_api_key_hook(self) + key = self.api_key.get(identifier, self.api_key.get(alias) if alias is not None else None) + if key: + prefix = self.api_key_prefix.get(identifier) + if prefix: + return "%s %s" % (prefix, key) + else: + return key + + def get_basic_auth_token(self): + """Gets HTTP basic authentication header (string). + + :return: The token for basic HTTP authentication. + """ + username = "" + if self.username is not None: + username = self.username + password = "" + if self.password is not None: + password = self.password + return urllib3.util.make_headers( + basic_auth=username + ':' + password + ).get('authorization') + + def auth_settings(self): + """Gets Auth Settings dict for api client. + + :return: The Auth Settings information dict. + """ + auth = {} + if self.access_token is not None: + auth['OAuth2'] = { + 'type': 'oauth2', + 'in': 'header', + 'key': 'Authorization', + 'value': 'Bearer ' + self.access_token + } + return auth + + def to_debug_report(self): + """Gets the essential information for debugging. + + :return: The report for debugging. + """ + return "Python SDK Debug Report:\n"\ + "OS: {env}\n"\ + "Python Version: {pyversion}\n"\ + "Version of the API: 0.0.1\n"\ + "SDK Package Version: 1.0.0".\ + format(env=sys.platform, pyversion=sys.version) + + def get_host_settings(self): + """Gets an array of host settings + + :return: An array of host settings + """ + return [ + { + 'url': "{scheme}://{host}/api/management/v1", + 'description': "Server URL when the port can be inferred from the scheme", + 'variables': { + 'scheme': { + 'description': "The scheme of the URI, either http or https.", + 'default_value': "https", + }, + 'host': { + 'description': "The host address for the specified server", + 'default_value': "localhost", + } + } + } + ] + + def get_host_from_settings(self, index, variables=None, servers=None): + """Gets host URL based on the index and variables + :param index: array index of the host settings + :param variables: hash of variable and the corresponding value + :param servers: an array of host settings or None + :return: URL based on host settings + """ + if index is None: + return self._base_path + + variables = {} if variables is None else variables + servers = self.get_host_settings() if servers is None else servers + + try: + server = servers[index] + except IndexError: + raise ValueError( + "Invalid index {0} when selecting the host settings. " + "Must be less than {1}".format(index, len(servers))) + + url = server['url'] + + # go through variables and replace placeholders + for variable_name, variable in server.get('variables', {}).items(): + used_value = variables.get( + variable_name, variable['default_value']) + + if 'enum_values' in variable \ + and used_value not in variable['enum_values']: + raise ValueError( + "The variable `{0}` in the host URL has invalid value " + "{1}. Must be {2}.".format( + variable_name, variables[variable_name], + variable['enum_values'])) + + url = url.replace("{" + variable_name + "}", used_value) + + return url + + @property + def host(self): + """Return generated host.""" + return self.get_host_from_settings(self.server_index, variables=self.server_variables) + + @host.setter + def host(self, value): + """Fix base path.""" + self._base_path = value + self.server_index = None diff --git a/regtests/client/python/polaris/management/exceptions.py b/regtests/client/python/polaris/management/exceptions.py new file mode 100644 index 0000000000..6be8f29051 --- /dev/null +++ b/regtests/client/python/polaris/management/exceptions.py @@ -0,0 +1,199 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + +from typing import Any, Optional +from typing_extensions import Self + +class OpenApiException(Exception): + """The base exception class for all OpenAPIExceptions""" + + +class ApiTypeError(OpenApiException, TypeError): + def __init__(self, msg, path_to_item=None, valid_classes=None, + key_type=None) -> None: + """ Raises an exception for TypeErrors + + Args: + msg (str): the exception message + + Keyword Args: + path_to_item (list): a list of keys an indices to get to the + current_item + None if unset + valid_classes (tuple): the primitive classes that current item + should be an instance of + None if unset + key_type (bool): False if our value is a value in a dict + True if it is a key in a dict + False if our item is an item in a list + None if unset + """ + self.path_to_item = path_to_item + self.valid_classes = valid_classes + self.key_type = key_type + full_msg = msg + if path_to_item: + full_msg = "{0} at {1}".format(msg, render_path(path_to_item)) + super(ApiTypeError, self).__init__(full_msg) + + +class ApiValueError(OpenApiException, ValueError): + def __init__(self, msg, path_to_item=None) -> None: + """ + Args: + msg (str): the exception message + + Keyword Args: + path_to_item (list) the path to the exception in the + received_data dict. None if unset + """ + + self.path_to_item = path_to_item + full_msg = msg + if path_to_item: + full_msg = "{0} at {1}".format(msg, render_path(path_to_item)) + super(ApiValueError, self).__init__(full_msg) + + +class ApiAttributeError(OpenApiException, AttributeError): + def __init__(self, msg, path_to_item=None) -> None: + """ + Raised when an attribute reference or assignment fails. + + Args: + msg (str): the exception message + + Keyword Args: + path_to_item (None/list) the path to the exception in the + received_data dict + """ + self.path_to_item = path_to_item + full_msg = msg + if path_to_item: + full_msg = "{0} at {1}".format(msg, render_path(path_to_item)) + super(ApiAttributeError, self).__init__(full_msg) + + +class ApiKeyError(OpenApiException, KeyError): + def __init__(self, msg, path_to_item=None) -> None: + """ + Args: + msg (str): the exception message + + Keyword Args: + path_to_item (None/list) the path to the exception in the + received_data dict + """ + self.path_to_item = path_to_item + full_msg = msg + if path_to_item: + full_msg = "{0} at {1}".format(msg, render_path(path_to_item)) + super(ApiKeyError, self).__init__(full_msg) + + +class ApiException(OpenApiException): + + def __init__( + self, + status=None, + reason=None, + http_resp=None, + *, + body: Optional[str] = None, + data: Optional[Any] = None, + ) -> None: + self.status = status + self.reason = reason + self.body = body + self.data = data + self.headers = None + + if http_resp: + if self.status is None: + self.status = http_resp.status + if self.reason is None: + self.reason = http_resp.reason + if self.body is None: + try: + self.body = http_resp.data.decode('utf-8') + except Exception: + pass + self.headers = http_resp.getheaders() + + @classmethod + def from_response( + cls, + *, + http_resp, + body: Optional[str], + data: Optional[Any], + ) -> Self: + if http_resp.status == 400: + raise BadRequestException(http_resp=http_resp, body=body, data=data) + + if http_resp.status == 401: + raise UnauthorizedException(http_resp=http_resp, body=body, data=data) + + if http_resp.status == 403: + raise ForbiddenException(http_resp=http_resp, body=body, data=data) + + if http_resp.status == 404: + raise NotFoundException(http_resp=http_resp, body=body, data=data) + + if 500 <= http_resp.status <= 599: + raise ServiceException(http_resp=http_resp, body=body, data=data) + raise ApiException(http_resp=http_resp, body=body, data=data) + + def __str__(self): + """Custom error messages for exception""" + error_message = "({0})\n"\ + "Reason: {1}\n".format(self.status, self.reason) + if self.headers: + error_message += "HTTP response headers: {0}\n".format( + self.headers) + + if self.data or self.body: + error_message += "HTTP response body: {0}\n".format(self.data or self.body) + + return error_message + + +class BadRequestException(ApiException): + pass + + +class NotFoundException(ApiException): + pass + + +class UnauthorizedException(ApiException): + pass + + +class ForbiddenException(ApiException): + pass + + +class ServiceException(ApiException): + pass + + +def render_path(path_to_item): + """Returns a string representation of a path""" + result = "" + for pth in path_to_item: + if isinstance(pth, int): + result += "[{0}]".format(pth) + else: + result += "['{0}']".format(pth) + return result diff --git a/regtests/client/python/polaris/management/models/__init__.py b/regtests/client/python/polaris/management/models/__init__.py new file mode 100644 index 0000000000..5b5c133836 --- /dev/null +++ b/regtests/client/python/polaris/management/models/__init__.py @@ -0,0 +1,56 @@ +# coding: utf-8 + +# flake8: noqa +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +# import models into model package +from polaris.management.models.add_grant_request import AddGrantRequest +from polaris.management.models.aws_storage_config_info import AwsStorageConfigInfo +from polaris.management.models.azure_storage_config_info import AzureStorageConfigInfo +from polaris.management.models.catalog import Catalog +from polaris.management.models.catalog_grant import CatalogGrant +from polaris.management.models.catalog_privilege import CatalogPrivilege +from polaris.management.models.catalog_properties import CatalogProperties +from polaris.management.models.catalog_role import CatalogRole +from polaris.management.models.catalog_roles import CatalogRoles +from polaris.management.models.catalogs import Catalogs +from polaris.management.models.create_catalog_request import CreateCatalogRequest +from polaris.management.models.create_catalog_role_request import CreateCatalogRoleRequest +from polaris.management.models.create_principal_request import CreatePrincipalRequest +from polaris.management.models.create_principal_role_request import CreatePrincipalRoleRequest +from polaris.management.models.external_catalog import ExternalCatalog +from polaris.management.models.file_storage_config_info import FileStorageConfigInfo +from polaris.management.models.gcp_storage_config_info import GcpStorageConfigInfo +from polaris.management.models.grant_catalog_role_request import GrantCatalogRoleRequest +from polaris.management.models.grant_principal_role_request import GrantPrincipalRoleRequest +from polaris.management.models.grant_resource import GrantResource +from polaris.management.models.grant_resources import GrantResources +from polaris.management.models.namespace_grant import NamespaceGrant +from polaris.management.models.namespace_privilege import NamespacePrivilege +from polaris.management.models.polaris_catalog import PolarisCatalog +from polaris.management.models.principal import Principal +from polaris.management.models.principal_role import PrincipalRole +from polaris.management.models.principal_roles import PrincipalRoles +from polaris.management.models.principal_with_credentials import PrincipalWithCredentials +from polaris.management.models.principal_with_credentials_credentials import PrincipalWithCredentialsCredentials +from polaris.management.models.principals import Principals +from polaris.management.models.revoke_grant_request import RevokeGrantRequest +from polaris.management.models.storage_config_info import StorageConfigInfo +from polaris.management.models.table_grant import TableGrant +from polaris.management.models.table_privilege import TablePrivilege +from polaris.management.models.update_catalog_request import UpdateCatalogRequest +from polaris.management.models.update_catalog_role_request import UpdateCatalogRoleRequest +from polaris.management.models.update_principal_request import UpdatePrincipalRequest +from polaris.management.models.update_principal_role_request import UpdatePrincipalRoleRequest +from polaris.management.models.view_grant import ViewGrant +from polaris.management.models.view_privilege import ViewPrivilege diff --git a/regtests/client/python/polaris/management/models/add_grant_request.py b/regtests/client/python/polaris/management/models/add_grant_request.py new file mode 100644 index 0000000000..b09e215ac3 --- /dev/null +++ b/regtests/client/python/polaris/management/models/add_grant_request.py @@ -0,0 +1,91 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict +from typing import Any, ClassVar, Dict, List, Optional +from polaris.management.models.grant_resource import GrantResource +from typing import Optional, Set +from typing_extensions import Self + +class AddGrantRequest(BaseModel): + """ + AddGrantRequest + """ # noqa: E501 + grant: Optional[GrantResource] = None + __properties: ClassVar[List[str]] = ["grant"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of AddGrantRequest from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of grant + if self.grant: + _dict['grant'] = self.grant.to_dict() + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of AddGrantRequest from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "grant": GrantResource.from_dict(obj["grant"]) if obj.get("grant") is not None else None + }) + return _obj + + diff --git a/regtests/client/python/polaris/management/models/aws_storage_config_info.py b/regtests/client/python/polaris/management/models/aws_storage_config_info.py new file mode 100644 index 0000000000..bec41d775e --- /dev/null +++ b/regtests/client/python/polaris/management/models/aws_storage_config_info.py @@ -0,0 +1,93 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import ConfigDict, Field, StrictStr +from typing import Any, ClassVar, Dict, List, Optional +from polaris.management.models.storage_config_info import StorageConfigInfo +from typing import Optional, Set +from typing_extensions import Self + + +class AwsStorageConfigInfo(StorageConfigInfo): + """ + aws storage configuration info + """ # noqa: E501 + role_arn: StrictStr = Field(description="the aws role arn that grants privileges on the S3 buckets", + alias="roleArn") + external_id: Optional[StrictStr] = Field(default=None, + description="an optional external id used to establish a trust relationship with AWS in the trust policy", + alias="externalId") + user_arn: Optional[StrictStr] = Field(default=None, description="the aws user arn used to assume the aws role", + alias="userArn") + __properties: ClassVar[List[str]] = ["storageType", "allowedLocations"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of AwsStorageConfigInfo from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of AwsStorageConfigInfo from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "storageType": obj.get("storageType"), + "allowedLocations": obj.get("allowedLocations"), + "roleArn": obj.get("roleArn") + }) + return _obj diff --git a/regtests/client/python/polaris/management/models/azure_storage_config_info.py b/regtests/client/python/polaris/management/models/azure_storage_config_info.py new file mode 100644 index 0000000000..9f79f0c7a2 --- /dev/null +++ b/regtests/client/python/polaris/management/models/azure_storage_config_info.py @@ -0,0 +1,91 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import ConfigDict, Field, StrictStr +from typing import Any, ClassVar, Dict, List, Optional +from polaris.management.models.storage_config_info import StorageConfigInfo +from typing import Optional, Set +from typing_extensions import Self + +class AzureStorageConfigInfo(StorageConfigInfo): + """ + azure storage configuration info + """ # noqa: E501 + tenant_id: StrictStr = Field(description="the tenant id that the storage accounts belong to", alias="tenantId") + multi_tenant_app_name: Optional[StrictStr] = Field(default=None, description="the name of the azure client application", alias="multiTenantAppName") + consent_url: Optional[StrictStr] = Field(default=None, description="URL to the Azure permissions request page", alias="consentUrl") + __properties: ClassVar[List[str]] = ["storageType", "allowedLocations"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of AzureStorageConfigInfo from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of AzureStorageConfigInfo from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "storageType": obj.get("storageType"), + "allowedLocations": obj.get("allowedLocations") + }) + return _obj + + diff --git a/regtests/client/python/polaris/management/models/catalog.py b/regtests/client/python/polaris/management/models/catalog.py new file mode 100644 index 0000000000..3d71762758 --- /dev/null +++ b/regtests/client/python/polaris/management/models/catalog.py @@ -0,0 +1,131 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from importlib import import_module +from pydantic import BaseModel, ConfigDict, Field, StrictInt, StrictStr, field_validator +from typing import Any, ClassVar, Dict, List, Optional, Union +from polaris.management.models.catalog_properties import CatalogProperties +from polaris.management.models.storage_config_info import StorageConfigInfo +from typing import Optional, Set +from typing_extensions import Self + +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from polaris.management.models.external_catalog import ExternalCatalog + from polaris.management.models.polaris_catalog import PolarisCatalog + +class Catalog(BaseModel): + """ + A catalog object. A catalog may be internal or external. Internal catalogs are managed entirely by an external catalog interface. Third party catalogs may be other Iceberg REST implementations or other services with their own proprietary APIs + """ # noqa: E501 + type: StrictStr = Field(description="the type of catalog - internal or external") + name: StrictStr = Field(description="The name of the catalog") + properties: CatalogProperties + create_timestamp: Optional[StrictInt] = Field(default=None, description="The creation time represented as unix epoch timestamp in milliseconds", alias="createTimestamp") + last_update_timestamp: Optional[StrictInt] = Field(default=None, description="The last update time represented as unix epoch timestamp in milliseconds", alias="lastUpdateTimestamp") + entity_version: Optional[StrictInt] = Field(default=None, description="The version of the catalog object used to determine if the catalog metadata has changed", alias="entityVersion") + storage_config_info: StorageConfigInfo = Field(alias="storageConfigInfo") + __properties: ClassVar[List[str]] = ["type", "name", "properties", "createTimestamp", "lastUpdateTimestamp", "entityVersion", "storageConfigInfo"] + + @field_validator('type') + def type_validate_enum(cls, value): + """Validates the enum""" + if value not in set(['INTERNAL', 'EXTERNAL']): + raise ValueError("must be one of enum values ('INTERNAL', 'EXTERNAL')") + return value + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + # JSON field name that stores the object type + __discriminator_property_name: ClassVar[str] = 'type' + + # discriminator mappings + __discriminator_value_class_map: ClassVar[Dict[str, str]] = { + 'EXTERNAL': 'ExternalCatalog','INTERNAL': 'PolarisCatalog' + } + + @classmethod + def get_discriminator_value(cls, obj: Dict[str, Any]) -> Optional[str]: + """Returns the discriminator value (object type) of the data""" + discriminator_value = obj[cls.__discriminator_property_name] + if discriminator_value: + return cls.__discriminator_value_class_map.get(discriminator_value) + else: + return None + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Union[ExternalCatalog, PolarisCatalog]]: + """Create an instance of Catalog from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of properties + if self.properties: + _dict['properties'] = self.properties.to_dict() + # override the default output from pydantic by calling `to_dict()` of storage_config_info + if self.storage_config_info: + _dict['storageConfigInfo'] = self.storage_config_info.to_dict() + return _dict + + @classmethod + def from_dict(cls, obj: Dict[str, Any]) -> Optional[Union[ExternalCatalog, PolarisCatalog]]: + """Create an instance of Catalog from a dict""" + # look up the object type based on discriminator mapping + object_type = cls.get_discriminator_value(obj) + if object_type == 'ExternalCatalog': + return import_module("polaris.management.models.external_catalog").ExternalCatalog.from_dict(obj) + if object_type == 'PolarisCatalog': + return import_module("polaris.management.models.polaris_catalog").PolarisCatalog.from_dict(obj) + + raise ValueError("Catalog failed to lookup discriminator value from " + + json.dumps(obj) + ". Discriminator property name: " + cls.__discriminator_property_name + + ", mapping: " + json.dumps(cls.__discriminator_value_class_map)) + + diff --git a/regtests/client/python/polaris/management/models/catalog_grant.py b/regtests/client/python/polaris/management/models/catalog_grant.py new file mode 100644 index 0000000000..e6bbd9e75e --- /dev/null +++ b/regtests/client/python/polaris/management/models/catalog_grant.py @@ -0,0 +1,90 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import ConfigDict +from typing import Any, ClassVar, Dict, List +from polaris.management.models.catalog_privilege import CatalogPrivilege +from polaris.management.models.grant_resource import GrantResource +from typing import Optional, Set +from typing_extensions import Self + +class CatalogGrant(GrantResource): + """ + CatalogGrant + """ # noqa: E501 + privilege: CatalogPrivilege + __properties: ClassVar[List[str]] = ["type", "privilege"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of CatalogGrant from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of CatalogGrant from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "type": obj.get("type"), + "privilege": obj.get("privilege") + }) + return _obj + + diff --git a/regtests/client/python/polaris/management/models/catalog_privilege.py b/regtests/client/python/polaris/management/models/catalog_privilege.py new file mode 100644 index 0000000000..df29cb62e8 --- /dev/null +++ b/regtests/client/python/polaris/management/models/catalog_privilege.py @@ -0,0 +1,60 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import json +from enum import Enum +from typing_extensions import Self + + +class CatalogPrivilege(str, Enum): + """ + CatalogPrivilege + """ + + """ + allowed enum values + """ + CATALOG_MANAGE_ACCESS = 'CATALOG_MANAGE_ACCESS' + CATALOG_MANAGE_CONTENT = 'CATALOG_MANAGE_CONTENT' + CATALOG_MANAGE_METADATA = 'CATALOG_MANAGE_METADATA' + CATALOG_READ_PROPERTIES = 'CATALOG_READ_PROPERTIES' + CATALOG_WRITE_PROPERTIES = 'CATALOG_WRITE_PROPERTIES' + NAMESPACE_CREATE = 'NAMESPACE_CREATE' + TABLE_CREATE = 'TABLE_CREATE' + VIEW_CREATE = 'VIEW_CREATE' + NAMESPACE_DROP = 'NAMESPACE_DROP' + TABLE_DROP = 'TABLE_DROP' + VIEW_DROP = 'VIEW_DROP' + NAMESPACE_LIST = 'NAMESPACE_LIST' + TABLE_LIST = 'TABLE_LIST' + VIEW_LIST = 'VIEW_LIST' + NAMESPACE_READ_PROPERTIES = 'NAMESPACE_READ_PROPERTIES' + TABLE_READ_PROPERTIES = 'TABLE_READ_PROPERTIES' + VIEW_READ_PROPERTIES = 'VIEW_READ_PROPERTIES' + NAMESPACE_WRITE_PROPERTIES = 'NAMESPACE_WRITE_PROPERTIES' + TABLE_WRITE_PROPERTIES = 'TABLE_WRITE_PROPERTIES' + VIEW_WRITE_PROPERTIES = 'VIEW_WRITE_PROPERTIES' + TABLE_READ_DATA = 'TABLE_READ_DATA' + TABLE_WRITE_DATA = 'TABLE_WRITE_DATA' + NAMESPACE_FULL_METADATA = 'NAMESPACE_FULL_METADATA' + TABLE_FULL_METADATA = 'TABLE_FULL_METADATA' + VIEW_FULL_METADATA = 'VIEW_FULL_METADATA' + + @classmethod + def from_json(cls, json_str: str) -> Self: + """Create an instance of CatalogPrivilege from a JSON string""" + return cls(json.loads(json_str)) + + diff --git a/regtests/client/python/polaris/management/models/catalog_properties.py b/regtests/client/python/polaris/management/models/catalog_properties.py new file mode 100644 index 0000000000..c6f0b16a89 --- /dev/null +++ b/regtests/client/python/polaris/management/models/catalog_properties.py @@ -0,0 +1,100 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictStr +from typing import Any, ClassVar, Dict, List +from typing import Optional, Set +from typing_extensions import Self + +class CatalogProperties(BaseModel): + """ + CatalogProperties + """ # noqa: E501 + default_base_location: StrictStr = Field(alias="default-base-location") + additional_properties: Dict[str, Any] = {} + __properties: ClassVar[List[str]] = ["default-base-location"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of CatalogProperties from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + * Fields in `self.additional_properties` are added to the output dict. + """ + excluded_fields: Set[str] = set([ + "additional_properties", + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # puts key-value pairs in additional_properties in the top level + if self.additional_properties is not None: + for _key, _value in self.additional_properties.items(): + _dict[_key] = _value + + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of CatalogProperties from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "default-base-location": obj.get("default-base-location") + }) + # store additional fields in additional_properties + for _key in obj.keys(): + if _key not in cls.__properties: + _obj.additional_properties[_key] = obj.get(_key) + + return _obj + + diff --git a/regtests/client/python/polaris/management/models/catalog_role.py b/regtests/client/python/polaris/management/models/catalog_role.py new file mode 100644 index 0000000000..3dd7d9a649 --- /dev/null +++ b/regtests/client/python/polaris/management/models/catalog_role.py @@ -0,0 +1,95 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictInt, StrictStr +from typing import Any, ClassVar, Dict, List, Optional +from typing import Optional, Set +from typing_extensions import Self + +class CatalogRole(BaseModel): + """ + CatalogRole + """ # noqa: E501 + name: StrictStr = Field(description="The name of the role") + properties: Optional[Dict[str, StrictStr]] = None + create_timestamp: Optional[StrictInt] = Field(default=None, alias="createTimestamp") + last_update_timestamp: Optional[StrictInt] = Field(default=None, alias="lastUpdateTimestamp") + entity_version: Optional[StrictInt] = Field(default=None, description="The version of the catalog role object used to determine if the catalog role metadata has changed", alias="entityVersion") + __properties: ClassVar[List[str]] = ["name", "properties", "createTimestamp", "lastUpdateTimestamp", "entityVersion"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of CatalogRole from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of CatalogRole from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "name": obj.get("name"), + "properties": obj.get("properties"), + "createTimestamp": obj.get("createTimestamp"), + "lastUpdateTimestamp": obj.get("lastUpdateTimestamp"), + "entityVersion": obj.get("entityVersion") + }) + return _obj + + diff --git a/regtests/client/python/polaris/management/models/catalog_roles.py b/regtests/client/python/polaris/management/models/catalog_roles.py new file mode 100644 index 0000000000..f944b332c4 --- /dev/null +++ b/regtests/client/python/polaris/management/models/catalog_roles.py @@ -0,0 +1,95 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field +from typing import Any, ClassVar, Dict, List +from polaris.management.models.catalog_role import CatalogRole +from typing import Optional, Set +from typing_extensions import Self + +class CatalogRoles(BaseModel): + """ + CatalogRoles + """ # noqa: E501 + roles: List[CatalogRole] = Field(description="The list of catalog roles") + __properties: ClassVar[List[str]] = ["roles"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of CatalogRoles from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of each item in roles (list) + _items = [] + if self.roles: + for _item in self.roles: + if _item: + _items.append(_item.to_dict()) + _dict['roles'] = _items + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of CatalogRoles from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "roles": [CatalogRole.from_dict(_item) for _item in obj["roles"]] if obj.get("roles") is not None else None + }) + return _obj + + diff --git a/regtests/client/python/polaris/management/models/catalogs.py b/regtests/client/python/polaris/management/models/catalogs.py new file mode 100644 index 0000000000..f47f029aae --- /dev/null +++ b/regtests/client/python/polaris/management/models/catalogs.py @@ -0,0 +1,95 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict +from typing import Any, ClassVar, Dict, List +from polaris.management.models.catalog import Catalog +from typing import Optional, Set +from typing_extensions import Self + +class Catalogs(BaseModel): + """ + A list of Catalog objects + """ # noqa: E501 + catalogs: List[Catalog] + __properties: ClassVar[List[str]] = ["catalogs"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of Catalogs from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of each item in catalogs (list) + _items = [] + if self.catalogs: + for _item in self.catalogs: + if _item: + _items.append(_item.to_dict()) + _dict['catalogs'] = _items + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of Catalogs from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "catalogs": [Catalog.from_dict(_item) for _item in obj["catalogs"]] if obj.get("catalogs") is not None else None + }) + return _obj + + diff --git a/regtests/client/python/polaris/management/models/create_catalog_request.py b/regtests/client/python/polaris/management/models/create_catalog_request.py new file mode 100644 index 0000000000..d0ecbe00e8 --- /dev/null +++ b/regtests/client/python/polaris/management/models/create_catalog_request.py @@ -0,0 +1,91 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict +from typing import Any, ClassVar, Dict, List +from polaris.management.models.catalog import Catalog +from typing import Optional, Set +from typing_extensions import Self + +class CreateCatalogRequest(BaseModel): + """ + Request to create a new catalog + """ # noqa: E501 + catalog: Catalog + __properties: ClassVar[List[str]] = ["catalog"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of CreateCatalogRequest from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of catalog + if self.catalog: + _dict['catalog'] = self.catalog.to_dict() + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of CreateCatalogRequest from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "catalog": Catalog.from_dict(obj["catalog"]) if obj.get("catalog") is not None else None + }) + return _obj + + diff --git a/regtests/client/python/polaris/management/models/create_catalog_role_request.py b/regtests/client/python/polaris/management/models/create_catalog_role_request.py new file mode 100644 index 0000000000..0c9be75d6b --- /dev/null +++ b/regtests/client/python/polaris/management/models/create_catalog_role_request.py @@ -0,0 +1,91 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field +from typing import Any, ClassVar, Dict, List, Optional +from polaris.management.models.catalog_role import CatalogRole +from typing import Optional, Set +from typing_extensions import Self + +class CreateCatalogRoleRequest(BaseModel): + """ + CreateCatalogRoleRequest + """ # noqa: E501 + catalog_role: Optional[CatalogRole] = Field(default=None, alias="catalogRole") + __properties: ClassVar[List[str]] = ["catalogRole"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of CreateCatalogRoleRequest from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of catalog_role + if self.catalog_role: + _dict['catalogRole'] = self.catalog_role.to_dict() + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of CreateCatalogRoleRequest from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "catalogRole": CatalogRole.from_dict(obj["catalogRole"]) if obj.get("catalogRole") is not None else None + }) + return _obj + + diff --git a/regtests/client/python/polaris/management/models/create_principal_request.py b/regtests/client/python/polaris/management/models/create_principal_request.py new file mode 100644 index 0000000000..f7091fb995 --- /dev/null +++ b/regtests/client/python/polaris/management/models/create_principal_request.py @@ -0,0 +1,93 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictBool +from typing import Any, ClassVar, Dict, List, Optional +from polaris.management.models.principal import Principal +from typing import Optional, Set +from typing_extensions import Self + +class CreatePrincipalRequest(BaseModel): + """ + CreatePrincipalRequest + """ # noqa: E501 + principal: Optional[Principal] = None + credential_rotation_required: Optional[StrictBool] = Field(default=None, description="If true, the initial credentials can only be used to call rotateCredentials", alias="credentialRotationRequired") + __properties: ClassVar[List[str]] = ["principal", "credentialRotationRequired"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of CreatePrincipalRequest from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of principal + if self.principal: + _dict['principal'] = self.principal.to_dict() + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of CreatePrincipalRequest from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "principal": Principal.from_dict(obj["principal"]) if obj.get("principal") is not None else None, + "credentialRotationRequired": obj.get("credentialRotationRequired") + }) + return _obj + + diff --git a/regtests/client/python/polaris/management/models/create_principal_role_request.py b/regtests/client/python/polaris/management/models/create_principal_role_request.py new file mode 100644 index 0000000000..0aea403c47 --- /dev/null +++ b/regtests/client/python/polaris/management/models/create_principal_role_request.py @@ -0,0 +1,91 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field +from typing import Any, ClassVar, Dict, List, Optional +from polaris.management.models.principal_role import PrincipalRole +from typing import Optional, Set +from typing_extensions import Self + +class CreatePrincipalRoleRequest(BaseModel): + """ + CreatePrincipalRoleRequest + """ # noqa: E501 + principal_role: Optional[PrincipalRole] = Field(default=None, alias="principalRole") + __properties: ClassVar[List[str]] = ["principalRole"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of CreatePrincipalRoleRequest from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of principal_role + if self.principal_role: + _dict['principalRole'] = self.principal_role.to_dict() + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of CreatePrincipalRoleRequest from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "principalRole": PrincipalRole.from_dict(obj["principalRole"]) if obj.get("principalRole") is not None else None + }) + return _obj + + diff --git a/regtests/client/python/polaris/management/models/external_catalog.py b/regtests/client/python/polaris/management/models/external_catalog.py new file mode 100644 index 0000000000..768451cdc6 --- /dev/null +++ b/regtests/client/python/polaris/management/models/external_catalog.py @@ -0,0 +1,103 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import ConfigDict, Field, StrictStr +from typing import Any, ClassVar, Dict, List, Optional +from polaris.management.models.catalog import Catalog +from polaris.management.models.catalog_properties import CatalogProperties +from polaris.management.models.storage_config_info import StorageConfigInfo +from typing import Optional, Set +from typing_extensions import Self + +class ExternalCatalog(Catalog): + """ + An externally managed catalog + """ # noqa: E501 + remote_url: Optional[StrictStr] = Field(default=None, description="URL to the remote catalog API", alias="remoteUrl") + __properties: ClassVar[List[str]] = ["type", "name", "properties", "createTimestamp", "lastUpdateTimestamp", "entityVersion", "storageConfigInfo", "remoteUrl"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of ExternalCatalog from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of properties + if self.properties: + _dict['properties'] = self.properties.to_dict() + # override the default output from pydantic by calling `to_dict()` of storage_config_info + if self.storage_config_info: + _dict['storageConfigInfo'] = self.storage_config_info.to_dict() + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of ExternalCatalog from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "type": obj.get("type") if obj.get("type") is not None else 'INTERNAL', + "name": obj.get("name"), + "properties": CatalogProperties.from_dict(obj["properties"]) if obj.get("properties") is not None else None, + "createTimestamp": obj.get("createTimestamp"), + "lastUpdateTimestamp": obj.get("lastUpdateTimestamp"), + "entityVersion": obj.get("entityVersion"), + "storageConfigInfo": StorageConfigInfo.from_dict(obj["storageConfigInfo"]) if obj.get("storageConfigInfo") is not None else None, + "remoteUrl": obj.get("remoteUrl") + }) + return _obj + + diff --git a/regtests/client/python/polaris/management/models/file_storage_config_info.py b/regtests/client/python/polaris/management/models/file_storage_config_info.py new file mode 100644 index 0000000000..87d6fbbbb7 --- /dev/null +++ b/regtests/client/python/polaris/management/models/file_storage_config_info.py @@ -0,0 +1,88 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import ConfigDict +from typing import Any, ClassVar, Dict, List +from polaris.management.models.storage_config_info import StorageConfigInfo +from typing import Optional, Set +from typing_extensions import Self + +class FileStorageConfigInfo(StorageConfigInfo): + """ + gcp storage configuration info + """ # noqa: E501 + __properties: ClassVar[List[str]] = ["storageType", "allowedLocations"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of FileStorageConfigInfo from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of FileStorageConfigInfo from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "storageType": obj.get("storageType"), + "allowedLocations": obj.get("allowedLocations") + }) + return _obj + + diff --git a/regtests/client/python/polaris/management/models/gcp_storage_config_info.py b/regtests/client/python/polaris/management/models/gcp_storage_config_info.py new file mode 100644 index 0000000000..cc5401f354 --- /dev/null +++ b/regtests/client/python/polaris/management/models/gcp_storage_config_info.py @@ -0,0 +1,89 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import ConfigDict, Field, StrictStr +from typing import Any, ClassVar, Dict, List, Optional +from polaris.management.models.storage_config_info import StorageConfigInfo +from typing import Optional, Set +from typing_extensions import Self + +class GcpStorageConfigInfo(StorageConfigInfo): + """ + gcp storage configuration info + """ # noqa: E501 + gcs_service_account: Optional[StrictStr] = Field(default=None, description="a Google cloud storage service account", alias="gcsServiceAccount") + __properties: ClassVar[List[str]] = ["storageType", "allowedLocations"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of GcpStorageConfigInfo from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of GcpStorageConfigInfo from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "storageType": obj.get("storageType"), + "allowedLocations": obj.get("allowedLocations") + }) + return _obj + + diff --git a/regtests/client/python/polaris/management/models/grant_catalog_role_request.py b/regtests/client/python/polaris/management/models/grant_catalog_role_request.py new file mode 100644 index 0000000000..ccd47ab25c --- /dev/null +++ b/regtests/client/python/polaris/management/models/grant_catalog_role_request.py @@ -0,0 +1,91 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field +from typing import Any, ClassVar, Dict, List, Optional +from polaris.management.models.catalog_role import CatalogRole +from typing import Optional, Set +from typing_extensions import Self + +class GrantCatalogRoleRequest(BaseModel): + """ + GrantCatalogRoleRequest + """ # noqa: E501 + catalog_role: Optional[CatalogRole] = Field(default=None, alias="catalogRole") + __properties: ClassVar[List[str]] = ["catalogRole"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of GrantCatalogRoleRequest from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of catalog_role + if self.catalog_role: + _dict['catalogRole'] = self.catalog_role.to_dict() + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of GrantCatalogRoleRequest from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "catalogRole": CatalogRole.from_dict(obj["catalogRole"]) if obj.get("catalogRole") is not None else None + }) + return _obj + + diff --git a/regtests/client/python/polaris/management/models/grant_principal_role_request.py b/regtests/client/python/polaris/management/models/grant_principal_role_request.py new file mode 100644 index 0000000000..d0d4cf5ae8 --- /dev/null +++ b/regtests/client/python/polaris/management/models/grant_principal_role_request.py @@ -0,0 +1,91 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field +from typing import Any, ClassVar, Dict, List, Optional +from polaris.management.models.principal_role import PrincipalRole +from typing import Optional, Set +from typing_extensions import Self + +class GrantPrincipalRoleRequest(BaseModel): + """ + GrantPrincipalRoleRequest + """ # noqa: E501 + principal_role: Optional[PrincipalRole] = Field(default=None, alias="principalRole") + __properties: ClassVar[List[str]] = ["principalRole"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of GrantPrincipalRoleRequest from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of principal_role + if self.principal_role: + _dict['principalRole'] = self.principal_role.to_dict() + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of GrantPrincipalRoleRequest from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "principalRole": PrincipalRole.from_dict(obj["principalRole"]) if obj.get("principalRole") is not None else None + }) + return _obj + + diff --git a/regtests/client/python/polaris/management/models/grant_resource.py b/regtests/client/python/polaris/management/models/grant_resource.py new file mode 100644 index 0000000000..f603d30a46 --- /dev/null +++ b/regtests/client/python/polaris/management/models/grant_resource.py @@ -0,0 +1,123 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from importlib import import_module +from pydantic import BaseModel, ConfigDict, StrictStr, field_validator +from typing import Any, ClassVar, Dict, List, Union +from typing import Optional, Set +from typing_extensions import Self + +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from polaris.management.models.catalog_grant import CatalogGrant + from polaris.management.models.namespace_grant import NamespaceGrant + from polaris.management.models.table_grant import TableGrant + from polaris.management.models.view_grant import ViewGrant + +class GrantResource(BaseModel): + """ + GrantResource + """ # noqa: E501 + type: StrictStr + __properties: ClassVar[List[str]] = ["type"] + + @field_validator('type') + def type_validate_enum(cls, value): + """Validates the enum""" + if value not in set(['catalog', 'namespace', 'table', 'view']): + raise ValueError("must be one of enum values ('catalog', 'namespace', 'table', 'view')") + return value + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + # JSON field name that stores the object type + __discriminator_property_name: ClassVar[str] = 'type' + + # discriminator mappings + __discriminator_value_class_map: ClassVar[Dict[str, str]] = { + 'catalog': 'CatalogGrant','namespace': 'NamespaceGrant','table': 'TableGrant','view': 'ViewGrant' + } + + @classmethod + def get_discriminator_value(cls, obj: Dict[str, Any]) -> Optional[str]: + """Returns the discriminator value (object type) of the data""" + discriminator_value = obj[cls.__discriminator_property_name] + if discriminator_value: + return cls.__discriminator_value_class_map.get(discriminator_value) + else: + return None + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Union[CatalogGrant, NamespaceGrant, TableGrant, ViewGrant]]: + """Create an instance of GrantResource from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Dict[str, Any]) -> Optional[Union[CatalogGrant, NamespaceGrant, TableGrant, ViewGrant]]: + """Create an instance of GrantResource from a dict""" + # look up the object type based on discriminator mapping + object_type = cls.get_discriminator_value(obj) + if object_type == 'CatalogGrant': + return import_module("polaris.management.models.catalog_grant").CatalogGrant.from_dict(obj) + if object_type == 'NamespaceGrant': + return import_module("polaris.management.models.namespace_grant").NamespaceGrant.from_dict(obj) + if object_type == 'TableGrant': + return import_module("polaris.management.models.table_grant").TableGrant.from_dict(obj) + if object_type == 'ViewGrant': + return import_module("polaris.management.models.view_grant").ViewGrant.from_dict(obj) + + raise ValueError("GrantResource failed to lookup discriminator value from " + + json.dumps(obj) + ". Discriminator property name: " + cls.__discriminator_property_name + + ", mapping: " + json.dumps(cls.__discriminator_value_class_map)) + + diff --git a/regtests/client/python/polaris/management/models/grant_resources.py b/regtests/client/python/polaris/management/models/grant_resources.py new file mode 100644 index 0000000000..03803e417b --- /dev/null +++ b/regtests/client/python/polaris/management/models/grant_resources.py @@ -0,0 +1,95 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict +from typing import Any, ClassVar, Dict, List +from polaris.management.models.grant_resource import GrantResource +from typing import Optional, Set +from typing_extensions import Self + +class GrantResources(BaseModel): + """ + GrantResources + """ # noqa: E501 + grants: List[GrantResource] + __properties: ClassVar[List[str]] = ["grants"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of GrantResources from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of each item in grants (list) + _items = [] + if self.grants: + for _item in self.grants: + if _item: + _items.append(_item.to_dict()) + _dict['grants'] = _items + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of GrantResources from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "grants": [GrantResource.from_dict(_item) for _item in obj["grants"]] if obj.get("grants") is not None else None + }) + return _obj + + diff --git a/regtests/client/python/polaris/management/models/namespace_grant.py b/regtests/client/python/polaris/management/models/namespace_grant.py new file mode 100644 index 0000000000..c30e473cc1 --- /dev/null +++ b/regtests/client/python/polaris/management/models/namespace_grant.py @@ -0,0 +1,92 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import ConfigDict, StrictStr +from typing import Any, ClassVar, Dict, List +from polaris.management.models.grant_resource import GrantResource +from polaris.management.models.namespace_privilege import NamespacePrivilege +from typing import Optional, Set +from typing_extensions import Self + +class NamespaceGrant(GrantResource): + """ + NamespaceGrant + """ # noqa: E501 + namespace: List[StrictStr] + privilege: NamespacePrivilege + __properties: ClassVar[List[str]] = ["type", "namespace", "privilege"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of NamespaceGrant from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of NamespaceGrant from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "type": obj.get("type"), + "namespace": obj.get("namespace"), + "privilege": obj.get("privilege") + }) + return _obj + + diff --git a/regtests/client/python/polaris/management/models/namespace_privilege.py b/regtests/client/python/polaris/management/models/namespace_privilege.py new file mode 100644 index 0000000000..1866363099 --- /dev/null +++ b/regtests/client/python/polaris/management/models/namespace_privilege.py @@ -0,0 +1,58 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import json +from enum import Enum +from typing_extensions import Self + + +class NamespacePrivilege(str, Enum): + """ + NamespacePrivilege + """ + + """ + allowed enum values + """ + CATALOG_MANAGE_ACCESS = 'CATALOG_MANAGE_ACCESS' + CATALOG_MANAGE_CONTENT = 'CATALOG_MANAGE_CONTENT' + CATALOG_MANAGE_METADATA = 'CATALOG_MANAGE_METADATA' + NAMESPACE_CREATE = 'NAMESPACE_CREATE' + TABLE_CREATE = 'TABLE_CREATE' + VIEW_CREATE = 'VIEW_CREATE' + NAMESPACE_DROP = 'NAMESPACE_DROP' + TABLE_DROP = 'TABLE_DROP' + VIEW_DROP = 'VIEW_DROP' + NAMESPACE_LIST = 'NAMESPACE_LIST' + TABLE_LIST = 'TABLE_LIST' + VIEW_LIST = 'VIEW_LIST' + NAMESPACE_READ_PROPERTIES = 'NAMESPACE_READ_PROPERTIES' + TABLE_READ_PROPERTIES = 'TABLE_READ_PROPERTIES' + VIEW_READ_PROPERTIES = 'VIEW_READ_PROPERTIES' + NAMESPACE_WRITE_PROPERTIES = 'NAMESPACE_WRITE_PROPERTIES' + TABLE_WRITE_PROPERTIES = 'TABLE_WRITE_PROPERTIES' + VIEW_WRITE_PROPERTIES = 'VIEW_WRITE_PROPERTIES' + TABLE_READ_DATA = 'TABLE_READ_DATA' + TABLE_WRITE_DATA = 'TABLE_WRITE_DATA' + NAMESPACE_FULL_METADATA = 'NAMESPACE_FULL_METADATA' + TABLE_FULL_METADATA = 'TABLE_FULL_METADATA' + VIEW_FULL_METADATA = 'VIEW_FULL_METADATA' + + @classmethod + def from_json(cls, json_str: str) -> Self: + """Create an instance of NamespacePrivilege from a JSON string""" + return cls(json.loads(json_str)) + + diff --git a/regtests/client/python/polaris/management/models/polaris_catalog.py b/regtests/client/python/polaris/management/models/polaris_catalog.py new file mode 100644 index 0000000000..d8b8672dfc --- /dev/null +++ b/regtests/client/python/polaris/management/models/polaris_catalog.py @@ -0,0 +1,101 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import ConfigDict +from typing import Any, ClassVar, Dict, List +from polaris.management.models.catalog import Catalog +from polaris.management.models.catalog_properties import CatalogProperties +from polaris.management.models.storage_config_info import StorageConfigInfo +from typing import Optional, Set +from typing_extensions import Self + +class PolarisCatalog(Catalog): + """ + The base catalog type - this contains all the fields necessary to construct an INTERNAL catalog + """ # noqa: E501 + __properties: ClassVar[List[str]] = ["type", "name", "properties", "createTimestamp", "lastUpdateTimestamp", "entityVersion", "storageConfigInfo"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of PolarisCatalog from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of properties + if self.properties: + _dict['properties'] = self.properties.to_dict() + # override the default output from pydantic by calling `to_dict()` of storage_config_info + if self.storage_config_info: + _dict['storageConfigInfo'] = self.storage_config_info.to_dict() + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of PolarisCatalog from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "type": obj.get("type") if obj.get("type") is not None else 'INTERNAL', + "name": obj.get("name"), + "properties": CatalogProperties.from_dict(obj["properties"]) if obj.get("properties") is not None else None, + "createTimestamp": obj.get("createTimestamp"), + "lastUpdateTimestamp": obj.get("lastUpdateTimestamp"), + "entityVersion": obj.get("entityVersion"), + "storageConfigInfo": StorageConfigInfo.from_dict(obj["storageConfigInfo"]) if obj.get("storageConfigInfo") is not None else None + }) + return _obj + + diff --git a/regtests/client/python/polaris/management/models/principal.py b/regtests/client/python/polaris/management/models/principal.py new file mode 100644 index 0000000000..d047520364 --- /dev/null +++ b/regtests/client/python/polaris/management/models/principal.py @@ -0,0 +1,97 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictInt, StrictStr +from typing import Any, ClassVar, Dict, List, Optional +from typing import Optional, Set +from typing_extensions import Self + +class Principal(BaseModel): + """ + A Polaris principal. + """ # noqa: E501 + name: StrictStr + client_id: Optional[StrictStr] = Field(default=None, description="The output-only OAuth clientId associated with this principal if applicable", alias="clientId") + properties: Optional[Dict[str, StrictStr]] = None + create_timestamp: Optional[StrictInt] = Field(default=None, alias="createTimestamp") + last_update_timestamp: Optional[StrictInt] = Field(default=None, alias="lastUpdateTimestamp") + entity_version: Optional[StrictInt] = Field(default=None, description="The version of the principal object used to determine if the principal metadata has changed", alias="entityVersion") + __properties: ClassVar[List[str]] = ["name", "clientId", "properties", "createTimestamp", "lastUpdateTimestamp", "entityVersion"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of Principal from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of Principal from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "name": obj.get("name"), + "clientId": obj.get("clientId"), + "properties": obj.get("properties"), + "createTimestamp": obj.get("createTimestamp"), + "lastUpdateTimestamp": obj.get("lastUpdateTimestamp"), + "entityVersion": obj.get("entityVersion") + }) + return _obj + + diff --git a/regtests/client/python/polaris/management/models/principal_role.py b/regtests/client/python/polaris/management/models/principal_role.py new file mode 100644 index 0000000000..b6367a6339 --- /dev/null +++ b/regtests/client/python/polaris/management/models/principal_role.py @@ -0,0 +1,95 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictInt, StrictStr +from typing import Any, ClassVar, Dict, List, Optional +from typing import Optional, Set +from typing_extensions import Self + +class PrincipalRole(BaseModel): + """ + PrincipalRole + """ # noqa: E501 + name: StrictStr = Field(description="The name of the role") + properties: Optional[Dict[str, StrictStr]] = None + create_timestamp: Optional[StrictInt] = Field(default=None, alias="createTimestamp") + last_update_timestamp: Optional[StrictInt] = Field(default=None, alias="lastUpdateTimestamp") + entity_version: Optional[StrictInt] = Field(default=None, description="The version of the principal role object used to determine if the principal role metadata has changed", alias="entityVersion") + __properties: ClassVar[List[str]] = ["name", "properties", "createTimestamp", "lastUpdateTimestamp", "entityVersion"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of PrincipalRole from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of PrincipalRole from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "name": obj.get("name"), + "properties": obj.get("properties"), + "createTimestamp": obj.get("createTimestamp"), + "lastUpdateTimestamp": obj.get("lastUpdateTimestamp"), + "entityVersion": obj.get("entityVersion") + }) + return _obj + + diff --git a/regtests/client/python/polaris/management/models/principal_roles.py b/regtests/client/python/polaris/management/models/principal_roles.py new file mode 100644 index 0000000000..ad9048be9c --- /dev/null +++ b/regtests/client/python/polaris/management/models/principal_roles.py @@ -0,0 +1,95 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict +from typing import Any, ClassVar, Dict, List +from polaris.management.models.principal_role import PrincipalRole +from typing import Optional, Set +from typing_extensions import Self + +class PrincipalRoles(BaseModel): + """ + PrincipalRoles + """ # noqa: E501 + roles: List[PrincipalRole] + __properties: ClassVar[List[str]] = ["roles"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of PrincipalRoles from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of each item in roles (list) + _items = [] + if self.roles: + for _item in self.roles: + if _item: + _items.append(_item.to_dict()) + _dict['roles'] = _items + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of PrincipalRoles from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "roles": [PrincipalRole.from_dict(_item) for _item in obj["roles"]] if obj.get("roles") is not None else None + }) + return _obj + + diff --git a/regtests/client/python/polaris/management/models/principal_with_credentials.py b/regtests/client/python/polaris/management/models/principal_with_credentials.py new file mode 100644 index 0000000000..9cc87b7880 --- /dev/null +++ b/regtests/client/python/polaris/management/models/principal_with_credentials.py @@ -0,0 +1,97 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict +from typing import Any, ClassVar, Dict, List +from polaris.management.models.principal import Principal +from polaris.management.models.principal_with_credentials_credentials import PrincipalWithCredentialsCredentials +from typing import Optional, Set +from typing_extensions import Self + +class PrincipalWithCredentials(BaseModel): + """ + A user with its client id and secret. This type is returned when a new principal is created or when its credentials are rotated + """ # noqa: E501 + principal: Principal + credentials: PrincipalWithCredentialsCredentials + __properties: ClassVar[List[str]] = ["principal", "credentials"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of PrincipalWithCredentials from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of principal + if self.principal: + _dict['principal'] = self.principal.to_dict() + # override the default output from pydantic by calling `to_dict()` of credentials + if self.credentials: + _dict['credentials'] = self.credentials.to_dict() + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of PrincipalWithCredentials from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "principal": Principal.from_dict(obj["principal"]) if obj.get("principal") is not None else None, + "credentials": PrincipalWithCredentialsCredentials.from_dict(obj["credentials"]) if obj.get("credentials") is not None else None + }) + return _obj + + diff --git a/regtests/client/python/polaris/management/models/principal_with_credentials_credentials.py b/regtests/client/python/polaris/management/models/principal_with_credentials_credentials.py new file mode 100644 index 0000000000..e95d33d6a3 --- /dev/null +++ b/regtests/client/python/polaris/management/models/principal_with_credentials_credentials.py @@ -0,0 +1,89 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictStr +from typing import Any, ClassVar, Dict, List, Optional +from typing import Optional, Set +from typing_extensions import Self + +class PrincipalWithCredentialsCredentials(BaseModel): + """ + PrincipalWithCredentialsCredentials + """ # noqa: E501 + client_id: Optional[StrictStr] = Field(default=None, alias="clientId") + client_secret: Optional[StrictStr] = Field(default=None, alias="clientSecret") + __properties: ClassVar[List[str]] = ["clientId", "clientSecret"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of PrincipalWithCredentialsCredentials from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of PrincipalWithCredentialsCredentials from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "clientId": obj.get("clientId"), + "clientSecret": obj.get("clientSecret") + }) + return _obj + + diff --git a/regtests/client/python/polaris/management/models/principals.py b/regtests/client/python/polaris/management/models/principals.py new file mode 100644 index 0000000000..5faff0916f --- /dev/null +++ b/regtests/client/python/polaris/management/models/principals.py @@ -0,0 +1,95 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict +from typing import Any, ClassVar, Dict, List +from polaris.management.models.principal import Principal +from typing import Optional, Set +from typing_extensions import Self + +class Principals(BaseModel): + """ + A list of Principals + """ # noqa: E501 + principals: List[Principal] + __properties: ClassVar[List[str]] = ["principals"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of Principals from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of each item in principals (list) + _items = [] + if self.principals: + for _item in self.principals: + if _item: + _items.append(_item.to_dict()) + _dict['principals'] = _items + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of Principals from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "principals": [Principal.from_dict(_item) for _item in obj["principals"]] if obj.get("principals") is not None else None + }) + return _obj + + diff --git a/regtests/client/python/polaris/management/models/revoke_grant_request.py b/regtests/client/python/polaris/management/models/revoke_grant_request.py new file mode 100644 index 0000000000..ff963e7118 --- /dev/null +++ b/regtests/client/python/polaris/management/models/revoke_grant_request.py @@ -0,0 +1,91 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict +from typing import Any, ClassVar, Dict, List, Optional +from polaris.management.models.grant_resource import GrantResource +from typing import Optional, Set +from typing_extensions import Self + +class RevokeGrantRequest(BaseModel): + """ + RevokeGrantRequest + """ # noqa: E501 + grant: Optional[GrantResource] = None + __properties: ClassVar[List[str]] = ["grant"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of RevokeGrantRequest from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of grant + if self.grant: + _dict['grant'] = self.grant.to_dict() + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of RevokeGrantRequest from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "grant": GrantResource.from_dict(obj["grant"]) if obj.get("grant") is not None else None + }) + return _obj + + diff --git a/regtests/client/python/polaris/management/models/storage_config_info.py b/regtests/client/python/polaris/management/models/storage_config_info.py new file mode 100644 index 0000000000..dc669b2261 --- /dev/null +++ b/regtests/client/python/polaris/management/models/storage_config_info.py @@ -0,0 +1,124 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from importlib import import_module +from pydantic import BaseModel, ConfigDict, Field, StrictStr, field_validator +from typing import Any, ClassVar, Dict, List, Optional, Union +from typing import Optional, Set +from typing_extensions import Self + +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from polaris.management.models.azure_storage_config_info import AzureStorageConfigInfo + from polaris.management.models.file_storage_config_info import FileStorageConfigInfo + from polaris.management.models.gcp_storage_config_info import GcpStorageConfigInfo + from polaris.management.models.aws_storage_config_info import AwsStorageConfigInfo + +class StorageConfigInfo(BaseModel): + """ + A storage configuration used by catalogs + """ # noqa: E501 + storage_type: StrictStr = Field(description="The cloud provider type this storage is built on. FILE is supported for testing purposes only", alias="storageType") + allowed_locations: Optional[List[StrictStr]] = Field(default=None, alias="allowedLocations") + __properties: ClassVar[List[str]] = ["storageType", "allowedLocations"] + + @field_validator('storage_type') + def storage_type_validate_enum(cls, value): + """Validates the enum""" + if value not in set(['S3', 'GCS', 'AZURE', 'FILE']): + raise ValueError("must be one of enum values ('S3', 'GCS', 'AZURE', 'FILE')") + return value + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + # JSON field name that stores the object type + __discriminator_property_name: ClassVar[str] = 'storageType' + + # discriminator mappings + __discriminator_value_class_map: ClassVar[Dict[str, str]] = { + 'AZURE': 'AzureStorageConfigInfo','FILE': 'FileStorageConfigInfo','GCS': 'GcpStorageConfigInfo','S3': 'AwsStorageConfigInfo' + } + + @classmethod + def get_discriminator_value(cls, obj: Dict[str, Any]) -> Optional[str]: + """Returns the discriminator value (object type) of the data""" + discriminator_value = obj[cls.__discriminator_property_name] + if discriminator_value: + return cls.__discriminator_value_class_map.get(discriminator_value) + else: + return None + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Union[AzureStorageConfigInfo, FileStorageConfigInfo, GcpStorageConfigInfo, AwsStorageConfigInfo]]: + """Create an instance of StorageConfigInfo from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Dict[str, Any]) -> Optional[Union[AzureStorageConfigInfo, FileStorageConfigInfo, GcpStorageConfigInfo, AwsStorageConfigInfo]]: + """Create an instance of StorageConfigInfo from a dict""" + # look up the object type based on discriminator mapping + object_type = cls.get_discriminator_value(obj) + if object_type == 'AzureStorageConfigInfo': + return import_module("polaris.management.models.azure_storage_config_info").AzureStorageConfigInfo.from_dict(obj) + if object_type == 'FileStorageConfigInfo': + return import_module("polaris.management.models.file_storage_config_info").FileStorageConfigInfo.from_dict(obj) + if object_type == 'GcpStorageConfigInfo': + return import_module("polaris.management.models.gcp_storage_config_info").GcpStorageConfigInfo.from_dict(obj) + if object_type == 'AwsStorageConfigInfo': + return import_module("polaris.management.models.aws_storage_config_info").AwsStorageConfigInfo.from_dict(obj) + + raise ValueError("StorageConfigInfo failed to lookup discriminator value from " + + json.dumps(obj) + ". Discriminator property name: " + cls.__discriminator_property_name + + ", mapping: " + json.dumps(cls.__discriminator_value_class_map)) + + diff --git a/regtests/client/python/polaris/management/models/table_grant.py b/regtests/client/python/polaris/management/models/table_grant.py new file mode 100644 index 0000000000..175e4b6ec3 --- /dev/null +++ b/regtests/client/python/polaris/management/models/table_grant.py @@ -0,0 +1,94 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import ConfigDict, Field, StrictStr +from typing import Any, ClassVar, Dict, List +from polaris.management.models.grant_resource import GrantResource +from polaris.management.models.table_privilege import TablePrivilege +from typing import Optional, Set +from typing_extensions import Self + +class TableGrant(GrantResource): + """ + TableGrant + """ # noqa: E501 + namespace: List[StrictStr] + table_name: StrictStr = Field(alias="tableName") + privilege: TablePrivilege + __properties: ClassVar[List[str]] = ["type", "namespace", "tableName", "privilege"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of TableGrant from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of TableGrant from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "type": obj.get("type"), + "namespace": obj.get("namespace"), + "tableName": obj.get("tableName"), + "privilege": obj.get("privilege") + }) + return _obj + + diff --git a/regtests/client/python/polaris/management/models/table_privilege.py b/regtests/client/python/polaris/management/models/table_privilege.py new file mode 100644 index 0000000000..8aacfc8acb --- /dev/null +++ b/regtests/client/python/polaris/management/models/table_privilege.py @@ -0,0 +1,44 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import json +from enum import Enum +from typing_extensions import Self + + +class TablePrivilege(str, Enum): + """ + TablePrivilege + """ + + """ + allowed enum values + """ + CATALOG_MANAGE_ACCESS = 'CATALOG_MANAGE_ACCESS' + TABLE_DROP = 'TABLE_DROP' + TABLE_LIST = 'TABLE_LIST' + TABLE_READ_PROPERTIES = 'TABLE_READ_PROPERTIES' + VIEW_READ_PROPERTIES = 'VIEW_READ_PROPERTIES' + TABLE_WRITE_PROPERTIES = 'TABLE_WRITE_PROPERTIES' + TABLE_READ_DATA = 'TABLE_READ_DATA' + TABLE_WRITE_DATA = 'TABLE_WRITE_DATA' + TABLE_FULL_METADATA = 'TABLE_FULL_METADATA' + + @classmethod + def from_json(cls, json_str: str) -> Self: + """Create an instance of TablePrivilege from a JSON string""" + return cls(json.loads(json_str)) + + diff --git a/regtests/client/python/polaris/management/models/update_catalog_request.py b/regtests/client/python/polaris/management/models/update_catalog_request.py new file mode 100644 index 0000000000..8f12d8218c --- /dev/null +++ b/regtests/client/python/polaris/management/models/update_catalog_request.py @@ -0,0 +1,95 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictInt, StrictStr +from typing import Any, ClassVar, Dict, List, Optional +from polaris.management.models.storage_config_info import StorageConfigInfo +from typing import Optional, Set +from typing_extensions import Self + +class UpdateCatalogRequest(BaseModel): + """ + Updates to apply to a Catalog + """ # noqa: E501 + current_entity_version: Optional[StrictInt] = Field(default=None, description="The version of the object onto which this update is applied; if the object changed, the update will fail and the caller should retry after fetching the latest version.", alias="currentEntityVersion") + properties: Optional[Dict[str, StrictStr]] = None + storage_config_info: Optional[StorageConfigInfo] = Field(default=None, alias="storageConfigInfo") + __properties: ClassVar[List[str]] = ["currentEntityVersion", "properties", "storageConfigInfo"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of UpdateCatalogRequest from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of storage_config_info + if self.storage_config_info: + _dict['storageConfigInfo'] = self.storage_config_info.to_dict() + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of UpdateCatalogRequest from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "currentEntityVersion": obj.get("currentEntityVersion"), + "properties": obj.get("properties"), + "storageConfigInfo": StorageConfigInfo.from_dict(obj["storageConfigInfo"]) if obj.get("storageConfigInfo") is not None else None + }) + return _obj + + diff --git a/regtests/client/python/polaris/management/models/update_catalog_role_request.py b/regtests/client/python/polaris/management/models/update_catalog_role_request.py new file mode 100644 index 0000000000..bbbd1762e2 --- /dev/null +++ b/regtests/client/python/polaris/management/models/update_catalog_role_request.py @@ -0,0 +1,89 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictInt, StrictStr +from typing import Any, ClassVar, Dict, List +from typing import Optional, Set +from typing_extensions import Self + +class UpdateCatalogRoleRequest(BaseModel): + """ + Updates to apply to a Catalog Role + """ # noqa: E501 + current_entity_version: StrictInt = Field(description="The version of the object onto which this update is applied; if the object changed, the update will fail and the caller should retry after fetching the latest version.", alias="currentEntityVersion") + properties: Dict[str, StrictStr] + __properties: ClassVar[List[str]] = ["currentEntityVersion", "properties"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of UpdateCatalogRoleRequest from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of UpdateCatalogRoleRequest from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "currentEntityVersion": obj.get("currentEntityVersion"), + "properties": obj.get("properties") + }) + return _obj + + diff --git a/regtests/client/python/polaris/management/models/update_principal_request.py b/regtests/client/python/polaris/management/models/update_principal_request.py new file mode 100644 index 0000000000..e3000f7fba --- /dev/null +++ b/regtests/client/python/polaris/management/models/update_principal_request.py @@ -0,0 +1,89 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictInt, StrictStr +from typing import Any, ClassVar, Dict, List +from typing import Optional, Set +from typing_extensions import Self + +class UpdatePrincipalRequest(BaseModel): + """ + Updates to apply to a Principal + """ # noqa: E501 + current_entity_version: StrictInt = Field(description="The version of the object onto which this update is applied; if the object changed, the update will fail and the caller should retry after fetching the latest version.", alias="currentEntityVersion") + properties: Dict[str, StrictStr] + __properties: ClassVar[List[str]] = ["currentEntityVersion", "properties"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of UpdatePrincipalRequest from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of UpdatePrincipalRequest from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "currentEntityVersion": obj.get("currentEntityVersion"), + "properties": obj.get("properties") + }) + return _obj + + diff --git a/regtests/client/python/polaris/management/models/update_principal_role_request.py b/regtests/client/python/polaris/management/models/update_principal_role_request.py new file mode 100644 index 0000000000..1b229ac150 --- /dev/null +++ b/regtests/client/python/polaris/management/models/update_principal_role_request.py @@ -0,0 +1,89 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictInt, StrictStr +from typing import Any, ClassVar, Dict, List +from typing import Optional, Set +from typing_extensions import Self + +class UpdatePrincipalRoleRequest(BaseModel): + """ + Updates to apply to a Principal Role + """ # noqa: E501 + current_entity_version: StrictInt = Field(description="The version of the object onto which this update is applied; if the object changed, the update will fail and the caller should retry after fetching the latest version.", alias="currentEntityVersion") + properties: Dict[str, StrictStr] + __properties: ClassVar[List[str]] = ["currentEntityVersion", "properties"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of UpdatePrincipalRoleRequest from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of UpdatePrincipalRoleRequest from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "currentEntityVersion": obj.get("currentEntityVersion"), + "properties": obj.get("properties") + }) + return _obj + + diff --git a/regtests/client/python/polaris/management/models/view_grant.py b/regtests/client/python/polaris/management/models/view_grant.py new file mode 100644 index 0000000000..469458c59e --- /dev/null +++ b/regtests/client/python/polaris/management/models/view_grant.py @@ -0,0 +1,94 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import ConfigDict, Field, StrictStr +from typing import Any, ClassVar, Dict, List +from polaris.management.models.grant_resource import GrantResource +from polaris.management.models.view_privilege import ViewPrivilege +from typing import Optional, Set +from typing_extensions import Self + +class ViewGrant(GrantResource): + """ + ViewGrant + """ # noqa: E501 + namespace: List[StrictStr] + view_name: StrictStr = Field(alias="viewName") + privilege: ViewPrivilege + __properties: ClassVar[List[str]] = ["type", "namespace", "viewName", "privilege"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of ViewGrant from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of ViewGrant from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "type": obj.get("type"), + "namespace": obj.get("namespace"), + "viewName": obj.get("viewName"), + "privilege": obj.get("privilege") + }) + return _obj + + diff --git a/regtests/client/python/polaris/management/models/view_privilege.py b/regtests/client/python/polaris/management/models/view_privilege.py new file mode 100644 index 0000000000..32390f9d2e --- /dev/null +++ b/regtests/client/python/polaris/management/models/view_privilege.py @@ -0,0 +1,42 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import json +from enum import Enum +from typing_extensions import Self + + +class ViewPrivilege(str, Enum): + """ + ViewPrivilege + """ + + """ + allowed enum values + """ + CATALOG_MANAGE_ACCESS = 'CATALOG_MANAGE_ACCESS' + VIEW_CREATE = 'VIEW_CREATE' + VIEW_DROP = 'VIEW_DROP' + VIEW_LIST = 'VIEW_LIST' + VIEW_READ_PROPERTIES = 'VIEW_READ_PROPERTIES' + VIEW_WRITE_PROPERTIES = 'VIEW_WRITE_PROPERTIES' + VIEW_FULL_METADATA = 'VIEW_FULL_METADATA' + + @classmethod + def from_json(cls, json_str: str) -> Self: + """Create an instance of ViewPrivilege from a JSON string""" + return cls(json.loads(json_str)) + + diff --git a/regtests/client/python/polaris/management/py.typed b/regtests/client/python/polaris/management/py.typed new file mode 100644 index 0000000000..e69de29bb2 diff --git a/regtests/client/python/polaris/management/rest.py b/regtests/client/python/polaris/management/rest.py new file mode 100644 index 0000000000..994871bb89 --- /dev/null +++ b/regtests/client/python/polaris/management/rest.py @@ -0,0 +1,257 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import io +import json +import re +import ssl + +import urllib3 + +from polaris.management.exceptions import ApiException, ApiValueError + +SUPPORTED_SOCKS_PROXIES = {"socks5", "socks5h", "socks4", "socks4a"} +RESTResponseType = urllib3.HTTPResponse + + +def is_socks_proxy_url(url): + if url is None: + return False + split_section = url.split("://") + if len(split_section) < 2: + return False + else: + return split_section[0].lower() in SUPPORTED_SOCKS_PROXIES + + +class RESTResponse(io.IOBase): + + def __init__(self, resp) -> None: + self.response = resp + self.status = resp.status + self.reason = resp.reason + self.data = None + + def read(self): + if self.data is None: + self.data = self.response.data + return self.data + + def getheaders(self): + """Returns a dictionary of the response headers.""" + return self.response.headers + + def getheader(self, name, default=None): + """Returns a given response header.""" + return self.response.headers.get(name, default) + + +class RESTClientObject: + + def __init__(self, configuration) -> None: + # urllib3.PoolManager will pass all kw parameters to connectionpool + # https://github.com/shazow/urllib3/blob/f9409436f83aeb79fbaf090181cd81b784f1b8ce/urllib3/poolmanager.py#L75 # noqa: E501 + # https://github.com/shazow/urllib3/blob/f9409436f83aeb79fbaf090181cd81b784f1b8ce/urllib3/connectionpool.py#L680 # noqa: E501 + # Custom SSL certificates and client certificates: http://urllib3.readthedocs.io/en/latest/advanced-usage.html # noqa: E501 + + # cert_reqs + if configuration.verify_ssl: + cert_reqs = ssl.CERT_REQUIRED + else: + cert_reqs = ssl.CERT_NONE + + pool_args = { + "cert_reqs": cert_reqs, + "ca_certs": configuration.ssl_ca_cert, + "cert_file": configuration.cert_file, + "key_file": configuration.key_file, + } + if configuration.assert_hostname is not None: + pool_args['assert_hostname'] = ( + configuration.assert_hostname + ) + + if configuration.retries is not None: + pool_args['retries'] = configuration.retries + + if configuration.tls_server_name: + pool_args['server_hostname'] = configuration.tls_server_name + + + if configuration.socket_options is not None: + pool_args['socket_options'] = configuration.socket_options + + if configuration.connection_pool_maxsize is not None: + pool_args['maxsize'] = configuration.connection_pool_maxsize + + # https pool manager + self.pool_manager: urllib3.PoolManager + + if configuration.proxy: + if is_socks_proxy_url(configuration.proxy): + from urllib3.contrib.socks import SOCKSProxyManager + pool_args["proxy_url"] = configuration.proxy + pool_args["headers"] = configuration.proxy_headers + self.pool_manager = SOCKSProxyManager(**pool_args) + else: + pool_args["proxy_url"] = configuration.proxy + pool_args["proxy_headers"] = configuration.proxy_headers + self.pool_manager = urllib3.ProxyManager(**pool_args) + else: + self.pool_manager = urllib3.PoolManager(**pool_args) + + def request( + self, + method, + url, + headers=None, + body=None, + post_params=None, + _request_timeout=None + ): + """Perform requests. + + :param method: http request method + :param url: http request url + :param headers: http request headers + :param body: request json body, for `application/json` + :param post_params: request post parameters, + `application/x-www-form-urlencoded` + and `multipart/form-data` + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + """ + method = method.upper() + assert method in [ + 'GET', + 'HEAD', + 'DELETE', + 'POST', + 'PUT', + 'PATCH', + 'OPTIONS' + ] + + if post_params and body: + raise ApiValueError( + "body parameter cannot be used with post_params parameter." + ) + + post_params = post_params or {} + headers = headers or {} + + timeout = None + if _request_timeout: + if isinstance(_request_timeout, (int, float)): + timeout = urllib3.Timeout(total=_request_timeout) + elif ( + isinstance(_request_timeout, tuple) + and len(_request_timeout) == 2 + ): + timeout = urllib3.Timeout( + connect=_request_timeout[0], + read=_request_timeout[1] + ) + + try: + # For `POST`, `PUT`, `PATCH`, `OPTIONS`, `DELETE` + if method in ['POST', 'PUT', 'PATCH', 'OPTIONS', 'DELETE']: + + # no content type provided or payload is json + content_type = headers.get('Content-Type') + if ( + not content_type + or re.search('json', content_type, re.IGNORECASE) + ): + request_body = None + if body is not None: + request_body = json.dumps(body) + r = self.pool_manager.request( + method, + url, + body=request_body, + timeout=timeout, + headers=headers, + preload_content=False + ) + elif content_type == 'application/x-www-form-urlencoded': + r = self.pool_manager.request( + method, + url, + fields=post_params, + encode_multipart=False, + timeout=timeout, + headers=headers, + preload_content=False + ) + elif content_type == 'multipart/form-data': + # must del headers['Content-Type'], or the correct + # Content-Type which generated by urllib3 will be + # overwritten. + del headers['Content-Type'] + # Ensures that dict objects are serialized + post_params = [(a, json.dumps(b)) if isinstance(b, dict) else (a,b) for a, b in post_params] + r = self.pool_manager.request( + method, + url, + fields=post_params, + encode_multipart=True, + timeout=timeout, + headers=headers, + preload_content=False + ) + # Pass a `string` parameter directly in the body to support + # other content types than JSON when `body` argument is + # provided in serialized form. + elif isinstance(body, str) or isinstance(body, bytes): + r = self.pool_manager.request( + method, + url, + body=body, + timeout=timeout, + headers=headers, + preload_content=False + ) + elif headers['Content-Type'] == 'text/plain' and isinstance(body, bool): + request_body = "true" if body else "false" + r = self.pool_manager.request( + method, + url, + body=request_body, + preload_content=False, + timeout=timeout, + headers=headers) + else: + # Cannot generate the request from given parameters + msg = """Cannot prepare a request message for provided + arguments. Please check that your arguments match + declared content type.""" + raise ApiException(status=0, reason=msg) + # For `GET`, `HEAD` + else: + r = self.pool_manager.request( + method, + url, + fields={}, + timeout=timeout, + headers=headers, + preload_content=False + ) + except urllib3.exceptions.SSLError as e: + msg = "\n".join([type(e).__name__, str(e)]) + raise ApiException(status=0, reason=msg) + + return RESTResponse(r) diff --git a/regtests/client/python/pyproject.toml b/regtests/client/python/pyproject.toml new file mode 100644 index 0000000000..2783814afb --- /dev/null +++ b/regtests/client/python/pyproject.toml @@ -0,0 +1,72 @@ +[tool.poetry] +name = "polaris" +version = "1.0.0" +description = "Polaris Management Service" +authors = ["OpenAPI Generator Community "] +license = "NoLicense" +readme = "README.md" +repository = "https://github.com/GIT_USER_ID/GIT_REPO_ID" +keywords = ["OpenAPI", "OpenAPI-Generator", "Polaris Management Service"] +include = ["polaris.management/py.typed"] + +[tool.poetry.dependencies] +python = "^3.8" + +urllib3 = "^1.25.3" +python-dateutil = ">=2.8.2" +pydantic = ">=2" +typing-extensions = ">=4.7.1" +boto3 = "==1.34.120" + +[tool.poetry.dev-dependencies] +pytest = ">=7.2.1" +tox = ">=3.9.0" +flake8 = ">=4.0.0" +types-python-dateutil = ">=2.8.19.14" +mypy = "1.4.1" + + +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" + +[tool.pylint.'MESSAGES CONTROL'] +extension-pkg-whitelist = "pydantic" + +[tool.mypy] +files = [ + "polaris", + #"test", # auto-generated tests + "tests", # hand-written tests +] +# TODO: enable "strict" once all these individual checks are passing +# strict = true + +# List from: https://mypy.readthedocs.io/en/stable/existing_code.html#introduce-stricter-options +warn_unused_configs = true +warn_redundant_casts = true +warn_unused_ignores = true + +## Getting these passing should be easy +strict_equality = true +strict_concatenate = true + +## Strongly recommend enabling this one as soon as you can +check_untyped_defs = true + +## These shouldn't be too much additional work, but may be tricky to +## get passing if you use a lot of untyped libraries +disallow_subclassing_any = true +disallow_untyped_decorators = true +disallow_any_generics = true + +### These next few are various gradations of forcing use of type annotations +#disallow_untyped_calls = true +#disallow_incomplete_defs = true +#disallow_untyped_defs = true +# +### This one isn't too hard to get passing, but return on investment is lower +#no_implicit_reexport = true +# +### This one can be tricky to get passing if you use a lot of untyped libraries +#warn_return_any = true diff --git a/regtests/client/python/requirements.txt b/regtests/client/python/requirements.txt new file mode 100644 index 0000000000..cc85509ec5 --- /dev/null +++ b/regtests/client/python/requirements.txt @@ -0,0 +1,5 @@ +python_dateutil >= 2.5.3 +setuptools >= 21.0.0 +urllib3 >= 1.25.3, < 2.1.0 +pydantic >= 2 +typing-extensions >= 4.7.1 diff --git a/regtests/client/python/setup.cfg b/regtests/client/python/setup.cfg new file mode 100644 index 0000000000..11433ee875 --- /dev/null +++ b/regtests/client/python/setup.cfg @@ -0,0 +1,2 @@ +[flake8] +max-line-length=99 diff --git a/regtests/client/python/setup.py b/regtests/client/python/setup.py new file mode 100644 index 0000000000..8fb1409ca2 --- /dev/null +++ b/regtests/client/python/setup.py @@ -0,0 +1,49 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from setuptools import setup, find_packages # noqa: H301 + +# To install the library, run the following +# +# python setup.py install +# +# prerequisite: setuptools +# http://pypi.python.org/pypi/setuptools +NAME = "polaris.management" +VERSION = "1.0.0" +PYTHON_REQUIRES = ">=3.7" +REQUIRES = [ + "urllib3 >= 1.25.3, < 2.1.0", + "python-dateutil", + "pydantic >= 2", + "typing-extensions >= 4.7.1", +] + +setup( + name=NAME, + version=VERSION, + description="Polaris Management Service", + author="OpenAPI Generator community", + author_email="team@openapitools.org", + url="", + keywords=["OpenAPI", "OpenAPI-Generator", "Polaris Management Service"], + install_requires=REQUIRES, + packages=find_packages(exclude=["test", "tests"]), + include_package_data=True, + long_description_content_type='text/markdown', + long_description="""\ + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + """, # noqa: E501 + package_data={"polaris.management": ["py.typed"]}, +) diff --git a/regtests/client/python/test-requirements.txt b/regtests/client/python/test-requirements.txt new file mode 100644 index 0000000000..8e6d8cb137 --- /dev/null +++ b/regtests/client/python/test-requirements.txt @@ -0,0 +1,5 @@ +pytest~=7.1.3 +pytest-cov>=2.8.1 +pytest-randomly>=3.12.0 +mypy>=1.4.1 +types-python-dateutil>=2.8.19 diff --git a/regtests/client/python/test/__init__.py b/regtests/client/python/test/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/regtests/client/python/test/test_add_grant_request.py b/regtests/client/python/test/test_add_grant_request.py new file mode 100644 index 0000000000..81ea2592ae --- /dev/null +++ b/regtests/client/python/test/test_add_grant_request.py @@ -0,0 +1,52 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.management.models.add_grant_request import AddGrantRequest + +class TestAddGrantRequest(unittest.TestCase): + """AddGrantRequest unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> AddGrantRequest: + """Test AddGrantRequest + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `AddGrantRequest` + """ + model = AddGrantRequest() + if include_optional: + return AddGrantRequest( + grant = polaris.management.models.grant_resource.GrantResource( + type = 'catalog', ) + ) + else: + return AddGrantRequest( + ) + """ + + def testAddGrantRequest(self): + """Test AddGrantRequest""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_add_partition_spec_update.py b/regtests/client/python/test/test_add_partition_spec_update.py new file mode 100644 index 0000000000..3450cccbb6 --- /dev/null +++ b/regtests/client/python/test/test_add_partition_spec_update.py @@ -0,0 +1,70 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.add_partition_spec_update import AddPartitionSpecUpdate + +class TestAddPartitionSpecUpdate(unittest.TestCase): + """AddPartitionSpecUpdate unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> AddPartitionSpecUpdate: + """Test AddPartitionSpecUpdate + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `AddPartitionSpecUpdate` + """ + model = AddPartitionSpecUpdate() + if include_optional: + return AddPartitionSpecUpdate( + action = 'add-spec', + spec = polaris.catalog.models.partition_spec.PartitionSpec( + spec_id = 56, + fields = [ + polaris.catalog.models.partition_field.PartitionField( + field_id = 56, + source_id = 56, + name = '', + transform = '["identity","year","month","day","hour","bucket[256]","truncate[16]"]', ) + ], ) + ) + else: + return AddPartitionSpecUpdate( + action = 'add-spec', + spec = polaris.catalog.models.partition_spec.PartitionSpec( + spec_id = 56, + fields = [ + polaris.catalog.models.partition_field.PartitionField( + field_id = 56, + source_id = 56, + name = '', + transform = '["identity","year","month","day","hour","bucket[256]","truncate[16]"]', ) + ], ), + ) + """ + + def testAddPartitionSpecUpdate(self): + """Test AddPartitionSpecUpdate""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_add_schema_update.py b/regtests/client/python/test/test_add_schema_update.py new file mode 100644 index 0000000000..f78d8e0654 --- /dev/null +++ b/regtests/client/python/test/test_add_schema_update.py @@ -0,0 +1,55 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.add_schema_update import AddSchemaUpdate + +class TestAddSchemaUpdate(unittest.TestCase): + """AddSchemaUpdate unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> AddSchemaUpdate: + """Test AddSchemaUpdate + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `AddSchemaUpdate` + """ + model = AddSchemaUpdate() + if include_optional: + return AddSchemaUpdate( + action = 'add-schema', + var_schema = None, + last_column_id = 56 + ) + else: + return AddSchemaUpdate( + action = 'add-schema', + var_schema = None, + ) + """ + + def testAddSchemaUpdate(self): + """Test AddSchemaUpdate""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_add_snapshot_update.py b/regtests/client/python/test/test_add_snapshot_update.py new file mode 100644 index 0000000000..d721250f28 --- /dev/null +++ b/regtests/client/python/test/test_add_snapshot_update.py @@ -0,0 +1,72 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.add_snapshot_update import AddSnapshotUpdate + +class TestAddSnapshotUpdate(unittest.TestCase): + """AddSnapshotUpdate unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> AddSnapshotUpdate: + """Test AddSnapshotUpdate + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `AddSnapshotUpdate` + """ + model = AddSnapshotUpdate() + if include_optional: + return AddSnapshotUpdate( + action = 'add-snapshot', + snapshot = polaris.catalog.models.snapshot.Snapshot( + snapshot_id = 56, + parent_snapshot_id = 56, + sequence_number = 56, + timestamp_ms = 56, + manifest_list = '', + summary = { + 'key' : '' + }, + schema_id = 56, ) + ) + else: + return AddSnapshotUpdate( + action = 'add-snapshot', + snapshot = polaris.catalog.models.snapshot.Snapshot( + snapshot_id = 56, + parent_snapshot_id = 56, + sequence_number = 56, + timestamp_ms = 56, + manifest_list = '', + summary = { + 'key' : '' + }, + schema_id = 56, ), + ) + """ + + def testAddSnapshotUpdate(self): + """Test AddSnapshotUpdate""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_add_sort_order_update.py b/regtests/client/python/test/test_add_sort_order_update.py new file mode 100644 index 0000000000..4a107cdf2d --- /dev/null +++ b/regtests/client/python/test/test_add_sort_order_update.py @@ -0,0 +1,70 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.add_sort_order_update import AddSortOrderUpdate + +class TestAddSortOrderUpdate(unittest.TestCase): + """AddSortOrderUpdate unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> AddSortOrderUpdate: + """Test AddSortOrderUpdate + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `AddSortOrderUpdate` + """ + model = AddSortOrderUpdate() + if include_optional: + return AddSortOrderUpdate( + action = 'add-sort-order', + sort_order = polaris.catalog.models.sort_order.SortOrder( + order_id = 56, + fields = [ + polaris.catalog.models.sort_field.SortField( + source_id = 56, + transform = '["identity","year","month","day","hour","bucket[256]","truncate[16]"]', + direction = 'asc', + null_order = 'nulls-first', ) + ], ) + ) + else: + return AddSortOrderUpdate( + action = 'add-sort-order', + sort_order = polaris.catalog.models.sort_order.SortOrder( + order_id = 56, + fields = [ + polaris.catalog.models.sort_field.SortField( + source_id = 56, + transform = '["identity","year","month","day","hour","bucket[256]","truncate[16]"]', + direction = 'asc', + null_order = 'nulls-first', ) + ], ), + ) + """ + + def testAddSortOrderUpdate(self): + """Test AddSortOrderUpdate""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_add_view_version_update.py b/regtests/client/python/test/test_add_view_version_update.py new file mode 100644 index 0000000000..49d5af5fe0 --- /dev/null +++ b/regtests/client/python/test/test_add_view_version_update.py @@ -0,0 +1,76 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.add_view_version_update import AddViewVersionUpdate + +class TestAddViewVersionUpdate(unittest.TestCase): + """AddViewVersionUpdate unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> AddViewVersionUpdate: + """Test AddViewVersionUpdate + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `AddViewVersionUpdate` + """ + model = AddViewVersionUpdate() + if include_optional: + return AddViewVersionUpdate( + action = 'add-view-version', + view_version = polaris.catalog.models.view_version.ViewVersion( + version_id = 56, + timestamp_ms = 56, + schema_id = 56, + summary = { + 'key' : '' + }, + representations = [ + null + ], + default_catalog = '', + default_namespace = ["accounting","tax"], ) + ) + else: + return AddViewVersionUpdate( + action = 'add-view-version', + view_version = polaris.catalog.models.view_version.ViewVersion( + version_id = 56, + timestamp_ms = 56, + schema_id = 56, + summary = { + 'key' : '' + }, + representations = [ + null + ], + default_catalog = '', + default_namespace = ["accounting","tax"], ), + ) + """ + + def testAddViewVersionUpdate(self): + """Test AddViewVersionUpdate""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_and_or_expression.py b/regtests/client/python/test/test_and_or_expression.py new file mode 100644 index 0000000000..6a9f2aa339 --- /dev/null +++ b/regtests/client/python/test/test_and_or_expression.py @@ -0,0 +1,56 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.and_or_expression import AndOrExpression + +class TestAndOrExpression(unittest.TestCase): + """AndOrExpression unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> AndOrExpression: + """Test AndOrExpression + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `AndOrExpression` + """ + model = AndOrExpression() + if include_optional: + return AndOrExpression( + type = '["eq","and","or","not","in","not-in","lt","lt-eq","gt","gt-eq","not-eq","starts-with","not-starts-with","is-null","not-null","is-nan","not-nan"]', + left = None, + right = None + ) + else: + return AndOrExpression( + type = '["eq","and","or","not","in","not-in","lt","lt-eq","gt","gt-eq","not-eq","starts-with","not-starts-with","is-null","not-null","is-nan","not-nan"]', + left = None, + right = None, + ) + """ + + def testAndOrExpression(self): + """Test AndOrExpression""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_assert_create.py b/regtests/client/python/test/test_assert_create.py new file mode 100644 index 0000000000..60d5ebc777 --- /dev/null +++ b/regtests/client/python/test/test_assert_create.py @@ -0,0 +1,52 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.assert_create import AssertCreate + +class TestAssertCreate(unittest.TestCase): + """AssertCreate unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> AssertCreate: + """Test AssertCreate + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `AssertCreate` + """ + model = AssertCreate() + if include_optional: + return AssertCreate( + type = 'assert-create' + ) + else: + return AssertCreate( + type = 'assert-create', + ) + """ + + def testAssertCreate(self): + """Test AssertCreate""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_assert_current_schema_id.py b/regtests/client/python/test/test_assert_current_schema_id.py new file mode 100644 index 0000000000..776bbe8e00 --- /dev/null +++ b/regtests/client/python/test/test_assert_current_schema_id.py @@ -0,0 +1,54 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.assert_current_schema_id import AssertCurrentSchemaId + +class TestAssertCurrentSchemaId(unittest.TestCase): + """AssertCurrentSchemaId unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> AssertCurrentSchemaId: + """Test AssertCurrentSchemaId + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `AssertCurrentSchemaId` + """ + model = AssertCurrentSchemaId() + if include_optional: + return AssertCurrentSchemaId( + type = 'assert-current-schema-id', + current_schema_id = 56 + ) + else: + return AssertCurrentSchemaId( + type = 'assert-current-schema-id', + current_schema_id = 56, + ) + """ + + def testAssertCurrentSchemaId(self): + """Test AssertCurrentSchemaId""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_assert_default_sort_order_id.py b/regtests/client/python/test/test_assert_default_sort_order_id.py new file mode 100644 index 0000000000..a7ab462f1e --- /dev/null +++ b/regtests/client/python/test/test_assert_default_sort_order_id.py @@ -0,0 +1,54 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.assert_default_sort_order_id import AssertDefaultSortOrderId + +class TestAssertDefaultSortOrderId(unittest.TestCase): + """AssertDefaultSortOrderId unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> AssertDefaultSortOrderId: + """Test AssertDefaultSortOrderId + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `AssertDefaultSortOrderId` + """ + model = AssertDefaultSortOrderId() + if include_optional: + return AssertDefaultSortOrderId( + type = 'assert-default-sort-order-id', + default_sort_order_id = 56 + ) + else: + return AssertDefaultSortOrderId( + type = 'assert-default-sort-order-id', + default_sort_order_id = 56, + ) + """ + + def testAssertDefaultSortOrderId(self): + """Test AssertDefaultSortOrderId""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_assert_default_spec_id.py b/regtests/client/python/test/test_assert_default_spec_id.py new file mode 100644 index 0000000000..b3b042150a --- /dev/null +++ b/regtests/client/python/test/test_assert_default_spec_id.py @@ -0,0 +1,54 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.assert_default_spec_id import AssertDefaultSpecId + +class TestAssertDefaultSpecId(unittest.TestCase): + """AssertDefaultSpecId unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> AssertDefaultSpecId: + """Test AssertDefaultSpecId + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `AssertDefaultSpecId` + """ + model = AssertDefaultSpecId() + if include_optional: + return AssertDefaultSpecId( + type = 'assert-default-spec-id', + default_spec_id = 56 + ) + else: + return AssertDefaultSpecId( + type = 'assert-default-spec-id', + default_spec_id = 56, + ) + """ + + def testAssertDefaultSpecId(self): + """Test AssertDefaultSpecId""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_assert_last_assigned_field_id.py b/regtests/client/python/test/test_assert_last_assigned_field_id.py new file mode 100644 index 0000000000..b1a7be799d --- /dev/null +++ b/regtests/client/python/test/test_assert_last_assigned_field_id.py @@ -0,0 +1,54 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.assert_last_assigned_field_id import AssertLastAssignedFieldId + +class TestAssertLastAssignedFieldId(unittest.TestCase): + """AssertLastAssignedFieldId unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> AssertLastAssignedFieldId: + """Test AssertLastAssignedFieldId + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `AssertLastAssignedFieldId` + """ + model = AssertLastAssignedFieldId() + if include_optional: + return AssertLastAssignedFieldId( + type = 'assert-last-assigned-field-id', + last_assigned_field_id = 56 + ) + else: + return AssertLastAssignedFieldId( + type = 'assert-last-assigned-field-id', + last_assigned_field_id = 56, + ) + """ + + def testAssertLastAssignedFieldId(self): + """Test AssertLastAssignedFieldId""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_assert_last_assigned_partition_id.py b/regtests/client/python/test/test_assert_last_assigned_partition_id.py new file mode 100644 index 0000000000..80c5ffdb84 --- /dev/null +++ b/regtests/client/python/test/test_assert_last_assigned_partition_id.py @@ -0,0 +1,54 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.assert_last_assigned_partition_id import AssertLastAssignedPartitionId + +class TestAssertLastAssignedPartitionId(unittest.TestCase): + """AssertLastAssignedPartitionId unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> AssertLastAssignedPartitionId: + """Test AssertLastAssignedPartitionId + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `AssertLastAssignedPartitionId` + """ + model = AssertLastAssignedPartitionId() + if include_optional: + return AssertLastAssignedPartitionId( + type = 'assert-last-assigned-partition-id', + last_assigned_partition_id = 56 + ) + else: + return AssertLastAssignedPartitionId( + type = 'assert-last-assigned-partition-id', + last_assigned_partition_id = 56, + ) + """ + + def testAssertLastAssignedPartitionId(self): + """Test AssertLastAssignedPartitionId""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_assert_ref_snapshot_id.py b/regtests/client/python/test/test_assert_ref_snapshot_id.py new file mode 100644 index 0000000000..86e4e881fc --- /dev/null +++ b/regtests/client/python/test/test_assert_ref_snapshot_id.py @@ -0,0 +1,56 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.assert_ref_snapshot_id import AssertRefSnapshotId + +class TestAssertRefSnapshotId(unittest.TestCase): + """AssertRefSnapshotId unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> AssertRefSnapshotId: + """Test AssertRefSnapshotId + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `AssertRefSnapshotId` + """ + model = AssertRefSnapshotId() + if include_optional: + return AssertRefSnapshotId( + type = 'assert-ref-snapshot-id', + ref = '', + snapshot_id = 56 + ) + else: + return AssertRefSnapshotId( + type = 'assert-ref-snapshot-id', + ref = '', + snapshot_id = 56, + ) + """ + + def testAssertRefSnapshotId(self): + """Test AssertRefSnapshotId""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_assert_table_uuid.py b/regtests/client/python/test/test_assert_table_uuid.py new file mode 100644 index 0000000000..58cff6f684 --- /dev/null +++ b/regtests/client/python/test/test_assert_table_uuid.py @@ -0,0 +1,54 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.assert_table_uuid import AssertTableUUID + +class TestAssertTableUUID(unittest.TestCase): + """AssertTableUUID unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> AssertTableUUID: + """Test AssertTableUUID + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `AssertTableUUID` + """ + model = AssertTableUUID() + if include_optional: + return AssertTableUUID( + type = 'assert-table-uuid', + uuid = '' + ) + else: + return AssertTableUUID( + type = 'assert-table-uuid', + uuid = '', + ) + """ + + def testAssertTableUUID(self): + """Test AssertTableUUID""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_assert_view_uuid.py b/regtests/client/python/test/test_assert_view_uuid.py new file mode 100644 index 0000000000..43a85d1512 --- /dev/null +++ b/regtests/client/python/test/test_assert_view_uuid.py @@ -0,0 +1,54 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.assert_view_uuid import AssertViewUUID + +class TestAssertViewUUID(unittest.TestCase): + """AssertViewUUID unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> AssertViewUUID: + """Test AssertViewUUID + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `AssertViewUUID` + """ + model = AssertViewUUID() + if include_optional: + return AssertViewUUID( + type = 'assert-view-uuid', + uuid = '' + ) + else: + return AssertViewUUID( + type = 'assert-view-uuid', + uuid = '', + ) + """ + + def testAssertViewUUID(self): + """Test AssertViewUUID""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_assign_uuid_update.py b/regtests/client/python/test/test_assign_uuid_update.py new file mode 100644 index 0000000000..cea2b51cee --- /dev/null +++ b/regtests/client/python/test/test_assign_uuid_update.py @@ -0,0 +1,54 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.assign_uuid_update import AssignUUIDUpdate + +class TestAssignUUIDUpdate(unittest.TestCase): + """AssignUUIDUpdate unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> AssignUUIDUpdate: + """Test AssignUUIDUpdate + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `AssignUUIDUpdate` + """ + model = AssignUUIDUpdate() + if include_optional: + return AssignUUIDUpdate( + action = 'assign-uuid', + uuid = '' + ) + else: + return AssignUUIDUpdate( + action = 'assign-uuid', + uuid = '', + ) + """ + + def testAssignUUIDUpdate(self): + """Test AssignUUIDUpdate""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_aws_storage_config_info.py b/regtests/client/python/test/test_aws_storage_config_info.py new file mode 100644 index 0000000000..c2dbb884fb --- /dev/null +++ b/regtests/client/python/test/test_aws_storage_config_info.py @@ -0,0 +1,54 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.management.models.aws_storage_config_info import AwsStorageConfigInfo + +class TestAwsStorageConfigInfo(unittest.TestCase): + """AwsStorageConfigInfo unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> AwsStorageConfigInfo: + """Test AwsStorageConfigInfo + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `AwsStorageConfigInfo` + """ + model = AwsStorageConfigInfo() + if include_optional: + return AwsStorageConfigInfo( + role_arn = 'arn:aws:iam::123456789001:principal/abc1-b-self1234', + external_id = '', + user_arn = 'arn:aws:iam::123456789001:user/abc1-b-self1234' + ) + else: + return AwsStorageConfigInfo( + role_arn = 'arn:aws:iam::123456789001:principal/abc1-b-self1234', + ) + """ + + def testAwsStorageConfigInfo(self): + """Test AwsStorageConfigInfo""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_azure_storage_config_info.py b/regtests/client/python/test/test_azure_storage_config_info.py new file mode 100644 index 0000000000..bca4296847 --- /dev/null +++ b/regtests/client/python/test/test_azure_storage_config_info.py @@ -0,0 +1,54 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.management.models.azure_storage_config_info import AzureStorageConfigInfo + +class TestAzureStorageConfigInfo(unittest.TestCase): + """AzureStorageConfigInfo unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> AzureStorageConfigInfo: + """Test AzureStorageConfigInfo + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `AzureStorageConfigInfo` + """ + model = AzureStorageConfigInfo() + if include_optional: + return AzureStorageConfigInfo( + tenant_id = '', + multi_tenant_app_name = '', + consent_url = '' + ) + else: + return AzureStorageConfigInfo( + tenant_id = '', + ) + """ + + def testAzureStorageConfigInfo(self): + """Test AzureStorageConfigInfo""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_base_update.py b/regtests/client/python/test/test_base_update.py new file mode 100644 index 0000000000..a4c263be3a --- /dev/null +++ b/regtests/client/python/test/test_base_update.py @@ -0,0 +1,52 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.base_update import BaseUpdate + +class TestBaseUpdate(unittest.TestCase): + """BaseUpdate unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> BaseUpdate: + """Test BaseUpdate + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `BaseUpdate` + """ + model = BaseUpdate() + if include_optional: + return BaseUpdate( + action = '' + ) + else: + return BaseUpdate( + action = '', + ) + """ + + def testBaseUpdate(self): + """Test BaseUpdate""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_blob_metadata.py b/regtests/client/python/test/test_blob_metadata.py new file mode 100644 index 0000000000..ad2108f421 --- /dev/null +++ b/regtests/client/python/test/test_blob_metadata.py @@ -0,0 +1,63 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.blob_metadata import BlobMetadata + +class TestBlobMetadata(unittest.TestCase): + """BlobMetadata unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> BlobMetadata: + """Test BlobMetadata + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `BlobMetadata` + """ + model = BlobMetadata() + if include_optional: + return BlobMetadata( + type = '', + snapshot_id = 56, + sequence_number = 56, + fields = [ + 56 + ], + properties = polaris.catalog.models.properties.properties() + ) + else: + return BlobMetadata( + type = '', + snapshot_id = 56, + sequence_number = 56, + fields = [ + 56 + ], + ) + """ + + def testBlobMetadata(self): + """Test BlobMetadata""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_catalog.py b/regtests/client/python/test/test_catalog.py new file mode 100644 index 0000000000..4621a8645a --- /dev/null +++ b/regtests/client/python/test/test_catalog.py @@ -0,0 +1,64 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.management.models.catalog import Catalog + +class TestCatalog(unittest.TestCase): + """Catalog unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> Catalog: + """Test Catalog + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `Catalog` + """ + model = Catalog() + if include_optional: + return Catalog( + type = 'INTERNAL', + name = '', + read_only = True, + properties = { + 'key' : '' + }, + create_timestamp = 56, + last_update_timestamp = 56, + entity_version = 56, + storage_config_info = polaris.management.models.storage_config_info.StorageConfigInfo( + storage_type = 'S3', + allowed_locations = For AWS [s3://bucketname/prefix/], for AZURE [abfss://container@storageaccount.blob.core.windows.net/prefix/], for GCP [gs://bucketname/prefix/], ) + ) + else: + return Catalog( + type = 'INTERNAL', + name = '', + ) + """ + + def testCatalog(self): + """Test Catalog""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_catalog_config.py b/regtests/client/python/test/test_catalog_config.py new file mode 100644 index 0000000000..1f32966dc8 --- /dev/null +++ b/regtests/client/python/test/test_catalog_config.py @@ -0,0 +1,62 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.catalog_config import CatalogConfig + +class TestCatalogConfig(unittest.TestCase): + """CatalogConfig unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> CatalogConfig: + """Test CatalogConfig + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `CatalogConfig` + """ + model = CatalogConfig() + if include_optional: + return CatalogConfig( + overrides = { + 'key' : '' + }, + defaults = { + 'key' : '' + } + ) + else: + return CatalogConfig( + overrides = { + 'key' : '' + }, + defaults = { + 'key' : '' + }, + ) + """ + + def testCatalogConfig(self): + """Test CatalogConfig""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_catalog_grant.py b/regtests/client/python/test/test_catalog_grant.py new file mode 100644 index 0000000000..3dd5ced200 --- /dev/null +++ b/regtests/client/python/test/test_catalog_grant.py @@ -0,0 +1,52 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.management.models.catalog_grant import CatalogGrant + +class TestCatalogGrant(unittest.TestCase): + """CatalogGrant unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> CatalogGrant: + """Test CatalogGrant + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `CatalogGrant` + """ + model = CatalogGrant() + if include_optional: + return CatalogGrant( + privilege = 'CATALOG_MANAGE_ACCESS' + ) + else: + return CatalogGrant( + privilege = 'CATALOG_MANAGE_ACCESS', + ) + """ + + def testCatalogGrant(self): + """Test CatalogGrant""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_catalog_privilege.py b/regtests/client/python/test/test_catalog_privilege.py new file mode 100644 index 0000000000..4aec06aed8 --- /dev/null +++ b/regtests/client/python/test/test_catalog_privilege.py @@ -0,0 +1,33 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.management.models.catalog_privilege import CatalogPrivilege + +class TestCatalogPrivilege(unittest.TestCase): + """CatalogPrivilege unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def testCatalogPrivilege(self): + """Test CatalogPrivilege""" + # inst = CatalogPrivilege() + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_catalog_properties.py b/regtests/client/python/test/test_catalog_properties.py new file mode 100644 index 0000000000..75d7a03a97 --- /dev/null +++ b/regtests/client/python/test/test_catalog_properties.py @@ -0,0 +1,52 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.management.models.catalog_properties import CatalogProperties + +class TestCatalogProperties(unittest.TestCase): + """CatalogProperties unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> CatalogProperties: + """Test CatalogProperties + include_optional is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `CatalogProperties` + """ + model = CatalogProperties() + if include_optional: + return CatalogProperties( + default_base_location = '' + ) + else: + return CatalogProperties( + default_base_location = '', + ) + """ + + def testCatalogProperties(self): + """Test CatalogProperties""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_catalog_role.py b/regtests/client/python/test/test_catalog_role.py new file mode 100644 index 0000000000..f55011539c --- /dev/null +++ b/regtests/client/python/test/test_catalog_role.py @@ -0,0 +1,58 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.management.models.catalog_role import CatalogRole + +class TestCatalogRole(unittest.TestCase): + """CatalogRole unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> CatalogRole: + """Test CatalogRole + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `CatalogRole` + """ + model = CatalogRole() + if include_optional: + return CatalogRole( + name = '', + properties = { + 'key' : '' + }, + create_timestamp = 56, + last_update_timestamp = 56, + entity_version = 56 + ) + else: + return CatalogRole( + name = '', + ) + """ + + def testCatalogRole(self): + """Test CatalogRole""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_catalog_roles.py b/regtests/client/python/test/test_catalog_roles.py new file mode 100644 index 0000000000..dd348af3d7 --- /dev/null +++ b/regtests/client/python/test/test_catalog_roles.py @@ -0,0 +1,70 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.management.models.catalog_roles import CatalogRoles + +class TestCatalogRoles(unittest.TestCase): + """CatalogRoles unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> CatalogRoles: + """Test CatalogRoles + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `CatalogRoles` + """ + model = CatalogRoles() + if include_optional: + return CatalogRoles( + roles = [ + polaris.management.models.catalog_role.CatalogRole( + name = '', + properties = { + 'key' : '' + }, + create_timestamp = 56, + last_update_timestamp = 56, + entity_version = 56, ) + ] + ) + else: + return CatalogRoles( + roles = [ + polaris.management.models.catalog_role.CatalogRole( + name = '', + properties = { + 'key' : '' + }, + create_timestamp = 56, + last_update_timestamp = 56, + entity_version = 56, ) + ], + ) + """ + + def testCatalogRoles(self): + """Test CatalogRoles""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_catalogs.py b/regtests/client/python/test/test_catalogs.py new file mode 100644 index 0000000000..cd5fc8e41c --- /dev/null +++ b/regtests/client/python/test/test_catalogs.py @@ -0,0 +1,80 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.management.models.catalogs import Catalogs + +class TestCatalogs(unittest.TestCase): + """Catalogs unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> Catalogs: + """Test Catalogs + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `Catalogs` + """ + model = Catalogs() + if include_optional: + return Catalogs( + catalogs = [ + polaris.management.models.catalog.Catalog( + type = 'INTERNAL', + name = '', + read_only = True, + properties = { + 'key' : '' + }, + create_timestamp = 56, + last_update_timestamp = 56, + entity_version = 56, + storage_config_info = polaris.management.models.storage_config_info.StorageConfigInfo( + storage_type = 'S3', + allowed_locations = For AWS [s3://bucketname/prefix/], for AZURE [abfss://container@storageaccount.blob.core.windows.net/prefix/], for GCP [gs://bucketname/prefix/], ), ) + ] + ) + else: + return Catalogs( + catalogs = [ + polaris.management.models.catalog.Catalog( + type = 'INTERNAL', + name = '', + read_only = True, + properties = { + 'key' : '' + }, + create_timestamp = 56, + last_update_timestamp = 56, + entity_version = 56, + storage_config_info = polaris.management.models.storage_config_info.StorageConfigInfo( + storage_type = 'S3', + allowed_locations = For AWS [s3://bucketname/prefix/], for AZURE [abfss://container@storageaccount.blob.core.windows.net/prefix/], for GCP [gs://bucketname/prefix/], ), ) + ], + ) + """ + + def testCatalogs(self): + """Test Catalogs""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_cli_parsing.py b/regtests/client/python/test/test_cli_parsing.py new file mode 100644 index 0000000000..b52d30feed --- /dev/null +++ b/regtests/client/python/test/test_cli_parsing.py @@ -0,0 +1,419 @@ + +import unittest +import io +from functools import reduce +from typing import List +from unittest.mock import patch, MagicMock + +from cli.command import Command +from cli.options.parser import Parser +from polaris.catalog import ApiClient +from polaris.management import PolarisDefaultApi + +INVALID_ARGS = 2 + + +class TestCliParsing(unittest.TestCase): + + def test_invalid_commands(self): + with self.assertRaises(SystemExit) as cm: + Parser.parse(['not-real-command!', 'list']) + self.assertEqual(cm.exception.code, INVALID_ARGS) + + with self.assertRaises(SystemExit) as cm: + Parser.parse(['catalogs', 'not-real-subcommand']) + self.assertEqual(cm.exception.code, INVALID_ARGS) + + with self.assertRaises(SystemExit) as cm: + Parser.parse(['catalogs', 'create']) # missing required input + self.assertEqual(cm.exception.code, INVALID_ARGS) + + with self.assertRaises(SystemExit) as cm: + Parser.parse(['catalogs', 'create', 'catalog_name', '--type', 'BANANA']) # invalid catalog type + self.assertEqual(cm.exception.code, INVALID_ARGS) + + with self.assertRaises(SystemExit) as cm: + Parser.parse(['catalogs', 'get', 'catalog_name', '--fake-flag']) + self.assertEqual(cm.exception.code, INVALID_ARGS) + + with self.assertRaises(SystemExit) as cm: + Parser.parse(['principals', 'create', 'name', '--type', 'bad']) + self.assertEqual(cm.exception.code, INVALID_ARGS) + + with self.assertRaises(SystemExit) as cm: + Parser.parse(['principals', 'update', 'name', '--client-id', 'something']) + self.assertEqual(cm.exception.code, INVALID_ARGS) + + with self.assertRaises(SystemExit) as cm: + Parser.parse(['privileges', 'catalog', '--catalog', 'c', '--catalog-role', 'r', 'privilege', 'grant']) + self.assertEqual(cm.exception.code, INVALID_ARGS) + + with self.assertRaises(SystemExit) as cm: + Parser.parse(['privileges', '--catalog', 'c', '--catalog-role', 'r', 'catalog', 'grant', 'privilege', + '--namespace', 'unexpected!']) + self.assertEqual(cm.exception.code, INVALID_ARGS) + + def _check_usage_output(self, f, needle='usage:'): + with patch('sys.stdout', new_callable=io.StringIO) as mock_stdout, \ + patch('sys.stderr', new_callable=io.StringIO) as mock_stderr: + with self.assertRaises(SystemExit) as cm: + f() + self.assertEqual(cm.exception.code, 0) + help_output = str(mock_stdout.getvalue()) + self.assertIn('usage:', help_output) + print(help_output) + + def test_usage(self): + self._check_usage_output(lambda: Parser.parse(['--help'])) + self._check_usage_output(lambda: Parser.parse(['catalogs', '--help'])) + self._check_usage_output(lambda: Parser.parse(['catalogs', 'create', '--help'])) + self._check_usage_output(lambda: Parser.parse(['catalogs', 'create', 'something', '--help'])) + + def test_extended_usage(self): + self._check_usage_output(lambda: Parser._build_parser().parse_args(['--help'], 'input:')) + self._check_usage_output(lambda: Parser._build_parser().parse_args(['catalogs', '--help'], 'input:')) + self._check_usage_output(lambda: Parser._build_parser().parse_args(['catalogs', 'create', '--help'], 'input:')) + self._check_usage_output(lambda: Parser._build_parser().parse_args([ + 'catalogs', 'create', 'c', '--help'], 'input:')) + self._check_usage_output(lambda: Parser._build_parser().parse_args([ + 'privileges', 'table', 'grant', '--help'], 'input:')) + self._check_usage_output(lambda: Parser.parse(['catalogs', 'create', 'something', '--help']), 'input:') + + def test_parsing_valid_commands(self): + Parser.parse(['catalogs', 'create', 'catalog_name']) + Parser.parse(['catalogs', 'create', 'catalog_name', '--type', 'internal']) + Parser.parse(['catalogs', 'create', 'catalog_name', '--type', 'INTERNAL']) + Parser.parse(['catalogs', 'list']) + Parser.parse(['catalogs', 'get', 'catalog_name']) + Parser.parse(['principals', 'list']) + Parser.parse(['--host', 'some-host', 'catalogs', 'list']) + Parser.parse(['privileges', '--catalog', 'foo', '--catalog-role', 'bar', 'catalog', 'grant', 'TABLE_READ_DATA']) + Parser.parse(['privileges', '--catalog', 'foo', '--catalog-role', 'bar', 'table', 'grant', + '--namespace', 'n', '--table', 't', 'TABLE_READ_DATA']) + Parser.parse(['privileges', '--catalog', 'foo', '--catalog-role', 'bar', 'table', 'revoke', + '--namespace', 'n', '--table', 't', 'TABLE_READ_DATA']) + + # These commands are valid for parsing, but may cause errors within the command itself + def test_parse_valid_commands(self): + Parser.parse(['catalogs', 'create', 'catalog_name', '--type', 'internal', '--remote-url', 'www.apache.org']) + Parser.parse(['privileges', 'table', 'grant', + '--namespace', 'n', '--table', 't', 'TABLE_READ_DATA']) + Parser.parse(['privileges', '--catalog', 'c', '--catalog-role', 'r', 'catalog', 'grant', 'fake-privilege']) + + def test_commands(self): + + def build_mock_client(): + client = MagicMock(spec=PolarisDefaultApi) + client.call_tracker = dict() + + def capture_method(method_name): + def _capture(*args, **kwargs): + client.call_tracker['_method'] = method_name + for i, arg in enumerate(args): + if arg is not None: + client.call_tracker[i] = arg + + return _capture + + for method_name in dir(client): + if callable(getattr(client, method_name)) and not method_name.startswith('__'): + setattr(client, method_name, + MagicMock(name=method_name, side_effect=capture_method(method_name))) + return client + mock_client = build_mock_client() + + def mock_execute(input: List[str]): + mock_client.call_tracker = dict() + + # Assuming Parser and Command are used to parse input and generate commands + options = Parser.parse(input) + command = Command.from_options(options) + + try: + command.execute(mock_client) + except AttributeError as e: + # Some commands may fail due to the mock, but the results should still match expectations + print(f'Suppressed error: {e}') + return mock_client.call_tracker + + def check_exception(f, exception_str): + throws = True + try: + f() + throws = False + except Exception as e: + self.assertIn(exception_str, str(e)) + self.assertTrue(throws, 'Exception should be raised') + + def check_arguments(result, method_name, args=dict()): + self.assertEqual(method_name, result['_method']) + + def get(obj, arg_string): + attributes = arg_string.split('.') + return reduce(getattr, attributes, obj) + + for arg, value in args.items(): + index, path = arg + if path is not None: + self.assertEqual(value, get(result[index], path)) + else: + self.assertEqual(value, result[index]) + + # Test various failing commands: + check_exception(lambda: mock_execute(['catalogs', 'create', 'my-catalog']), + '--storage-type') + check_exception(lambda: mock_execute(['catalogs', 'create', 'my-catalog', '--storage-type', 'gcs']), + '--default-base-location') + check_exception(lambda: mock_execute(['catalogs', 'create', 'my-catalog', '--type', 'external', + '--default-base-location', 'x', '--storage-type', 'gcs']), + '--remote-url') + check_exception(lambda: mock_execute(['catalog-roles', 'get', 'foo']), + '--catalog') + check_exception(lambda: mock_execute(['catalogs', 'update', 'foo', '--property', 'bad-format']), + 'bad-format') + check_exception(lambda: mock_execute(['privileges', '--catalog', 'foo', '--catalog-role', 'bar', + 'catalog', 'grant', 'TABLE_READ_MORE_BOOKS']), + 'catalog privilege: TABLE_READ_MORE_BOOKS') + check_exception(lambda: mock_execute(['catalogs', 'create', 'my-catalog', '--storage-type', 'gcs', + '--allowed-location', 'a', '--allowed-location', 'b', + '--role-arn', 'ra', '--default-base-location', 'x']), + 'gcs') + + # Test various correct commands: + check_arguments( + mock_execute(['catalogs', 'create', 'my-catalog', '--storage-type', 'gcs', '--default-base-location', 'x']), + 'create_catalog', { + (0, 'catalog.name'): 'my-catalog', + (0, 'catalog.storage_config_info.storage_type'): 'GCS', + (0, 'catalog.properties.default_base_location'): 'x', + }) + check_arguments( + mock_execute(['catalogs', 'create', 'my-catalog', '--type', 'external', '--remote-url', 'foo.bar', + '--storage-type', 'gcs', '--default-base-location', 'dbl']), + 'create_catalog', { + (0, 'catalog.name'): 'my-catalog', + (0, 'catalog.type'): 'EXTERNAL', + (0, 'catalog.remote_url'): 'foo.bar', + }) + check_arguments( + mock_execute([ + 'catalogs', 'create', 'my-catalog', '--storage-type', 's3', + '--allowed-location', 'a', '--allowed-location', 'b', '--role-arn', 'ra', + '--user-arn', 'ua', '--external-id', 'ei', '--default-base-location', 'x']), + 'create_catalog', { + (0, 'catalog.name'): 'my-catalog', + (0, 'catalog.storage_config_info.storage_type'): 'S3', + (0, 'catalog.properties.default_base_location'): 'x', + (0, 'catalog.storage_config_info.allowed_locations'): ['a', 'b'], + }) + check_arguments(mock_execute(['catalogs', 'list']), 'list_catalogs') + check_arguments(mock_execute(['catalogs', 'delete', 'foo']), 'delete_catalog', { + (0, None): 'foo', + }) + check_arguments(mock_execute(['catalogs', 'get', 'foo']), 'get_catalog', { + (0, None): 'foo', + }) + check_arguments( + mock_execute(['catalogs', 'update', 'foo', '--default-base-location', 'x']), + 'get_catalog', { + (0, None): 'foo', + }) + check_arguments( + mock_execute(['principals', 'create', 'foo', '--client-id', 'id', '--property', 'key=value']), + 'create_principal', { + (0, 'principal.name'): 'foo', + (0, 'principal.client_id'): 'id', + (0, 'principal.properties'): {'key': 'value'}, + }) + check_arguments( + mock_execute(['principals', 'delete', 'foo']), + 'delete_principal', { + (0, None): 'foo', + }) + check_arguments( + mock_execute(['principals', 'get', 'foo']), + 'get_principal', { + (0, None): 'foo', + }) + check_arguments(mock_execute(['principals', 'list']), 'list_principals') + check_arguments( + mock_execute(['principals', 'rotate-credentials', 'foo']), + 'rotate_credentials', { + (0, None): 'foo', + }) + check_arguments( + mock_execute(['principals', 'update', 'foo', '--property', 'key=value']), + 'get_principal', { + (0, None): 'foo', + }) + check_arguments( + mock_execute(['principal-roles', 'create', 'foo']), + 'create_principal_role', { + (0, 'principal_role.name'): 'foo', + }) + check_arguments( + mock_execute(['principal-roles', 'delete', 'foo']), + 'delete_principal_role', { + (0, None): 'foo', + }) + check_arguments( + mock_execute(['principal-roles', 'delete', 'foo']), + 'delete_principal_role', { + (0, None): 'foo', + }) + check_arguments( + mock_execute(['principal-roles', 'get', 'foo']), + 'get_principal_role', { + (0, None): 'foo', + }) + check_arguments( + mock_execute(['principal-roles', 'get', 'foo']), + 'get_principal_role', { + (0, None): 'foo', + }) + check_arguments(mock_execute(['principal-roles', 'list']), 'list_principal_roles') + check_arguments( + mock_execute(['principal-roles', 'list', '--principal', 'foo']), + 'list_principal_roles_assigned', { + (0, None): 'foo', + }) + check_arguments( + mock_execute(['principal-roles', 'update', 'foo', '--property', 'key=value']), + 'get_principal_role', { + (0, None): 'foo' + }) + check_arguments( + mock_execute(['principal-roles', 'update', 'foo', '--property', 'key=value']), + 'get_principal_role', { + (0, None): 'foo', + }) + check_arguments( + mock_execute(['principal-roles', 'grant', 'bar', '--principal', 'foo']), + 'assign_principal_role', { + (0, None): 'foo', + (1, 'principal_role.name'): 'bar', + }) + check_arguments( + mock_execute(['principal-roles', 'revoke', 'bar', '--principal', 'foo']), + 'revoke_principal_role', { + (0, None): 'foo', + (1, None): 'bar', + }) + check_arguments( + mock_execute( + ['catalog-roles', 'create', 'foo', '--catalog', 'bar', '--property', 'key=value']), + 'create_catalog_role', { + (0, None): 'bar', + (1, 'catalog_role.name'): 'foo', + (1, 'catalog_role.properties'): {'key': 'value'}, + }) + check_arguments( + mock_execute( + ['catalog-roles', 'delete', 'foo', '--catalog', 'bar']), + 'delete_catalog_role', { + (0, None): 'bar', + (1, None): 'foo', + }) + check_arguments( + mock_execute( + ['catalog-roles', 'get', 'foo', '--catalog', 'bar']), + 'get_catalog_role', { + (0, None): 'bar', + (1, None): 'foo', + }) + check_arguments(mock_execute( + ['catalog-roles', 'list', 'foo']), + 'list_catalog_roles', { + (0, None): 'foo', + }) + check_arguments(mock_execute( + ['catalog-roles', 'list', 'foo', '--principal-role', 'bar']), + 'list_catalog_roles_for_principal_role', { + (0, None): 'bar', + (1, None): 'foo', + }) + check_arguments(mock_execute( + ['catalog-roles', 'update', 'foo', '--catalog', 'bar', '--property', 'key=value']), + 'get_catalog_role', { + (0, None): 'bar', + (1, None): 'foo', + }) + check_arguments( + mock_execute(['catalog-roles', 'grant', '--principal-role', 'foo', '--catalog', 'bar', 'baz']), + 'assign_catalog_role_to_principal_role', { + (0, None): 'foo', + (1, None): 'bar', + (2, 'catalog_role.name'): 'baz', + }) + check_arguments( + mock_execute(['catalog-roles', 'revoke', '--principal-role', 'foo', '--catalog', 'bar', 'baz']), + 'revoke_catalog_role_from_principal_role', { + (0, None): 'foo', + (1, None): 'bar', + (2, None): 'baz', + }) + check_arguments( + mock_execute( + ['privileges', '--catalog', 'foo', '--catalog-role', 'bar', 'catalog', 'grant', 'TABLE_READ_DATA']), + 'add_grant_to_catalog_role', { + (0, None): 'foo', + (1, None): 'bar', + (2, 'grant.privilege.value'): 'TABLE_READ_DATA', + }) + check_arguments( + mock_execute( + ['privileges', '--catalog', 'foo', '--catalog-role', 'bar', 'catalog', 'revoke', 'TABLE_READ_DATA']), + 'revoke_grant_from_catalog_role', { + (0, None): 'foo', + (1, None): 'bar', + (2, None): False, + (3, 'grant.privilege.value'): 'TABLE_READ_DATA', + }) + check_arguments( + mock_execute( + ['privileges', '--catalog', 'foo', '--catalog-role', 'bar', 'namespace', 'grant', '--namespace', 'a.b.c', + 'TABLE_READ_DATA']), + 'add_grant_to_catalog_role', { + (0, None): 'foo', + (1, None): 'bar', + (2, 'grant.privilege.value'): 'TABLE_READ_DATA', + (2, 'grant.namespace'): ['a', 'b', 'c'], + }) + check_arguments( + mock_execute( + ['privileges', '--catalog', 'foo', '--catalog-role', 'bar', 'table', 'grant', '--namespace', 'a.b.c', + '--table', 't', 'TABLE_READ_DATA']), + 'add_grant_to_catalog_role', { + (0, None): 'foo', + (1, None): 'bar', + (2, 'grant.privilege.value'): 'TABLE_READ_DATA', + (2, 'grant.namespace'): ['a', 'b', 'c'], + (2, 'grant.table_name'): 't', + }) + check_arguments( + mock_execute( + ['privileges', '--catalog', 'foo', '--catalog-role', 'bar', 'table', 'revoke', '--namespace', 'a.b.c', + '--table', 't', '--cascade', 'TABLE_READ_DATA']), + 'revoke_grant_from_catalog_role', { + (0, None): 'foo', + (1, None): 'bar', + (2, None): True, + (3, 'grant.privilege.value'): 'TABLE_READ_DATA', + (3, 'grant.namespace'): ['a', 'b', 'c'], + (3, 'grant.table_name'): 't', + }) + check_arguments( + mock_execute( + ['privileges', '--catalog', 'foo', '--catalog-role', 'bar', 'view', 'grant', '--namespace', 'a.b.c', + '--view', 'v', 'VIEW_CREATE']), + 'add_grant_to_catalog_role', { + (0, None): 'foo', + (1, None): 'bar', + (2, 'grant.privilege.value'): 'VIEW_CREATE', + (2, 'grant.namespace'): ['a', 'b', 'c'], + (2, 'grant.view_name'): 'v', + }) + + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_commit_report.py b/regtests/client/python/test/test_commit_report.py new file mode 100644 index 0000000000..122b669b7a --- /dev/null +++ b/regtests/client/python/test/test_commit_report.py @@ -0,0 +1,63 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.commit_report import CommitReport + +class TestCommitReport(unittest.TestCase): + """CommitReport unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> CommitReport: + """Test CommitReport + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `CommitReport` + """ + model = CommitReport() + if include_optional: + return CommitReport( + table_name = '', + snapshot_id = 56, + sequence_number = 56, + operation = '', + metrics = {"metrics":{"total-planning-duration":{"count":1,"time-unit":"nanoseconds","total-duration":2644235116},"result-data-files":{"unit":"count","value":1},"result-delete-files":{"unit":"count","value":0},"total-data-manifests":{"unit":"count","value":1},"total-delete-manifests":{"unit":"count","value":0},"scanned-data-manifests":{"unit":"count","value":1},"skipped-data-manifests":{"unit":"count","value":0},"total-file-size-bytes":{"unit":"bytes","value":10},"total-delete-file-size-bytes":{"unit":"bytes","value":0}}}, + metadata = { + 'key' : '' + } + ) + else: + return CommitReport( + table_name = '', + snapshot_id = 56, + sequence_number = 56, + operation = '', + metrics = {"metrics":{"total-planning-duration":{"count":1,"time-unit":"nanoseconds","total-duration":2644235116},"result-data-files":{"unit":"count","value":1},"result-delete-files":{"unit":"count","value":0},"total-data-manifests":{"unit":"count","value":1},"total-delete-manifests":{"unit":"count","value":0},"scanned-data-manifests":{"unit":"count","value":1},"skipped-data-manifests":{"unit":"count","value":0},"total-file-size-bytes":{"unit":"bytes","value":10},"total-delete-file-size-bytes":{"unit":"bytes","value":0}}}, + ) + """ + + def testCommitReport(self): + """Test CommitReport""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_commit_table_request.py b/regtests/client/python/test/test_commit_table_request.py new file mode 100644 index 0000000000..156d93c01f --- /dev/null +++ b/regtests/client/python/test/test_commit_table_request.py @@ -0,0 +1,67 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.commit_table_request import CommitTableRequest + +class TestCommitTableRequest(unittest.TestCase): + """CommitTableRequest unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> CommitTableRequest: + """Test CommitTableRequest + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `CommitTableRequest` + """ + model = CommitTableRequest() + if include_optional: + return CommitTableRequest( + identifier = polaris.catalog.models.table_identifier.TableIdentifier( + namespace = ["accounting","tax"], + name = '', ), + requirements = [ + polaris.catalog.models.table_requirement.TableRequirement( + type = '', ) + ], + updates = [ + null + ] + ) + else: + return CommitTableRequest( + requirements = [ + polaris.catalog.models.table_requirement.TableRequirement( + type = '', ) + ], + updates = [ + null + ], + ) + """ + + def testCommitTableRequest(self): + """Test CommitTableRequest""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_commit_table_response.py b/regtests/client/python/test/test_commit_table_response.py new file mode 100644 index 0000000000..4f679a31ec --- /dev/null +++ b/regtests/client/python/test/test_commit_table_response.py @@ -0,0 +1,236 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.commit_table_response import CommitTableResponse + +class TestCommitTableResponse(unittest.TestCase): + """CommitTableResponse unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> CommitTableResponse: + """Test CommitTableResponse + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `CommitTableResponse` + """ + model = CommitTableResponse() + if include_optional: + return CommitTableResponse( + metadata_location = '', + metadata = polaris.catalog.models.table_metadata.TableMetadata( + format_version = 1, + table_uuid = '', + location = '', + last_updated_ms = 56, + properties = { + 'key' : '' + }, + schemas = [ + null + ], + current_schema_id = 56, + last_column_id = 56, + partition_specs = [ + polaris.catalog.models.partition_spec.PartitionSpec( + spec_id = 56, + fields = [ + polaris.catalog.models.partition_field.PartitionField( + field_id = 56, + source_id = 56, + name = '', + transform = '["identity","year","month","day","hour","bucket[256]","truncate[16]"]', ) + ], ) + ], + default_spec_id = 56, + last_partition_id = 56, + sort_orders = [ + polaris.catalog.models.sort_order.SortOrder( + order_id = 56, + fields = [ + polaris.catalog.models.sort_field.SortField( + source_id = 56, + transform = '["identity","year","month","day","hour","bucket[256]","truncate[16]"]', + direction = 'asc', + null_order = 'nulls-first', ) + ], ) + ], + default_sort_order_id = 56, + snapshots = [ + polaris.catalog.models.snapshot.Snapshot( + snapshot_id = 56, + parent_snapshot_id = 56, + sequence_number = 56, + timestamp_ms = 56, + manifest_list = '', + summary = { + 'key' : '' + }, + schema_id = 56, ) + ], + refs = { + 'key' : polaris.catalog.models.snapshot_reference.SnapshotReference( + type = 'tag', + snapshot_id = 56, + max_ref_age_ms = 56, + max_snapshot_age_ms = 56, + min_snapshots_to_keep = 56, ) + }, + current_snapshot_id = 56, + last_sequence_number = 56, + snapshot_log = [ + polaris.catalog.models.snapshot_log_inner.SnapshotLog_inner( + snapshot_id = 56, + timestamp_ms = 56, ) + ], + metadata_log = [ + polaris.catalog.models.metadata_log_inner.MetadataLog_inner( + metadata_file = '', + timestamp_ms = 56, ) + ], + statistics_files = [ + polaris.catalog.models.statistics_file.StatisticsFile( + snapshot_id = 56, + statistics_path = '', + file_size_in_bytes = 56, + file_footer_size_in_bytes = 56, + blob_metadata = [ + polaris.catalog.models.blob_metadata.BlobMetadata( + type = '', + snapshot_id = 56, + sequence_number = 56, + fields = [ + 56 + ], ) + ], ) + ], + partition_statistics_files = [ + polaris.catalog.models.partition_statistics_file.PartitionStatisticsFile( + snapshot_id = 56, + statistics_path = '', + file_size_in_bytes = 56, ) + ], ) + ) + else: + return CommitTableResponse( + metadata_location = '', + metadata = polaris.catalog.models.table_metadata.TableMetadata( + format_version = 1, + table_uuid = '', + location = '', + last_updated_ms = 56, + properties = { + 'key' : '' + }, + schemas = [ + null + ], + current_schema_id = 56, + last_column_id = 56, + partition_specs = [ + polaris.catalog.models.partition_spec.PartitionSpec( + spec_id = 56, + fields = [ + polaris.catalog.models.partition_field.PartitionField( + field_id = 56, + source_id = 56, + name = '', + transform = '["identity","year","month","day","hour","bucket[256]","truncate[16]"]', ) + ], ) + ], + default_spec_id = 56, + last_partition_id = 56, + sort_orders = [ + polaris.catalog.models.sort_order.SortOrder( + order_id = 56, + fields = [ + polaris.catalog.models.sort_field.SortField( + source_id = 56, + transform = '["identity","year","month","day","hour","bucket[256]","truncate[16]"]', + direction = 'asc', + null_order = 'nulls-first', ) + ], ) + ], + default_sort_order_id = 56, + snapshots = [ + polaris.catalog.models.snapshot.Snapshot( + snapshot_id = 56, + parent_snapshot_id = 56, + sequence_number = 56, + timestamp_ms = 56, + manifest_list = '', + summary = { + 'key' : '' + }, + schema_id = 56, ) + ], + refs = { + 'key' : polaris.catalog.models.snapshot_reference.SnapshotReference( + type = 'tag', + snapshot_id = 56, + max_ref_age_ms = 56, + max_snapshot_age_ms = 56, + min_snapshots_to_keep = 56, ) + }, + current_snapshot_id = 56, + last_sequence_number = 56, + snapshot_log = [ + polaris.catalog.models.snapshot_log_inner.SnapshotLog_inner( + snapshot_id = 56, + timestamp_ms = 56, ) + ], + metadata_log = [ + polaris.catalog.models.metadata_log_inner.MetadataLog_inner( + metadata_file = '', + timestamp_ms = 56, ) + ], + statistics_files = [ + polaris.catalog.models.statistics_file.StatisticsFile( + snapshot_id = 56, + statistics_path = '', + file_size_in_bytes = 56, + file_footer_size_in_bytes = 56, + blob_metadata = [ + polaris.catalog.models.blob_metadata.BlobMetadata( + type = '', + snapshot_id = 56, + sequence_number = 56, + fields = [ + 56 + ], ) + ], ) + ], + partition_statistics_files = [ + polaris.catalog.models.partition_statistics_file.PartitionStatisticsFile( + snapshot_id = 56, + statistics_path = '', + file_size_in_bytes = 56, ) + ], ), + ) + """ + + def testCommitTableResponse(self): + """Test CommitTableResponse""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_commit_transaction_request.py b/regtests/client/python/test/test_commit_transaction_request.py new file mode 100644 index 0000000000..ced46110b5 --- /dev/null +++ b/regtests/client/python/test/test_commit_transaction_request.py @@ -0,0 +1,76 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.commit_transaction_request import CommitTransactionRequest + +class TestCommitTransactionRequest(unittest.TestCase): + """CommitTransactionRequest unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> CommitTransactionRequest: + """Test CommitTransactionRequest + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `CommitTransactionRequest` + """ + model = CommitTransactionRequest() + if include_optional: + return CommitTransactionRequest( + table_changes = [ + polaris.catalog.models.commit_table_request.CommitTableRequest( + identifier = polaris.catalog.models.table_identifier.TableIdentifier( + namespace = ["accounting","tax"], + name = '', ), + requirements = [ + polaris.catalog.models.table_requirement.TableRequirement( + type = '', ) + ], + updates = [ + null + ], ) + ] + ) + else: + return CommitTransactionRequest( + table_changes = [ + polaris.catalog.models.commit_table_request.CommitTableRequest( + identifier = polaris.catalog.models.table_identifier.TableIdentifier( + namespace = ["accounting","tax"], + name = '', ), + requirements = [ + polaris.catalog.models.table_requirement.TableRequirement( + type = '', ) + ], + updates = [ + null + ], ) + ], + ) + """ + + def testCommitTransactionRequest(self): + """Test CommitTransactionRequest""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_commit_view_request.py b/regtests/client/python/test/test_commit_view_request.py new file mode 100644 index 0000000000..2600455358 --- /dev/null +++ b/regtests/client/python/test/test_commit_view_request.py @@ -0,0 +1,63 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.commit_view_request import CommitViewRequest + +class TestCommitViewRequest(unittest.TestCase): + """CommitViewRequest unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> CommitViewRequest: + """Test CommitViewRequest + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `CommitViewRequest` + """ + model = CommitViewRequest() + if include_optional: + return CommitViewRequest( + identifier = polaris.catalog.models.table_identifier.TableIdentifier( + namespace = ["accounting","tax"], + name = '', ), + requirements = [ + polaris.catalog.models.view_requirement.ViewRequirement( + type = '', ) + ], + updates = [ + null + ] + ) + else: + return CommitViewRequest( + updates = [ + null + ], + ) + """ + + def testCommitViewRequest(self): + """Test CommitViewRequest""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_content_file.py b/regtests/client/python/test/test_content_file.py new file mode 100644 index 0000000000..37b59a878c --- /dev/null +++ b/regtests/client/python/test/test_content_file.py @@ -0,0 +1,68 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.content_file import ContentFile + +class TestContentFile(unittest.TestCase): + """ContentFile unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> ContentFile: + """Test ContentFile + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `ContentFile` + """ + model = ContentFile() + if include_optional: + return ContentFile( + content = '', + file_path = '', + file_format = 'avro', + spec_id = 56, + partition = [1,"bar"], + file_size_in_bytes = 56, + record_count = 56, + key_metadata = '78797A', + split_offsets = [ + 56 + ], + sort_order_id = 56 + ) + else: + return ContentFile( + content = '', + file_path = '', + file_format = 'avro', + spec_id = 56, + file_size_in_bytes = 56, + record_count = 56, + ) + """ + + def testContentFile(self): + """Test ContentFile""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_count_map.py b/regtests/client/python/test/test_count_map.py new file mode 100644 index 0000000000..c203cc0351 --- /dev/null +++ b/regtests/client/python/test/test_count_map.py @@ -0,0 +1,56 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.count_map import CountMap + +class TestCountMap(unittest.TestCase): + """CountMap unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> CountMap: + """Test CountMap + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `CountMap` + """ + model = CountMap() + if include_optional: + return CountMap( + keys = [ + 42 + ], + values = [ + 9223372036854775807 + ] + ) + else: + return CountMap( + ) + """ + + def testCountMap(self): + """Test CountMap""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_counter_result.py b/regtests/client/python/test/test_counter_result.py new file mode 100644 index 0000000000..46a487cc0c --- /dev/null +++ b/regtests/client/python/test/test_counter_result.py @@ -0,0 +1,54 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.counter_result import CounterResult + +class TestCounterResult(unittest.TestCase): + """CounterResult unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> CounterResult: + """Test CounterResult + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `CounterResult` + """ + model = CounterResult() + if include_optional: + return CounterResult( + unit = '', + value = 56 + ) + else: + return CounterResult( + unit = '', + value = 56, + ) + """ + + def testCounterResult(self): + """Test CounterResult""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_create_catalog_request.py b/regtests/client/python/test/test_create_catalog_request.py new file mode 100644 index 0000000000..efdfd1c413 --- /dev/null +++ b/regtests/client/python/test/test_create_catalog_request.py @@ -0,0 +1,76 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.management.models.create_catalog_request import CreateCatalogRequest + +class TestCreateCatalogRequest(unittest.TestCase): + """CreateCatalogRequest unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> CreateCatalogRequest: + """Test CreateCatalogRequest + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `CreateCatalogRequest` + """ + model = CreateCatalogRequest() + if include_optional: + return CreateCatalogRequest( + catalog = polaris.management.models.catalog.Catalog( + type = 'INTERNAL', + name = '', + read_only = True, + properties = { + 'key' : '' + }, + create_timestamp = 56, + last_update_timestamp = 56, + entity_version = 56, + storage_config_info = polaris.management.models.storage_config_info.StorageConfigInfo( + storage_type = 'S3', + allowed_locations = For AWS [s3://bucketname/prefix/], for AZURE [abfss://container@storageaccount.blob.core.windows.net/prefix/], for GCP [gs://bucketname/prefix/], ), ) + ) + else: + return CreateCatalogRequest( + catalog = polaris.management.models.catalog.Catalog( + type = 'INTERNAL', + name = '', + read_only = True, + properties = { + 'key' : '' + }, + create_timestamp = 56, + last_update_timestamp = 56, + entity_version = 56, + storage_config_info = polaris.management.models.storage_config_info.StorageConfigInfo( + storage_type = 'S3', + allowed_locations = For AWS [s3://bucketname/prefix/], for AZURE [abfss://container@storageaccount.blob.core.windows.net/prefix/], for GCP [gs://bucketname/prefix/], ), ), + ) + """ + + def testCreateCatalogRequest(self): + """Test CreateCatalogRequest""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_create_catalog_role_request.py b/regtests/client/python/test/test_create_catalog_role_request.py new file mode 100644 index 0000000000..2465059c6c --- /dev/null +++ b/regtests/client/python/test/test_create_catalog_role_request.py @@ -0,0 +1,58 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.management.models.create_catalog_role_request import CreateCatalogRoleRequest + +class TestCreateCatalogRoleRequest(unittest.TestCase): + """CreateCatalogRoleRequest unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> CreateCatalogRoleRequest: + """Test CreateCatalogRoleRequest + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `CreateCatalogRoleRequest` + """ + model = CreateCatalogRoleRequest() + if include_optional: + return CreateCatalogRoleRequest( + catalog_role = polaris.management.models.catalog_role.CatalogRole( + name = '', + properties = { + 'key' : '' + }, + create_timestamp = 56, + last_update_timestamp = 56, + entity_version = 56, ) + ) + else: + return CreateCatalogRoleRequest( + ) + """ + + def testCreateCatalogRoleRequest(self): + """Test CreateCatalogRoleRequest""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_create_namespace_request.py b/regtests/client/python/test/test_create_namespace_request.py new file mode 100644 index 0000000000..cc2259a04c --- /dev/null +++ b/regtests/client/python/test/test_create_namespace_request.py @@ -0,0 +1,53 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.create_namespace_request import CreateNamespaceRequest + +class TestCreateNamespaceRequest(unittest.TestCase): + """CreateNamespaceRequest unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> CreateNamespaceRequest: + """Test CreateNamespaceRequest + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `CreateNamespaceRequest` + """ + model = CreateNamespaceRequest() + if include_optional: + return CreateNamespaceRequest( + namespace = ["accounting","tax"], + properties = {"owner":"Hank Bendickson"} + ) + else: + return CreateNamespaceRequest( + namespace = ["accounting","tax"], + ) + """ + + def testCreateNamespaceRequest(self): + """Test CreateNamespaceRequest""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_create_namespace_response.py b/regtests/client/python/test/test_create_namespace_response.py new file mode 100644 index 0000000000..ed31dbaab9 --- /dev/null +++ b/regtests/client/python/test/test_create_namespace_response.py @@ -0,0 +1,53 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.create_namespace_response import CreateNamespaceResponse + +class TestCreateNamespaceResponse(unittest.TestCase): + """CreateNamespaceResponse unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> CreateNamespaceResponse: + """Test CreateNamespaceResponse + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `CreateNamespaceResponse` + """ + model = CreateNamespaceResponse() + if include_optional: + return CreateNamespaceResponse( + namespace = ["accounting","tax"], + properties = {"owner":"Ralph","created_at":"1452120468"} + ) + else: + return CreateNamespaceResponse( + namespace = ["accounting","tax"], + ) + """ + + def testCreateNamespaceResponse(self): + """Test CreateNamespaceResponse""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_create_principal_request.py b/regtests/client/python/test/test_create_principal_request.py new file mode 100644 index 0000000000..b0cb65d033 --- /dev/null +++ b/regtests/client/python/test/test_create_principal_request.py @@ -0,0 +1,60 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.management.models.create_principal_request import CreatePrincipalRequest + +class TestCreatePrincipalRequest(unittest.TestCase): + """CreatePrincipalRequest unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> CreatePrincipalRequest: + """Test CreatePrincipalRequest + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `CreatePrincipalRequest` + """ + model = CreatePrincipalRequest() + if include_optional: + return CreatePrincipalRequest( + principal = polaris.management.models.principal.Principal( + type = 'SERVICE', + name = '', + client_id = '', + properties = { + 'key' : '' + }, + create_timestamp = 56, + last_update_timestamp = 56, + entity_version = 56, ) + ) + else: + return CreatePrincipalRequest( + ) + """ + + def testCreatePrincipalRequest(self): + """Test CreatePrincipalRequest""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_create_principal_role_request.py b/regtests/client/python/test/test_create_principal_role_request.py new file mode 100644 index 0000000000..43a7f4987f --- /dev/null +++ b/regtests/client/python/test/test_create_principal_role_request.py @@ -0,0 +1,58 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.management.models.create_principal_role_request import CreatePrincipalRoleRequest + +class TestCreatePrincipalRoleRequest(unittest.TestCase): + """CreatePrincipalRoleRequest unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> CreatePrincipalRoleRequest: + """Test CreatePrincipalRoleRequest + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `CreatePrincipalRoleRequest` + """ + model = CreatePrincipalRoleRequest() + if include_optional: + return CreatePrincipalRoleRequest( + principal_role = polaris.management.models.principal_role.PrincipalRole( + name = '', + properties = { + 'key' : '' + }, + create_timestamp = 56, + last_update_timestamp = 56, + entity_version = 56, ) + ) + else: + return CreatePrincipalRoleRequest( + ) + """ + + def testCreatePrincipalRoleRequest(self): + """Test CreatePrincipalRoleRequest""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_create_table_request.py b/regtests/client/python/test/test_create_table_request.py new file mode 100644 index 0000000000..a1bc40c406 --- /dev/null +++ b/regtests/client/python/test/test_create_table_request.py @@ -0,0 +1,77 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.create_table_request import CreateTableRequest + +class TestCreateTableRequest(unittest.TestCase): + """CreateTableRequest unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> CreateTableRequest: + """Test CreateTableRequest + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `CreateTableRequest` + """ + model = CreateTableRequest() + if include_optional: + return CreateTableRequest( + name = '', + location = '', + var_schema = None, + partition_spec = polaris.catalog.models.partition_spec.PartitionSpec( + spec_id = 56, + fields = [ + polaris.catalog.models.partition_field.PartitionField( + field_id = 56, + source_id = 56, + name = '', + transform = '["identity","year","month","day","hour","bucket[256]","truncate[16]"]', ) + ], ), + write_order = polaris.catalog.models.sort_order.SortOrder( + order_id = 56, + fields = [ + polaris.catalog.models.sort_field.SortField( + source_id = 56, + transform = '["identity","year","month","day","hour","bucket[256]","truncate[16]"]', + direction = 'asc', + null_order = 'nulls-first', ) + ], ), + stage_create = True, + properties = { + 'key' : '' + } + ) + else: + return CreateTableRequest( + name = '', + var_schema = None, + ) + """ + + def testCreateTableRequest(self): + """Test CreateTableRequest""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_create_view_request.py b/regtests/client/python/test/test_create_view_request.py new file mode 100644 index 0000000000..ddfe2c178b --- /dev/null +++ b/regtests/client/python/test/test_create_view_request.py @@ -0,0 +1,85 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.create_view_request import CreateViewRequest + +class TestCreateViewRequest(unittest.TestCase): + """CreateViewRequest unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> CreateViewRequest: + """Test CreateViewRequest + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `CreateViewRequest` + """ + model = CreateViewRequest() + if include_optional: + return CreateViewRequest( + name = '', + location = '', + var_schema = None, + view_version = polaris.catalog.models.view_version.ViewVersion( + version_id = 56, + timestamp_ms = 56, + schema_id = 56, + summary = { + 'key' : '' + }, + representations = [ + null + ], + default_catalog = '', + default_namespace = ["accounting","tax"], ), + properties = { + 'key' : '' + } + ) + else: + return CreateViewRequest( + name = '', + var_schema = None, + view_version = polaris.catalog.models.view_version.ViewVersion( + version_id = 56, + timestamp_ms = 56, + schema_id = 56, + summary = { + 'key' : '' + }, + representations = [ + null + ], + default_catalog = '', + default_namespace = ["accounting","tax"], ), + properties = { + 'key' : '' + }, + ) + """ + + def testCreateViewRequest(self): + """Test CreateViewRequest""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_data_file.py b/regtests/client/python/test/test_data_file.py new file mode 100644 index 0000000000..d972ed9f66 --- /dev/null +++ b/regtests/client/python/test/test_data_file.py @@ -0,0 +1,58 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.data_file import DataFile + +class TestDataFile(unittest.TestCase): + """DataFile unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> DataFile: + """Test DataFile + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `DataFile` + """ + model = DataFile() + if include_optional: + return DataFile( + content = 'data', + column_sizes = {"keys":[1,2],"values":[100,200]}, + value_counts = {"keys":[1,2],"values":[100,200]}, + null_value_counts = {"keys":[1,2],"values":[100,200]}, + nan_value_counts = {"keys":[1,2],"values":[100,200]}, + lower_bounds = {"keys":[1,2],"values":[100,"test"]}, + upper_bounds = {"keys":[1,2],"values":[100,"test"]} + ) + else: + return DataFile( + content = 'data', + ) + """ + + def testDataFile(self): + """Test DataFile""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_equality_delete_file.py b/regtests/client/python/test/test_equality_delete_file.py new file mode 100644 index 0000000000..71417c944f --- /dev/null +++ b/regtests/client/python/test/test_equality_delete_file.py @@ -0,0 +1,55 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.equality_delete_file import EqualityDeleteFile + +class TestEqualityDeleteFile(unittest.TestCase): + """EqualityDeleteFile unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> EqualityDeleteFile: + """Test EqualityDeleteFile + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `EqualityDeleteFile` + """ + model = EqualityDeleteFile() + if include_optional: + return EqualityDeleteFile( + content = 'equality-deletes', + equality_ids = [ + 56 + ] + ) + else: + return EqualityDeleteFile( + content = 'equality-deletes', + ) + """ + + def testEqualityDeleteFile(self): + """Test EqualityDeleteFile""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_error_model.py b/regtests/client/python/test/test_error_model.py new file mode 100644 index 0000000000..b5a20cf44e --- /dev/null +++ b/regtests/client/python/test/test_error_model.py @@ -0,0 +1,59 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.error_model import ErrorModel + +class TestErrorModel(unittest.TestCase): + """ErrorModel unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> ErrorModel: + """Test ErrorModel + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `ErrorModel` + """ + model = ErrorModel() + if include_optional: + return ErrorModel( + message = '', + type = 'NoSuchNamespaceException', + code = 404, + stack = [ + '' + ] + ) + else: + return ErrorModel( + message = '', + type = 'NoSuchNamespaceException', + code = 404, + ) + """ + + def testErrorModel(self): + """Test ErrorModel""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_expression.py b/regtests/client/python/test/test_expression.py new file mode 100644 index 0000000000..9a2cde5d5f --- /dev/null +++ b/regtests/client/python/test/test_expression.py @@ -0,0 +1,68 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.expression import Expression + +class TestExpression(unittest.TestCase): + """Expression unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> Expression: + """Test Expression + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `Expression` + """ + model = Expression() + if include_optional: + return Expression( + type = '["eq","and","or","not","in","not-in","lt","lt-eq","gt","gt-eq","not-eq","starts-with","not-starts-with","is-null","not-null","is-nan","not-nan"]', + left = None, + right = None, + child = None, + term = None, + values = [ + None + ], + value = polaris.catalog.models.value.value() + ) + else: + return Expression( + type = '["eq","and","or","not","in","not-in","lt","lt-eq","gt","gt-eq","not-eq","starts-with","not-starts-with","is-null","not-null","is-nan","not-nan"]', + left = None, + right = None, + child = None, + term = None, + values = [ + None + ], + value = polaris.catalog.models.value.value(), + ) + """ + + def testExpression(self): + """Test Expression""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_external_catalog.py b/regtests/client/python/test/test_external_catalog.py new file mode 100644 index 0000000000..37007bf268 --- /dev/null +++ b/regtests/client/python/test/test_external_catalog.py @@ -0,0 +1,52 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.management.models.external_catalog import ExternalCatalog + +class TestExternalCatalog(unittest.TestCase): + """ExternalCatalog unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> ExternalCatalog: + """Test ExternalCatalog + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `ExternalCatalog` + """ + model = ExternalCatalog() + if include_optional: + return ExternalCatalog( + remote_url = '' + ) + else: + return ExternalCatalog( + remote_url = '', + ) + """ + + def testExternalCatalog(self): + """Test ExternalCatalog""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_file_format.py b/regtests/client/python/test/test_file_format.py new file mode 100644 index 0000000000..42f52233af --- /dev/null +++ b/regtests/client/python/test/test_file_format.py @@ -0,0 +1,33 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.file_format import FileFormat + +class TestFileFormat(unittest.TestCase): + """FileFormat unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def testFileFormat(self): + """Test FileFormat""" + # inst = FileFormat() + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_file_storage_config_info.py b/regtests/client/python/test/test_file_storage_config_info.py new file mode 100644 index 0000000000..599c095532 --- /dev/null +++ b/regtests/client/python/test/test_file_storage_config_info.py @@ -0,0 +1,50 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.management.models.file_storage_config_info import FileStorageConfigInfo + +class TestFileStorageConfigInfo(unittest.TestCase): + """FileStorageConfigInfo unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> FileStorageConfigInfo: + """Test FileStorageConfigInfo + include_optional is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `FileStorageConfigInfo` + """ + model = FileStorageConfigInfo() + if include_optional: + return FileStorageConfigInfo( + ) + else: + return FileStorageConfigInfo( + ) + """ + + def testFileStorageConfigInfo(self): + """Test FileStorageConfigInfo""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_gcp_storage_config_info.py b/regtests/client/python/test/test_gcp_storage_config_info.py new file mode 100644 index 0000000000..426574508a --- /dev/null +++ b/regtests/client/python/test/test_gcp_storage_config_info.py @@ -0,0 +1,51 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.management.models.gcp_storage_config_info import GcpStorageConfigInfo + +class TestGcpStorageConfigInfo(unittest.TestCase): + """GcpStorageConfigInfo unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> GcpStorageConfigInfo: + """Test GcpStorageConfigInfo + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `GcpStorageConfigInfo` + """ + model = GcpStorageConfigInfo() + if include_optional: + return GcpStorageConfigInfo( + gcs_service_account = '' + ) + else: + return GcpStorageConfigInfo( + ) + """ + + def testGcpStorageConfigInfo(self): + """Test GcpStorageConfigInfo""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_get_namespace_response.py b/regtests/client/python/test/test_get_namespace_response.py new file mode 100644 index 0000000000..6151839db7 --- /dev/null +++ b/regtests/client/python/test/test_get_namespace_response.py @@ -0,0 +1,53 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.get_namespace_response import GetNamespaceResponse + +class TestGetNamespaceResponse(unittest.TestCase): + """GetNamespaceResponse unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> GetNamespaceResponse: + """Test GetNamespaceResponse + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `GetNamespaceResponse` + """ + model = GetNamespaceResponse() + if include_optional: + return GetNamespaceResponse( + namespace = ["accounting","tax"], + properties = {"owner":"Ralph","transient_lastDdlTime":"1452120468"} + ) + else: + return GetNamespaceResponse( + namespace = ["accounting","tax"], + ) + """ + + def testGetNamespaceResponse(self): + """Test GetNamespaceResponse""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_grant_catalog_role_request.py b/regtests/client/python/test/test_grant_catalog_role_request.py new file mode 100644 index 0000000000..b953b70973 --- /dev/null +++ b/regtests/client/python/test/test_grant_catalog_role_request.py @@ -0,0 +1,58 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.management.models.grant_catalog_role_request import GrantCatalogRoleRequest + +class TestGrantCatalogRoleRequest(unittest.TestCase): + """GrantCatalogRoleRequest unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> GrantCatalogRoleRequest: + """Test GrantCatalogRoleRequest + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `GrantCatalogRoleRequest` + """ + model = GrantCatalogRoleRequest() + if include_optional: + return GrantCatalogRoleRequest( + catalog_role = polaris.management.models.catalog_role.CatalogRole( + name = '', + properties = { + 'key' : '' + }, + create_timestamp = 56, + last_update_timestamp = 56, + entity_version = 56, ) + ) + else: + return GrantCatalogRoleRequest( + ) + """ + + def testGrantCatalogRoleRequest(self): + """Test GrantCatalogRoleRequest""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_grant_principal_role_request.py b/regtests/client/python/test/test_grant_principal_role_request.py new file mode 100644 index 0000000000..74555e8202 --- /dev/null +++ b/regtests/client/python/test/test_grant_principal_role_request.py @@ -0,0 +1,58 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.management.models.grant_principal_role_request import GrantPrincipalRoleRequest + +class TestGrantPrincipalRoleRequest(unittest.TestCase): + """GrantPrincipalRoleRequest unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> GrantPrincipalRoleRequest: + """Test GrantPrincipalRoleRequest + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `GrantPrincipalRoleRequest` + """ + model = GrantPrincipalRoleRequest() + if include_optional: + return GrantPrincipalRoleRequest( + principal_role = polaris.management.models.principal_role.PrincipalRole( + name = '', + properties = { + 'key' : '' + }, + create_timestamp = 56, + last_update_timestamp = 56, + entity_version = 56, ) + ) + else: + return GrantPrincipalRoleRequest( + ) + """ + + def testGrantPrincipalRoleRequest(self): + """Test GrantPrincipalRoleRequest""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_grant_resource.py b/regtests/client/python/test/test_grant_resource.py new file mode 100644 index 0000000000..8b0852af56 --- /dev/null +++ b/regtests/client/python/test/test_grant_resource.py @@ -0,0 +1,52 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.management.models.grant_resource import GrantResource + +class TestGrantResource(unittest.TestCase): + """GrantResource unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> GrantResource: + """Test GrantResource + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `GrantResource` + """ + model = GrantResource() + if include_optional: + return GrantResource( + type = '' + ) + else: + return GrantResource( + type = '', + ) + """ + + def testGrantResource(self): + """Test GrantResource""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_grant_resources.py b/regtests/client/python/test/test_grant_resources.py new file mode 100644 index 0000000000..f621c6c4bc --- /dev/null +++ b/regtests/client/python/test/test_grant_resources.py @@ -0,0 +1,58 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.management.models.grant_resources import GrantResources + +class TestGrantResources(unittest.TestCase): + """GrantResources unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> GrantResources: + """Test GrantResources + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `GrantResources` + """ + model = GrantResources() + if include_optional: + return GrantResources( + grants = [ + polaris.management.models.grant_resource.GrantResource( + type = '', ) + ] + ) + else: + return GrantResources( + grants = [ + polaris.management.models.grant_resource.GrantResource( + type = '', ) + ], + ) + """ + + def testGrantResources(self): + """Test GrantResources""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_iceberg_catalog_api.py b/regtests/client/python/test/test_iceberg_catalog_api.py new file mode 100644 index 0000000000..a6bafd37fc --- /dev/null +++ b/regtests/client/python/test/test_iceberg_catalog_api.py @@ -0,0 +1,199 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.api.iceberg_catalog_api import IcebergCatalogAPI + + +class TestIcebergCatalogAPI(unittest.TestCase): + """IcebergCatalogAPI unit test stubs""" + + def setUp(self) -> None: + self.api = IcebergCatalogAPI() + + def tearDown(self) -> None: + pass + + def test_commit_transaction(self) -> None: + """Test case for commit_transaction + + Commit updates to multiple tables in an atomic operation + """ + pass + + def test_create_namespace(self) -> None: + """Test case for create_namespace + + Create a namespace + """ + pass + + def test_create_table(self) -> None: + """Test case for create_table + + Create a table in the given namespace + """ + pass + + def test_create_view(self) -> None: + """Test case for create_view + + Create a view in the given namespace + """ + pass + + def test_drop_namespace(self) -> None: + """Test case for drop_namespace + + Drop a namespace from the catalog. Namespace must be empty. + """ + pass + + def test_drop_table(self) -> None: + """Test case for drop_table + + Drop a table from the catalog + """ + pass + + def test_drop_view(self) -> None: + """Test case for drop_view + + Drop a view from the catalog + """ + pass + + def test_list_namespaces(self) -> None: + """Test case for list_namespaces + + List namespaces, optionally providing a parent namespace to list underneath + """ + pass + + def test_list_tables(self) -> None: + """Test case for list_tables + + List all table identifiers underneath a given namespace + """ + pass + + def test_list_views(self) -> None: + """Test case for list_views + + List all view identifiers underneath a given namespace + """ + pass + + def test_load_namespace_metadata(self) -> None: + """Test case for load_namespace_metadata + + Load the metadata properties for a namespace + """ + pass + + def test_load_table(self) -> None: + """Test case for load_table + + Load a table from the catalog + """ + pass + + def test_load_view(self) -> None: + """Test case for load_view + + Load a view from the catalog + """ + pass + + def test_namespace_exists(self) -> None: + """Test case for namespace_exists + + Check if a namespace exists + """ + pass + + def test_register_table(self) -> None: + """Test case for register_table + + Register a table in the given namespace using given metadata file location + """ + pass + + def test_rename_table(self) -> None: + """Test case for rename_table + + Rename a table from its current name to a new name + """ + pass + + def test_rename_view(self) -> None: + """Test case for rename_view + + Rename a view from its current name to a new name + """ + pass + + def test_replace_view(self) -> None: + """Test case for replace_view + + Replace a view + """ + pass + + def test_report_metrics(self) -> None: + """Test case for report_metrics + + Send a metrics report to this endpoint to be processed by the backend + """ + pass + + def test_send_notification(self) -> None: + """Test case for send_notification + + Sends a notification to the table + """ + pass + + def test_table_exists(self) -> None: + """Test case for table_exists + + Check if a table exists + """ + pass + + def test_update_properties(self) -> None: + """Test case for update_properties + + Set or remove properties on a namespace + """ + pass + + def test_update_table(self) -> None: + """Test case for update_table + + Commit updates to a table + """ + pass + + def test_view_exists(self) -> None: + """Test case for view_exists + + Check if a view exists + """ + pass + + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_iceberg_configuration_api.py b/regtests/client/python/test/test_iceberg_configuration_api.py new file mode 100644 index 0000000000..db36b459e2 --- /dev/null +++ b/regtests/client/python/test/test_iceberg_configuration_api.py @@ -0,0 +1,38 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.api.iceberg_configuration_api import IcebergConfigurationAPI + + +class TestIcebergConfigurationAPI(unittest.TestCase): + """IcebergConfigurationAPI unit test stubs""" + + def setUp(self) -> None: + self.api = IcebergConfigurationAPI() + + def tearDown(self) -> None: + pass + + def test_get_config(self) -> None: + """Test case for get_config + + List all catalog configuration settings + """ + pass + + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_iceberg_error_response.py b/regtests/client/python/test/test_iceberg_error_response.py new file mode 100644 index 0000000000..24375dd5a5 --- /dev/null +++ b/regtests/client/python/test/test_iceberg_error_response.py @@ -0,0 +1,64 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.iceberg_error_response import IcebergErrorResponse + +class TestIcebergErrorResponse(unittest.TestCase): + """IcebergErrorResponse unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> IcebergErrorResponse: + """Test IcebergErrorResponse + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `IcebergErrorResponse` + """ + model = IcebergErrorResponse() + if include_optional: + return IcebergErrorResponse( + error = polaris.catalog.models.error_model.ErrorModel( + message = '', + type = 'NoSuchNamespaceException', + code = 404, + stack = [ + '' + ], ) + ) + else: + return IcebergErrorResponse( + error = polaris.catalog.models.error_model.ErrorModel( + message = '', + type = 'NoSuchNamespaceException', + code = 404, + stack = [ + '' + ], ), + ) + """ + + def testIcebergErrorResponse(self): + """Test IcebergErrorResponse""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_iceberg_o_auth2_api.py b/regtests/client/python/test/test_iceberg_o_auth2_api.py new file mode 100644 index 0000000000..b3c6903475 --- /dev/null +++ b/regtests/client/python/test/test_iceberg_o_auth2_api.py @@ -0,0 +1,38 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.api.iceberg_o_auth2_api import IcebergOAuth2API + + +class TestIcebergOAuth2API(unittest.TestCase): + """IcebergOAuth2API unit test stubs""" + + def setUp(self) -> None: + self.api = IcebergOAuth2API() + + def tearDown(self) -> None: + pass + + def test_get_token(self) -> None: + """Test case for get_token + + Get a token using an OAuth2 flow + """ + pass + + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_list_namespaces_response.py b/regtests/client/python/test/test_list_namespaces_response.py new file mode 100644 index 0000000000..9ac72ef0e5 --- /dev/null +++ b/regtests/client/python/test/test_list_namespaces_response.py @@ -0,0 +1,54 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.list_namespaces_response import ListNamespacesResponse + +class TestListNamespacesResponse(unittest.TestCase): + """ListNamespacesResponse unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> ListNamespacesResponse: + """Test ListNamespacesResponse + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `ListNamespacesResponse` + """ + model = ListNamespacesResponse() + if include_optional: + return ListNamespacesResponse( + next_page_token = '', + namespaces = [ + ["accounting","tax"] + ] + ) + else: + return ListNamespacesResponse( + ) + """ + + def testListNamespacesResponse(self): + """Test ListNamespacesResponse""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_list_tables_response.py b/regtests/client/python/test/test_list_tables_response.py new file mode 100644 index 0000000000..94ba6afcd7 --- /dev/null +++ b/regtests/client/python/test/test_list_tables_response.py @@ -0,0 +1,56 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.list_tables_response import ListTablesResponse + +class TestListTablesResponse(unittest.TestCase): + """ListTablesResponse unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> ListTablesResponse: + """Test ListTablesResponse + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `ListTablesResponse` + """ + model = ListTablesResponse() + if include_optional: + return ListTablesResponse( + next_page_token = '', + identifiers = [ + polaris.catalog.models.table_identifier.TableIdentifier( + namespace = ["accounting","tax"], + name = '', ) + ] + ) + else: + return ListTablesResponse( + ) + """ + + def testListTablesResponse(self): + """Test ListTablesResponse""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_list_type.py b/regtests/client/python/test/test_list_type.py new file mode 100644 index 0000000000..ff9306a9f4 --- /dev/null +++ b/regtests/client/python/test/test_list_type.py @@ -0,0 +1,58 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.list_type import ListType + +class TestListType(unittest.TestCase): + """ListType unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> ListType: + """Test ListType + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `ListType` + """ + model = ListType() + if include_optional: + return ListType( + type = 'list', + element_id = 56, + element = None, + element_required = True + ) + else: + return ListType( + type = 'list', + element_id = 56, + element = None, + element_required = True, + ) + """ + + def testListType(self): + """Test ListType""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_literal_expression.py b/regtests/client/python/test/test_literal_expression.py new file mode 100644 index 0000000000..ccd9ac88a1 --- /dev/null +++ b/regtests/client/python/test/test_literal_expression.py @@ -0,0 +1,56 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.literal_expression import LiteralExpression + +class TestLiteralExpression(unittest.TestCase): + """LiteralExpression unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> LiteralExpression: + """Test LiteralExpression + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `LiteralExpression` + """ + model = LiteralExpression() + if include_optional: + return LiteralExpression( + type = '["eq","and","or","not","in","not-in","lt","lt-eq","gt","gt-eq","not-eq","starts-with","not-starts-with","is-null","not-null","is-nan","not-nan"]', + term = None, + value = polaris.catalog.models.value.value() + ) + else: + return LiteralExpression( + type = '["eq","and","or","not","in","not-in","lt","lt-eq","gt","gt-eq","not-eq","starts-with","not-starts-with","is-null","not-null","is-nan","not-nan"]', + term = None, + value = polaris.catalog.models.value.value(), + ) + """ + + def testLiteralExpression(self): + """Test LiteralExpression""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_load_table_result.py b/regtests/client/python/test/test_load_table_result.py new file mode 100644 index 0000000000..b10866d79c --- /dev/null +++ b/regtests/client/python/test/test_load_table_result.py @@ -0,0 +1,238 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.load_table_result import LoadTableResult + +class TestLoadTableResult(unittest.TestCase): + """LoadTableResult unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> LoadTableResult: + """Test LoadTableResult + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `LoadTableResult` + """ + model = LoadTableResult() + if include_optional: + return LoadTableResult( + metadata_location = '', + metadata = polaris.catalog.models.table_metadata.TableMetadata( + format_version = 1, + table_uuid = '', + location = '', + last_updated_ms = 56, + properties = { + 'key' : '' + }, + schemas = [ + null + ], + current_schema_id = 56, + last_column_id = 56, + partition_specs = [ + polaris.catalog.models.partition_spec.PartitionSpec( + spec_id = 56, + fields = [ + polaris.catalog.models.partition_field.PartitionField( + field_id = 56, + source_id = 56, + name = '', + transform = '["identity","year","month","day","hour","bucket[256]","truncate[16]"]', ) + ], ) + ], + default_spec_id = 56, + last_partition_id = 56, + sort_orders = [ + polaris.catalog.models.sort_order.SortOrder( + order_id = 56, + fields = [ + polaris.catalog.models.sort_field.SortField( + source_id = 56, + transform = '["identity","year","month","day","hour","bucket[256]","truncate[16]"]', + direction = 'asc', + null_order = 'nulls-first', ) + ], ) + ], + default_sort_order_id = 56, + snapshots = [ + polaris.catalog.models.snapshot.Snapshot( + snapshot_id = 56, + parent_snapshot_id = 56, + sequence_number = 56, + timestamp_ms = 56, + manifest_list = '', + summary = { + 'key' : '' + }, + schema_id = 56, ) + ], + refs = { + 'key' : polaris.catalog.models.snapshot_reference.SnapshotReference( + type = 'tag', + snapshot_id = 56, + max_ref_age_ms = 56, + max_snapshot_age_ms = 56, + min_snapshots_to_keep = 56, ) + }, + current_snapshot_id = 56, + last_sequence_number = 56, + snapshot_log = [ + polaris.catalog.models.snapshot_log_inner.SnapshotLog_inner( + snapshot_id = 56, + timestamp_ms = 56, ) + ], + metadata_log = [ + polaris.catalog.models.metadata_log_inner.MetadataLog_inner( + metadata_file = '', + timestamp_ms = 56, ) + ], + statistics_files = [ + polaris.catalog.models.statistics_file.StatisticsFile( + snapshot_id = 56, + statistics_path = '', + file_size_in_bytes = 56, + file_footer_size_in_bytes = 56, + blob_metadata = [ + polaris.catalog.models.blob_metadata.BlobMetadata( + type = '', + snapshot_id = 56, + sequence_number = 56, + fields = [ + 56 + ], ) + ], ) + ], + partition_statistics_files = [ + polaris.catalog.models.partition_statistics_file.PartitionStatisticsFile( + snapshot_id = 56, + statistics_path = '', + file_size_in_bytes = 56, ) + ], ), + config = { + 'key' : '' + } + ) + else: + return LoadTableResult( + metadata = polaris.catalog.models.table_metadata.TableMetadata( + format_version = 1, + table_uuid = '', + location = '', + last_updated_ms = 56, + properties = { + 'key' : '' + }, + schemas = [ + null + ], + current_schema_id = 56, + last_column_id = 56, + partition_specs = [ + polaris.catalog.models.partition_spec.PartitionSpec( + spec_id = 56, + fields = [ + polaris.catalog.models.partition_field.PartitionField( + field_id = 56, + source_id = 56, + name = '', + transform = '["identity","year","month","day","hour","bucket[256]","truncate[16]"]', ) + ], ) + ], + default_spec_id = 56, + last_partition_id = 56, + sort_orders = [ + polaris.catalog.models.sort_order.SortOrder( + order_id = 56, + fields = [ + polaris.catalog.models.sort_field.SortField( + source_id = 56, + transform = '["identity","year","month","day","hour","bucket[256]","truncate[16]"]', + direction = 'asc', + null_order = 'nulls-first', ) + ], ) + ], + default_sort_order_id = 56, + snapshots = [ + polaris.catalog.models.snapshot.Snapshot( + snapshot_id = 56, + parent_snapshot_id = 56, + sequence_number = 56, + timestamp_ms = 56, + manifest_list = '', + summary = { + 'key' : '' + }, + schema_id = 56, ) + ], + refs = { + 'key' : polaris.catalog.models.snapshot_reference.SnapshotReference( + type = 'tag', + snapshot_id = 56, + max_ref_age_ms = 56, + max_snapshot_age_ms = 56, + min_snapshots_to_keep = 56, ) + }, + current_snapshot_id = 56, + last_sequence_number = 56, + snapshot_log = [ + polaris.catalog.models.snapshot_log_inner.SnapshotLog_inner( + snapshot_id = 56, + timestamp_ms = 56, ) + ], + metadata_log = [ + polaris.catalog.models.metadata_log_inner.MetadataLog_inner( + metadata_file = '', + timestamp_ms = 56, ) + ], + statistics_files = [ + polaris.catalog.models.statistics_file.StatisticsFile( + snapshot_id = 56, + statistics_path = '', + file_size_in_bytes = 56, + file_footer_size_in_bytes = 56, + blob_metadata = [ + polaris.catalog.models.blob_metadata.BlobMetadata( + type = '', + snapshot_id = 56, + sequence_number = 56, + fields = [ + 56 + ], ) + ], ) + ], + partition_statistics_files = [ + polaris.catalog.models.partition_statistics_file.PartitionStatisticsFile( + snapshot_id = 56, + statistics_path = '', + file_size_in_bytes = 56, ) + ], ), + ) + """ + + def testLoadTableResult(self): + """Test LoadTableResult""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_load_view_result.py b/regtests/client/python/test/test_load_view_result.py new file mode 100644 index 0000000000..b42144b1da --- /dev/null +++ b/regtests/client/python/test/test_load_view_result.py @@ -0,0 +1,115 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.load_view_result import LoadViewResult + +class TestLoadViewResult(unittest.TestCase): + """LoadViewResult unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> LoadViewResult: + """Test LoadViewResult + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `LoadViewResult` + """ + model = LoadViewResult() + if include_optional: + return LoadViewResult( + metadata_location = '', + metadata = polaris.catalog.models.view_metadata.ViewMetadata( + view_uuid = '', + format_version = 1, + location = '', + current_version_id = 56, + versions = [ + polaris.catalog.models.view_version.ViewVersion( + version_id = 56, + timestamp_ms = 56, + schema_id = 56, + summary = { + 'key' : '' + }, + representations = [ + null + ], + default_catalog = '', + default_namespace = ["accounting","tax"], ) + ], + version_log = [ + polaris.catalog.models.view_history_entry.ViewHistoryEntry( + version_id = 56, + timestamp_ms = 56, ) + ], + schemas = [ + null + ], + properties = { + 'key' : '' + }, ), + config = { + 'key' : '' + } + ) + else: + return LoadViewResult( + metadata_location = '', + metadata = polaris.catalog.models.view_metadata.ViewMetadata( + view_uuid = '', + format_version = 1, + location = '', + current_version_id = 56, + versions = [ + polaris.catalog.models.view_version.ViewVersion( + version_id = 56, + timestamp_ms = 56, + schema_id = 56, + summary = { + 'key' : '' + }, + representations = [ + null + ], + default_catalog = '', + default_namespace = ["accounting","tax"], ) + ], + version_log = [ + polaris.catalog.models.view_history_entry.ViewHistoryEntry( + version_id = 56, + timestamp_ms = 56, ) + ], + schemas = [ + null + ], + properties = { + 'key' : '' + }, ), + ) + """ + + def testLoadViewResult(self): + """Test LoadViewResult""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_map_type.py b/regtests/client/python/test/test_map_type.py new file mode 100644 index 0000000000..b9fbb17d5f --- /dev/null +++ b/regtests/client/python/test/test_map_type.py @@ -0,0 +1,62 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.map_type import MapType + +class TestMapType(unittest.TestCase): + """MapType unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> MapType: + """Test MapType + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `MapType` + """ + model = MapType() + if include_optional: + return MapType( + type = 'map', + key_id = 56, + key = None, + value_id = 56, + value = None, + value_required = True + ) + else: + return MapType( + type = 'map', + key_id = 56, + key = None, + value_id = 56, + value = None, + value_required = True, + ) + """ + + def testMapType(self): + """Test MapType""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_metadata_log_inner.py b/regtests/client/python/test/test_metadata_log_inner.py new file mode 100644 index 0000000000..6668edffb2 --- /dev/null +++ b/regtests/client/python/test/test_metadata_log_inner.py @@ -0,0 +1,54 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.metadata_log_inner import MetadataLogInner + +class TestMetadataLogInner(unittest.TestCase): + """MetadataLogInner unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> MetadataLogInner: + """Test MetadataLogInner + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `MetadataLogInner` + """ + model = MetadataLogInner() + if include_optional: + return MetadataLogInner( + metadata_file = '', + timestamp_ms = 56 + ) + else: + return MetadataLogInner( + metadata_file = '', + timestamp_ms = 56, + ) + """ + + def testMetadataLogInner(self): + """Test MetadataLogInner""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_metric_result.py b/regtests/client/python/test/test_metric_result.py new file mode 100644 index 0000000000..0a1ba72d9a --- /dev/null +++ b/regtests/client/python/test/test_metric_result.py @@ -0,0 +1,60 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.metric_result import MetricResult + +class TestMetricResult(unittest.TestCase): + """MetricResult unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> MetricResult: + """Test MetricResult + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `MetricResult` + """ + model = MetricResult() + if include_optional: + return MetricResult( + unit = '', + value = 56, + time_unit = '', + count = 56, + total_duration = 56 + ) + else: + return MetricResult( + unit = '', + value = 56, + time_unit = '', + count = 56, + total_duration = 56, + ) + """ + + def testMetricResult(self): + """Test MetricResult""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_model_schema.py b/regtests/client/python/test/test_model_schema.py new file mode 100644 index 0000000000..e1629f5d39 --- /dev/null +++ b/regtests/client/python/test/test_model_schema.py @@ -0,0 +1,72 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.model_schema import ModelSchema + +class TestModelSchema(unittest.TestCase): + """ModelSchema unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> ModelSchema: + """Test ModelSchema + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `ModelSchema` + """ + model = ModelSchema() + if include_optional: + return ModelSchema( + type = 'struct', + fields = [ + polaris.catalog.models.struct_field.StructField( + id = 56, + name = '', + type = null, + required = True, + doc = '', ) + ], + schema_id = 56, + identifier_field_ids = [ + 56 + ] + ) + else: + return ModelSchema( + type = 'struct', + fields = [ + polaris.catalog.models.struct_field.StructField( + id = 56, + name = '', + type = null, + required = True, + doc = '', ) + ], + ) + """ + + def testModelSchema(self): + """Test ModelSchema""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_namespace_grant.py b/regtests/client/python/test/test_namespace_grant.py new file mode 100644 index 0000000000..76be3edbd2 --- /dev/null +++ b/regtests/client/python/test/test_namespace_grant.py @@ -0,0 +1,58 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.management.models.namespace_grant import NamespaceGrant + +class TestNamespaceGrant(unittest.TestCase): + """NamespaceGrant unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> NamespaceGrant: + """Test NamespaceGrant + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `NamespaceGrant` + """ + model = NamespaceGrant() + if include_optional: + return NamespaceGrant( + namespace = [ + '' + ], + privilege = 'CATALOG_MANAGE_ACCESS' + ) + else: + return NamespaceGrant( + namespace = [ + '' + ], + privilege = 'CATALOG_MANAGE_ACCESS', + ) + """ + + def testNamespaceGrant(self): + """Test NamespaceGrant""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_namespace_privilege.py b/regtests/client/python/test/test_namespace_privilege.py new file mode 100644 index 0000000000..5dbd90e7be --- /dev/null +++ b/regtests/client/python/test/test_namespace_privilege.py @@ -0,0 +1,33 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.management.models.namespace_privilege import NamespacePrivilege + +class TestNamespacePrivilege(unittest.TestCase): + """NamespacePrivilege unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def testNamespacePrivilege(self): + """Test NamespacePrivilege""" + # inst = NamespacePrivilege() + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_not_expression.py b/regtests/client/python/test/test_not_expression.py new file mode 100644 index 0000000000..a33577d6bc --- /dev/null +++ b/regtests/client/python/test/test_not_expression.py @@ -0,0 +1,54 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.not_expression import NotExpression + +class TestNotExpression(unittest.TestCase): + """NotExpression unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> NotExpression: + """Test NotExpression + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `NotExpression` + """ + model = NotExpression() + if include_optional: + return NotExpression( + type = '["eq","and","or","not","in","not-in","lt","lt-eq","gt","gt-eq","not-eq","starts-with","not-starts-with","is-null","not-null","is-nan","not-nan"]', + child = None + ) + else: + return NotExpression( + type = '["eq","and","or","not","in","not-in","lt","lt-eq","gt","gt-eq","not-eq","starts-with","not-starts-with","is-null","not-null","is-nan","not-nan"]', + child = None, + ) + """ + + def testNotExpression(self): + """Test NotExpression""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_notification_request.py b/regtests/client/python/test/test_notification_request.py new file mode 100644 index 0000000000..326637b3f8 --- /dev/null +++ b/regtests/client/python/test/test_notification_request.py @@ -0,0 +1,149 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.notification_request import NotificationRequest + +class TestNotificationRequest(unittest.TestCase): + """NotificationRequest unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> NotificationRequest: + """Test NotificationRequest + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `NotificationRequest` + """ + model = NotificationRequest() + if include_optional: + return NotificationRequest( + notification_type = '', + payload = polaris.catalog.models.table_update_notification.TableUpdateNotification( + table_name = '', + timestamp = 56, + table_uuid = '', + metadata_location = '', + metadata = polaris.catalog.models.table_metadata.TableMetadata( + format_version = 1, + table_uuid = '', + location = '', + last_updated_ms = 56, + properties = { + 'key' : '' + }, + schemas = [ + null + ], + current_schema_id = 56, + last_column_id = 56, + partition_specs = [ + polaris.catalog.models.partition_spec.PartitionSpec( + spec_id = 56, + fields = [ + polaris.catalog.models.partition_field.PartitionField( + field_id = 56, + source_id = 56, + name = '', + transform = '["identity","year","month","day","hour","bucket[256]","truncate[16]"]', ) + ], ) + ], + default_spec_id = 56, + last_partition_id = 56, + sort_orders = [ + polaris.catalog.models.sort_order.SortOrder( + order_id = 56, + fields = [ + polaris.catalog.models.sort_field.SortField( + source_id = 56, + transform = '["identity","year","month","day","hour","bucket[256]","truncate[16]"]', + direction = 'asc', + null_order = 'nulls-first', ) + ], ) + ], + default_sort_order_id = 56, + snapshots = [ + polaris.catalog.models.snapshot.Snapshot( + snapshot_id = 56, + parent_snapshot_id = 56, + sequence_number = 56, + timestamp_ms = 56, + manifest_list = '', + summary = { + 'key' : '' + }, + schema_id = 56, ) + ], + refs = { + 'key' : polaris.catalog.models.snapshot_reference.SnapshotReference( + type = 'tag', + snapshot_id = 56, + max_ref_age_ms = 56, + max_snapshot_age_ms = 56, + min_snapshots_to_keep = 56, ) + }, + current_snapshot_id = 56, + last_sequence_number = 56, + snapshot_log = [ + polaris.catalog.models.snapshot_log_inner.SnapshotLog_inner( + snapshot_id = 56, + timestamp_ms = 56, ) + ], + metadata_log = [ + polaris.catalog.models.metadata_log_inner.MetadataLog_inner( + metadata_file = '', + timestamp_ms = 56, ) + ], + statistics_files = [ + polaris.catalog.models.statistics_file.StatisticsFile( + snapshot_id = 56, + statistics_path = '', + file_size_in_bytes = 56, + file_footer_size_in_bytes = 56, + blob_metadata = [ + polaris.catalog.models.blob_metadata.BlobMetadata( + type = '', + snapshot_id = 56, + sequence_number = 56, + fields = [ + 56 + ], ) + ], ) + ], + partition_statistics_files = [ + polaris.catalog.models.partition_statistics_file.PartitionStatisticsFile( + snapshot_id = 56, + statistics_path = '', + file_size_in_bytes = 56, ) + ], ), ) + ) + else: + return NotificationRequest( + notification_type = '', + ) + """ + + def testNotificationRequest(self): + """Test NotificationRequest""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_notification_type.py b/regtests/client/python/test/test_notification_type.py new file mode 100644 index 0000000000..a936754eab --- /dev/null +++ b/regtests/client/python/test/test_notification_type.py @@ -0,0 +1,33 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.notification_type import NotificationType + +class TestNotificationType(unittest.TestCase): + """NotificationType unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def testNotificationType(self): + """Test NotificationType""" + # inst = NotificationType() + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_null_order.py b/regtests/client/python/test/test_null_order.py new file mode 100644 index 0000000000..883bf472b9 --- /dev/null +++ b/regtests/client/python/test/test_null_order.py @@ -0,0 +1,33 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.null_order import NullOrder + +class TestNullOrder(unittest.TestCase): + """NullOrder unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def testNullOrder(self): + """Test NullOrder""" + # inst = NullOrder() + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_o_auth_error.py b/regtests/client/python/test/test_o_auth_error.py new file mode 100644 index 0000000000..d4018a6e2d --- /dev/null +++ b/regtests/client/python/test/test_o_auth_error.py @@ -0,0 +1,54 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.o_auth_error import OAuthError + +class TestOAuthError(unittest.TestCase): + """OAuthError unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> OAuthError: + """Test OAuthError + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `OAuthError` + """ + model = OAuthError() + if include_optional: + return OAuthError( + error = 'invalid_request', + error_description = '', + error_uri = '' + ) + else: + return OAuthError( + error = 'invalid_request', + ) + """ + + def testOAuthError(self): + """Test OAuthError""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_o_auth_token_response.py b/regtests/client/python/test/test_o_auth_token_response.py new file mode 100644 index 0000000000..1c47e0fcd7 --- /dev/null +++ b/regtests/client/python/test/test_o_auth_token_response.py @@ -0,0 +1,58 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.o_auth_token_response import OAuthTokenResponse + +class TestOAuthTokenResponse(unittest.TestCase): + """OAuthTokenResponse unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> OAuthTokenResponse: + """Test OAuthTokenResponse + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `OAuthTokenResponse` + """ + model = OAuthTokenResponse() + if include_optional: + return OAuthTokenResponse( + access_token = '', + token_type = 'bearer', + expires_in = 56, + issued_token_type = 'urn:ietf:params:oauth:token-type:access_token', + refresh_token = '', + scope = '' + ) + else: + return OAuthTokenResponse( + access_token = '', + token_type = 'bearer', + ) + """ + + def testOAuthTokenResponse(self): + """Test OAuthTokenResponse""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_partition_field.py b/regtests/client/python/test/test_partition_field.py new file mode 100644 index 0000000000..7b9344e92d --- /dev/null +++ b/regtests/client/python/test/test_partition_field.py @@ -0,0 +1,57 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.partition_field import PartitionField + +class TestPartitionField(unittest.TestCase): + """PartitionField unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> PartitionField: + """Test PartitionField + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `PartitionField` + """ + model = PartitionField() + if include_optional: + return PartitionField( + field_id = 56, + source_id = 56, + name = '', + transform = '["identity","year","month","day","hour","bucket[256]","truncate[16]"]' + ) + else: + return PartitionField( + source_id = 56, + name = '', + transform = '["identity","year","month","day","hour","bucket[256]","truncate[16]"]', + ) + """ + + def testPartitionField(self): + """Test PartitionField""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_partition_spec.py b/regtests/client/python/test/test_partition_spec.py new file mode 100644 index 0000000000..1e8b23b9cd --- /dev/null +++ b/regtests/client/python/test/test_partition_spec.py @@ -0,0 +1,65 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.partition_spec import PartitionSpec + +class TestPartitionSpec(unittest.TestCase): + """PartitionSpec unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> PartitionSpec: + """Test PartitionSpec + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `PartitionSpec` + """ + model = PartitionSpec() + if include_optional: + return PartitionSpec( + spec_id = 56, + fields = [ + polaris.catalog.models.partition_field.PartitionField( + field_id = 56, + source_id = 56, + name = '', + transform = '["identity","year","month","day","hour","bucket[256]","truncate[16]"]', ) + ] + ) + else: + return PartitionSpec( + fields = [ + polaris.catalog.models.partition_field.PartitionField( + field_id = 56, + source_id = 56, + name = '', + transform = '["identity","year","month","day","hour","bucket[256]","truncate[16]"]', ) + ], + ) + """ + + def testPartitionSpec(self): + """Test PartitionSpec""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_partition_statistics_file.py b/regtests/client/python/test/test_partition_statistics_file.py new file mode 100644 index 0000000000..7e446786df --- /dev/null +++ b/regtests/client/python/test/test_partition_statistics_file.py @@ -0,0 +1,56 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.partition_statistics_file import PartitionStatisticsFile + +class TestPartitionStatisticsFile(unittest.TestCase): + """PartitionStatisticsFile unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> PartitionStatisticsFile: + """Test PartitionStatisticsFile + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `PartitionStatisticsFile` + """ + model = PartitionStatisticsFile() + if include_optional: + return PartitionStatisticsFile( + snapshot_id = 56, + statistics_path = '', + file_size_in_bytes = 56 + ) + else: + return PartitionStatisticsFile( + snapshot_id = 56, + statistics_path = '', + file_size_in_bytes = 56, + ) + """ + + def testPartitionStatisticsFile(self): + """Test PartitionStatisticsFile""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_polaris_catalog.py b/regtests/client/python/test/test_polaris_catalog.py new file mode 100644 index 0000000000..65bdebff22 --- /dev/null +++ b/regtests/client/python/test/test_polaris_catalog.py @@ -0,0 +1,50 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.management.models.polaris_catalog import PolarisCatalog + +class TestPolarisCatalog(unittest.TestCase): + """PolarisCatalog unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> PolarisCatalog: + """Test PolarisCatalog + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `PolarisCatalog` + """ + model = PolarisCatalog() + if include_optional: + return PolarisCatalog( + ) + else: + return PolarisCatalog( + ) + """ + + def testPolarisCatalog(self): + """Test PolarisCatalog""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_polaris_default_api.py b/regtests/client/python/test/test_polaris_default_api.py new file mode 100644 index 0000000000..63d295ffc5 --- /dev/null +++ b/regtests/client/python/test/test_polaris_default_api.py @@ -0,0 +1,223 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.management.api.polaris_default_api import PolarisDefaultApi + + +class TestPolarisDefaultApi(unittest.TestCase): + """PolarisDefaultApi unit test stubs""" + + def setUp(self) -> None: + self.api = PolarisDefaultApi() + + def tearDown(self) -> None: + pass + + def test_add_grant_to_catalog_role(self) -> None: + """Test case for add_grant_to_catalog_role + + """ + pass + + def test_assign_catalog_role_to_principal_role(self) -> None: + """Test case for assign_catalog_role_to_principal_role + + """ + pass + + def test_assign_principal_role(self) -> None: + """Test case for assign_principal_role + + """ + pass + + def test_create_catalog(self) -> None: + """Test case for create_catalog + + """ + pass + + def test_create_catalog_role(self) -> None: + """Test case for create_catalog_role + + """ + pass + + def test_create_principal(self) -> None: + """Test case for create_principal + + """ + pass + + def test_create_principal_role(self) -> None: + """Test case for create_principal_role + + """ + pass + + def test_delete_catalog(self) -> None: + """Test case for delete_catalog + + """ + pass + + def test_delete_catalog_role(self) -> None: + """Test case for delete_catalog_role + + """ + pass + + def test_delete_principal(self) -> None: + """Test case for delete_principal + + """ + pass + + def test_delete_principal_role(self) -> None: + """Test case for delete_principal_role + + """ + pass + + def test_get_catalog(self) -> None: + """Test case for get_catalog + + """ + pass + + def test_get_catalog_role(self) -> None: + """Test case for get_catalog_role + + """ + pass + + def test_get_principal(self) -> None: + """Test case for get_principal + + """ + pass + + def test_get_principal_role(self) -> None: + """Test case for get_principal_role + + """ + pass + + def test_list_assignee_principal_roles_for_catalog_role(self) -> None: + """Test case for list_assignee_principal_roles_for_catalog_role + + """ + pass + + def test_list_assignee_principals_for_principal_role(self) -> None: + """Test case for list_assignee_principals_for_principal_role + + """ + pass + + def test_list_catalog_roles(self) -> None: + """Test case for list_catalog_roles + + """ + pass + + def test_list_catalog_roles_for_principal_role(self) -> None: + """Test case for list_catalog_roles_for_principal_role + + """ + pass + + def test_list_catalogs(self) -> None: + """Test case for list_catalogs + + """ + pass + + def test_list_grants_for_catalog_role(self) -> None: + """Test case for list_grants_for_catalog_role + + """ + pass + + def test_list_principal_roles(self) -> None: + """Test case for list_principal_roles + + """ + pass + + def test_list_principal_roles_assigned(self) -> None: + """Test case for list_principal_roles_assigned + + """ + pass + + def test_list_principals(self) -> None: + """Test case for list_principals + + """ + pass + + def test_revoke_catalog_role_from_principal_role(self) -> None: + """Test case for revoke_catalog_role_from_principal_role + + """ + pass + + def test_revoke_grant_from_catalog_role(self) -> None: + """Test case for revoke_grant_from_catalog_role + + """ + pass + + def test_revoke_principal_role(self) -> None: + """Test case for revoke_principal_role + + """ + pass + + def test_rotate_credentials(self) -> None: + """Test case for rotate_credentials + + """ + pass + + def test_update_catalog(self) -> None: + """Test case for update_catalog + + """ + pass + + def test_update_catalog_role(self) -> None: + """Test case for update_catalog_role + + """ + pass + + def test_update_principal(self) -> None: + """Test case for update_principal + + """ + pass + + def test_update_principal_role(self) -> None: + """Test case for update_principal_role + + """ + pass + + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_position_delete_file.py b/regtests/client/python/test/test_position_delete_file.py new file mode 100644 index 0000000000..5f2cde2d32 --- /dev/null +++ b/regtests/client/python/test/test_position_delete_file.py @@ -0,0 +1,52 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.position_delete_file import PositionDeleteFile + +class TestPositionDeleteFile(unittest.TestCase): + """PositionDeleteFile unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> PositionDeleteFile: + """Test PositionDeleteFile + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `PositionDeleteFile` + """ + model = PositionDeleteFile() + if include_optional: + return PositionDeleteFile( + content = 'position-deletes' + ) + else: + return PositionDeleteFile( + content = 'position-deletes', + ) + """ + + def testPositionDeleteFile(self): + """Test PositionDeleteFile""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_primitive_type_value.py b/regtests/client/python/test/test_primitive_type_value.py new file mode 100644 index 0000000000..13498f5e71 --- /dev/null +++ b/regtests/client/python/test/test_primitive_type_value.py @@ -0,0 +1,50 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.primitive_type_value import PrimitiveTypeValue + +class TestPrimitiveTypeValue(unittest.TestCase): + """PrimitiveTypeValue unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> PrimitiveTypeValue: + """Test PrimitiveTypeValue + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `PrimitiveTypeValue` + """ + model = PrimitiveTypeValue() + if include_optional: + return PrimitiveTypeValue( + ) + else: + return PrimitiveTypeValue( + ) + """ + + def testPrimitiveTypeValue(self): + """Test PrimitiveTypeValue""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_principal.py b/regtests/client/python/test/test_principal.py new file mode 100644 index 0000000000..2ccfacc30d --- /dev/null +++ b/regtests/client/python/test/test_principal.py @@ -0,0 +1,61 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.management.models.principal import Principal + +class TestPrincipal(unittest.TestCase): + """Principal unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> Principal: + """Test Principal + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `Principal` + """ + model = Principal() + if include_optional: + return Principal( + type = 'SERVICE', + name = '', + client_id = '', + properties = { + 'key' : '' + }, + create_timestamp = 56, + last_update_timestamp = 56, + entity_version = 56 + ) + else: + return Principal( + type = 'SERVICE', + name = '', + ) + """ + + def testPrincipal(self): + """Test Principal""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_principal_role.py b/regtests/client/python/test/test_principal_role.py new file mode 100644 index 0000000000..7d32a66fd6 --- /dev/null +++ b/regtests/client/python/test/test_principal_role.py @@ -0,0 +1,58 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.management.models.principal_role import PrincipalRole + +class TestPrincipalRole(unittest.TestCase): + """PrincipalRole unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> PrincipalRole: + """Test PrincipalRole + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `PrincipalRole` + """ + model = PrincipalRole() + if include_optional: + return PrincipalRole( + name = '', + properties = { + 'key' : '' + }, + create_timestamp = 56, + last_update_timestamp = 56, + entity_version = 56 + ) + else: + return PrincipalRole( + name = '', + ) + """ + + def testPrincipalRole(self): + """Test PrincipalRole""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_principal_roles.py b/regtests/client/python/test/test_principal_roles.py new file mode 100644 index 0000000000..46f5193aa1 --- /dev/null +++ b/regtests/client/python/test/test_principal_roles.py @@ -0,0 +1,70 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.management.models.principal_roles import PrincipalRoles + +class TestPrincipalRoles(unittest.TestCase): + """PrincipalRoles unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> PrincipalRoles: + """Test PrincipalRoles + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `PrincipalRoles` + """ + model = PrincipalRoles() + if include_optional: + return PrincipalRoles( + roles = [ + polaris.management.models.principal_role.PrincipalRole( + name = '', + properties = { + 'key' : '' + }, + create_timestamp = 56, + last_update_timestamp = 56, + entity_version = 56, ) + ] + ) + else: + return PrincipalRoles( + roles = [ + polaris.management.models.principal_role.PrincipalRole( + name = '', + properties = { + 'key' : '' + }, + create_timestamp = 56, + last_update_timestamp = 56, + entity_version = 56, ) + ], + ) + """ + + def testPrincipalRoles(self): + """Test PrincipalRoles""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_principal_with_credentials.py b/regtests/client/python/test/test_principal_with_credentials.py new file mode 100644 index 0000000000..b497b71286 --- /dev/null +++ b/regtests/client/python/test/test_principal_with_credentials.py @@ -0,0 +1,76 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.management.models.principal_with_credentials import PrincipalWithCredentials + +class TestPrincipalWithCredentials(unittest.TestCase): + """PrincipalWithCredentials unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> PrincipalWithCredentials: + """Test PrincipalWithCredentials + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `PrincipalWithCredentials` + """ + model = PrincipalWithCredentials() + if include_optional: + return PrincipalWithCredentials( + principal = polaris.management.models.principal.Principal( + type = 'SERVICE', + name = '', + client_id = '', + properties = { + 'key' : '' + }, + create_timestamp = 56, + last_update_timestamp = 56, + entity_version = 56, ), + credentials = polaris.management.models.principal_with_credentials_credentials.PrincipalWithCredentials_credentials( + client_id = '', + client_secret = '', ) + ) + else: + return PrincipalWithCredentials( + principal = polaris.management.models.principal.Principal( + type = 'SERVICE', + name = '', + client_id = '', + properties = { + 'key' : '' + }, + create_timestamp = 56, + last_update_timestamp = 56, + entity_version = 56, ), + credentials = polaris.management.models.principal_with_credentials_credentials.PrincipalWithCredentials_credentials( + client_id = '', + client_secret = '', ), + ) + """ + + def testPrincipalWithCredentials(self): + """Test PrincipalWithCredentials""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_principal_with_credentials_credentials.py b/regtests/client/python/test/test_principal_with_credentials_credentials.py new file mode 100644 index 0000000000..ca0585c019 --- /dev/null +++ b/regtests/client/python/test/test_principal_with_credentials_credentials.py @@ -0,0 +1,52 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.management.models.principal_with_credentials_credentials import PrincipalWithCredentialsCredentials + +class TestPrincipalWithCredentialsCredentials(unittest.TestCase): + """PrincipalWithCredentialsCredentials unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> PrincipalWithCredentialsCredentials: + """Test PrincipalWithCredentialsCredentials + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `PrincipalWithCredentialsCredentials` + """ + model = PrincipalWithCredentialsCredentials() + if include_optional: + return PrincipalWithCredentialsCredentials( + client_id = '', + client_secret = '' + ) + else: + return PrincipalWithCredentialsCredentials( + ) + """ + + def testPrincipalWithCredentialsCredentials(self): + """Test PrincipalWithCredentialsCredentials""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_principals.py b/regtests/client/python/test/test_principals.py new file mode 100644 index 0000000000..628714b539 --- /dev/null +++ b/regtests/client/python/test/test_principals.py @@ -0,0 +1,74 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.management.models.principals import Principals + +class TestPrincipals(unittest.TestCase): + """Principals unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> Principals: + """Test Principals + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `Principals` + """ + model = Principals() + if include_optional: + return Principals( + principals = [ + polaris.management.models.principal.Principal( + type = 'SERVICE', + name = '', + client_id = '', + properties = { + 'key' : '' + }, + create_timestamp = 56, + last_update_timestamp = 56, + entity_version = 56, ) + ] + ) + else: + return Principals( + principals = [ + polaris.management.models.principal.Principal( + type = 'SERVICE', + name = '', + client_id = '', + properties = { + 'key' : '' + }, + create_timestamp = 56, + last_update_timestamp = 56, + entity_version = 56, ) + ], + ) + """ + + def testPrincipals(self): + """Test Principals""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_register_table_request.py b/regtests/client/python/test/test_register_table_request.py new file mode 100644 index 0000000000..3267b200f6 --- /dev/null +++ b/regtests/client/python/test/test_register_table_request.py @@ -0,0 +1,54 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.register_table_request import RegisterTableRequest + +class TestRegisterTableRequest(unittest.TestCase): + """RegisterTableRequest unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> RegisterTableRequest: + """Test RegisterTableRequest + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `RegisterTableRequest` + """ + model = RegisterTableRequest() + if include_optional: + return RegisterTableRequest( + name = '', + metadata_location = '' + ) + else: + return RegisterTableRequest( + name = '', + metadata_location = '', + ) + """ + + def testRegisterTableRequest(self): + """Test RegisterTableRequest""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_remove_partition_statistics_update.py b/regtests/client/python/test/test_remove_partition_statistics_update.py new file mode 100644 index 0000000000..7028034d80 --- /dev/null +++ b/regtests/client/python/test/test_remove_partition_statistics_update.py @@ -0,0 +1,54 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.remove_partition_statistics_update import RemovePartitionStatisticsUpdate + +class TestRemovePartitionStatisticsUpdate(unittest.TestCase): + """RemovePartitionStatisticsUpdate unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> RemovePartitionStatisticsUpdate: + """Test RemovePartitionStatisticsUpdate + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `RemovePartitionStatisticsUpdate` + """ + model = RemovePartitionStatisticsUpdate() + if include_optional: + return RemovePartitionStatisticsUpdate( + action = 'remove-partition-statistics', + snapshot_id = 56 + ) + else: + return RemovePartitionStatisticsUpdate( + action = 'remove-partition-statistics', + snapshot_id = 56, + ) + """ + + def testRemovePartitionStatisticsUpdate(self): + """Test RemovePartitionStatisticsUpdate""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_remove_properties_update.py b/regtests/client/python/test/test_remove_properties_update.py new file mode 100644 index 0000000000..8c78854400 --- /dev/null +++ b/regtests/client/python/test/test_remove_properties_update.py @@ -0,0 +1,58 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.remove_properties_update import RemovePropertiesUpdate + +class TestRemovePropertiesUpdate(unittest.TestCase): + """RemovePropertiesUpdate unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> RemovePropertiesUpdate: + """Test RemovePropertiesUpdate + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `RemovePropertiesUpdate` + """ + model = RemovePropertiesUpdate() + if include_optional: + return RemovePropertiesUpdate( + action = 'remove-properties', + removals = [ + '' + ] + ) + else: + return RemovePropertiesUpdate( + action = 'remove-properties', + removals = [ + '' + ], + ) + """ + + def testRemovePropertiesUpdate(self): + """Test RemovePropertiesUpdate""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_remove_snapshot_ref_update.py b/regtests/client/python/test/test_remove_snapshot_ref_update.py new file mode 100644 index 0000000000..91dd203c5e --- /dev/null +++ b/regtests/client/python/test/test_remove_snapshot_ref_update.py @@ -0,0 +1,54 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.remove_snapshot_ref_update import RemoveSnapshotRefUpdate + +class TestRemoveSnapshotRefUpdate(unittest.TestCase): + """RemoveSnapshotRefUpdate unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> RemoveSnapshotRefUpdate: + """Test RemoveSnapshotRefUpdate + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `RemoveSnapshotRefUpdate` + """ + model = RemoveSnapshotRefUpdate() + if include_optional: + return RemoveSnapshotRefUpdate( + action = 'remove-snapshot-ref', + ref_name = '' + ) + else: + return RemoveSnapshotRefUpdate( + action = 'remove-snapshot-ref', + ref_name = '', + ) + """ + + def testRemoveSnapshotRefUpdate(self): + """Test RemoveSnapshotRefUpdate""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_remove_snapshots_update.py b/regtests/client/python/test/test_remove_snapshots_update.py new file mode 100644 index 0000000000..6bb43297f9 --- /dev/null +++ b/regtests/client/python/test/test_remove_snapshots_update.py @@ -0,0 +1,58 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.remove_snapshots_update import RemoveSnapshotsUpdate + +class TestRemoveSnapshotsUpdate(unittest.TestCase): + """RemoveSnapshotsUpdate unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> RemoveSnapshotsUpdate: + """Test RemoveSnapshotsUpdate + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `RemoveSnapshotsUpdate` + """ + model = RemoveSnapshotsUpdate() + if include_optional: + return RemoveSnapshotsUpdate( + action = 'remove-snapshots', + snapshot_ids = [ + 56 + ] + ) + else: + return RemoveSnapshotsUpdate( + action = 'remove-snapshots', + snapshot_ids = [ + 56 + ], + ) + """ + + def testRemoveSnapshotsUpdate(self): + """Test RemoveSnapshotsUpdate""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_remove_statistics_update.py b/regtests/client/python/test/test_remove_statistics_update.py new file mode 100644 index 0000000000..d3fe59a079 --- /dev/null +++ b/regtests/client/python/test/test_remove_statistics_update.py @@ -0,0 +1,54 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.remove_statistics_update import RemoveStatisticsUpdate + +class TestRemoveStatisticsUpdate(unittest.TestCase): + """RemoveStatisticsUpdate unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> RemoveStatisticsUpdate: + """Test RemoveStatisticsUpdate + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `RemoveStatisticsUpdate` + """ + model = RemoveStatisticsUpdate() + if include_optional: + return RemoveStatisticsUpdate( + action = 'remove-statistics', + snapshot_id = 56 + ) + else: + return RemoveStatisticsUpdate( + action = 'remove-statistics', + snapshot_id = 56, + ) + """ + + def testRemoveStatisticsUpdate(self): + """Test RemoveStatisticsUpdate""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_rename_table_request.py b/regtests/client/python/test/test_rename_table_request.py new file mode 100644 index 0000000000..2d0256b3e7 --- /dev/null +++ b/regtests/client/python/test/test_rename_table_request.py @@ -0,0 +1,62 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.rename_table_request import RenameTableRequest + +class TestRenameTableRequest(unittest.TestCase): + """RenameTableRequest unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> RenameTableRequest: + """Test RenameTableRequest + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `RenameTableRequest` + """ + model = RenameTableRequest() + if include_optional: + return RenameTableRequest( + source = polaris.catalog.models.table_identifier.TableIdentifier( + namespace = ["accounting","tax"], + name = '', ), + destination = polaris.catalog.models.table_identifier.TableIdentifier( + namespace = ["accounting","tax"], + name = '', ) + ) + else: + return RenameTableRequest( + source = polaris.catalog.models.table_identifier.TableIdentifier( + namespace = ["accounting","tax"], + name = '', ), + destination = polaris.catalog.models.table_identifier.TableIdentifier( + namespace = ["accounting","tax"], + name = '', ), + ) + """ + + def testRenameTableRequest(self): + """Test RenameTableRequest""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_report_metrics_request.py b/regtests/client/python/test/test_report_metrics_request.py new file mode 100644 index 0000000000..4fa51dd2f9 --- /dev/null +++ b/regtests/client/python/test/test_report_metrics_request.py @@ -0,0 +1,81 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.report_metrics_request import ReportMetricsRequest + +class TestReportMetricsRequest(unittest.TestCase): + """ReportMetricsRequest unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> ReportMetricsRequest: + """Test ReportMetricsRequest + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `ReportMetricsRequest` + """ + model = ReportMetricsRequest() + if include_optional: + return ReportMetricsRequest( + report_type = '', + table_name = '', + snapshot_id = 56, + filter = None, + schema_id = 56, + projected_field_ids = [ + 56 + ], + projected_field_names = [ + '' + ], + metrics = {"metrics":{"total-planning-duration":{"count":1,"time-unit":"nanoseconds","total-duration":2644235116},"result-data-files":{"unit":"count","value":1},"result-delete-files":{"unit":"count","value":0},"total-data-manifests":{"unit":"count","value":1},"total-delete-manifests":{"unit":"count","value":0},"scanned-data-manifests":{"unit":"count","value":1},"skipped-data-manifests":{"unit":"count","value":0},"total-file-size-bytes":{"unit":"bytes","value":10},"total-delete-file-size-bytes":{"unit":"bytes","value":0}}}, + metadata = { + 'key' : '' + }, + sequence_number = 56, + operation = '' + ) + else: + return ReportMetricsRequest( + report_type = '', + table_name = '', + snapshot_id = 56, + filter = None, + schema_id = 56, + projected_field_ids = [ + 56 + ], + projected_field_names = [ + '' + ], + metrics = {"metrics":{"total-planning-duration":{"count":1,"time-unit":"nanoseconds","total-duration":2644235116},"result-data-files":{"unit":"count","value":1},"result-delete-files":{"unit":"count","value":0},"total-data-manifests":{"unit":"count","value":1},"total-delete-manifests":{"unit":"count","value":0},"scanned-data-manifests":{"unit":"count","value":1},"skipped-data-manifests":{"unit":"count","value":0},"total-file-size-bytes":{"unit":"bytes","value":10},"total-delete-file-size-bytes":{"unit":"bytes","value":0}}}, + sequence_number = 56, + operation = '', + ) + """ + + def testReportMetricsRequest(self): + """Test ReportMetricsRequest""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_revoke_grant_request.py b/regtests/client/python/test/test_revoke_grant_request.py new file mode 100644 index 0000000000..50f013350c --- /dev/null +++ b/regtests/client/python/test/test_revoke_grant_request.py @@ -0,0 +1,52 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.management.models.revoke_grant_request import RevokeGrantRequest + +class TestRevokeGrantRequest(unittest.TestCase): + """RevokeGrantRequest unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> RevokeGrantRequest: + """Test RevokeGrantRequest + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `RevokeGrantRequest` + """ + model = RevokeGrantRequest() + if include_optional: + return RevokeGrantRequest( + grant = polaris.management.models.grant_resource.GrantResource( + type = 'catalog', ) + ) + else: + return RevokeGrantRequest( + ) + """ + + def testRevokeGrantRequest(self): + """Test RevokeGrantRequest""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_scan_report.py b/regtests/client/python/test/test_scan_report.py new file mode 100644 index 0000000000..82ba62bb76 --- /dev/null +++ b/regtests/client/python/test/test_scan_report.py @@ -0,0 +1,75 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.scan_report import ScanReport + +class TestScanReport(unittest.TestCase): + """ScanReport unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> ScanReport: + """Test ScanReport + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `ScanReport` + """ + model = ScanReport() + if include_optional: + return ScanReport( + table_name = '', + snapshot_id = 56, + filter = None, + schema_id = 56, + projected_field_ids = [ + 56 + ], + projected_field_names = [ + '' + ], + metrics = {"metrics":{"total-planning-duration":{"count":1,"time-unit":"nanoseconds","total-duration":2644235116},"result-data-files":{"unit":"count","value":1},"result-delete-files":{"unit":"count","value":0},"total-data-manifests":{"unit":"count","value":1},"total-delete-manifests":{"unit":"count","value":0},"scanned-data-manifests":{"unit":"count","value":1},"skipped-data-manifests":{"unit":"count","value":0},"total-file-size-bytes":{"unit":"bytes","value":10},"total-delete-file-size-bytes":{"unit":"bytes","value":0}}}, + metadata = { + 'key' : '' + } + ) + else: + return ScanReport( + table_name = '', + snapshot_id = 56, + filter = None, + schema_id = 56, + projected_field_ids = [ + 56 + ], + projected_field_names = [ + '' + ], + metrics = {"metrics":{"total-planning-duration":{"count":1,"time-unit":"nanoseconds","total-duration":2644235116},"result-data-files":{"unit":"count","value":1},"result-delete-files":{"unit":"count","value":0},"total-data-manifests":{"unit":"count","value":1},"total-delete-manifests":{"unit":"count","value":0},"scanned-data-manifests":{"unit":"count","value":1},"skipped-data-manifests":{"unit":"count","value":0},"total-file-size-bytes":{"unit":"bytes","value":10},"total-delete-file-size-bytes":{"unit":"bytes","value":0}}}, + ) + """ + + def testScanReport(self): + """Test ScanReport""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_set_current_schema_update.py b/regtests/client/python/test/test_set_current_schema_update.py new file mode 100644 index 0000000000..8dbb4c95f8 --- /dev/null +++ b/regtests/client/python/test/test_set_current_schema_update.py @@ -0,0 +1,54 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.set_current_schema_update import SetCurrentSchemaUpdate + +class TestSetCurrentSchemaUpdate(unittest.TestCase): + """SetCurrentSchemaUpdate unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> SetCurrentSchemaUpdate: + """Test SetCurrentSchemaUpdate + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `SetCurrentSchemaUpdate` + """ + model = SetCurrentSchemaUpdate() + if include_optional: + return SetCurrentSchemaUpdate( + action = 'set-current-schema', + schema_id = 56 + ) + else: + return SetCurrentSchemaUpdate( + action = 'set-current-schema', + schema_id = 56, + ) + """ + + def testSetCurrentSchemaUpdate(self): + """Test SetCurrentSchemaUpdate""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_set_current_view_version_update.py b/regtests/client/python/test/test_set_current_view_version_update.py new file mode 100644 index 0000000000..492b19a9e6 --- /dev/null +++ b/regtests/client/python/test/test_set_current_view_version_update.py @@ -0,0 +1,54 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.set_current_view_version_update import SetCurrentViewVersionUpdate + +class TestSetCurrentViewVersionUpdate(unittest.TestCase): + """SetCurrentViewVersionUpdate unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> SetCurrentViewVersionUpdate: + """Test SetCurrentViewVersionUpdate + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `SetCurrentViewVersionUpdate` + """ + model = SetCurrentViewVersionUpdate() + if include_optional: + return SetCurrentViewVersionUpdate( + action = 'set-current-view-version', + view_version_id = 56 + ) + else: + return SetCurrentViewVersionUpdate( + action = 'set-current-view-version', + view_version_id = 56, + ) + """ + + def testSetCurrentViewVersionUpdate(self): + """Test SetCurrentViewVersionUpdate""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_set_default_sort_order_update.py b/regtests/client/python/test/test_set_default_sort_order_update.py new file mode 100644 index 0000000000..a9a333fc09 --- /dev/null +++ b/regtests/client/python/test/test_set_default_sort_order_update.py @@ -0,0 +1,54 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.set_default_sort_order_update import SetDefaultSortOrderUpdate + +class TestSetDefaultSortOrderUpdate(unittest.TestCase): + """SetDefaultSortOrderUpdate unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> SetDefaultSortOrderUpdate: + """Test SetDefaultSortOrderUpdate + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `SetDefaultSortOrderUpdate` + """ + model = SetDefaultSortOrderUpdate() + if include_optional: + return SetDefaultSortOrderUpdate( + action = 'set-default-sort-order', + sort_order_id = 56 + ) + else: + return SetDefaultSortOrderUpdate( + action = 'set-default-sort-order', + sort_order_id = 56, + ) + """ + + def testSetDefaultSortOrderUpdate(self): + """Test SetDefaultSortOrderUpdate""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_set_default_spec_update.py b/regtests/client/python/test/test_set_default_spec_update.py new file mode 100644 index 0000000000..61f074e51e --- /dev/null +++ b/regtests/client/python/test/test_set_default_spec_update.py @@ -0,0 +1,54 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.set_default_spec_update import SetDefaultSpecUpdate + +class TestSetDefaultSpecUpdate(unittest.TestCase): + """SetDefaultSpecUpdate unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> SetDefaultSpecUpdate: + """Test SetDefaultSpecUpdate + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `SetDefaultSpecUpdate` + """ + model = SetDefaultSpecUpdate() + if include_optional: + return SetDefaultSpecUpdate( + action = 'set-default-spec', + spec_id = 56 + ) + else: + return SetDefaultSpecUpdate( + action = 'set-default-spec', + spec_id = 56, + ) + """ + + def testSetDefaultSpecUpdate(self): + """Test SetDefaultSpecUpdate""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_set_expression.py b/regtests/client/python/test/test_set_expression.py new file mode 100644 index 0000000000..7f91657382 --- /dev/null +++ b/regtests/client/python/test/test_set_expression.py @@ -0,0 +1,60 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.set_expression import SetExpression + +class TestSetExpression(unittest.TestCase): + """SetExpression unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> SetExpression: + """Test SetExpression + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `SetExpression` + """ + model = SetExpression() + if include_optional: + return SetExpression( + type = '["eq","and","or","not","in","not-in","lt","lt-eq","gt","gt-eq","not-eq","starts-with","not-starts-with","is-null","not-null","is-nan","not-nan"]', + term = None, + values = [ + None + ] + ) + else: + return SetExpression( + type = '["eq","and","or","not","in","not-in","lt","lt-eq","gt","gt-eq","not-eq","starts-with","not-starts-with","is-null","not-null","is-nan","not-nan"]', + term = None, + values = [ + None + ], + ) + """ + + def testSetExpression(self): + """Test SetExpression""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_set_location_update.py b/regtests/client/python/test/test_set_location_update.py new file mode 100644 index 0000000000..6ff145f61b --- /dev/null +++ b/regtests/client/python/test/test_set_location_update.py @@ -0,0 +1,54 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.set_location_update import SetLocationUpdate + +class TestSetLocationUpdate(unittest.TestCase): + """SetLocationUpdate unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> SetLocationUpdate: + """Test SetLocationUpdate + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `SetLocationUpdate` + """ + model = SetLocationUpdate() + if include_optional: + return SetLocationUpdate( + action = 'set-location', + location = '' + ) + else: + return SetLocationUpdate( + action = 'set-location', + location = '', + ) + """ + + def testSetLocationUpdate(self): + """Test SetLocationUpdate""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_set_partition_statistics_update.py b/regtests/client/python/test/test_set_partition_statistics_update.py new file mode 100644 index 0000000000..004ce9bbfd --- /dev/null +++ b/regtests/client/python/test/test_set_partition_statistics_update.py @@ -0,0 +1,60 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.set_partition_statistics_update import SetPartitionStatisticsUpdate + +class TestSetPartitionStatisticsUpdate(unittest.TestCase): + """SetPartitionStatisticsUpdate unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> SetPartitionStatisticsUpdate: + """Test SetPartitionStatisticsUpdate + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `SetPartitionStatisticsUpdate` + """ + model = SetPartitionStatisticsUpdate() + if include_optional: + return SetPartitionStatisticsUpdate( + action = 'set-partition-statistics', + partition_statistics = polaris.catalog.models.partition_statistics_file.PartitionStatisticsFile( + snapshot_id = 56, + statistics_path = '', + file_size_in_bytes = 56, ) + ) + else: + return SetPartitionStatisticsUpdate( + action = 'set-partition-statistics', + partition_statistics = polaris.catalog.models.partition_statistics_file.PartitionStatisticsFile( + snapshot_id = 56, + statistics_path = '', + file_size_in_bytes = 56, ), + ) + """ + + def testSetPartitionStatisticsUpdate(self): + """Test SetPartitionStatisticsUpdate""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_set_properties_update.py b/regtests/client/python/test/test_set_properties_update.py new file mode 100644 index 0000000000..ca25bcdf5a --- /dev/null +++ b/regtests/client/python/test/test_set_properties_update.py @@ -0,0 +1,58 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.set_properties_update import SetPropertiesUpdate + +class TestSetPropertiesUpdate(unittest.TestCase): + """SetPropertiesUpdate unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> SetPropertiesUpdate: + """Test SetPropertiesUpdate + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `SetPropertiesUpdate` + """ + model = SetPropertiesUpdate() + if include_optional: + return SetPropertiesUpdate( + action = 'set-properties', + updates = { + 'key' : '' + } + ) + else: + return SetPropertiesUpdate( + action = 'set-properties', + updates = { + 'key' : '' + }, + ) + """ + + def testSetPropertiesUpdate(self): + """Test SetPropertiesUpdate""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_set_snapshot_ref_update.py b/regtests/client/python/test/test_set_snapshot_ref_update.py new file mode 100644 index 0000000000..3e7dc74bdc --- /dev/null +++ b/regtests/client/python/test/test_set_snapshot_ref_update.py @@ -0,0 +1,61 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.set_snapshot_ref_update import SetSnapshotRefUpdate + +class TestSetSnapshotRefUpdate(unittest.TestCase): + """SetSnapshotRefUpdate unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> SetSnapshotRefUpdate: + """Test SetSnapshotRefUpdate + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `SetSnapshotRefUpdate` + """ + model = SetSnapshotRefUpdate() + if include_optional: + return SetSnapshotRefUpdate( + action = 'set-snapshot-ref', + ref_name = '', + type = 'tag', + snapshot_id = 56, + max_ref_age_ms = 56, + max_snapshot_age_ms = 56, + min_snapshots_to_keep = 56 + ) + else: + return SetSnapshotRefUpdate( + action = 'set-snapshot-ref', + ref_name = '', + type = 'tag', + snapshot_id = 56, + ) + """ + + def testSetSnapshotRefUpdate(self): + """Test SetSnapshotRefUpdate""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_set_statistics_update.py b/regtests/client/python/test/test_set_statistics_update.py new file mode 100644 index 0000000000..066e88ee3b --- /dev/null +++ b/regtests/client/python/test/test_set_statistics_update.py @@ -0,0 +1,84 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.set_statistics_update import SetStatisticsUpdate + +class TestSetStatisticsUpdate(unittest.TestCase): + """SetStatisticsUpdate unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> SetStatisticsUpdate: + """Test SetStatisticsUpdate + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `SetStatisticsUpdate` + """ + model = SetStatisticsUpdate() + if include_optional: + return SetStatisticsUpdate( + action = 'set-statistics', + snapshot_id = 56, + statistics = polaris.catalog.models.statistics_file.StatisticsFile( + snapshot_id = 56, + statistics_path = '', + file_size_in_bytes = 56, + file_footer_size_in_bytes = 56, + blob_metadata = [ + polaris.catalog.models.blob_metadata.BlobMetadata( + type = '', + snapshot_id = 56, + sequence_number = 56, + fields = [ + 56 + ], + properties = polaris.catalog.models.properties.properties(), ) + ], ) + ) + else: + return SetStatisticsUpdate( + action = 'set-statistics', + snapshot_id = 56, + statistics = polaris.catalog.models.statistics_file.StatisticsFile( + snapshot_id = 56, + statistics_path = '', + file_size_in_bytes = 56, + file_footer_size_in_bytes = 56, + blob_metadata = [ + polaris.catalog.models.blob_metadata.BlobMetadata( + type = '', + snapshot_id = 56, + sequence_number = 56, + fields = [ + 56 + ], + properties = polaris.catalog.models.properties.properties(), ) + ], ), + ) + """ + + def testSetStatisticsUpdate(self): + """Test SetStatisticsUpdate""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_snapshot.py b/regtests/client/python/test/test_snapshot.py new file mode 100644 index 0000000000..1b57912764 --- /dev/null +++ b/regtests/client/python/test/test_snapshot.py @@ -0,0 +1,65 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.snapshot import Snapshot + +class TestSnapshot(unittest.TestCase): + """Snapshot unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> Snapshot: + """Test Snapshot + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `Snapshot` + """ + model = Snapshot() + if include_optional: + return Snapshot( + snapshot_id = 56, + parent_snapshot_id = 56, + sequence_number = 56, + timestamp_ms = 56, + manifest_list = '', + summary = { + 'key' : '' + }, + schema_id = 56 + ) + else: + return Snapshot( + snapshot_id = 56, + timestamp_ms = 56, + manifest_list = '', + summary = { + 'key' : '' + }, + ) + """ + + def testSnapshot(self): + """Test Snapshot""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_snapshot_log_inner.py b/regtests/client/python/test/test_snapshot_log_inner.py new file mode 100644 index 0000000000..0b14e15882 --- /dev/null +++ b/regtests/client/python/test/test_snapshot_log_inner.py @@ -0,0 +1,54 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.snapshot_log_inner import SnapshotLogInner + +class TestSnapshotLogInner(unittest.TestCase): + """SnapshotLogInner unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> SnapshotLogInner: + """Test SnapshotLogInner + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `SnapshotLogInner` + """ + model = SnapshotLogInner() + if include_optional: + return SnapshotLogInner( + snapshot_id = 56, + timestamp_ms = 56 + ) + else: + return SnapshotLogInner( + snapshot_id = 56, + timestamp_ms = 56, + ) + """ + + def testSnapshotLogInner(self): + """Test SnapshotLogInner""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_snapshot_reference.py b/regtests/client/python/test/test_snapshot_reference.py new file mode 100644 index 0000000000..5266837c87 --- /dev/null +++ b/regtests/client/python/test/test_snapshot_reference.py @@ -0,0 +1,57 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.snapshot_reference import SnapshotReference + +class TestSnapshotReference(unittest.TestCase): + """SnapshotReference unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> SnapshotReference: + """Test SnapshotReference + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `SnapshotReference` + """ + model = SnapshotReference() + if include_optional: + return SnapshotReference( + type = 'tag', + snapshot_id = 56, + max_ref_age_ms = 56, + max_snapshot_age_ms = 56, + min_snapshots_to_keep = 56 + ) + else: + return SnapshotReference( + type = 'tag', + snapshot_id = 56, + ) + """ + + def testSnapshotReference(self): + """Test SnapshotReference""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_snapshot_summary.py b/regtests/client/python/test/test_snapshot_summary.py new file mode 100644 index 0000000000..0d985f7ce3 --- /dev/null +++ b/regtests/client/python/test/test_snapshot_summary.py @@ -0,0 +1,52 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.snapshot_summary import SnapshotSummary + +class TestSnapshotSummary(unittest.TestCase): + """SnapshotSummary unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> SnapshotSummary: + """Test SnapshotSummary + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `SnapshotSummary` + """ + model = SnapshotSummary() + if include_optional: + return SnapshotSummary( + operation = 'append' + ) + else: + return SnapshotSummary( + operation = 'append', + ) + """ + + def testSnapshotSummary(self): + """Test SnapshotSummary""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_sort_direction.py b/regtests/client/python/test/test_sort_direction.py new file mode 100644 index 0000000000..9e1e62832a --- /dev/null +++ b/regtests/client/python/test/test_sort_direction.py @@ -0,0 +1,33 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.sort_direction import SortDirection + +class TestSortDirection(unittest.TestCase): + """SortDirection unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def testSortDirection(self): + """Test SortDirection""" + # inst = SortDirection() + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_sort_field.py b/regtests/client/python/test/test_sort_field.py new file mode 100644 index 0000000000..50ab4463a3 --- /dev/null +++ b/regtests/client/python/test/test_sort_field.py @@ -0,0 +1,58 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.sort_field import SortField + +class TestSortField(unittest.TestCase): + """SortField unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> SortField: + """Test SortField + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `SortField` + """ + model = SortField() + if include_optional: + return SortField( + source_id = 56, + transform = '["identity","year","month","day","hour","bucket[256]","truncate[16]"]', + direction = 'asc', + null_order = 'nulls-first' + ) + else: + return SortField( + source_id = 56, + transform = '["identity","year","month","day","hour","bucket[256]","truncate[16]"]', + direction = 'asc', + null_order = 'nulls-first', + ) + """ + + def testSortField(self): + """Test SortField""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_sort_order.py b/regtests/client/python/test/test_sort_order.py new file mode 100644 index 0000000000..31357142a4 --- /dev/null +++ b/regtests/client/python/test/test_sort_order.py @@ -0,0 +1,66 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.sort_order import SortOrder + +class TestSortOrder(unittest.TestCase): + """SortOrder unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> SortOrder: + """Test SortOrder + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `SortOrder` + """ + model = SortOrder() + if include_optional: + return SortOrder( + order_id = 56, + fields = [ + polaris.catalog.models.sort_field.SortField( + source_id = 56, + transform = '["identity","year","month","day","hour","bucket[256]","truncate[16]"]', + direction = 'asc', + null_order = 'nulls-first', ) + ] + ) + else: + return SortOrder( + order_id = 56, + fields = [ + polaris.catalog.models.sort_field.SortField( + source_id = 56, + transform = '["identity","year","month","day","hour","bucket[256]","truncate[16]"]', + direction = 'asc', + null_order = 'nulls-first', ) + ], + ) + """ + + def testSortOrder(self): + """Test SortOrder""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_sql_view_representation.py b/regtests/client/python/test/test_sql_view_representation.py new file mode 100644 index 0000000000..cf926ed455 --- /dev/null +++ b/regtests/client/python/test/test_sql_view_representation.py @@ -0,0 +1,56 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.sql_view_representation import SQLViewRepresentation + +class TestSQLViewRepresentation(unittest.TestCase): + """SQLViewRepresentation unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> SQLViewRepresentation: + """Test SQLViewRepresentation + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `SQLViewRepresentation` + """ + model = SQLViewRepresentation() + if include_optional: + return SQLViewRepresentation( + type = '', + sql = '', + dialect = '' + ) + else: + return SQLViewRepresentation( + type = '', + sql = '', + dialect = '', + ) + """ + + def testSQLViewRepresentation(self): + """Test SQLViewRepresentation""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_statistics_file.py b/regtests/client/python/test/test_statistics_file.py new file mode 100644 index 0000000000..5a458aa6c6 --- /dev/null +++ b/regtests/client/python/test/test_statistics_file.py @@ -0,0 +1,78 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.statistics_file import StatisticsFile + +class TestStatisticsFile(unittest.TestCase): + """StatisticsFile unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> StatisticsFile: + """Test StatisticsFile + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `StatisticsFile` + """ + model = StatisticsFile() + if include_optional: + return StatisticsFile( + snapshot_id = 56, + statistics_path = '', + file_size_in_bytes = 56, + file_footer_size_in_bytes = 56, + blob_metadata = [ + polaris.catalog.models.blob_metadata.BlobMetadata( + type = '', + snapshot_id = 56, + sequence_number = 56, + fields = [ + 56 + ], + properties = polaris.catalog.models.properties.properties(), ) + ] + ) + else: + return StatisticsFile( + snapshot_id = 56, + statistics_path = '', + file_size_in_bytes = 56, + file_footer_size_in_bytes = 56, + blob_metadata = [ + polaris.catalog.models.blob_metadata.BlobMetadata( + type = '', + snapshot_id = 56, + sequence_number = 56, + fields = [ + 56 + ], + properties = polaris.catalog.models.properties.properties(), ) + ], + ) + """ + + def testStatisticsFile(self): + """Test StatisticsFile""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_storage_config_info.py b/regtests/client/python/test/test_storage_config_info.py new file mode 100644 index 0000000000..5293afa4e8 --- /dev/null +++ b/regtests/client/python/test/test_storage_config_info.py @@ -0,0 +1,53 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.management.models.storage_config_info import StorageConfigInfo + +class TestStorageConfigInfo(unittest.TestCase): + """StorageConfigInfo unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> StorageConfigInfo: + """Test StorageConfigInfo + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `StorageConfigInfo` + """ + model = StorageConfigInfo() + if include_optional: + return StorageConfigInfo( + storage_type = 'S3', + allowed_locations = For AWS [s3://bucketname/prefix/], for AZURE [abfss://container@storageaccount.blob.core.windows.net/prefix/], for GCP [gs://bucketname/prefix/] + ) + else: + return StorageConfigInfo( + storage_type = 'S3', + ) + """ + + def testStorageConfigInfo(self): + """Test StorageConfigInfo""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_struct_field.py b/regtests/client/python/test/test_struct_field.py new file mode 100644 index 0000000000..ced146839d --- /dev/null +++ b/regtests/client/python/test/test_struct_field.py @@ -0,0 +1,59 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.struct_field import StructField + +class TestStructField(unittest.TestCase): + """StructField unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> StructField: + """Test StructField + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `StructField` + """ + model = StructField() + if include_optional: + return StructField( + id = 56, + name = '', + type = None, + required = True, + doc = '' + ) + else: + return StructField( + id = 56, + name = '', + type = None, + required = True, + ) + """ + + def testStructField(self): + """Test StructField""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_struct_type.py b/regtests/client/python/test/test_struct_type.py new file mode 100644 index 0000000000..abce7ed757 --- /dev/null +++ b/regtests/client/python/test/test_struct_type.py @@ -0,0 +1,68 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.struct_type import StructType + +class TestStructType(unittest.TestCase): + """StructType unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> StructType: + """Test StructType + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `StructType` + """ + model = StructType() + if include_optional: + return StructType( + type = 'struct', + fields = [ + polaris.catalog.models.struct_field.StructField( + id = 56, + name = '', + type = null, + required = True, + doc = '', ) + ] + ) + else: + return StructType( + type = 'struct', + fields = [ + polaris.catalog.models.struct_field.StructField( + id = 56, + name = '', + type = null, + required = True, + doc = '', ) + ], + ) + """ + + def testStructType(self): + """Test StructType""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_table_grant.py b/regtests/client/python/test/test_table_grant.py new file mode 100644 index 0000000000..361c69eb30 --- /dev/null +++ b/regtests/client/python/test/test_table_grant.py @@ -0,0 +1,60 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.management.models.table_grant import TableGrant + +class TestTableGrant(unittest.TestCase): + """TableGrant unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> TableGrant: + """Test TableGrant + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `TableGrant` + """ + model = TableGrant() + if include_optional: + return TableGrant( + namespace = [ + '' + ], + table_name = '', + privilege = 'CATALOG_MANAGE_ACCESS' + ) + else: + return TableGrant( + namespace = [ + '' + ], + table_name = '', + privilege = 'CATALOG_MANAGE_ACCESS', + ) + """ + + def testTableGrant(self): + """Test TableGrant""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_table_identifier.py b/regtests/client/python/test/test_table_identifier.py new file mode 100644 index 0000000000..bbbb700a43 --- /dev/null +++ b/regtests/client/python/test/test_table_identifier.py @@ -0,0 +1,54 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.table_identifier import TableIdentifier + +class TestTableIdentifier(unittest.TestCase): + """TableIdentifier unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> TableIdentifier: + """Test TableIdentifier + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `TableIdentifier` + """ + model = TableIdentifier() + if include_optional: + return TableIdentifier( + namespace = ["accounting","tax"], + name = '' + ) + else: + return TableIdentifier( + namespace = ["accounting","tax"], + name = '', + ) + """ + + def testTableIdentifier(self): + """Test TableIdentifier""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_table_metadata.py b/regtests/client/python/test/test_table_metadata.py new file mode 100644 index 0000000000..63af8e44b2 --- /dev/null +++ b/regtests/client/python/test/test_table_metadata.py @@ -0,0 +1,144 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.table_metadata import TableMetadata + +class TestTableMetadata(unittest.TestCase): + """TableMetadata unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> TableMetadata: + """Test TableMetadata + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `TableMetadata` + """ + model = TableMetadata() + if include_optional: + return TableMetadata( + format_version = 1, + table_uuid = '', + location = '', + last_updated_ms = 56, + properties = { + 'key' : '' + }, + schemas = [ + null + ], + current_schema_id = 56, + last_column_id = 56, + partition_specs = [ + polaris.catalog.models.partition_spec.PartitionSpec( + spec_id = 56, + fields = [ + polaris.catalog.models.partition_field.PartitionField( + field_id = 56, + source_id = 56, + name = '', + transform = '["identity","year","month","day","hour","bucket[256]","truncate[16]"]', ) + ], ) + ], + default_spec_id = 56, + last_partition_id = 56, + sort_orders = [ + polaris.catalog.models.sort_order.SortOrder( + order_id = 56, + fields = [ + polaris.catalog.models.sort_field.SortField( + source_id = 56, + transform = '["identity","year","month","day","hour","bucket[256]","truncate[16]"]', + direction = 'asc', + null_order = 'nulls-first', ) + ], ) + ], + default_sort_order_id = 56, + snapshots = [ + polaris.catalog.models.snapshot.Snapshot( + snapshot_id = 56, + parent_snapshot_id = 56, + sequence_number = 56, + timestamp_ms = 56, + manifest_list = '', + summary = { + 'key' : '' + }, + schema_id = 56, ) + ], + refs = { + 'key' : polaris.catalog.models.snapshot_reference.SnapshotReference( + type = 'tag', + snapshot_id = 56, + max_ref_age_ms = 56, + max_snapshot_age_ms = 56, + min_snapshots_to_keep = 56, ) + }, + current_snapshot_id = 56, + last_sequence_number = 56, + snapshot_log = [ + polaris.catalog.models.snapshot_log_inner.SnapshotLog_inner( + snapshot_id = 56, + timestamp_ms = 56, ) + ], + metadata_log = [ + polaris.catalog.models.metadata_log_inner.MetadataLog_inner( + metadata_file = '', + timestamp_ms = 56, ) + ], + statistics_files = [ + polaris.catalog.models.statistics_file.StatisticsFile( + snapshot_id = 56, + statistics_path = '', + file_size_in_bytes = 56, + file_footer_size_in_bytes = 56, + blob_metadata = [ + polaris.catalog.models.blob_metadata.BlobMetadata( + type = '', + snapshot_id = 56, + sequence_number = 56, + fields = [ + 56 + ], + properties = polaris.catalog.models.properties.properties(), ) + ], ) + ], + partition_statistics_files = [ + polaris.catalog.models.partition_statistics_file.PartitionStatisticsFile( + snapshot_id = 56, + statistics_path = '', + file_size_in_bytes = 56, ) + ] + ) + else: + return TableMetadata( + format_version = 1, + table_uuid = '', + ) + """ + + def testTableMetadata(self): + """Test TableMetadata""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_table_privilege.py b/regtests/client/python/test/test_table_privilege.py new file mode 100644 index 0000000000..e64b60286b --- /dev/null +++ b/regtests/client/python/test/test_table_privilege.py @@ -0,0 +1,33 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.management.models.table_privilege import TablePrivilege + +class TestTablePrivilege(unittest.TestCase): + """TablePrivilege unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def testTablePrivilege(self): + """Test TablePrivilege""" + # inst = TablePrivilege() + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_table_requirement.py b/regtests/client/python/test/test_table_requirement.py new file mode 100644 index 0000000000..1d51bfe1d7 --- /dev/null +++ b/regtests/client/python/test/test_table_requirement.py @@ -0,0 +1,52 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.table_requirement import TableRequirement + +class TestTableRequirement(unittest.TestCase): + """TableRequirement unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> TableRequirement: + """Test TableRequirement + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `TableRequirement` + """ + model = TableRequirement() + if include_optional: + return TableRequirement( + type = '' + ) + else: + return TableRequirement( + type = '', + ) + """ + + def testTableRequirement(self): + """Test TableRequirement""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_table_update.py b/regtests/client/python/test/test_table_update.py new file mode 100644 index 0000000000..acbbe95da9 --- /dev/null +++ b/regtests/client/python/test/test_table_update.py @@ -0,0 +1,178 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.table_update import TableUpdate + +class TestTableUpdate(unittest.TestCase): + """TableUpdate unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> TableUpdate: + """Test TableUpdate + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `TableUpdate` + """ + model = TableUpdate() + if include_optional: + return TableUpdate( + action = '', + format_version = 56, + var_schema = None, + last_column_id = 56, + schema_id = 56, + spec = polaris.catalog.models.partition_spec.PartitionSpec( + spec_id = 56, + fields = [ + polaris.catalog.models.partition_field.PartitionField( + field_id = 56, + source_id = 56, + name = '', + transform = '["identity","year","month","day","hour","bucket[256]","truncate[16]"]', ) + ], ), + spec_id = 56, + sort_order = polaris.catalog.models.sort_order.SortOrder( + order_id = 56, + fields = [ + polaris.catalog.models.sort_field.SortField( + source_id = 56, + transform = '["identity","year","month","day","hour","bucket[256]","truncate[16]"]', + direction = 'asc', + null_order = 'nulls-first', ) + ], ), + sort_order_id = 56, + snapshot = polaris.catalog.models.snapshot.Snapshot( + snapshot_id = 56, + parent_snapshot_id = 56, + sequence_number = 56, + timestamp_ms = 56, + manifest_list = '', + summary = { + 'key' : '' + }, + schema_id = 56, ), + ref_name = '', + type = 'tag', + snapshot_id = 56, + max_ref_age_ms = 56, + max_snapshot_age_ms = 56, + min_snapshots_to_keep = 56, + snapshot_ids = [ + 56 + ], + location = '', + updates = { + 'key' : '' + }, + removals = [ + '' + ], + statistics = polaris.catalog.models.statistics_file.StatisticsFile( + snapshot_id = 56, + statistics_path = '', + file_size_in_bytes = 56, + file_footer_size_in_bytes = 56, + blob_metadata = [ + polaris.catalog.models.blob_metadata.BlobMetadata( + type = '', + snapshot_id = 56, + sequence_number = 56, + fields = [ + 56 + ], + properties = polaris.catalog.models.properties.properties(), ) + ], ) + ) + else: + return TableUpdate( + action = '', + format_version = 56, + var_schema = None, + schema_id = 56, + spec = polaris.catalog.models.partition_spec.PartitionSpec( + spec_id = 56, + fields = [ + polaris.catalog.models.partition_field.PartitionField( + field_id = 56, + source_id = 56, + name = '', + transform = '["identity","year","month","day","hour","bucket[256]","truncate[16]"]', ) + ], ), + spec_id = 56, + sort_order = polaris.catalog.models.sort_order.SortOrder( + order_id = 56, + fields = [ + polaris.catalog.models.sort_field.SortField( + source_id = 56, + transform = '["identity","year","month","day","hour","bucket[256]","truncate[16]"]', + direction = 'asc', + null_order = 'nulls-first', ) + ], ), + sort_order_id = 56, + snapshot = polaris.catalog.models.snapshot.Snapshot( + snapshot_id = 56, + parent_snapshot_id = 56, + sequence_number = 56, + timestamp_ms = 56, + manifest_list = '', + summary = { + 'key' : '' + }, + schema_id = 56, ), + ref_name = '', + type = 'tag', + snapshot_id = 56, + snapshot_ids = [ + 56 + ], + location = '', + updates = { + 'key' : '' + }, + removals = [ + '' + ], + statistics = polaris.catalog.models.statistics_file.StatisticsFile( + snapshot_id = 56, + statistics_path = '', + file_size_in_bytes = 56, + file_footer_size_in_bytes = 56, + blob_metadata = [ + polaris.catalog.models.blob_metadata.BlobMetadata( + type = '', + snapshot_id = 56, + sequence_number = 56, + fields = [ + 56 + ], + properties = polaris.catalog.models.properties.properties(), ) + ], ), + ) + """ + + def testTableUpdate(self): + """Test TableUpdate""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_table_update_notification.py b/regtests/client/python/test/test_table_update_notification.py new file mode 100644 index 0000000000..81f5e59898 --- /dev/null +++ b/regtests/client/python/test/test_table_update_notification.py @@ -0,0 +1,150 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.table_update_notification import TableUpdateNotification + +class TestTableUpdateNotification(unittest.TestCase): + """TableUpdateNotification unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> TableUpdateNotification: + """Test TableUpdateNotification + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `TableUpdateNotification` + """ + model = TableUpdateNotification() + if include_optional: + return TableUpdateNotification( + table_name = '', + timestamp = 56, + table_uuid = '', + metadata_location = '', + metadata = polaris.catalog.models.table_metadata.TableMetadata( + format_version = 1, + table_uuid = '', + location = '', + last_updated_ms = 56, + properties = { + 'key' : '' + }, + schemas = [ + null + ], + current_schema_id = 56, + last_column_id = 56, + partition_specs = [ + polaris.catalog.models.partition_spec.PartitionSpec( + spec_id = 56, + fields = [ + polaris.catalog.models.partition_field.PartitionField( + field_id = 56, + source_id = 56, + name = '', + transform = '["identity","year","month","day","hour","bucket[256]","truncate[16]"]', ) + ], ) + ], + default_spec_id = 56, + last_partition_id = 56, + sort_orders = [ + polaris.catalog.models.sort_order.SortOrder( + order_id = 56, + fields = [ + polaris.catalog.models.sort_field.SortField( + source_id = 56, + transform = '["identity","year","month","day","hour","bucket[256]","truncate[16]"]', + direction = 'asc', + null_order = 'nulls-first', ) + ], ) + ], + default_sort_order_id = 56, + snapshots = [ + polaris.catalog.models.snapshot.Snapshot( + snapshot_id = 56, + parent_snapshot_id = 56, + sequence_number = 56, + timestamp_ms = 56, + manifest_list = '', + summary = { + 'key' : '' + }, + schema_id = 56, ) + ], + refs = { + 'key' : polaris.catalog.models.snapshot_reference.SnapshotReference( + type = 'tag', + snapshot_id = 56, + max_ref_age_ms = 56, + max_snapshot_age_ms = 56, + min_snapshots_to_keep = 56, ) + }, + current_snapshot_id = 56, + last_sequence_number = 56, + snapshot_log = [ + polaris.catalog.models.snapshot_log_inner.SnapshotLog_inner( + snapshot_id = 56, + timestamp_ms = 56, ) + ], + metadata_log = [ + polaris.catalog.models.metadata_log_inner.MetadataLog_inner( + metadata_file = '', + timestamp_ms = 56, ) + ], + statistics_files = [ + polaris.catalog.models.statistics_file.StatisticsFile( + snapshot_id = 56, + statistics_path = '', + file_size_in_bytes = 56, + file_footer_size_in_bytes = 56, + blob_metadata = [ + polaris.catalog.models.blob_metadata.BlobMetadata( + type = '', + snapshot_id = 56, + sequence_number = 56, + fields = [ + 56 + ], ) + ], ) + ], + partition_statistics_files = [ + polaris.catalog.models.partition_statistics_file.PartitionStatisticsFile( + snapshot_id = 56, + statistics_path = '', + file_size_in_bytes = 56, ) + ], ) + ) + else: + return TableUpdateNotification( + table_name = '', + timestamp = 56, + table_uuid = '', + metadata_location = '', + ) + """ + + def testTableUpdateNotification(self): + """Test TableUpdateNotification""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_term.py b/regtests/client/python/test/test_term.py new file mode 100644 index 0000000000..7b715146b3 --- /dev/null +++ b/regtests/client/python/test/test_term.py @@ -0,0 +1,56 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.term import Term + +class TestTerm(unittest.TestCase): + """Term unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> Term: + """Test Term + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `Term` + """ + model = Term() + if include_optional: + return Term( + type = 'transform', + transform = '["identity","year","month","day","hour","bucket[256]","truncate[16]"]', + term = '["column-name"]' + ) + else: + return Term( + type = 'transform', + transform = '["identity","year","month","day","hour","bucket[256]","truncate[16]"]', + term = '["column-name"]', + ) + """ + + def testTerm(self): + """Test Term""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_timer_result.py b/regtests/client/python/test/test_timer_result.py new file mode 100644 index 0000000000..c22c1d49d0 --- /dev/null +++ b/regtests/client/python/test/test_timer_result.py @@ -0,0 +1,56 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.timer_result import TimerResult + +class TestTimerResult(unittest.TestCase): + """TimerResult unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> TimerResult: + """Test TimerResult + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `TimerResult` + """ + model = TimerResult() + if include_optional: + return TimerResult( + time_unit = '', + count = 56, + total_duration = 56 + ) + else: + return TimerResult( + time_unit = '', + count = 56, + total_duration = 56, + ) + """ + + def testTimerResult(self): + """Test TimerResult""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_token_type.py b/regtests/client/python/test/test_token_type.py new file mode 100644 index 0000000000..3bb7c123aa --- /dev/null +++ b/regtests/client/python/test/test_token_type.py @@ -0,0 +1,33 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.token_type import TokenType + +class TestTokenType(unittest.TestCase): + """TokenType unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def testTokenType(self): + """Test TokenType""" + # inst = TokenType() + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_transform_term.py b/regtests/client/python/test/test_transform_term.py new file mode 100644 index 0000000000..8ab517bbfa --- /dev/null +++ b/regtests/client/python/test/test_transform_term.py @@ -0,0 +1,56 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.transform_term import TransformTerm + +class TestTransformTerm(unittest.TestCase): + """TransformTerm unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> TransformTerm: + """Test TransformTerm + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `TransformTerm` + """ + model = TransformTerm() + if include_optional: + return TransformTerm( + type = 'transform', + transform = '["identity","year","month","day","hour","bucket[256]","truncate[16]"]', + term = '["column-name"]' + ) + else: + return TransformTerm( + type = 'transform', + transform = '["identity","year","month","day","hour","bucket[256]","truncate[16]"]', + term = '["column-name"]', + ) + """ + + def testTransformTerm(self): + """Test TransformTerm""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_type.py b/regtests/client/python/test/test_type.py new file mode 100644 index 0000000000..21ff2b6b68 --- /dev/null +++ b/regtests/client/python/test/test_type.py @@ -0,0 +1,84 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.type import Type + +class TestType(unittest.TestCase): + """Type unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> Type: + """Test Type + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `Type` + """ + model = Type() + if include_optional: + return Type( + type = 'struct', + fields = [ + polaris.catalog.models.struct_field.StructField( + id = 56, + name = '', + type = null, + required = True, + doc = '', ) + ], + element_id = 56, + element = None, + element_required = True, + key_id = 56, + key = None, + value_id = 56, + value = None, + value_required = True + ) + else: + return Type( + type = 'struct', + fields = [ + polaris.catalog.models.struct_field.StructField( + id = 56, + name = '', + type = null, + required = True, + doc = '', ) + ], + element_id = 56, + element = None, + element_required = True, + key_id = 56, + key = None, + value_id = 56, + value = None, + value_required = True, + ) + """ + + def testType(self): + """Test Type""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_unary_expression.py b/regtests/client/python/test/test_unary_expression.py new file mode 100644 index 0000000000..93be78ba34 --- /dev/null +++ b/regtests/client/python/test/test_unary_expression.py @@ -0,0 +1,56 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.unary_expression import UnaryExpression + +class TestUnaryExpression(unittest.TestCase): + """UnaryExpression unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> UnaryExpression: + """Test UnaryExpression + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `UnaryExpression` + """ + model = UnaryExpression() + if include_optional: + return UnaryExpression( + type = '["eq","and","or","not","in","not-in","lt","lt-eq","gt","gt-eq","not-eq","starts-with","not-starts-with","is-null","not-null","is-nan","not-nan"]', + term = None, + value = polaris.catalog.models.value.value() + ) + else: + return UnaryExpression( + type = '["eq","and","or","not","in","not-in","lt","lt-eq","gt","gt-eq","not-eq","starts-with","not-starts-with","is-null","not-null","is-nan","not-nan"]', + term = None, + value = polaris.catalog.models.value.value(), + ) + """ + + def testUnaryExpression(self): + """Test UnaryExpression""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_update_catalog_request.py b/regtests/client/python/test/test_update_catalog_request.py new file mode 100644 index 0000000000..50276de2b2 --- /dev/null +++ b/regtests/client/python/test/test_update_catalog_request.py @@ -0,0 +1,57 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.management.models.update_catalog_request import UpdateCatalogRequest + +class TestUpdateCatalogRequest(unittest.TestCase): + """UpdateCatalogRequest unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> UpdateCatalogRequest: + """Test UpdateCatalogRequest + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `UpdateCatalogRequest` + """ + model = UpdateCatalogRequest() + if include_optional: + return UpdateCatalogRequest( + current_entity_version = 56, + properties = { + 'key' : '' + }, + storage_config_info = polaris.management.models.storage_config_info.StorageConfigInfo( + storage_type = 'S3', + allowed_locations = For AWS [s3://bucketname/prefix/], for AZURE [abfss://container@storageaccount.blob.core.windows.net/prefix/], for GCP [gs://bucketname/prefix/], ) + ) + else: + return UpdateCatalogRequest( + ) + """ + + def testUpdateCatalogRequest(self): + """Test UpdateCatalogRequest""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_update_catalog_role_request.py b/regtests/client/python/test/test_update_catalog_role_request.py new file mode 100644 index 0000000000..6a4247320e --- /dev/null +++ b/regtests/client/python/test/test_update_catalog_role_request.py @@ -0,0 +1,54 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.management.models.update_catalog_role_request import UpdateCatalogRoleRequest + +class TestUpdateCatalogRoleRequest(unittest.TestCase): + """UpdateCatalogRoleRequest unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> UpdateCatalogRoleRequest: + """Test UpdateCatalogRoleRequest + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `UpdateCatalogRoleRequest` + """ + model = UpdateCatalogRoleRequest() + if include_optional: + return UpdateCatalogRoleRequest( + current_entity_version = 56, + properties = { + 'key' : '' + } + ) + else: + return UpdateCatalogRoleRequest( + ) + """ + + def testUpdateCatalogRoleRequest(self): + """Test UpdateCatalogRoleRequest""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_update_namespace_properties_request.py b/regtests/client/python/test/test_update_namespace_properties_request.py new file mode 100644 index 0000000000..c27f6dd3ab --- /dev/null +++ b/regtests/client/python/test/test_update_namespace_properties_request.py @@ -0,0 +1,52 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.update_namespace_properties_request import UpdateNamespacePropertiesRequest + +class TestUpdateNamespacePropertiesRequest(unittest.TestCase): + """UpdateNamespacePropertiesRequest unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> UpdateNamespacePropertiesRequest: + """Test UpdateNamespacePropertiesRequest + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `UpdateNamespacePropertiesRequest` + """ + model = UpdateNamespacePropertiesRequest() + if include_optional: + return UpdateNamespacePropertiesRequest( + removals = ["department","access_group"], + updates = {"owner":"Hank Bendickson"} + ) + else: + return UpdateNamespacePropertiesRequest( + ) + """ + + def testUpdateNamespacePropertiesRequest(self): + """Test UpdateNamespacePropertiesRequest""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_update_namespace_properties_response.py b/regtests/client/python/test/test_update_namespace_properties_response.py new file mode 100644 index 0000000000..958f8012aa --- /dev/null +++ b/regtests/client/python/test/test_update_namespace_properties_response.py @@ -0,0 +1,65 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.update_namespace_properties_response import UpdateNamespacePropertiesResponse + +class TestUpdateNamespacePropertiesResponse(unittest.TestCase): + """UpdateNamespacePropertiesResponse unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> UpdateNamespacePropertiesResponse: + """Test UpdateNamespacePropertiesResponse + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `UpdateNamespacePropertiesResponse` + """ + model = UpdateNamespacePropertiesResponse() + if include_optional: + return UpdateNamespacePropertiesResponse( + updated = [ + '' + ], + removed = [ + '' + ], + missing = [ + '' + ] + ) + else: + return UpdateNamespacePropertiesResponse( + updated = [ + '' + ], + removed = [ + '' + ], + ) + """ + + def testUpdateNamespacePropertiesResponse(self): + """Test UpdateNamespacePropertiesResponse""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_update_principal_request.py b/regtests/client/python/test/test_update_principal_request.py new file mode 100644 index 0000000000..870e7de3a0 --- /dev/null +++ b/regtests/client/python/test/test_update_principal_request.py @@ -0,0 +1,54 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.management.models.update_principal_request import UpdatePrincipalRequest + +class TestUpdatePrincipalRequest(unittest.TestCase): + """UpdatePrincipalRequest unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> UpdatePrincipalRequest: + """Test UpdatePrincipalRequest + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `UpdatePrincipalRequest` + """ + model = UpdatePrincipalRequest() + if include_optional: + return UpdatePrincipalRequest( + current_entity_version = 56, + properties = { + 'key' : '' + } + ) + else: + return UpdatePrincipalRequest( + ) + """ + + def testUpdatePrincipalRequest(self): + """Test UpdatePrincipalRequest""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_update_principal_role_request.py b/regtests/client/python/test/test_update_principal_role_request.py new file mode 100644 index 0000000000..03d1084f7b --- /dev/null +++ b/regtests/client/python/test/test_update_principal_role_request.py @@ -0,0 +1,54 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.management.models.update_principal_role_request import UpdatePrincipalRoleRequest + +class TestUpdatePrincipalRoleRequest(unittest.TestCase): + """UpdatePrincipalRoleRequest unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> UpdatePrincipalRoleRequest: + """Test UpdatePrincipalRoleRequest + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `UpdatePrincipalRoleRequest` + """ + model = UpdatePrincipalRoleRequest() + if include_optional: + return UpdatePrincipalRoleRequest( + current_entity_version = 56, + properties = { + 'key' : '' + } + ) + else: + return UpdatePrincipalRoleRequest( + ) + """ + + def testUpdatePrincipalRoleRequest(self): + """Test UpdatePrincipalRoleRequest""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_upgrade_format_version_update.py b/regtests/client/python/test/test_upgrade_format_version_update.py new file mode 100644 index 0000000000..d6cdbc32fc --- /dev/null +++ b/regtests/client/python/test/test_upgrade_format_version_update.py @@ -0,0 +1,54 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.upgrade_format_version_update import UpgradeFormatVersionUpdate + +class TestUpgradeFormatVersionUpdate(unittest.TestCase): + """UpgradeFormatVersionUpdate unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> UpgradeFormatVersionUpdate: + """Test UpgradeFormatVersionUpdate + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `UpgradeFormatVersionUpdate` + """ + model = UpgradeFormatVersionUpdate() + if include_optional: + return UpgradeFormatVersionUpdate( + action = 'upgrade-format-version', + format_version = 56 + ) + else: + return UpgradeFormatVersionUpdate( + action = 'upgrade-format-version', + format_version = 56, + ) + """ + + def testUpgradeFormatVersionUpdate(self): + """Test UpgradeFormatVersionUpdate""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_value_map.py b/regtests/client/python/test/test_value_map.py new file mode 100644 index 0000000000..4348f07dc4 --- /dev/null +++ b/regtests/client/python/test/test_value_map.py @@ -0,0 +1,56 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.value_map import ValueMap + +class TestValueMap(unittest.TestCase): + """ValueMap unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> ValueMap: + """Test ValueMap + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `ValueMap` + """ + model = ValueMap() + if include_optional: + return ValueMap( + keys = [ + 42 + ], + values = [ + null + ] + ) + else: + return ValueMap( + ) + """ + + def testValueMap(self): + """Test ValueMap""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_view_grant.py b/regtests/client/python/test/test_view_grant.py new file mode 100644 index 0000000000..40e54843c2 --- /dev/null +++ b/regtests/client/python/test/test_view_grant.py @@ -0,0 +1,60 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.management.models.view_grant import ViewGrant + +class TestViewGrant(unittest.TestCase): + """ViewGrant unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> ViewGrant: + """Test ViewGrant + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `ViewGrant` + """ + model = ViewGrant() + if include_optional: + return ViewGrant( + namespace = [ + '' + ], + view_name = '', + privilege = 'CATALOG_MANAGE_ACCESS' + ) + else: + return ViewGrant( + namespace = [ + '' + ], + view_name = '', + privilege = 'CATALOG_MANAGE_ACCESS', + ) + """ + + def testViewGrant(self): + """Test ViewGrant""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_view_history_entry.py b/regtests/client/python/test/test_view_history_entry.py new file mode 100644 index 0000000000..2d4120fad4 --- /dev/null +++ b/regtests/client/python/test/test_view_history_entry.py @@ -0,0 +1,54 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.view_history_entry import ViewHistoryEntry + +class TestViewHistoryEntry(unittest.TestCase): + """ViewHistoryEntry unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> ViewHistoryEntry: + """Test ViewHistoryEntry + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `ViewHistoryEntry` + """ + model = ViewHistoryEntry() + if include_optional: + return ViewHistoryEntry( + version_id = 56, + timestamp_ms = 56 + ) + else: + return ViewHistoryEntry( + version_id = 56, + timestamp_ms = 56, + ) + """ + + def testViewHistoryEntry(self): + """Test ViewHistoryEntry""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_view_metadata.py b/regtests/client/python/test/test_view_metadata.py new file mode 100644 index 0000000000..d06b5e482c --- /dev/null +++ b/regtests/client/python/test/test_view_metadata.py @@ -0,0 +1,105 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.view_metadata import ViewMetadata + +class TestViewMetadata(unittest.TestCase): + """ViewMetadata unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> ViewMetadata: + """Test ViewMetadata + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `ViewMetadata` + """ + model = ViewMetadata() + if include_optional: + return ViewMetadata( + view_uuid = '', + format_version = 1, + location = '', + current_version_id = 56, + versions = [ + polaris.catalog.models.view_version.ViewVersion( + version_id = 56, + timestamp_ms = 56, + schema_id = 56, + summary = { + 'key' : '' + }, + representations = [ + null + ], + default_catalog = '', + default_namespace = ["accounting","tax"], ) + ], + version_log = [ + polaris.catalog.models.view_history_entry.ViewHistoryEntry( + version_id = 56, + timestamp_ms = 56, ) + ], + schemas = [ + null + ], + properties = { + 'key' : '' + } + ) + else: + return ViewMetadata( + view_uuid = '', + format_version = 1, + location = '', + current_version_id = 56, + versions = [ + polaris.catalog.models.view_version.ViewVersion( + version_id = 56, + timestamp_ms = 56, + schema_id = 56, + summary = { + 'key' : '' + }, + representations = [ + null + ], + default_catalog = '', + default_namespace = ["accounting","tax"], ) + ], + version_log = [ + polaris.catalog.models.view_history_entry.ViewHistoryEntry( + version_id = 56, + timestamp_ms = 56, ) + ], + schemas = [ + null + ], + ) + """ + + def testViewMetadata(self): + """Test ViewMetadata""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_view_privilege.py b/regtests/client/python/test/test_view_privilege.py new file mode 100644 index 0000000000..07aa2eb1c6 --- /dev/null +++ b/regtests/client/python/test/test_view_privilege.py @@ -0,0 +1,33 @@ +# coding: utf-8 + +""" + Polaris Management Service + + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.management.models.view_privilege import ViewPrivilege + +class TestViewPrivilege(unittest.TestCase): + """ViewPrivilege unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def testViewPrivilege(self): + """Test ViewPrivilege""" + # inst = ViewPrivilege() + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_view_representation.py b/regtests/client/python/test/test_view_representation.py new file mode 100644 index 0000000000..38db50ed6e --- /dev/null +++ b/regtests/client/python/test/test_view_representation.py @@ -0,0 +1,56 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.view_representation import ViewRepresentation + +class TestViewRepresentation(unittest.TestCase): + """ViewRepresentation unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> ViewRepresentation: + """Test ViewRepresentation + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `ViewRepresentation` + """ + model = ViewRepresentation() + if include_optional: + return ViewRepresentation( + type = '', + sql = '', + dialect = '' + ) + else: + return ViewRepresentation( + type = '', + sql = '', + dialect = '', + ) + """ + + def testViewRepresentation(self): + """Test ViewRepresentation""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_view_requirement.py b/regtests/client/python/test/test_view_requirement.py new file mode 100644 index 0000000000..b9dbb86549 --- /dev/null +++ b/regtests/client/python/test/test_view_requirement.py @@ -0,0 +1,52 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.view_requirement import ViewRequirement + +class TestViewRequirement(unittest.TestCase): + """ViewRequirement unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> ViewRequirement: + """Test ViewRequirement + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `ViewRequirement` + """ + model = ViewRequirement() + if include_optional: + return ViewRequirement( + type = '' + ) + else: + return ViewRequirement( + type = '', + ) + """ + + def testViewRequirement(self): + """Test ViewRequirement""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_view_update.py b/regtests/client/python/test/test_view_update.py new file mode 100644 index 0000000000..5d028557e0 --- /dev/null +++ b/regtests/client/python/test/test_view_update.py @@ -0,0 +1,97 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.view_update import ViewUpdate + +class TestViewUpdate(unittest.TestCase): + """ViewUpdate unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> ViewUpdate: + """Test ViewUpdate + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `ViewUpdate` + """ + model = ViewUpdate() + if include_optional: + return ViewUpdate( + action = '', + format_version = 56, + var_schema = None, + last_column_id = 56, + location = '', + updates = { + 'key' : '' + }, + removals = [ + '' + ], + view_version = polaris.catalog.models.view_version.ViewVersion( + version_id = 56, + timestamp_ms = 56, + schema_id = 56, + summary = { + 'key' : '' + }, + representations = [ + null + ], + default_catalog = '', + default_namespace = ["accounting","tax"], ), + view_version_id = 56 + ) + else: + return ViewUpdate( + action = '', + format_version = 56, + var_schema = None, + location = '', + updates = { + 'key' : '' + }, + removals = [ + '' + ], + view_version = polaris.catalog.models.view_version.ViewVersion( + version_id = 56, + timestamp_ms = 56, + schema_id = 56, + summary = { + 'key' : '' + }, + representations = [ + null + ], + default_catalog = '', + default_namespace = ["accounting","tax"], ), + view_version_id = 56, + ) + """ + + def testViewUpdate(self): + """Test ViewUpdate""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/test/test_view_version.py b/regtests/client/python/test/test_view_version.py new file mode 100644 index 0000000000..909181a1b5 --- /dev/null +++ b/regtests/client/python/test/test_view_version.py @@ -0,0 +1,71 @@ +# coding: utf-8 + +""" + Apache Iceberg REST Catalog API + + Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. + + The version of the OpenAPI document: 0.0.1 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from polaris.catalog.models.view_version import ViewVersion + +class TestViewVersion(unittest.TestCase): + """ViewVersion unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> ViewVersion: + """Test ViewVersion + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `ViewVersion` + """ + model = ViewVersion() + if include_optional: + return ViewVersion( + version_id = 56, + timestamp_ms = 56, + schema_id = 56, + summary = { + 'key' : '' + }, + representations = [ + null + ], + default_catalog = '', + default_namespace = ["accounting","tax"] + ) + else: + return ViewVersion( + version_id = 56, + timestamp_ms = 56, + schema_id = 56, + summary = { + 'key' : '' + }, + representations = [ + null + ], + default_namespace = ["accounting","tax"], + ) + """ + + def testViewVersion(self): + """Test ViewVersion""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/regtests/client/python/tox.ini b/regtests/client/python/tox.ini new file mode 100644 index 0000000000..71bd9833c7 --- /dev/null +++ b/regtests/client/python/tox.ini @@ -0,0 +1,9 @@ +[tox] +envlist = py3 + +[testenv] +deps=-r{toxinidir}/requirements.txt + -r{toxinidir}/test-requirements.txt + +commands= + pytest --cov=polaris.catalog diff --git a/regtests/credentials/.keep b/regtests/credentials/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/regtests/output/.keep b/regtests/output/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/regtests/pyspark-setup.sh b/regtests/pyspark-setup.sh new file mode 100755 index 0000000000..a9c3be9efa --- /dev/null +++ b/regtests/pyspark-setup.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +if [ ! -d ~/polaris-venv ]; then + python3 -m venv ~/polaris-venv +fi + +. ~/polaris-venv/bin/activate + +pip install poetry==1.5.0 + +cd client/python +python3 -m poetry install +deactivate \ No newline at end of file diff --git a/regtests/run.sh b/regtests/run.sh new file mode 100755 index 0000000000..4bc2a3a31a --- /dev/null +++ b/regtests/run.sh @@ -0,0 +1,131 @@ +#!/bin/bash + +# Run without args to run all tests, or single arg for single test. + +if [ -z "${SPARK_HOME}"]; then + export SPARK_HOME=$(realpath ~/spark-3.5.1-bin-hadoop3-scala2.13) +fi +export PYTHONPATH="${SPARK_HOME}/python/:${SPARK_HOME}/python/lib/py4j-0.10.9.7-src.zip:$PYTHONPATH" + +FMT_RED='\033[0;31m' +FMT_GREEN='\033[0;32m' +FMT_NC='\033[0m' + +function loginfo() { + echo "$(date): ${@}" +} +function loggreen() { + echo -e "${FMT_GREEN}$(date): ${@}${FMT_NC}" +} +function logred() { + echo -e "${FMT_RED}$(date): ${@}${FMT_NC}" +} + +REGTEST_HOME=$(dirname $(realpath $0)) +cd ${REGTEST_HOME} + +./setup.sh + +# start the python venv +. ~/polaris-venv/bin/activate + +if [ -z "${1}" ]; then + loginfo 'Running all tests' + TEST_LIST="$(find t_* -wholename '*t_*/src/*')" +else + loginfo "Running single test ${1}" + TEST_LIST=${1} +fi + +export PYTHONDONTWRITEBYTECODE=1 + +NUM_FAILURES=0 +NUM_SUCCESSES=0 + +export AWS_ACCESS_KEY_ID='' +export AWS_SECRET_ACCESS_KEY='' + +for TEST_FILE in ${TEST_LIST}; do + TEST_SUITE=$(dirname $(dirname ${TEST_FILE})) + TEST_SHORTNAME=$(basename ${TEST_FILE}) + if [[ "${TEST_SHORTNAME}" =~ .*.py ]]; then + # skip non-test python files + if [[ ! "${TEST_SHORTNAME}" =~ ^test_.*.py ]]; then + continue + fi + loginfo "Starting pytest ${TEST_SUITE}:${TEST_SHORTNAME}" + python3 -m pytest $TESTFILE + CODE=$? + if [[ $CODE -ne 0 ]]; then + logred "Test FAILED: ${TEST_SUITE}:${TEST_SHORTNAME}" + NUM_FAILURES=$(( NUM_FAILURES + 1 )) + else + loggreen "Test SUCCEEDED: ${TEST_SUITE}:${TEST_SHORTNAME}" + fi + continue + fi + if [[ "${TEST_SHORTNAME}" =~ .*.azure.*.sh ]]; then + if [ -z "${AZURE_CLIENT_ID}" ] || [ -z "${AZURE_CLIENT_SECRET}" ] || [ -z "${AZURE_TENANT_ID}" ] ; then + loginfo "Azure tests not enabled, skip running test ${TEST_FILE}" + continue + fi + fi + if [[ "${TEST_SHORTNAME}" =~ .*.s3_cross_region.*.sh ]]; then + if [ -z "$AWS_CROSS_REGION_TEST_ENABLED" ] || [ "$AWS_CROSS_REGION_TEST_ENABLED" != "true" ] ] ; then + loginfo "AWS cross region tests not enabled, skip running test ${TEST_FILE}" + continue + fi + fi + if [[ "${TEST_SHORTNAME}" =~ .*.s3.*.sh ]]; then + if [ -z "$AWS_TEST_ENABLED" ] || [ "$AWS_TEST_ENABLED" != "true" ] || [ -z "$AWS_TEST_BASE" ] ; then + loginfo "AWS tests not enabled, skip running test ${TEST_FILE}" + continue + fi + fi + if [[ "${TEST_SHORTNAME}" =~ .*.gcp.sh ]]; then + # this variable should be the location of your gcp service account key in json + # it is required by running polaris against local + gcp + # example: export GOOGLE_APPLICATION_CREDENTIALS="/home/schen/google_account/google_service_account.json" + if [ -z "$GCS_TEST_ENABLED" ] || [ "$GCS_TEST_ENABLED" != "true" ] || [ -z "${GOOGLE_APPLICATION_CREDENTIALS}" ] ; then + loginfo "GCS tests not enabled, skip running test ${TEST_FILE}" + continue + fi + fi + loginfo "Starting test ${TEST_SUITE}:${TEST_SHORTNAME}" + + TEST_TMPDIR="/tmp/polaris-regtests/${TEST_SUITE}" + TEST_STDERR="${TEST_TMPDIR}/${TEST_SHORTNAME}.stderr" + TEST_STDOUT="${TEST_TMPDIR}/${TEST_SHORTNAME}.stdout" + + mkdir -p ${TEST_TMPDIR} + if (( ${VERBOSE} )); then + ./${TEST_FILE} 2>${TEST_STDERR} | grep -v 'loading settings' | tee ${TEST_STDOUT} + else + ./${TEST_FILE} 2>${TEST_STDERR} | grep -v 'loading settings' > ${TEST_STDOUT} + fi + loginfo "Test run concluded for ${TEST_SUITE}:${TEST_SHORTNAME}" + + TEST_REF="$(realpath ${TEST_SUITE})/ref/${TEST_SHORTNAME}.ref" + touch ${TEST_REF} + if cmp --silent ${TEST_STDOUT} ${TEST_REF}; then + loggreen "Test SUCCEEDED: ${TEST_SUITE}:${TEST_SHORTNAME}" + NUM_SUCCESSES=$(( NUM_SUCCESSES + 1 )) + else + logred "Test FAILED: ${TEST_SUITE}:${TEST_SHORTNAME}" + echo '#!/bin/bash' > ${TEST_TMPDIR}/${TEST_SHORTNAME}.fixdiffs.sh + echo "meld ${TEST_STDOUT} ${TEST_REF}" >> ${TEST_TMPDIR}/${TEST_SHORTNAME}.fixdiffs.sh + chmod 750 ${TEST_TMPDIR}/${TEST_SHORTNAME}.fixdiffs.sh + logred "To compare and fix diffs (if 'meld' installed): ${TEST_TMPDIR}/${TEST_SHORTNAME}.fixdiffs.sh" + logred "Or manually diff: diff ${TEST_STDOUT} ${TEST_REF}" + logred "See stderr from test run for additional diagnostics: ${TEST_STDERR}" + diff ${TEST_STDOUT} ${TEST_REF} + NUM_FAILURES=$(( NUM_FAILURES + 1 )) + fi +done + +loginfo "Tests completed with ${NUM_SUCCESSES} successes and ${NUM_FAILURES} failures" +if (( ${NUM_FAILURES} > 0 )); then + exit 1 +else + exit 0 +fi diff --git a/regtests/run_spark_sql.sh b/regtests/run_spark_sql.sh new file mode 100755 index 0000000000..4b9ca1f39b --- /dev/null +++ b/regtests/run_spark_sql.sh @@ -0,0 +1,60 @@ +#!/bin/bash +# +# Run this to open an interactive spark-sql shell talking to a catalog named "manual_spark" +# +# You must run 'use polaris;' as your first query in the spark-sql shell. + +REGTEST_HOME=$(dirname $(realpath $0)) +cd ${REGTEST_HOME} + +./setup.sh + +if [ -z "${SPARK_HOME}"]; then + export SPARK_HOME=$(realpath ~/spark-3.5.1-bin-hadoop3-scala2.13) +fi + +SPARK_BEARER_TOKEN="${REGTEST_ROOT_BEARER_TOKEN:-principal:root;realm:default-realm}" + +# Use local filesystem by default +curl -X POST -H "Authorization: Bearer ${SPARK_BEARER_TOKEN}" -H 'Accepts: application/json' -H 'Content-Type: application/json' \ + http://${POLARIS_HOST:-localhost}:8181/api/management/v1/catalogs \ + -d '{ + "catalog": { + "name": "manual_spark", + "type": "INTERNAL", + "readOnly": false, + "properties": { + "default-base-location": "file:///tmp/polaris/" + }, + "storageConfigInfo": { + "storageType": "FILE", + "allowedLocations": [ + "file:///tmp" + ] + } + } + }' + +# Use the following instead of below to use s3 instead of local filesystem +#curl -i -X POST -H "Authorization: Bearer ${SPARK_BEARER_TOKEN}" -H 'Accepts: application/json' -H 'Content-Type: application/json' \ +# http://${POLARIS_HOST:-localhost}:8181/api/management/v1/catalogs \ +# -d "{\"name\": \"manual_spark\", \"id\": 100, \"type\": \"INTERNAL\", \"readOnly\": false, \"properties\": {\"default-base-location\": \"s3://${S3_BUCKET}/${USER}/polaris/\"}}" + +# Add TABLE_WRITE_DATA to the catalog's catalog_admin role since by default it can only manage access and metadata +curl -i -X PUT -H "Authorization: Bearer ${SPARK_BEARER_TOKEN}" -H 'Accepts: application/json' -H 'Content-Type: application/json' \ + http://${POLARIS_HOST:-localhost}:8181/api/management/v1/catalogs/manual_spark/catalog-roles/catalog_admin/grants \ + -d '{"type": "catalog", "privilege": "TABLE_WRITE_DATA"}' > /dev/stderr + +# For now, also explicitly assign the catalog_admin to the service_admin. Remove once GS fully rolled out for auto-assign. +curl -i -X PUT -H "Authorization: Bearer ${SPARK_BEARER_TOKEN}" -H 'Accepts: application/json' -H 'Content-Type: application/json' \ + http://${POLARIS_HOST:-localhost}:8181/api/management/v1/principal-roles/service_admin/catalog-roles/manual_spark \ + -d '{"name": "catalog_admin"}' > /dev/stderr + +curl -X GET -H "Authorization: Bearer ${SPARK_BEARER_TOKEN}" -H 'Accepts: application/json' -H 'Content-Type: application/json' \ + http://${POLARIS_HOST:-localhost}:8181/api/management/v1/catalogs/manual_spark + +echo ${SPARK_HOME}/bin/spark-sql -S --conf spark.sql.catalog.polaris.token="${SPARK_BEARER_TOKEN}" +${SPARK_HOME}/bin/spark-sql -S --conf spark.sql.catalog.polaris.token="${SPARK_BEARER_TOKEN}" \ + --conf spark.sql.catalog.polaris.warehouse=manual_spark \ + --conf spark.sql.defaultCatalog=polaris \ + --conf spark.sql.extensions=org.apache.iceberg.spark.extensions.IcebergSparkSessionExtensions diff --git a/regtests/setup.sh b/regtests/setup.sh new file mode 100755 index 0000000000..9dbeb7dd1d --- /dev/null +++ b/regtests/setup.sh @@ -0,0 +1,105 @@ +#!/bin/bash + +# Idempotent setup for regression tests. Run manually or let run.sh auto-run. +# +# Warning - first time setup may download large amounts of files +# Warning - may clobber conf/spark-defaults.conf + +set -x + +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) + +if [ -z "${SPARK_HOME}" ]; then + SPARK_HOME=$(realpath ~/spark-3.5.1-bin-hadoop3-scala2.13) +fi +SPARK_CONF="${SPARK_HOME}/conf/spark-defaults.conf" +export PYTHONPATH="${SPARK_HOME}/python/:${SPARK_HOME}/python/lib/py4j-0.10.9.7-src.zip:$PYTHONPATH" + +# Ensure binaries are downloaded locally +echo 'Verifying Spark binaries...' +if ! [ -f ${SPARK_HOME}/bin/spark-sql ]; then + echo 'Setting up Spark...' + if ! [ -f ~/spark-3.5.1-bin-hadoop3-scala2.13.tgz ]; then + echo 'Downloading spark distro...' + wget -O ~/spark-3.5.1-bin-hadoop3-scala2.13.tgz https://dlcdn.apache.org/spark/spark-3.5.1/spark-3.5.1-bin-hadoop3-scala2.13.tgz + if ! [ -f ~/spark-3.5.1-bin-hadoop3-scala2.13.tgz ]; then + if [[ "${OSTYPE}" == "darwin"* ]]; then + echo "Detected OS: mac. Running 'brew install wget' to try again." + brew install wget + wget -O ~/spark-3.5.1-bin-hadoop3-scala2.13.tgz https://dlcdn.apache.org/spark/spark-3.5.1/spark-3.5.1-bin-hadoop3-scala2.13.tgz + fi + fi + else + echo 'Found existing Spark tarball' + fi + tar xzvf ~/spark-3.5.1-bin-hadoop3-scala2.13.tgz -C ~ + echo 'Done!' + SPARK_HOME=$(realpath ~/spark-3.5.1-bin-hadoop3-scala2.13) + SPARK_CONF="${SPARK_HOME}/conf/spark-defaults.conf" +else + echo 'Verified Spark distro already installed.' +fi + +# Download the iceberg cloud provider bundles needed +echo 'Verified bundle jars installed.' +if ! [ -f ${SPARK_HOME}/jars/iceberg-azure-bundle-1.5.2.jar ]; then + echo 'Download azure bundle jar...' + wget -O ${SPARK_HOME}/jars/iceberg-azure-bundle-1.5.2.jar https://search.maven.org/remotecontent?filepath=org/apache/iceberg/iceberg-azure-bundle/1.5.2/iceberg-azure-bundle-1.5.2.jar + if ! [ -f ${SPARK_HOME}/jars/iceberg-azure-bundle-1.5.2.jar ]; then + if [[ "${OSTYPE}" == "darwin"* ]]; then + echo "Detected OS: mac. Running 'brew install wget' to try again." + brew install wget + wget -O ${SPARK_HOME}/jars/iceberg-azure-bundle-1.5.2.jar https://search.maven.org/remotecontent?filepath=org/apache/iceberg/iceberg-azure-bundle/1.5.2/iceberg-azure-bundle-1.5.2.jar + fi + fi +else + echo 'Verified azure bundle jar already installed' +fi +if ! [ -f ${SPARK_HOME}/jars/iceberg-gcp-bundle-1.5.2.jar ]; then + echo 'Download azure bundle jar...' + wget -O ${SPARK_HOME}/jars/iceberg-gcp-bundle-1.5.2.jar https://search.maven.org/remotecontent?filepath=org/apache/iceberg/iceberg-gcp-bundle/1.5.2/iceberg-gcp-bundle-1.5.2.jar + if ! [ -f ${SPARK_HOME}/jars/iceberg-gcp-bundle-1.5.2.jar ]; then + if [[ "${OSTYPE}" == "darwin"* ]]; then + echo "Detected OS: mac. Running 'brew install wget' to try again." + brew install wget + wget -O ${SPARK_HOME}/jars/iceberg-gcp-bundle-1.5.2.jar https://search.maven.org/remotecontent?filepath=org/apache/iceberg/iceberg-gcp-bundle/1.5.2/iceberg-gcp-bundle-1.5.2.jar + fi + fi +else + echo 'Verified gcp bundle jar already installed' +fi + +# Ensure Spark boilerplate conf is set +echo 'Verifying Spark conf...' +if grep 'POLARIS_TESTCONF_V5' ${SPARK_CONF} 2>/dev/null; then + echo 'Verified spark conf' +else + echo 'Setting spark conf...' + # Instead of clobbering existing spark conf, just comment it all out in case it was customized carefully. + sed -i 's/^/# /' ${SPARK_CONF} +cat << EOF >> ${SPARK_CONF} + +# POLARIS_TESTCONF_V5 +spark.jars.packages org.apache.iceberg:iceberg-spark-runtime-3.5_2.12:1.5.2,org.apache.hadoop:hadoop-aws:3.4.0,software.amazon.awssdk:bundle:2.23.19,software.amazon.awssdk:url-connection-client:2.23.19 +spark.hadoop.fs.s3.impl org.apache.hadoop.fs.s3a.S3AFileSystem +spark.hadoop.fs.AbstractFileSystem.s3.impl org.apache.hadoop.fs.s3a.S3A +spark.sql.variable.substitute true + +spark.driver.extraJavaOptions -Dderby.system.home=/tmp/derby + +spark.sql.catalog.polaris=org.apache.iceberg.spark.SparkCatalog +spark.sql.catalog.polaris.type=rest +spark.sql.catalog.polaris.uri=http://${POLARIS_HOST:-localhost}:8181/api/catalog +spark.sql.catalog.polaris.warehouse=snowflake +spark.sql.catalog.polaris.header.X-Iceberg-Access-Delegation=true +spark.sql.catalog.polaris.client.region=us-west-2 +EOF + echo 'Success!' +fi + +# setup python venv and install polaris client library and test dependencies +pushd $SCRIPT_DIR && ./pyspark-setup.sh && popd + +# bootstrap dependencies so that future queries don't need to wait for the downloads. +# this is mostly useful for building the Docker image with all needed dependencies +${SPARK_HOME}/bin/spark-sql -e "SELECT 1" diff --git a/regtests/t_hello_world/ref/hello_world.sh.ref b/regtests/t_hello_world/ref/hello_world.sh.ref new file mode 100755 index 0000000000..cd0875583a --- /dev/null +++ b/regtests/t_hello_world/ref/hello_world.sh.ref @@ -0,0 +1 @@ +Hello world! diff --git a/regtests/t_hello_world/src/hello_world.sh b/regtests/t_hello_world/src/hello_world.sh new file mode 100755 index 0000000000..485e2f637a --- /dev/null +++ b/regtests/t_hello_world/src/hello_world.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +echo "Hello world!" diff --git a/regtests/t_oauth/test_oauth2_tokens.py b/regtests/t_oauth/test_oauth2_tokens.py new file mode 100644 index 0000000000..699db9050b --- /dev/null +++ b/regtests/t_oauth/test_oauth2_tokens.py @@ -0,0 +1,52 @@ +""" +Simple class to test OAuth endpoints in the Polaris Service. +""" +import argparse +import requests +import urllib + + +def main(base_uri, client_id, client_secret): + """ + Args: + base_uri: The Base URI (ex: http://localhost:8181) + client_id: The Client ID of the OAuth2 Client to Use + client_secret: The Client Secret of the OAuth2 Client to Use + """ + oauth_uri = base_uri + '/api/catalog/v1/oauth/tokens' + headers = {} # may have client id / secret in the future + payload = { + "client_id": client_id, + "client_secret": client_secret, + "grant_type": "client_credentials" + } + r = requests.post( + oauth_uri, + headers=headers, + data=payload) + data = r.json() + + if 'error' in data: + # Cannot continue at this point + print("Unable to obtain an OAuth Token, see error below") + print(data) + return + + # Get the actual token and remove out hint/version + token = data['access_token'] + print("Successfully obtained OAuth token\n\n") + + # Let's call a sample endpoint. The "/config" one seems like the best bet + headers = {"Authorization": f"Bearer {token}"} + config_uri = base_uri + "/api/catalog/v1/config" + r = requests.get(config_uri, headers=headers) + print(r.text) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument("--base-uri", help="The Base Polaris Server URI (ex: http://localhost:8181", type=str) + parser.add_argument("--client-id", help="The Client ID of the OAuth2 Client Integration", type=str) + parser.add_argument("--client-secret", help="The Client Secret of the OAuth2 Client Integration", type=str) + args = parser.parse_args() + main(args.base_uri, args.client_id, args.client_secret) diff --git a/regtests/t_pyspark/src/conftest.py b/regtests/t_pyspark/src/conftest.py new file mode 100644 index 0000000000..e75bbf0970 --- /dev/null +++ b/regtests/t_pyspark/src/conftest.py @@ -0,0 +1,128 @@ +import codecs +import os +from typing import List + +import pytest + +from polaris.catalog.api.iceberg_catalog_api import IcebergCatalogAPI +from polaris.catalog.api_client import ApiClient as CatalogApiClient +from polaris.management import Catalog, AwsStorageConfigInfo, ApiClient, PolarisDefaultApi, Configuration, \ + CreateCatalogRequest, GrantCatalogRoleRequest, CatalogRole, ApiException, AddGrantRequest, CatalogGrant, \ + CatalogPrivilege, CreateCatalogRoleRequest + + +@pytest.fixture +def polaris_host(): + return os.getenv('POLARIS_HOST', 'localhost') + + +@pytest.fixture +def polaris_port(): + return int(os.getenv('POLARIS_PORT', '8181')) + + +@pytest.fixture +def polaris_url(polaris_host, polaris_port): + return f"http://{polaris_host}:{polaris_port}/api/management/v1" + + +@pytest.fixture +def polaris_catalog_url(polaris_host, polaris_port): + return f"http://{polaris_host}:{polaris_port}/api/catalog" + +@pytest.fixture +def test_bucket(): + return os.getenv('AWS_STORAGE_BUCKET') + +@pytest.fixture +def aws_role_arn(): + return os.getenv('AWS_ROLE_ARN') + +@pytest.fixture +def catalog_client(polaris_catalog_url): + """ + Create an iceberg catalog client with root credentials + :param polaris_catalog_url: + :param snowman: + :return: + """ + client = CatalogApiClient( + Configuration(access_token=os.getenv('REGTEST_ROOT_BEARER_TOKEN', 'principal:root;realm:default-realm'), + host=polaris_catalog_url)) + return IcebergCatalogAPI(client) + + +@pytest.fixture +def snowflake_catalog(root_client, catalog_client, test_bucket, aws_role_arn): + storage_conf = AwsStorageConfigInfo(storage_type="S3", + allowed_locations=[f"s3://{test_bucket}/polaris_test/"], + role_arn=aws_role_arn) + catalog_name = 'snowflake' + catalog = Catalog(name=catalog_name, type='INTERNAL', properties={ + "default-base-location": f"s3://{test_bucket}/polaris_test/snowflake_catalog"}, + storage_config_info=storage_conf) + catalog.storage_config_info = storage_conf + try: + root_client.create_catalog(create_catalog_request=CreateCatalogRequest(catalog=catalog)) + resp = root_client.get_catalog(catalog_name=catalog.name) + root_client.assign_catalog_role_to_principal_role(principal_role_name='service_admin', + catalog_name=catalog_name, + grant_catalog_role_request=GrantCatalogRoleRequest( + catalog_role=CatalogRole(name='catalog_admin'))) + writer_catalog_role = create_catalog_role(root_client, resp, 'admin_writer') + root_client.add_grant_to_catalog_role(catalog_name, writer_catalog_role.name, + AddGrantRequest(grant=CatalogGrant(catalog_name=catalog_name, + type='catalog', + privilege=CatalogPrivilege.CATALOG_MANAGE_CONTENT))) + root_client.assign_catalog_role_to_principal_role(principal_role_name='service_admin', + catalog_name=catalog_name, + grant_catalog_role_request=GrantCatalogRoleRequest( + catalog_role=writer_catalog_role)) + yield resp + finally: + namespaces = catalog_client.list_namespaces(catalog_name) + for n in namespaces.namespaces: + clear_namespace(catalog_name, catalog_client, n) + catalog_roles = root_client.list_catalog_roles(catalog_name) + for r in catalog_roles.roles: + if r.name != 'catalog_admin': + root_client.delete_catalog_role(catalog_name, r.name) + root_client.delete_catalog(catalog_name=catalog_name) + + +def create_catalog_role(api, catalog, role_name): + catalog_role = CatalogRole(name=role_name) + try: + api.create_catalog_role(catalog_name=catalog.name, + create_catalog_role_request=CreateCatalogRoleRequest(catalog_role=catalog_role)) + return api.get_catalog_role(catalog_name=catalog.name, catalog_role_name=role_name) + except ApiException as e: + return api.get_catalog_role(catalog_name=catalog.name, catalog_role_name=role_name) + else: + raise e + + +def clear_namespace(catalog: str, catalog_client: IcebergCatalogAPI, namespace: List[str]): + formatted_namespace = format_namespace(namespace) + tables = catalog_client.list_tables(catalog, formatted_namespace) + for t in tables.identifiers: + catalog_client.drop_table(catalog, format_namespace(t.namespace), t.name, purge_requested=True) + views = catalog_client.list_views(catalog, formatted_namespace) + for v in views.identifiers: + catalog_client.drop_view(catalog, format_namespace(v.namespace), v.name) + nested_namespaces = catalog_client.list_namespaces(catalog, parent=formatted_namespace) + for n in nested_namespaces.namespaces: + clear_namespace(catalog, catalog_client, n) + catalog_client.drop_namespace(catalog, formatted_namespace) + + +def format_namespace(namespace): + return codecs.decode("1F", "hex").decode("UTF-8").join(namespace) + + +@pytest.fixture +def root_client(polaris_host, polaris_url): + client = ApiClient(Configuration(access_token=os.getenv('REGTEST_ROOT_BEARER_TOKEN', 'principal:root;realm:default-realm'), + host=polaris_url)) + api = PolarisDefaultApi(client) + return api diff --git a/regtests/t_pyspark/src/iceberg_spark.py b/regtests/t_pyspark/src/iceberg_spark.py new file mode 100644 index 0000000000..3993faaaea --- /dev/null +++ b/regtests/t_pyspark/src/iceberg_spark.py @@ -0,0 +1,108 @@ +"""Spark connector with different catalog types.""" +from typing import Any, Dict, List, Optional, Union + +from pyspark.errors import PySparkRuntimeError +from pyspark.sql import SparkSession + + +class IcebergSparkSession: + """Create a Spark session that connects to Polaris. + + The session is expected to be used within a with statement, as in: + + with IcebergSparkSession( + credentials=f"{client_id}:{client_secret}", + aws_region='us-west-2', + polaris_url="http://polaris:8181/api/catalog", + catalog_name="catalog_name" + ) as spark: + spark.sql(f"USE snowflake.{hybrid_executor.database}.{hybrid_executor.schema}") + table_list = spark.sql("SHOW TABLES").collect() + """ + + def __init__( + self, + bearer_token: str = None, + credentials: str = None, + aws_region: str = "us-west-2", + catalog_name: str = None, + polaris_url: str = None, + realm: str = 'default-realm' + ): + """Constructor for Iceberg Spark session. Sets the member variables.""" + self.bearer_token = bearer_token + self.credentials = credentials + self.aws_region = aws_region + self.catalog_name = catalog_name + self.polaris_url = polaris_url + self.realm = realm + + def get_catalog_name(self): + """Get the catalog name of this spark session based on catalog_type.""" + return self.catalog_name + + def get_session(self): + """Get the real spark session.""" + return self.spark_session + + def sql(self, query: str, args: Optional[Union[Dict[str, Any], List]] = None, **kwargs): + """Wrapper for the sql function of SparkSession.""" + return self.spark_session.sql(query, args, **kwargs) + + def __enter__(self): + """Initial method for Iceberg Spark session. Creates a Spark session with specified configs. + """ + packages = [ + "org.apache.iceberg:iceberg-spark-runtime-3.5_2.12:1.5.0", + "org.apache.hadoop:hadoop-aws:3.4.0", + "software.amazon.awssdk:bundle:2.23.19", + "software.amazon.awssdk:url-connection-client:2.23.19", + ] + excludes = ["org.checkerframework:checker-qual", "com.google.errorprone:error_prone_annotations"] + + packages_string = ",".join(packages) + excludes_string = ",".join(excludes) + catalog_name = self.get_catalog_name() + + creds = self.credentials + credConfig = f"spark.sql.catalog.{catalog_name}.credential" + if self.bearer_token is not None: + creds = self.bearer_token + credConfig = f"spark.sql.catalog.{catalog_name}.token" + spark_session_builder = ( + SparkSession.builder.config("spark.jars.packages", packages_string) + .config("spark.jars.excludes", excludes_string) + .config("spark.sql.iceberg.vectorization.enabled", "false") + .config("spark.hadoop.fs.s3.impl", "org.apache.hadoop.fs.s3a.S3AFileSystem") + .config("spark.history.fs.logDirectory", "/home/iceberg/spark-events") + .config("spark.sql.extensions", "org.apache.iceberg.spark.extensions.IcebergSparkSessionExtensions") + .config( + "spark.hadoop.fs.s3a.aws.credentials.provider", + "org.apache.hadoop.fs.s3a.TemporaryAWSCredentialsProvider", + ) + .config( + f"spark.sql.catalog.{catalog_name}", "org.apache.iceberg.spark.SparkCatalog" + ) + .config(f"spark.sql.catalog.{catalog_name}.header.X-Iceberg-Access-Delegation", "true") + .config(f"spark.sql.catalog.{catalog_name}.type", "rest") + .config(f"spark.sql.catalog.{catalog_name}.uri", self.polaris_url) + .config(f"spark.sql.catalog.{catalog_name}.warehouse", self.catalog_name) + .config(f"spark.sql.catalog.{catalog_name}.scope", 'PRINCIPAL_ROLE:ALL') + .config(f"spark.sql.catalog.{catalog_name}.header.realm", self.realm) + .config(f"spark.sql.catalog.{catalog_name}.client.region", self.aws_region) + .config(credConfig, creds) + .config("spark.ui.showConsoleProgress", False) + ) + + self.spark_session = spark_session_builder.getOrCreate() + self.quiet_logs(self.spark_session.sparkContext) + return self + + def quiet_logs(self, sc): + logger = sc._jvm.org.apache.log4j + logger.LogManager.getLogger("org").setLevel(logger.Level.ERROR) + logger.LogManager.getLogger("akka").setLevel(logger.Level.ERROR) + + def __exit__(self, exc_type, exc_val, exc_tb): + """Destructor for Iceberg Spark session. Stops the Spark session.""" + self.spark_session.stop() diff --git a/regtests/t_pyspark/src/test_spark_sql_s3_with_privileges.py b/regtests/t_pyspark/src/test_spark_sql_s3_with_privileges.py new file mode 100644 index 0000000000..8e67e6feb0 --- /dev/null +++ b/regtests/t_pyspark/src/test_spark_sql_s3_with_privileges.py @@ -0,0 +1,600 @@ +import codecs +import os +import time +import uuid + +import botocore +import pytest +import boto3 +from urllib.parse import unquote + +from iceberg_spark import IcebergSparkSession +from polaris.catalog.api.iceberg_catalog_api import IcebergCatalogAPI +from polaris.catalog.api.iceberg_o_auth2_api import IcebergOAuth2API +from polaris.catalog.api_client import ApiClient as CatalogApiClient +from polaris.catalog.configuration import Configuration +from polaris.management import PolarisDefaultApi, Principal, PrincipalRole, CatalogRole, \ + CatalogGrant, CatalogPrivilege, ApiException, CreateCatalogRoleRequest, CreatePrincipalRoleRequest, \ + CreatePrincipalRequest, AddGrantRequest, GrantCatalogRoleRequest, GrantPrincipalRoleRequest +from polaris.management import ApiClient as ManagementApiClient + + +@pytest.fixture +def snowman(polaris_url, polaris_catalog_url, root_client, snowflake_catalog): + """ + create the snowman principal with full table/namespace privileges + :param root_client: + :param snowflake_catalog: + :return: + """ + snowman_name = "snowman" + table_writer_rolename = "table_writer" + snowflake_writer_rolename = "snowflake_writer" + try: + snowman = create_principal(polaris_url, polaris_catalog_url, root_client, snowman_name) + writer_principal_role = create_principal_role(root_client, table_writer_rolename) + writer_catalog_role = create_catalog_role(root_client, snowflake_catalog, snowflake_writer_rolename) + root_client.assign_catalog_role_to_principal_role(principal_role_name=writer_principal_role.name, + catalog_name=snowflake_catalog.name, + grant_catalog_role_request=GrantCatalogRoleRequest( + catalog_role=writer_catalog_role)) + root_client.add_grant_to_catalog_role(snowflake_catalog.name, writer_catalog_role.name, + AddGrantRequest(grant=CatalogGrant(catalog_name=snowflake_catalog.name, + type='catalog', + privilege=CatalogPrivilege.TABLE_FULL_METADATA))) + root_client.add_grant_to_catalog_role(snowflake_catalog.name, writer_catalog_role.name, + AddGrantRequest(grant=CatalogGrant(catalog_name=snowflake_catalog.name, + type='catalog', + privilege=CatalogPrivilege.VIEW_FULL_METADATA))) + root_client.add_grant_to_catalog_role(snowflake_catalog.name, writer_catalog_role.name, + AddGrantRequest(grant=CatalogGrant(catalog_name=snowflake_catalog.name, + type='catalog', + privilege=CatalogPrivilege.TABLE_WRITE_DATA))) + root_client.add_grant_to_catalog_role(snowflake_catalog.name, writer_catalog_role.name, + AddGrantRequest(grant=CatalogGrant(catalog_name=snowflake_catalog.name, + type='catalog', + privilege=CatalogPrivilege.NAMESPACE_FULL_METADATA))) + + root_client.assign_principal_role(snowman.principal.name, + grant_principal_role_request=GrantPrincipalRoleRequest( + principal_role=writer_principal_role)) + yield snowman + finally: + root_client.delete_principal(snowman_name) + root_client.delete_principal_role(principal_role_name=table_writer_rolename) + root_client.delete_catalog_role(catalog_role_name=snowflake_writer_rolename, catalog_name=snowflake_catalog.name) + + +@pytest.fixture +def reader(polaris_url, polaris_catalog_url, root_client, snowflake_catalog): + """ + create the test_reader principal with table/namespace list and read privileges + + :param root_client: + :param snowflake_catalog: + :return: + """ + reader_principal_name = 'test_reader' + reader_principal_role_name = "table_reader" + reader_catalog_role_name = 'snowflake_reader' + try: + reader = create_principal(polaris_url, polaris_catalog_url, root_client, reader_principal_name) + reader_principal_role = create_principal_role(root_client, reader_principal_role_name) + reader_catalog_role = create_catalog_role(root_client, snowflake_catalog, reader_catalog_role_name) + + root_client.assign_catalog_role_to_principal_role(principal_role_name=reader_principal_role.name, + catalog_name=snowflake_catalog.name, + grant_catalog_role_request=GrantCatalogRoleRequest( + catalog_role=reader_catalog_role)) + root_client.assign_principal_role(reader.principal.name, + grant_principal_role_request=GrantPrincipalRoleRequest( + principal_role=reader_principal_role)) + root_client.add_grant_to_catalog_role(snowflake_catalog.name, reader_catalog_role.name, + AddGrantRequest(grant=CatalogGrant(catalog_name=snowflake_catalog.name, + type='catalog', + privilege=CatalogPrivilege.TABLE_READ_DATA))) + root_client.add_grant_to_catalog_role(snowflake_catalog.name, reader_catalog_role.name, + AddGrantRequest(grant=CatalogGrant(catalog_name=snowflake_catalog.name, + type='catalog', + privilege=CatalogPrivilege.TABLE_LIST))) + root_client.add_grant_to_catalog_role(snowflake_catalog.name, reader_catalog_role.name, + AddGrantRequest(grant=CatalogGrant(catalog_name=snowflake_catalog.name, + type='catalog', + privilege=CatalogPrivilege.TABLE_READ_PROPERTIES))) + root_client.add_grant_to_catalog_role(snowflake_catalog.name, reader_catalog_role.name, + AddGrantRequest(grant=CatalogGrant(catalog_name=snowflake_catalog.name, + type='catalog', + privilege=CatalogPrivilege.NAMESPACE_LIST))) + root_client.add_grant_to_catalog_role(snowflake_catalog.name, reader_catalog_role.name, + AddGrantRequest(grant=CatalogGrant(catalog_name=snowflake_catalog.name, + type='catalog', + privilege=CatalogPrivilege.NAMESPACE_READ_PROPERTIES))) + yield reader + finally: + root_client.delete_principal(reader_principal_name) + root_client.delete_principal_role(principal_role_name=reader_principal_role_name) + root_client.delete_catalog_role(catalog_role_name=reader_catalog_role_name, catalog_name=snowflake_catalog.name) + + +@pytest.fixture +def snowman_catalog_client(polaris_catalog_url, snowman): + """ + Create an iceberg catalog client with snowman credentials + :param polaris_catalog_url: + :param snowman: + :return: + """ + client = CatalogApiClient(Configuration(username=snowman.principal.client_id, + password=snowman.credentials.client_secret, + host=polaris_catalog_url)) + oauth_api = IcebergOAuth2API(client) + token = oauth_api.get_token(scope='PRINCIPAL_ROLE:ALL', client_id=snowman.principal.client_id, + client_secret=snowman.credentials.client_secret, + grant_type='client_credentials', + _headers={'realm': 'default-realm'}) + + return IcebergCatalogAPI(CatalogApiClient(Configuration(access_token=token.access_token, + host=polaris_catalog_url))) + + +@pytest.fixture +def reader_catalog_client(polaris_catalog_url, reader): + """ + Create an iceberg catalog client with test_reader credentials + :param polaris_catalog_url: + :param reader: + :return: + """ + client = CatalogApiClient(Configuration(username=reader.principal.client_id, + password=reader.credentials.client_secret, + host=polaris_catalog_url)) + oauth_api = IcebergOAuth2API(client) + token = oauth_api.get_token(scope='PRINCIPAL_ROLE:ALL', client_id=reader.principal.client_id, + client_secret=reader.credentials.client_secret, + grant_type='client_credentials', + _headers={'realm': 'default-realm'}) + + return IcebergCatalogAPI(CatalogApiClient(Configuration(access_token=token.access_token, + host=polaris_catalog_url))) + + +@pytest.mark.skipif(os.environ.get('AWS_TEST_ENABLED', 'False').lower() != 'true', reason='AWS_TEST_ENABLED is not set or is false') +def test_spark_credentials(root_client, snowflake_catalog, polaris_catalog_url, snowman, reader): + """ + Basic spark test - using snowman, create namespaces and a table. Insert into the table and read records back. + + Using the reader principal's credentials verify read access. Validate the reader cannot insert into the table. + :param root_client: + :param snowflake_catalog: + :param polaris_catalog_url: + :param snowman: + :param reader: + :return: + """ + with IcebergSparkSession(credentials=f'{snowman.principal.client_id}:{snowman.credentials.client_secret}', + catalog_name=snowflake_catalog.name, + polaris_url=polaris_catalog_url) as spark: + spark.sql(f'USE {snowflake_catalog.name}') + spark.sql('CREATE NAMESPACE db1') + spark.sql('CREATE NAMESPACE db1.schema') + spark.sql('SHOW NAMESPACES') + spark.sql('USE db1.schema') + spark.sql('CREATE TABLE iceberg_table (col1 int, col2 string)') + spark.sql('SHOW TABLES') + spark.sql("""INSERT INTO iceberg_table VALUES + (10, 'mystring'), + (20, 'anotherstring'), + (30, null) + """) + count = spark.sql("SELECT * FROM iceberg_table").count() + assert count == 3 + + # switch users to the reader. we can query, show namespaces, but we can't insert + with IcebergSparkSession(credentials=f'{reader.principal.client_id}:{reader.credentials.client_secret}', + catalog_name=snowflake_catalog.name, + polaris_url=polaris_catalog_url) as spark: + spark.sql(f'USE {snowflake_catalog.name}') + spark.sql('SHOW NAMESPACES') + spark.sql('USE db1.schema') + count = spark.sql("SELECT * FROM iceberg_table").count() + assert count == 3 + try: + spark.sql("""INSERT INTO iceberg_table VALUES + (10, 'mystring'), + (20, 'anotherstring'), + (30, null) + """) + pytest.fail("Expected exception when trying to write without permission") + except: + print("Exception caught attempting to write without permission") + + # switch back to delete stuff + with IcebergSparkSession(credentials=f'{snowman.principal.client_id}:{snowman.credentials.client_secret}', + catalog_name=snowflake_catalog.name, + polaris_url=polaris_catalog_url) as spark: + spark.sql(f'USE {snowflake_catalog.name}') + spark.sql('USE db1.schema') + spark.sql('DROP TABLE iceberg_table') + spark.sql(f'USE {snowflake_catalog.name}') + spark.sql('DROP NAMESPACE db1.schema') + spark.sql('DROP NAMESPACE db1') + + +@pytest.mark.skipif(os.environ.get('AWS_TEST_ENABLED', 'False').lower() != 'true', reason='AWS_TEST_ENABLED is not set or is false') +def test_spark_credentials_can_delete_after_purge(root_client, snowflake_catalog, polaris_catalog_url, snowman, + snowman_catalog_client, test_bucket): + """ + Using snowman, create namespaces and a table. Insert into the table in multiple operations and update existing records + to generate multiple metadata.json files and manfiests. Drop the table with purge=true. Poll S3 and validate all of + the files are deleted. + + Using the reader principal's credentials verify read access. Validate the reader cannot insert into the table. + :param root_client: + :param snowflake_catalog: + :param polaris_catalog_url: + :param snowman: + :param reader: + :return: + """ + with IcebergSparkSession(credentials=f'{snowman.principal.client_id}:{snowman.credentials.client_secret}', + catalog_name=snowflake_catalog.name, + polaris_url=polaris_catalog_url) as spark: + table_name = f'iceberg_test_table_{str(uuid.uuid4())[-10:]}' + spark.sql(f'USE {snowflake_catalog.name}') + spark.sql('CREATE NAMESPACE db1') + spark.sql('CREATE NAMESPACE db1.schema') + spark.sql('SHOW NAMESPACES') + spark.sql('USE db1.schema') + spark.sql(f'CREATE TABLE {table_name} (col1 int, col2 string)') + spark.sql('SHOW TABLES') + + # several inserts and an update, which should cause earlier files to show up as deleted in the later manifests + spark.sql(f"""INSERT INTO {table_name} VALUES + (10, 'mystring'), + (20, 'anotherstring'), + (30, null) + """) + spark.sql(f"""INSERT INTO {table_name} VALUES + (40, 'mystring'), + (50, 'anotherstring'), + (60, null) + """) + spark.sql(f"""INSERT INTO {table_name} VALUES + (70, 'mystring'), + (80, 'anotherstring'), + (90, null) + """) + spark.sql(f"UPDATE {table_name} SET col2='changed string' WHERE col1 BETWEEN 20 AND 50") + count = spark.sql(f"SELECT * FROM {table_name}").count() + + assert count == 9 + + # fetch aws credentials to examine the metadata files + response = snowman_catalog_client.load_table(snowflake_catalog.name, unquote('db1%1Fschema'), table_name, + "true") + assert response.config is not None + assert 's3.access-key-id' in response.config + assert 's3.secret-access-key' in response.config + assert 's3.session-token' in response.config + + s3 = boto3.client('s3', + aws_access_key_id=response.config['s3.access-key-id'], + aws_secret_access_key=response.config['s3.secret-access-key'], + aws_session_token=response.config['s3.session-token']) + + objects = s3.list_objects(Bucket=test_bucket, Delimiter='/', + Prefix=f'polaris_test/snowflake_catalog/db1/schema/{table_name}/data/') + assert objects is not None + assert 'Contents' in objects + assert len(objects['Contents']) >= 4 # idk, it varies - at least one file for each inser and one for the update + print(f"Found {len(objects['Contents'])} data files in S3 before drop") + + objects = s3.list_objects(Bucket=test_bucket, Delimiter='/', + Prefix=f'polaris_test/snowflake_catalog/db1/schema/{table_name}/metadata/') + assert objects is not None + assert 'Contents' in objects + assert len(objects['Contents']) == 15 # 5 metadata.json files, 4 manifest lists, and 6 manifests + print(f"Found {len(objects['Contents'])} metadata files in S3 before drop") + + # use the api client to ensure the purge flag is set to true + snowman_catalog_client.drop_table(snowflake_catalog.name, + codecs.decode("1F", "hex").decode("UTF-8").join(['db1', 'schema']), table_name, + purge_requested=True) + spark.sql('DROP NAMESPACE db1.schema') + spark.sql('DROP NAMESPACE db1') + print("Dropped table with purge - waiting for files to be deleted") + attempts = 0 + + # watch the data directory. metadata will be deleted first, so if data directory is clear, we can expect + # metadatat diretory to be clear also + while 'Contents' in objects and len(objects['Contents']) > 0 and attempts < 60: + time.sleep(1) # seconds, not milliseconds ;) + objects = s3.list_objects(Bucket=test_bucket, Delimiter='/', + Prefix=f'polaris_test/snowflake_catalog/db1/schema/{table_name}/data/') + attempts = attempts + 1 + + if 'Contents' in objects and len(objects['Contents']) > 0: + pytest.fail(f"Expected all data to be deleted, but found metadata files {objects['Contents']}") + + objects = s3.list_objects(Bucket=test_bucket, Delimiter='/', + Prefix=f'polaris_test/snowflake_catalog/db1/schema/{table_name}/data/') + if 'Contents' in objects and len(objects['Contents']) > 0: + pytest.fail(f"Expected all data to be deleted, but found data files {objects['Contents']}") + + +@pytest.mark.skipif(os.environ.get('AWS_TEST_ENABLED', 'False').lower() != 'true', reason='AWS_TEST_ENABLED is not set or is false') +# @pytest.mark.skip(reason="This test is flaky") +def test_spark_credentials_can_create_views(snowflake_catalog, polaris_catalog_url, snowman): + """ + Using snowman, create namespaces and a table. Insert into the table in multiple operations and update existing records + to generate multiple metadata.json files and manifests. Create a view on the table. Verify the state of the view + matches the state of the table. + + Using the reader principal's credentials verify read access. Validate the reader cannot insert into the table. + :param snowflake_catalog: + :param polaris_catalog_url: + :param snowman: + :return: + """ + with IcebergSparkSession(credentials=f'{snowman.principal.client_id}:{snowman.credentials.client_secret}', + catalog_name=snowflake_catalog.name, + polaris_url=polaris_catalog_url) as spark: + table_name = f'iceberg_test_table_{str(uuid.uuid4())[-10:]}' + spark.sql(f'USE {snowflake_catalog.name}') + spark.sql('CREATE NAMESPACE db1') + spark.sql('CREATE NAMESPACE db1.schema') + spark.sql('SHOW NAMESPACES') + spark.sql('USE db1.schema') + spark.sql(f'CREATE TABLE {table_name} (col1 int, col2 string)') + spark.sql('SHOW TABLES') + + # several inserts + spark.sql(f"""INSERT INTO {table_name} VALUES + (10, 'mystring'), + (20, 'anotherstring'), + (30, null) + """) + spark.sql(f"""INSERT INTO {table_name} VALUES + (40, 'mystring'), + (50, 'anotherstring'), + (60, null) + """) + spark.sql(f"""INSERT INTO {table_name} VALUES + (70, 'mystring'), + (80, 'anotherstring'), + (90, null) + """) + # verify the view reflects the current state of the table + spark.sql(f"CREATE VIEW {table_name}_view AS SELECT col2 FROM {table_name} where col1 > 30 ORDER BY col1 DESC") + view_records = spark.sql(f"SELECT * FROM {table_name}_view").collect() + assert len(view_records) == 6 + assert len(view_records[0]) == 1 + assert view_records[1][0] == 'anotherstring' + assert view_records[5][0] == 'mystring' + + # Update some records. Assert the view reflects the new state + spark.sql(f"UPDATE {table_name} SET col2='changed string' WHERE col1 BETWEEN 20 AND 50") + view_records = spark.sql(f"SELECT * FROM {table_name}_view").collect() + assert len(view_records) == 6 + assert view_records[5][0] == 'changed string' + + +@pytest.mark.skipif(os.environ.get('AWS_TEST_ENABLED', 'False').lower() != 'true', reason='AWS_TEST_ENABLED is not set or is false') +def test_spark_credentials_s3_direct_with_write(root_client, snowflake_catalog, polaris_catalog_url, + snowman, snowman_catalog_client, test_bucket): + """ + Create two tables using Spark. Then call the loadTable api directly with snowman token to fetch the vended credentials + for the first table. + Verify that the credentials returned to snowman can read and write to the table's directory in S3, but don't allow + reads or writes to the other table's directory + :param root_client: + :param snowflake_catalog: + :param polaris_catalog_url: + :param snowman_catalog_client: + :param reader_catalog_client: + :return: + """ + with IcebergSparkSession(credentials=f'{snowman.principal.client_id}:{snowman.credentials.client_secret}', + catalog_name=snowflake_catalog.name, + polaris_url=polaris_catalog_url) as spark: + spark.sql(f'USE {snowflake_catalog.name}') + spark.sql('CREATE NAMESPACE db1') + spark.sql('CREATE NAMESPACE db1.schema') + spark.sql('USE db1.schema') + spark.sql('CREATE TABLE iceberg_table (col1 int, col2 string)') + spark.sql('CREATE TABLE iceberg_table_2 (col1 int, col2 string)') + + table2_metadata = snowman_catalog_client.load_table(snowflake_catalog.name, unquote('db1%1Fschema'), + "iceberg_table_2", + "s3_direct_with_write_table2").metadata_location + response = snowman_catalog_client.load_table(snowflake_catalog.name, unquote('db1%1Fschema'), "iceberg_table", + "s3_direct_with_write") + assert response.config is not None + assert 's3.access-key-id' in response.config + assert 's3.secret-access-key' in response.config + assert 's3.session-token' in response.config + + s3 = boto3.client('s3', + aws_access_key_id=response.config['s3.access-key-id'], + aws_secret_access_key=response.config['s3.secret-access-key'], + aws_session_token=response.config['s3.session-token']) + + objects = s3.list_objects(Bucket=test_bucket, Delimiter='/', + Prefix='polaris_test/snowflake_catalog/db1/schema/iceberg_table/metadata/') + assert objects is not None + assert 'Contents' in objects + assert len(objects['Contents']) > 0 + + metadata_file = next(f for f in objects['Contents'] if f['Key'].endswith('metadata.json')) + assert metadata_file is not None + + metadata_contents = s3.get_object(Bucket=test_bucket, Key=metadata_file['Key']) + assert metadata_contents is not None + assert metadata_contents['ContentLength'] > 0 + + put_object = s3.put_object(Bucket=test_bucket, Key=f"{metadata_file['Key']}.bak", + Body=metadata_contents['Body'].read()) + assert put_object is not None + assert 'VersionId' in put_object + assert put_object['VersionId'] is not None + + # list files in the other table's directory. The access policy should restrict this + try: + objects = s3.list_objects(Bucket=test_bucket, Delimiter='/', + Prefix='polaris_test/snowflake_catalog/db1/schema/iceberg_table_2/metadata/') + pytest.fail('Expected exception listing file outside of table directory') + except botocore.exceptions.ClientError as error: + print(error) + + try: + metadata_contents = s3.get_object(Bucket=test_bucket, Key=table2_metadata) + pytest.fail("Expected exception reading file outside of table directory") + except botocore.exceptions.ClientError as error: + print(error) + + with IcebergSparkSession(credentials=f'{snowman.principal.client_id}:{snowman.credentials.client_secret}', + catalog_name=snowflake_catalog.name, + polaris_url=polaris_catalog_url) as spark: + spark.sql(f'USE {snowflake_catalog.name}') + spark.sql('USE db1.schema') + spark.sql('DROP TABLE iceberg_table') + spark.sql('DROP TABLE iceberg_table_2') + spark.sql(f'USE {snowflake_catalog.name}') + spark.sql('DROP NAMESPACE db1.schema') + spark.sql('DROP NAMESPACE db1') + + +@pytest.mark.skipif(os.environ.get('AWS_TEST_ENABLED', 'false').lower() != 'true', reason='AWS_TEST_ENABLED is not set or is false') +def test_spark_credentials_s3_direct_without_write(root_client, snowflake_catalog, polaris_catalog_url, + snowman, reader_catalog_client, test_bucket): + """ + Create two tables using Spark. Then call the loadTable api directly with test_reader token to fetch the vended + credentials for the first table. + Verify that the credentials returned to test_reader allow reads, but don't allow writes to the table's directory + and don't allow reads or writes anywhere else on S3. This verifies that Polaris's authz model does not only prevent + users from updating metadata to enforce read-only access, but uses credential scoping to enforce restrictions at + the storage layer. + :param root_client: + :param snowflake_catalog: + :param polaris_catalog_url: + :param reader_catalog_client: + :return: + """ + with IcebergSparkSession(credentials=f'{snowman.principal.client_id}:{snowman.credentials.client_secret}', + catalog_name=snowflake_catalog.name, + polaris_url=polaris_catalog_url) as spark: + spark.sql(f'USE {snowflake_catalog.name}') + spark.sql('CREATE NAMESPACE db1') + spark.sql('CREATE NAMESPACE db1.schema') + spark.sql('USE db1.schema') + spark.sql('CREATE TABLE iceberg_table (col1 int, col2 string)') + spark.sql('CREATE TABLE iceberg_table_2 (col1 int, col2 string)') + + table2_metadata = reader_catalog_client.load_table(snowflake_catalog.name, unquote('db1%1Fschema'), + "iceberg_table_2", + "s3_direct_with_write_table2").metadata_location + + response = reader_catalog_client.load_table(snowflake_catalog.name, unquote('db1%1Fschema'), "iceberg_table", + "s3_direct_without_write") + assert response.config is not None + assert 's3.access-key-id' in response.config + assert 's3.secret-access-key' in response.config + assert 's3.session-token' in response.config + + s3 = boto3.client('s3', + aws_access_key_id=response.config['s3.access-key-id'], + aws_secret_access_key=response.config['s3.secret-access-key'], + aws_session_token=response.config['s3.session-token']) + + objects = s3.list_objects(Bucket=test_bucket, Delimiter='/', + Prefix='polaris_test/snowflake_catalog/db1/schema/iceberg_table/metadata/') + assert objects is not None + assert 'Contents' in objects + assert len(objects['Contents']) > 0 + + metadata_file = next(f for f in objects['Contents'] if f['Key'].endswith('metadata.json')) + assert metadata_file is not None + + metadata_contents = s3.get_object(Bucket=test_bucket, Key=metadata_file['Key']) + assert metadata_contents is not None + assert metadata_contents['ContentLength'] > 0 + + # try to write. Expect it to fail + try: + put_object = s3.put_object(Bucket=test_bucket, Key=f"{metadata_file['Key']}.bak", + Body=metadata_contents['Body'].read()) + pytest.fail("Expect exception trying to write to table directory") + except botocore.exceptions.ClientError as error: + print(error) + + # list files in the other table's directory. The access policy should restrict this + try: + objects = s3.list_objects(Bucket=test_bucket, Delimiter='/', + Prefix='polaris_test/snowflake_catalog/db1/schema/iceberg_table_2/metadata/') + pytest.fail('Expected exception listing file outside of table directory') + except botocore.exceptions.ClientError as error: + print(error) + + try: + metadata_contents = s3.get_object(Bucket=test_bucket, Key=table2_metadata) + pytest.fail("Expected exception reading file outside of table directory") + except botocore.exceptions.ClientError as error: + print(error) + + with IcebergSparkSession(credentials=f'{snowman.principal.client_id}:{snowman.credentials.client_secret}', + catalog_name=snowflake_catalog.name, + polaris_url=polaris_catalog_url) as spark: + spark.sql(f'USE {snowflake_catalog.name}') + spark.sql('USE db1.schema') + spark.sql('DROP TABLE iceberg_table') + spark.sql('DROP TABLE iceberg_table_2') + spark.sql(f'USE {snowflake_catalog.name}') + spark.sql('DROP NAMESPACE db1.schema') + spark.sql('DROP NAMESPACE db1') + + +def create_principal(polaris_url, polaris_catalog_url, api, principal_name): + principal = Principal(name=principal_name, type="SERVICE") + try: + principal_result = api.create_principal(CreatePrincipalRequest(principal=principal)) + + token_client = CatalogApiClient(Configuration(username=principal_result.principal.client_id, + password=principal_result.credentials.client_secret, + host=polaris_catalog_url)) + oauth_api = IcebergOAuth2API(token_client) + token = oauth_api.get_token(scope='PRINCIPAL_ROLE:ALL', client_id=principal_result.principal.client_id, + client_secret=principal_result.credentials.client_secret, + grant_type='client_credentials', + _headers={'realm': 'default-realm'}) + rotate_client = ManagementApiClient(Configuration(access_token=token.access_token, + host=polaris_url)) + rotate_api = PolarisDefaultApi(rotate_client) + + rotate_credentials = rotate_api.rotate_credentials(principal_name=principal_name) + return rotate_credentials + except ApiException as e: + if e.status == 409: + return rotate_api.rotate_credentials(principal_name=principal_name) + else: + raise e + + +def create_catalog_role(api, catalog, role_name): + catalog_role = CatalogRole(name=role_name) + try: + api.create_catalog_role(catalog_name=catalog.name, + create_catalog_role_request=CreateCatalogRoleRequest(catalog_role=catalog_role)) + return api.get_catalog_role(catalog_name=catalog.name, catalog_role_name=role_name) + except ApiException as e: + return api.get_catalog_role(catalog_name=catalog.name, catalog_role_name=role_name) + else: + raise e + + +def create_principal_role(api, role_name): + principal_role = PrincipalRole(name=role_name) + try: + api.create_principal_role(CreatePrincipalRoleRequest(principal_role=principal_role)) + return api.get_principal_role(principal_role_name=role_name) + except ApiException as e: + return api.get_principal_role(principal_role_name=role_name) diff --git a/regtests/t_spark_sql/ref/spark_sql_azure_blob.sh.ref b/regtests/t_spark_sql/ref/spark_sql_azure_blob.sh.ref new file mode 100755 index 0000000000..5c18f802cd --- /dev/null +++ b/regtests/t_spark_sql/ref/spark_sql_azure_blob.sh.ref @@ -0,0 +1,35 @@ +{"defaults":{"default-base-location":"abfss://polaris-container@polarisadls.blob.core.windows.net/polaris-test/spark_sql_blob_catalog/"},"overrides":{"prefix":"spark_sql_azure_blob_catalog"}} +Catalog created +spark-sql (default)> use polaris; +spark-sql ()> show namespaces; +spark-sql ()> create namespace db1; +spark-sql ()> create namespace db2; +spark-sql ()> show namespaces; +db1 +db2 +spark-sql ()> + > create namespace db1.schema1; +spark-sql ()> show namespaces; +db1 +db2 +spark-sql ()> show namespaces in db1; +db1.schema1 +spark-sql ()> + > create table db1.schema1.tbl1 (col1 int); +spark-sql ()> show tables in db1; +spark-sql ()> use db1.schema1; +spark-sql (db1.schema1)> + > insert into tbl1 values (123), (234); +spark-sql (db1.schema1)> select * from tbl1; +123 +234 +spark-sql (db1.schema1)> + > drop table tbl1 purge; +spark-sql (db1.schema1)> show tables; +spark-sql (db1.schema1)> drop namespace db1.schema1; +spark-sql (db1.schema1)> drop namespace db1; +spark-sql (db1.schema1)> show namespaces; +db2 +spark-sql (db1.schema1)> drop namespace db2; +spark-sql (db1.schema1)> show namespaces; +spark-sql (db1.schema1)> diff --git a/regtests/t_spark_sql/ref/spark_sql_azure_dfs.sh.ref b/regtests/t_spark_sql/ref/spark_sql_azure_dfs.sh.ref new file mode 100755 index 0000000000..422389565d --- /dev/null +++ b/regtests/t_spark_sql/ref/spark_sql_azure_dfs.sh.ref @@ -0,0 +1,35 @@ +{"defaults":{"default-base-location":"abfss://polaris-container@polarisadls.dfs.core.windows.net/polaris-test/spark_sql_dfs_catalog/"},"overrides":{"prefix":"spark_sql_azure_dfs_catalog"}} +Catalog created +spark-sql (default)> use polaris; +spark-sql ()> show namespaces; +spark-sql ()> create namespace db1; +spark-sql ()> create namespace db2; +spark-sql ()> show namespaces; +db1 +db2 +spark-sql ()> + > create namespace db1.schema1; +spark-sql ()> show namespaces; +db1 +db2 +spark-sql ()> show namespaces in db1; +db1.schema1 +spark-sql ()> + > create table db1.schema1.tbl1 (col1 int); +spark-sql ()> show tables in db1; +spark-sql ()> use db1.schema1; +spark-sql (db1.schema1)> + > insert into tbl1 values (123), (234); +spark-sql (db1.schema1)> select * from tbl1; +123 +234 +spark-sql (db1.schema1)> + > drop table tbl1 purge; +spark-sql (db1.schema1)> show tables; +spark-sql (db1.schema1)> drop namespace db1.schema1; +spark-sql (db1.schema1)> drop namespace db1; +spark-sql (db1.schema1)> show namespaces; +db2 +spark-sql (db1.schema1)> drop namespace db2; +spark-sql (db1.schema1)> show namespaces; +spark-sql (db1.schema1)> diff --git a/regtests/t_spark_sql/ref/spark_sql_basic.sh.ref b/regtests/t_spark_sql/ref/spark_sql_basic.sh.ref new file mode 100755 index 0000000000..1ab8f91896 --- /dev/null +++ b/regtests/t_spark_sql/ref/spark_sql_basic.sh.ref @@ -0,0 +1,39 @@ +{"defaults":{"default-base-location":"file:///tmp/spark_sql_s3_catalog"},"overrides":{"prefix":"spark_sql_basic_catalog"}} +Catalog created +spark-sql (default)> use polaris; +spark-sql ()> show namespaces; +spark-sql ()> create namespace db1; +spark-sql ()> create namespace db2; +spark-sql ()> show namespaces; +db1 +db2 +spark-sql ()> + > create namespace db1.schema1; +spark-sql ()> show namespaces; +db1 +db2 +spark-sql ()> show namespaces in db1; +db1.schema1 +spark-sql ()> + > create table db1.schema1.tbl1 (col1 int); +spark-sql ()> show tables in db1; +spark-sql ()> show tables in db1.schema1; +tbl1 +spark-sql ()> use db1.schema1; +spark-sql (db1.schema1)> show tables; +tbl1 +spark-sql (db1.schema1)> + > insert into tbl1 values (123), (234); +spark-sql (db1.schema1)> select * from tbl1; +123 +234 +spark-sql (db1.schema1)> + > drop table tbl1 purge; +spark-sql (db1.schema1)> show tables; +spark-sql (db1.schema1)> drop namespace db1.schema1; +spark-sql (db1.schema1)> drop namespace db1; +spark-sql (db1.schema1)> show namespaces; +db2 +spark-sql (db1.schema1)> drop namespace db2; +spark-sql (db1.schema1)> show namespaces; +spark-sql (db1.schema1)> diff --git a/regtests/t_spark_sql/ref/spark_sql_gcp.sh.ref b/regtests/t_spark_sql/ref/spark_sql_gcp.sh.ref new file mode 100755 index 0000000000..f083b9a0af --- /dev/null +++ b/regtests/t_spark_sql/ref/spark_sql_gcp.sh.ref @@ -0,0 +1,35 @@ +{"defaults":{"default-base-location":"gs://polaris-test1/polaris_test/spark_sql_gcp_catalog/"},"overrides":{"prefix":"spark_sql_gcp_catalog"}} +Catalog created +spark-sql (default)> use polaris; +spark-sql ()> show namespaces; +spark-sql ()> create namespace db1; +spark-sql ()> create namespace db2; +spark-sql ()> show namespaces; +db1 +db2 +spark-sql ()> + > create namespace db1.schema1; +spark-sql ()> show namespaces; +db1 +db2 +spark-sql ()> show namespaces in db1; +db1.schema1 +spark-sql ()> + > create table db1.schema1.tbl1 (col1 int); +spark-sql ()> show tables in db1; +spark-sql ()> use db1.schema1; +spark-sql (db1.schema1)> + > insert into tbl1 values (123), (234); +spark-sql (db1.schema1)> select * from tbl1; +123 +234 +spark-sql (db1.schema1)> + > drop table tbl1 purge; +spark-sql (db1.schema1)> show tables; +spark-sql (db1.schema1)> drop namespace db1.schema1; +spark-sql (db1.schema1)> drop namespace db1; +spark-sql (db1.schema1)> show namespaces; +db2 +spark-sql (db1.schema1)> drop namespace db2; +spark-sql (db1.schema1)> show namespaces; +spark-sql (db1.schema1)> diff --git a/regtests/t_spark_sql/ref/spark_sql_s3.sh.ref b/regtests/t_spark_sql/ref/spark_sql_s3.sh.ref new file mode 100755 index 0000000000..885663c151 --- /dev/null +++ b/regtests/t_spark_sql/ref/spark_sql_s3.sh.ref @@ -0,0 +1,35 @@ +{"defaults":{"default-base-location":"s3://datalake-storage-team/polaris_test/spark_sql_s3_catalog"},"overrides":{"prefix":"spark_sql_s3_catalog"}} +Catalog created +spark-sql (default)> use polaris; +spark-sql ()> show namespaces; +spark-sql ()> create namespace db1; +spark-sql ()> create namespace db2; +spark-sql ()> show namespaces; +db1 +db2 +spark-sql ()> + > create namespace db1.schema1; +spark-sql ()> show namespaces; +db1 +db2 +spark-sql ()> show namespaces in db1; +db1.schema1 +spark-sql ()> + > create table db1.schema1.tbl1 (col1 int); +spark-sql ()> show tables in db1; +spark-sql ()> use db1.schema1; +spark-sql (db1.schema1)> + > insert into tbl1 values (123), (234); +spark-sql (db1.schema1)> select * from tbl1; +123 +234 +spark-sql (db1.schema1)> + > drop table tbl1 purge; +spark-sql (db1.schema1)> show tables; +spark-sql (db1.schema1)> drop namespace db1.schema1; +spark-sql (db1.schema1)> drop namespace db1; +spark-sql (db1.schema1)> show namespaces; +db2 +spark-sql (db1.schema1)> drop namespace db2; +spark-sql (db1.schema1)> show namespaces; +spark-sql (db1.schema1)> diff --git a/regtests/t_spark_sql/ref/spark_sql_s3_cross_region.sh.ref b/regtests/t_spark_sql/ref/spark_sql_s3_cross_region.sh.ref new file mode 100644 index 0000000000..957214cc17 --- /dev/null +++ b/regtests/t_spark_sql/ref/spark_sql_s3_cross_region.sh.ref @@ -0,0 +1,35 @@ +{"defaults":{"default-base-location":"s3://sfc-role-stage-for-reg-test-do-not-modify-write-only/polaris_test/spark_sql_s3_cross_region_catalog/"},"overrides":{"prefix":"spark_sql_s3_cross_region_catalog"}} +Catalog created +spark-sql (default)> use polaris; +spark-sql ()> show namespaces; +spark-sql ()> create namespace db1; +spark-sql ()> create namespace db2; +spark-sql ()> show namespaces; +db1 +db2 +spark-sql ()> + > create namespace db1.schema1; +spark-sql ()> show namespaces; +db1 +db2 +spark-sql ()> show namespaces in db1; +db1.schema1 +spark-sql ()> + > create table db1.schema1.tbl1 (col1 int); +spark-sql ()> show tables in db1; +spark-sql ()> use db1.schema1; +spark-sql (db1.schema1)> + > insert into tbl1 values (123), (234); +spark-sql (db1.schema1)> select * from tbl1; +123 +234 +spark-sql (db1.schema1)> + > drop table tbl1 purge; +spark-sql (db1.schema1)> show tables; +spark-sql (db1.schema1)> drop namespace db1.schema1; +spark-sql (db1.schema1)> drop namespace db1; +spark-sql (db1.schema1)> show namespaces; +db2 +spark-sql (db1.schema1)> drop namespace db2; +spark-sql (db1.schema1)> show namespaces; +spark-sql (db1.schema1)> diff --git a/regtests/t_spark_sql/ref/spark_sql_views.sh.ref b/regtests/t_spark_sql/ref/spark_sql_views.sh.ref new file mode 100755 index 0000000000..44e64f2c29 --- /dev/null +++ b/regtests/t_spark_sql/ref/spark_sql_views.sh.ref @@ -0,0 +1,52 @@ +{"defaults":{"default-base-location":"file:///tmp/spark_sql_s3_catalog"},"overrides":{"prefix":"spark_sql_views_catalog"}} +Catalog created +spark-sql (default)> use polaris; +spark-sql ()> show namespaces; +spark-sql ()> create namespace db1; +spark-sql ()> create namespace db2; +spark-sql ()> show namespaces; +db1 +db2 +spark-sql ()> + > create namespace db1.schema1; +spark-sql ()> show namespaces; +db1 +db2 +spark-sql ()> show namespaces in db1; +db1.schema1 +spark-sql ()> + > create table db1.schema1.tbl1 (col1 int, col2 string); +spark-sql ()> show tables in db1; +spark-sql ()> show tables in db1.schema1; +tbl1 +spark-sql ()> use db1.schema1; +spark-sql (db1.schema1)> show tables; +tbl1 +spark-sql (db1.schema1)> + > insert into tbl1 values (123, 'hello'), (234, 'world'); +spark-sql (db1.schema1)> select * from tbl1; +123 hello +234 world +spark-sql (db1.schema1)> + > create view db1.schema1.v1 (strcol) as select col2 from tbl1 order by col1 DESC; +spark-sql (db1.schema1)> show views in db1.schema1; +db1.schema1 v1 false +spark-sql (db1.schema1)> select * from v1; +world +hello +spark-sql (db1.schema1)> + > update tbl1 set col2 = 'world2' where col1 = 234; +spark-sql (db1.schema1)> select * from v1; +world2 +hello +spark-sql (db1.schema1)> + > drop view v1; +spark-sql (db1.schema1)> drop table tbl1 purge; +spark-sql (db1.schema1)> show tables; +spark-sql (db1.schema1)> drop namespace db1.schema1; +spark-sql (db1.schema1)> drop namespace db1; +spark-sql (db1.schema1)> show namespaces; +db2 +spark-sql (db1.schema1)> drop namespace db2; +spark-sql (db1.schema1)> show namespaces; +spark-sql (db1.schema1)> diff --git a/regtests/t_spark_sql/src/spark_sql_azure_blob.sh b/regtests/t_spark_sql/src/spark_sql_azure_blob.sh new file mode 100755 index 0000000000..787e6b415e --- /dev/null +++ b/regtests/t_spark_sql/src/spark_sql_azure_blob.sh @@ -0,0 +1,51 @@ +#!/bin/bash + +SPARK_BEARER_TOKEN="${REGTEST_ROOT_BEARER_TOKEN:-principal:root;realm:realm1}" + +curl -i -X POST -H "Authorization: Bearer ${SPARK_BEARER_TOKEN}" -H 'Accepts: application/json' -H 'Content-Type: application/json' \ + http://${POLARIS_HOST:-localhost}:8181/api/management/v1/catalogs \ + -d "{\"name\": \"spark_sql_azure_blob_catalog\", \"id\": 101, \"type\": \"INTERNAL\", \"readOnly\": false, \"properties\": {\"default-base-location\": \"${AZURE_BLOB_TEST_BASE}/polaris-test/spark_sql_blob_catalog/\"}, \"storageConfigInfo\": {\"storageType\": \"AZURE\", \"allowedLocations\": [\"${AZURE_BLOB_TEST_BASE}/polaris-test/spark_sql_blob_catalog2/\"], \"tenantId\": \"${AZURE_TENANT_ID}\"}}" > /dev/stderr + +# Add TABLE_WRITE_DATA to the catalog's catalog_admin role since by default it can only manage access and metadata +curl -i -X PUT -H "Authorization: Bearer ${SPARK_BEARER_TOKEN}" -H 'Accepts: application/json' -H 'Content-Type: application/json' \ + http://${POLARIS_HOST:-localhost}:8181/api/management/v1/catalogs/spark_sql_azure_blob_catalog/catalog-roles/catalog_admin/grants \ + -d '{"type": "catalog", "privilege": "TABLE_WRITE_DATA"}' > /dev/stderr + +# For now, also explicitly assign the catalog_admin to the service_admin. Remove once GS fully rolled out for auto-assign. +curl -i -X PUT -H "Authorization: Bearer ${SPARK_BEARER_TOKEN}" -H 'Accepts: application/json' -H 'Content-Type: application/json' \ + http://${POLARIS_HOST:-localhost}:8181/api/management/v1/principal-roles/service_admin/catalog-roles/spark_sql_azure_blob_catalog \ + -d '{"name": "catalog_admin"}' > /dev/stderr + +curl -H "Authorization: Bearer ${SPARK_BEARER_TOKEN}" -H 'Accepts: application/json' -H 'Content-Type: application/json' \ + "http://${POLARIS_HOST:-localhost}:8181/api/catalog/v1/config?warehouse=spark_sql_azure_blob_catalog" +echo +echo "Catalog created" +cat << EOF | ${SPARK_HOME}/bin/spark-sql -S --conf spark.sql.catalog.polaris.token="${SPARK_BEARER_TOKEN}" --conf spark.sql.catalog.polaris.warehouse=spark_sql_azure_blob_catalog; +use polaris; +show namespaces; +create namespace db1; +create namespace db2; +show namespaces; + +create namespace db1.schema1; +show namespaces; +show namespaces in db1; + +create table db1.schema1.tbl1 (col1 int); +show tables in db1; +use db1.schema1; + +insert into tbl1 values (123), (234); +select * from tbl1; + +drop table tbl1 purge; +show tables; +drop namespace db1.schema1; +drop namespace db1; +show namespaces; +drop namespace db2; +show namespaces; +EOF + +curl -i -X DELETE -H "Authorization: Bearer ${SPARK_BEARER_TOKEN}" -H 'Accepts: application/json' -H 'Content-Type: application/json' \ + http://${POLARIS_HOST:-localhost}:8181/api/management/v1/catalogs/spark_sql_azure_blob_catalog > /dev/stderr diff --git a/regtests/t_spark_sql/src/spark_sql_azure_dfs.sh b/regtests/t_spark_sql/src/spark_sql_azure_dfs.sh new file mode 100755 index 0000000000..2e270732ec --- /dev/null +++ b/regtests/t_spark_sql/src/spark_sql_azure_dfs.sh @@ -0,0 +1,51 @@ +#!/bin/bash + +SPARK_BEARER_TOKEN="${REGTEST_ROOT_BEARER_TOKEN:-principal:root;realm:realm1}" + +curl -i -X POST -H "Authorization: Bearer ${SPARK_BEARER_TOKEN}" -H 'Accepts: application/json' -H 'Content-Type: application/json' \ + http://${POLARIS_HOST:-localhost}:8181/api/management/v1/catalogs \ + -d "{\"name\": \"spark_sql_azure_dfs_catalog\", \"id\": 101, \"type\": \"INTERNAL\", \"readOnly\": false, \"properties\": {\"default-base-location\": \"${AZURE_DFS_TEST_BASE}/polaris-test/spark_sql_dfs_catalog/\"}, \"storageConfigInfo\": {\"storageType\": \"AZURE\", \"allowedLocations\": [\"${AZURE_DFS_TEST_BASE}/polaris-test/spark_sql_dfs_catalog2/\"], \"tenantId\": \"$AZURE_TENANT_ID\"}}" > /dev/stderr + +# Add TABLE_WRITE_DATA to the catalog's catalog_admin role since by default it can only manage access and metadata +curl -i -X PUT -H "Authorization: Bearer ${SPARK_BEARER_TOKEN}" -H 'Accepts: application/json' -H 'Content-Type: application/json' \ + http://${POLARIS_HOST:-localhost}:8181/api/management/v1/catalogs/spark_sql_azure_dfs_catalog/catalog-roles/catalog_admin/grants \ + -d '{"type": "catalog", "privilege": "TABLE_WRITE_DATA"}' > /dev/stderr + +# For now, also explicitly assign the catalog_admin to the service_admin. Remove once GS fully rolled out for auto-assign. +curl -i -X PUT -H "Authorization: Bearer ${SPARK_BEARER_TOKEN}" -H 'Accepts: application/json' -H 'Content-Type: application/json' \ + http://${POLARIS_HOST:-localhost}:8181/api/management/v1/principal-roles/service_admin/catalog-roles/spark_sql_azure_dfs_catalog \ + -d '{"name": "catalog_admin"}' > /dev/stderr + +curl -H "Authorization: Bearer ${SPARK_BEARER_TOKEN}" -H 'Accepts: application/json' -H 'Content-Type: application/json' \ + "http://${POLARIS_HOST:-localhost}:8181/api/catalog/v1/config?warehouse=spark_sql_azure_dfs_catalog" +echo +echo "Catalog created" +cat << EOF | ${SPARK_HOME}/bin/spark-sql -S --conf spark.sql.catalog.polaris.token="${SPARK_BEARER_TOKEN}" --conf spark.sql.catalog.polaris.warehouse=spark_sql_azure_dfs_catalog +use polaris; +show namespaces; +create namespace db1; +create namespace db2; +show namespaces; + +create namespace db1.schema1; +show namespaces; +show namespaces in db1; + +create table db1.schema1.tbl1 (col1 int); +show tables in db1; +use db1.schema1; + +insert into tbl1 values (123), (234); +select * from tbl1; + +drop table tbl1 purge; +show tables; +drop namespace db1.schema1; +drop namespace db1; +show namespaces; +drop namespace db2; +show namespaces; +EOF + +curl -i -X DELETE -H "Authorization: Bearer ${SPARK_BEARER_TOKEN}" -H 'Accepts: application/json' -H 'Content-Type: application/json' \ + http://${POLARIS_HOST:-localhost}:8181/api/management/v1/catalogs/spark_sql_azure_dfs_catalog > /dev/stderr diff --git a/regtests/t_spark_sql/src/spark_sql_basic.sh b/regtests/t_spark_sql/src/spark_sql_basic.sh new file mode 100755 index 0000000000..dda952e8c2 --- /dev/null +++ b/regtests/t_spark_sql/src/spark_sql_basic.sh @@ -0,0 +1,53 @@ +#!/bin/bash + +SPARK_BEARER_TOKEN="${REGTEST_ROOT_BEARER_TOKEN:-principal:root;realm:realm1}" + +curl -i -X POST -H "Authorization: Bearer ${SPARK_BEARER_TOKEN}" -H 'Accepts: application/json' -H 'Content-Type: application/json' \ + http://${POLARIS_HOST:-localhost}:8181/api/management/v1/catalogs \ + -d '{"name": "spark_sql_basic_catalog", "id": 100, "type": "INTERNAL", "readOnly": false, "properties": {"default-base-location": "file:///tmp/spark_sql_s3_catalog"}, "storageConfigInfo": {"storageType": "FILE", "allowedLocations": ["file:///tmp"]}}' > /dev/stderr + +# Add TABLE_WRITE_DATA to the catalog's catalog_admin role since by default it can only manage access and metadata +curl -i -X PUT -H "Authorization: Bearer ${SPARK_BEARER_TOKEN}" -H 'Accepts: application/json' -H 'Content-Type: application/json' \ + http://${POLARIS_HOST:-localhost}:8181/api/management/v1/catalogs/spark_sql_basic_catalog/catalog-roles/catalog_admin/grants \ + -d '{"type": "catalog", "privilege": "TABLE_WRITE_DATA"}' > /dev/stderr + +# For now, also explicitly assign the catalog_admin to the service_admin. Remove once GS fully rolled out for auto-assign. +curl -i -X PUT -H "Authorization: Bearer ${SPARK_BEARER_TOKEN}" -H 'Accepts: application/json' -H 'Content-Type: application/json' \ + http://${POLARIS_HOST:-localhost}:8181/api/management/v1/principal-roles/service_admin/catalog-roles/spark_sql_basic_catalog \ + -d '{"name": "catalog_admin"}' > /dev/stderr + +curl -H "Authorization: Bearer ${SPARK_BEARER_TOKEN}" -H 'Accepts: application/json' -H 'Content-Type: application/json' \ + "http://${POLARIS_HOST:-localhost}:8181/api/catalog/v1/config?warehouse=spark_sql_basic_catalog" +echo +echo "Catalog created" +cat << EOF | ${SPARK_HOME}/bin/spark-sql -S --conf spark.sql.catalog.polaris.token="${SPARK_BEARER_TOKEN}" --conf spark.sql.catalog.polaris.warehouse=spark_sql_basic_catalog +use polaris; +show namespaces; +create namespace db1; +create namespace db2; +show namespaces; + +create namespace db1.schema1; +show namespaces; +show namespaces in db1; + +create table db1.schema1.tbl1 (col1 int); +show tables in db1; +show tables in db1.schema1; +use db1.schema1; +show tables; + +insert into tbl1 values (123), (234); +select * from tbl1; + +drop table tbl1 purge; +show tables; +drop namespace db1.schema1; +drop namespace db1; +show namespaces; +drop namespace db2; +show namespaces; +EOF + +curl -i -X DELETE -H "Authorization: Bearer ${SPARK_BEARER_TOKEN}" -H 'Accepts: application/json' -H 'Content-Type: application/json' \ + http://${POLARIS_HOST:-localhost}:8181/api/management/v1/catalogs/spark_sql_basic_catalog > /dev/stderr diff --git a/regtests/t_spark_sql/src/spark_sql_gcp.sh b/regtests/t_spark_sql/src/spark_sql_gcp.sh new file mode 100755 index 0000000000..76a4cb2f85 --- /dev/null +++ b/regtests/t_spark_sql/src/spark_sql_gcp.sh @@ -0,0 +1,51 @@ +#!/bin/bash + +SPARK_BEARER_TOKEN="${REGTEST_ROOT_BEARER_TOKEN:-principal:root;realm:realm1}" + +curl -i -X POST -H "Authorization: Bearer ${SPARK_BEARER_TOKEN}" -H 'Accepts: application/json' -H 'Content-Type: application/json' \ + http://${POLARIS_HOST:-localhost}:8181/api/management/v1/catalogs \ + -d "{\"name\": \"spark_sql_gcp_catalog\", \"id\": 100, \"type\": \"INTERNAL\", \"readOnly\": false, \"properties\": {\"default-base-location\": \"${GCS_TEST_BASE}/polaris_test/spark_sql_gcp_catalog/\"}, \"storageConfigInfo\": {\"storageType\": \"GCS\", \"allowedLocations\": [\"${GCS_TEST_BASE}/polaris_test/spark_sql_gcp_catalog2/\"]}}" > /dev/stderr + +# Add TABLE_WRITE_DATA to the catalog's catalog_admin role since by default it can only manage access and metadata +curl -i -X PUT -H "Authorization: Bearer ${SPARK_BEARER_TOKEN}" -H 'Accepts: application/json' -H 'Content-Type: application/json' \ + http://${POLARIS_HOST:-localhost}:8181/api/management/v1/catalogs/spark_sql_gcp_catalog/catalog-roles/catalog_admin/grants \ + -d '{"type": "catalog", "privilege": "TABLE_WRITE_DATA"}' > /dev/stderr + +# For now, also explicitly assign the catalog_admin to the service_admin. Remove once GS fully rolled out for auto-assign. +curl -i -X PUT -H "Authorization: Bearer ${SPARK_BEARER_TOKEN}" -H 'Accepts: application/json' -H 'Content-Type: application/json' \ + http://${POLARIS_HOST:-localhost}:8181/api/management/v1/principal-roles/service_admin/catalog-roles/spark_sql_gcp_catalog \ + -d '{"name": "catalog_admin"}' > /dev/stderr + +curl -H "Authorization: Bearer ${SPARK_BEARER_TOKEN}" -H 'Accepts: application/json' -H 'Content-Type: application/json' \ + "http://${POLARIS_HOST:-localhost}:8181/api/catalog/v1/config?warehouse=spark_sql_gcp_catalog" +echo +echo "Catalog created" +cat << EOF | ${SPARK_HOME}/bin/spark-sql -S --conf spark.sql.catalog.polaris.token="${SPARK_BEARER_TOKEN}" --conf spark.sql.catalog.polaris.warehouse=spark_sql_gcp_catalog +use polaris; +show namespaces; +create namespace db1; +create namespace db2; +show namespaces; + +create namespace db1.schema1; +show namespaces; +show namespaces in db1; + +create table db1.schema1.tbl1 (col1 int); +show tables in db1; +use db1.schema1; + +insert into tbl1 values (123), (234); +select * from tbl1; + +drop table tbl1 purge; +show tables; +drop namespace db1.schema1; +drop namespace db1; +show namespaces; +drop namespace db2; +show namespaces; +EOF + +curl -i -X DELETE -H "Authorization: Bearer ${SPARK_BEARER_TOKEN}" -H 'Accepts: application/json' -H 'Content-Type: application/json' \ + http://${POLARIS_HOST:-localhost}:8181/api/management/v1/catalogs/spark_sql_gcp_catalog > /dev/stderr diff --git a/regtests/t_spark_sql/src/spark_sql_s3.sh b/regtests/t_spark_sql/src/spark_sql_s3.sh new file mode 100755 index 0000000000..c4a7fdbad9 --- /dev/null +++ b/regtests/t_spark_sql/src/spark_sql_s3.sh @@ -0,0 +1,56 @@ +#!/bin/bash + +if [ -z "$AWS_TEST_ENABLED" ] || [ "$AWS_TEST_ENABLED" != "true" ]; then + echo "AWS_TEST_ENABLED is not set to 'true'. Skipping test." + exit 0 +fi + +SPARK_BEARER_TOKEN="${REGTEST_ROOT_BEARER_TOKEN:-principal:root;realm:realm1}" + +curl -i -X POST -H "Authorization: Bearer ${SPARK_BEARER_TOKEN}" -H 'Accepts: application/json' -H 'Content-Type: application/json' \ + http://${POLARIS_HOST:-localhost}:8181/api/management/v1/catalogs \ + -d "{\"name\": \"spark_sql_s3_catalog\", \"id\": 100, \"type\": \"INTERNAL\", \"readOnly\": false, \"properties\": {\"default-base-location\": \"s3://datalake-storage-team/polaris_test/spark_sql_s3_catalog\"}, \"storageConfigInfo\": {\"storageType\": \"S3\", \"allowedLocations\": [\"${AWS_TEST_BASE}/polaris_test/\"], \"roleArn\": \"arn:aws:iam::631484165566:role/datalake-storage-integration-role\"}}" > /dev/stderr + +# Add TABLE_WRITE_DATA to the catalog's catalog_admin role since by default it can only manage access and metadata +curl -i -X PUT -H "Authorization: Bearer ${SPARK_BEARER_TOKEN}" -H 'Accepts: application/json' -H 'Content-Type: application/json' \ + http://${POLARIS_HOST:-localhost}:8181/api/management/v1/catalogs/spark_sql_s3_catalog/catalog-roles/catalog_admin/grants \ + -d '{"type": "catalog", "privilege": "TABLE_WRITE_DATA"}' > /dev/stderr + +# For now, also explicitly assign the catalog_admin to the service_admin. Remove once GS fully rolled out for auto-assign. +curl -i -X PUT -H "Authorization: Bearer ${SPARK_BEARER_TOKEN}" -H 'Accepts: application/json' -H 'Content-Type: application/json' \ + http://${POLARIS_HOST:-localhost}:8181/api/management/v1/principal-roles/service_admin/catalog-roles/spark_sql_s3_catalog \ + -d '{"name": "catalog_admin"}' > /dev/stderr + +curl -H "Authorization: Bearer ${SPARK_BEARER_TOKEN}" -H 'Accepts: application/json' -H 'Content-Type: application/json' \ + "http://${POLARIS_HOST:-localhost}:8181/api/catalog/v1/config?warehouse=spark_sql_s3_catalog" +echo +echo "Catalog created" +cat << EOF | ${SPARK_HOME}/bin/spark-sql -S --conf spark.sql.catalog.polaris.token="${SPARK_BEARER_TOKEN}" --conf spark.sql.catalog.polaris.warehouse=spark_sql_s3_catalog +use polaris; +show namespaces; +create namespace db1; +create namespace db2; +show namespaces; + +create namespace db1.schema1; +show namespaces; +show namespaces in db1; + +create table db1.schema1.tbl1 (col1 int); +show tables in db1; +use db1.schema1; + +insert into tbl1 values (123), (234); +select * from tbl1; + +drop table tbl1 purge; +show tables; +drop namespace db1.schema1; +drop namespace db1; +show namespaces; +drop namespace db2; +show namespaces; +EOF + +curl -i -X DELETE -H "Authorization: Bearer ${SPARK_BEARER_TOKEN}" -H 'Accepts: application/json' -H 'Content-Type: application/json' \ + http://${POLARIS_HOST:-localhost}:8181/api/management/v1/catalogs/spark_sql_s3_catalog > /dev/stderr diff --git a/regtests/t_spark_sql/src/spark_sql_s3_cross_region.sh b/regtests/t_spark_sql/src/spark_sql_s3_cross_region.sh new file mode 100644 index 0000000000..a9e3fb868f --- /dev/null +++ b/regtests/t_spark_sql/src/spark_sql_s3_cross_region.sh @@ -0,0 +1,58 @@ +#!/bin/bash + +if [ -z "$AWS_CROSS_REGION_TEST_ENABLED" ] || [ "$AWS_CROSS_REGION_TEST_ENABLED" != "true" ]; then + echo "AWS_CROSS_REGION_TEST_ENABLED is not set to 'true'. Skipping test." + exit 0 +fi + +SPARK_BEARER_TOKEN="${REGTEST_ROOT_BEARER_TOKEN:-principal:root;realm:realm1}" +BUCKET="${AWS_CROSS_REGION_BUCKET}" +ROLE_ARN="${AWS_ROLE_FOR_CROSS_REGION_BUCKET}" + +curl -i -X POST -H "Authorization: Bearer ${SPARK_BEARER_TOKEN}" -H 'Accepts: application/json' -H 'Content-Type: application/json' \ + http://${POLARIS_HOST:-localhost}:8181/api/management/v1/catalogs \ + -d '{"name": "spark_sql_s3_cross_region_catalog", "id": 100, "type": "INTERNAL", "readOnly": false, "properties": {"default-base-location": "s3://${BUCKET}/polaris_test/spark_sql_s3_cross_region_catalog/"}, "storageConfigInfo": {"storageType": "S3", "allowedLocations": ["s3://${BUCKET}/polaris_test/"], "roleArn": "${ROLE_ARN}"}}' > /dev/stderr + +# Add TABLE_WRITE_DATA to the catalog's catalog_admin role since by default it can only manage access and metadata +curl -i -X PUT -H "Authorization: Bearer ${SPARK_BEARER_TOKEN}" -H 'Accepts: application/json' -H 'Content-Type: application/json' \ + http://${POLARIS_HOST:-localhost}:8181/api/management/v1/catalogs/spark_sql_s3_cross_region_catalog/catalog-roles/catalog_admin/grants \ + -d '{"type": "catalog", "privilege": "TABLE_WRITE_DATA"}' > /dev/stderr + +# For now, also explicitly assign the catalog_admin to the service_admin. Remove once GS fully rolled out for auto-assign. +curl -i -X PUT -H "Authorization: Bearer ${SPARK_BEARER_TOKEN}" -H 'Accepts: application/json' -H 'Content-Type: application/json' \ + http://${POLARIS_HOST:-localhost}:8181/api/management/v1/principal-roles/service_admin/catalog-roles/spark_sql_s3_cross_region_catalog \ + -d '{"name": "catalog_admin"}' > /dev/stderr + +curl -H "Authorization: Bearer ${SPARK_BEARER_TOKEN}" -H 'Accepts: application/json' -H 'Content-Type: application/json' \ + "http://${POLARIS_HOST:-localhost}:8181/api/catalog/v1/config?warehouse=spark_sql_s3_cross_region_catalog" +echo +echo "Catalog created" +cat << EOF | ${SPARK_HOME}/bin/spark-sql -S --conf spark.sql.catalog.polaris.token="${SPARK_BEARER_TOKEN}" --conf spark.sql.catalog.polaris.warehouse=spark_sql_s3_cross_region_catalog +use polaris; +show namespaces; +create namespace db1; +create namespace db2; +show namespaces; + +create namespace db1.schema1; +show namespaces; +show namespaces in db1; + +create table db1.schema1.tbl1 (col1 int); +show tables in db1; +use db1.schema1; + +insert into tbl1 values (123), (234); +select * from tbl1; + +drop table tbl1 purge; +show tables; +drop namespace db1.schema1; +drop namespace db1; +show namespaces; +drop namespace db2; +show namespaces; +EOF + +curl -i -X DELETE -H "Authorization: Bearer ${SPARK_BEARER_TOKEN}" -H 'Accepts: application/json' -H 'Content-Type: application/json' \ + http://${POLARIS_HOST:-localhost}:8181/api/management/v1/catalogs/spark_sql_s3_catalog > /dev/stderr diff --git a/regtests/t_spark_sql/src/spark_sql_views.sh b/regtests/t_spark_sql/src/spark_sql_views.sh new file mode 100755 index 0000000000..c9b74eec09 --- /dev/null +++ b/regtests/t_spark_sql/src/spark_sql_views.sh @@ -0,0 +1,63 @@ +#!/bin/bash + +SPARK_BEARER_TOKEN="${REGTEST_ROOT_BEARER_TOKEN:-principal:root;realm:default-realm}" + +curl -i -X POST -H "Authorization: Bearer ${SPARK_BEARER_TOKEN}" -H 'Accepts: application/json' -H 'Content-Type: application/json' \ + http://${POLARIS_HOST:-localhost}:8181/api/management/v1/catalogs \ + -d '{"name": "spark_sql_views_catalog", "id": 100, "type": "INTERNAL", "readOnly": false, "properties": {"default-base-location": "file:///tmp/spark_sql_s3_catalog"}, "storageConfigInfo": {"storageType": "FILE", "allowedLocations": ["file:///tmp"]}}' > /dev/stderr + +# Add TABLE_WRITE_DATA to the catalog's catalog_admin role since by default it can only manage access and metadata +curl -i -X PUT -H "Authorization: Bearer ${SPARK_BEARER_TOKEN}" -H 'Accepts: application/json' -H 'Content-Type: application/json' \ + http://${POLARIS_HOST:-localhost}:8181/api/management/v1/catalogs/spark_sql_views_catalog/catalog-roles/catalog_admin/grants \ + -d '{"type": "catalog", "privilege": "TABLE_WRITE_DATA"}' > /dev/stderr + +# For now, also explicitly assign the catalog_admin to the service_admin. Remove once GS fully rolled out for auto-assign. +curl -i -X PUT -H "Authorization: Bearer ${SPARK_BEARER_TOKEN}" -H 'Accepts: application/json' -H 'Content-Type: application/json' \ + http://${POLARIS_HOST:-localhost}:8181/api/management/v1/principal-roles/service_admin/catalog-roles/spark_sql_views_catalog \ + -d '{"name": "catalog_admin"}' > /dev/stderr + +curl -H "Authorization: Bearer ${SPARK_BEARER_TOKEN}" -H 'Accepts: application/json' -H 'Content-Type: application/json' \ + "http://${POLARIS_HOST:-localhost}:8181/api/catalog/v1/config?warehouse=spark_sql_views_catalog" +echo +echo "Catalog created" +cat << EOF | ${SPARK_HOME}/bin/spark-sql -S --conf spark.sql.catalog.polaris.token="${SPARK_BEARER_TOKEN}" \ + --conf spark.sql.catalog.polaris.warehouse=spark_sql_views_catalog \ + --conf spark.sql.extensions=org.apache.iceberg.spark.extensions.IcebergSparkSessionExtensions +use polaris; +show namespaces; +create namespace db1; +create namespace db2; +show namespaces; + +create namespace db1.schema1; +show namespaces; +show namespaces in db1; + +create table db1.schema1.tbl1 (col1 int, col2 string); +show tables in db1; +show tables in db1.schema1; +use db1.schema1; +show tables; + +insert into tbl1 values (123, 'hello'), (234, 'world'); +select * from tbl1; + +create view db1.schema1.v1 (strcol) as select col2 from tbl1 order by col1 DESC; +show views in db1.schema1; +select * from v1; + +update tbl1 set col2 = 'world2' where col1 = 234; +select * from v1; + +drop view v1; +drop table tbl1 purge; +show tables; +drop namespace db1.schema1; +drop namespace db1; +show namespaces; +drop namespace db2; +show namespaces; +EOF + +curl -i -X DELETE -H "Authorization: Bearer ${SPARK_BEARER_TOKEN}" -H 'Accepts: application/json' -H 'Content-Type: application/json' \ + http://${POLARIS_HOST:-localhost}:8181/api/management/v1/catalogs/spark_sql_views_catalog > /dev/stderr diff --git a/server-templates/api.mustache b/server-templates/api.mustache new file mode 100644 index 0000000000..4a4faa7803 --- /dev/null +++ b/server-templates/api.mustache @@ -0,0 +1,100 @@ +package {{package}}; + +{{#imports}} +import {{import}}; +{{/imports}} + +import io.polaris.service.resource.TimedApi; + +import java.util.Map; +import java.util.List; + +import java.io.InputStream; + +import jakarta.annotation.security.RolesAllowed; + +import {{javaxPackage}}.ws.rs.Consumes; +import {{javaxPackage}}.ws.rs.Produces; +import {{javaxPackage}}.ws.rs.DELETE; +import {{javaxPackage}}.ws.rs.GET; +import {{javaxPackage}}.ws.rs.HEAD; +import {{javaxPackage}}.ws.rs.PATCH; +import {{javaxPackage}}.ws.rs.POST; +import {{javaxPackage}}.ws.rs.PUT; +import {{javaxPackage}}.ws.rs.Path; +import {{javaxPackage}}.ws.rs.DefaultValue; +import {{javaxPackage}}.ws.rs.PathParam; +import {{javaxPackage}}.ws.rs.HeaderParam; +import {{javaxPackage}}.ws.rs.QueryParam; +import {{javaxPackage}}.ws.rs.FormParam; +import {{javaxPackage}}.ws.rs.core.Response; +import {{javaxPackage}}.servlet.http.HttpServletRequest; +import {{javaxPackage}}.servlet.http.HttpServletResponse; +import {{javaxPackage}}.ws.rs.core.Context; +import {{javaxPackage}}.ws.rs.core.SecurityContext; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +{{#useBeanValidation}} +import {{javaxPackage}}.validation.constraints.*; +import {{javaxPackage}}.validation.Valid; +{{/useBeanValidation}} +{{#operations}}{{#operation}}{{#isMultipart}}import org.jboss.resteasy.plugins.providers.multipart.MultipartFormDataInput; +{{/isMultipart}}{{/operation}}{{/operations}} +{{! +Note that this template is copied /modified from +https://github.com/OpenAPITools/openapi-generator/blob/783e68c7acbbdcbb2282d167d1644b069f12d486/modules/openapi-generator/src/main/resources/JavaJaxRS/resteasy/api.mustache +It is updated to remove all swagger annotations +}} +/** + * The {{{baseName}}} API interface + * + * This file is automatically generated by the OpenAPI Code Generator based on configuratipn in the + * build.gradle file. + * + */ +@Path("{{contextPath}}{{commonPath}}"){{#hasConsumes}} +@Consumes({ {{#consumes}}"{{{mediaType}}}"{{^-last}}, {{/-last}}{{/consumes}} }){{/hasConsumes}}{{#hasProduces}} +@Produces({ {{#produces}}"{{{mediaType}}}"{{^-last}}, {{/-last}}{{/produces}} }){{/hasProduces}} +{{>generatedAnnotation}} +{{#operations}} +public class {{classname}} { + private static final Logger LOGGER = LoggerFactory.getLogger({{classname}}.class); + + private final {{classname}}Service service; + + public {{classname}}({{classname}}Service service) { + this.service = service; + } + +{{#operation}} + /** + * {{^notes}}{{{summary}}}{{/notes}}{{{notes}}} + * + * Response type {@link {{{returnBaseType}}}} + *{{#allParams}} @param {{paramName}} {{#required}}Required -{{/required}} {{description}} + *{{/allParams}}{{#responses}} + * @return {{{code}}} - {{{message}}}{{/responses}} + */ + @{{httpMethod}}{{#subresourceOperation}} + @Path("{{{path}}}"){{/subresourceOperation}}{{#hasConsumes}} + @Consumes({ {{#consumes}}"{{{mediaType}}}"{{^-last}}, {{/-last}}{{/consumes}} }){{/hasConsumes}}{{#hasProduces}} + @Produces({ {{#produces}}"{{{mediaType}}}"{{^-last}}, {{/-last}}{{/produces}} }){{/hasProduces}}{{#hasAuthMethods}} + {{#authMethods}}{{#isOAuth}}@RolesAllowed({ {{#scopes}}"{{scope}}"{{^-last}}, {{/-last}}{{/scopes}} }){{/isOAuth}}{{/authMethods}}{{/hasAuthMethods}} + @TimedApi("{{metricsPrefix}}.{{baseName}}.{{nickname}}") + public Response {{nickname}}({{#isMultipart}}MultipartFormDataInput input,{{/isMultipart}}{{#allParams}}{{>queryParams}}{{>pathParams}}{{>headerParams}}{{>bodyParams}}{{^isMultipart}}{{>formParams}},{{/isMultipart}}{{#isMultipart}}{{^isFormParam}},{{/isFormParam}}{{/isMultipart}}{{/allParams}}@Context SecurityContext securityContext) { +{{! Don't log form or header params in case there are secrets, e.g., OAuth tokens }} + LOGGER.atDebug().setMessage("Invoking {{baseName}} with params") + .addKeyValue("operation", "{{nickname}}"){{#allParams}}{{^isHeaderParam}}{{^isFormParam}} + .addKeyValue("{{paramName}}", {{^isBodyParam}}{{paramName}}{{/isBodyParam}}{{#isBodyParam}}String.valueOf({{paramName}}){{/isBodyParam}}){{/isFormParam}}{{/isHeaderParam}}{{/allParams}} + .log(); + + Response ret = + service.{{nickname}}({{#isMultipart}}input,{{/isMultipart}}{{#allParams}}{{^isMultipart}}{{paramName}},{{/isMultipart}}{{#isMultipart}}{{^isFormParam}}{{paramName}},{{/isFormParam}}{{/isMultipart}}{{/allParams}}securityContext); + LOGGER.debug("Completed execution of {{nickname}} API with status code {}", ret.getStatus()); + return ret; + } +{{/operation}} +} +{{/operations}} diff --git a/server-templates/apiService.mustache b/server-templates/apiService.mustache new file mode 100644 index 0000000000..2d199fb8e0 --- /dev/null +++ b/server-templates/apiService.mustache @@ -0,0 +1,42 @@ +package {{package}}; + +{{#operations}}{{#operation}}{{#isMultipart}}import org.jboss.resteasy.plugins.providers.multipart.MultipartFormDataInput; +{{/isMultipart}}{{/operation}}{{/operations}} + +{{#imports}}import {{import}}; +{{/imports}} + +import java.util.List; + +import java.io.InputStream; + +{{#useBeanValidation}} +import {{javaxPackage}}.validation.constraints.*; +import {{javaxPackage}}.validation.Valid; +{{/useBeanValidation}} +import {{javaxPackage}}.ws.rs.core.Response; +import {{javaxPackage}}.ws.rs.core.SecurityContext; + +{{! +Note that this template is copied from https://github.com/OpenAPITools/openapi-generator/blob/783e68c7acbbdcbb2282d167d1644b069f12d486/modules/openapi-generator/src/main/resources/JavaJaxRS/resteasy/apiService.mustache +It is here to remove some unsupported imports and to update the default implementation to return a +501 response code +}} +/** + * Service interface for implementations of the {{classname}}Service. Provides default + * implemntations for all service methods that return 501 error codes (not implemented). + * + * This file is automatically generated by the OpenAPI Code Generator based on configuration in the + * pom.xml file in the module. + * + */ +{{>generatedAnnotation}} +{{#operations}} +public interface {{classname}}Service { + {{#operation}} + default Response {{nickname}}({{#isMultipart}}MultipartFormDataInput input,{{/isMultipart}}{{#allParams}}{{>serviceQueryParams}}{{>servicePathParams}}{{>serviceHeaderParams}}{{>serviceBodyParams}}{{^isMultipart}}{{>serviceFormParams}},{{/isMultipart}}{{#isMultipart}}{{^isFormParam}},{{/isFormParam}}{{/isMultipart}}{{/allParams}}SecurityContext securityContext) { + return Response.status(501).build(); // not implemented + } + {{/operation}} +} +{{/operations}} \ No newline at end of file diff --git a/server-templates/apiServiceImpl.mustache b/server-templates/apiServiceImpl.mustache new file mode 100644 index 0000000000..4d146d17b8 --- /dev/null +++ b/server-templates/apiServiceImpl.mustache @@ -0,0 +1,43 @@ +package {{package}}.impl; + +{{#operations}}{{#operation}}{{#isMultipart}}import org.jboss.resteasy.plugins.providers.multipart.MultipartFormDataInput; +{{/isMultipart}}{{/operation}}{{/operations}} + +import {{package}}.{{classname}}Service; +{{#imports}}import {{import}}; +{{/imports}} + +import java.util.List; + +import java.io.InputStream; + +{{#useBeanValidation}} +import {{javaxPackage}}.validation.constraints.*; +import {{javaxPackage}}.validation.Valid; +{{/useBeanValidation}} +import {{javaxPackage}}.ws.rs.core.Response; +import {{javaxPackage}}.ws.rs.core.SecurityContext; + +{{! +Note that this template is copied from https://github.com/OpenAPITools/openapi-generator/blob/783e68c7acbbdcbb2282d167d1644b069f12d486/modules/openapi-generator/src/main/resources/JavaJaxRS/resteasy/apiServiceImpl.mustache +It is here to remove some unsupported imports (ApiResponseMessage, openapi.tools.*) +}} +/** + * Default implementation of the {{classname}}Service. Provides default + * implemntations for all service methods that return 501 error codes (not implemented). + * + * This file is automatically generated by the OpenAPI Code Generator based on configuration in the + * pom.xml file in the module. + * + * DO NOT EDIT THIS FILE BY HAND - CHANGES WILL BE AUTOMATICALLY OVERWRITTEN + */ +{{>generatedAnnotation}} +{{#operations}} +public class {{classname}}ServiceImpl implements {{classname}}Service { + {{#operation}} + public Response {{nickname}}({{#isMultipart}}MultipartFormDataInput input,{{/isMultipart}}{{#allParams}}{{>serviceQueryParams}}{{>servicePathParams}}{{>serviceHeaderParams}}{{>serviceBodyParams}}{{^isMultipart}}{{>serviceFormParams}},{{/isMultipart}}{{#isMultipart}}{{^isFormParam}},{{/isFormParam}}{{/isMultipart}}{{/allParams}}SecurityContext securityContext) { + return Response.status(501).build(); // not implemented + } + {{/operation}} +} +{{/operations}} \ No newline at end of file diff --git a/server-templates/bodyParams.mustache b/server-templates/bodyParams.mustache new file mode 100644 index 0000000000..b6b8d354cd --- /dev/null +++ b/server-templates/bodyParams.mustache @@ -0,0 +1,4 @@ +{{! +Note that this template is copied from https://github.com/OpenAPITools/openapi-generator/blob/783e68c7acbbdcbb2282d167d1644b069f12d486/modules/openapi-generator/src/main/resources/JavaJaxRS/resteasy/bodyParams.mustache +It is here to remove some unsupported swagger annotations +}}{{#isBodyParam}}{{#useBeanValidation}}{{#required}} @NotNull{{/required}} @Valid{{/useBeanValidation}} {{{dataType}}} {{paramName}}{{/isBodyParam}} \ No newline at end of file diff --git a/server-templates/formParams.mustache b/server-templates/formParams.mustache new file mode 100644 index 0000000000..187ea8a849 --- /dev/null +++ b/server-templates/formParams.mustache @@ -0,0 +1,4 @@ +{{! +Note that this template is copied from https://github.com/OpenAPITools/openapi-generator/blob/783e68c7acbbdcbb2282d167d1644b069f12d486/modules/openapi-generator/src/main/resources/JavaJaxRS/resteasy/bodyParams.mustache +It is here to remove some unsupported swagger annotations +}}{{#isFormParam}}{{^isFile}}@FormParam("{{baseName}}") {{{dataType}}} {{paramName}}{{/isFile}}{{/isFormParam}} \ No newline at end of file diff --git a/server-templates/headerParams.mustache b/server-templates/headerParams.mustache new file mode 100644 index 0000000000..f062941d44 --- /dev/null +++ b/server-templates/headerParams.mustache @@ -0,0 +1,4 @@ +{{! +Note that this template is copied from https://github.com/OpenAPITools/openapi-generator/blob/783e68c7acbbdcbb2282d167d1644b069f12d486/modules/openapi-generator/src/main/resources/JavaJaxRS/resteasy/headerParams.mustache +It is here to remove some unsupported swagger annotations +}}{{#isHeaderParam}}@HeaderParam("{{baseName}}") {{{dataType}}} {{paramName}}{{/isHeaderParam}} \ No newline at end of file diff --git a/server-templates/pojo.mustache b/server-templates/pojo.mustache new file mode 100644 index 0000000000..56849730d3 --- /dev/null +++ b/server-templates/pojo.mustache @@ -0,0 +1,219 @@ +import io.swagger.annotations.*; +{{#useBeanValidation}}import jakarta.validation.Valid;{{/useBeanValidation}} +{{#additionalPropertiesType}} +import com.fasterxml.jackson.annotation.JsonValue; +{{/additionalPropertiesType}} +{{! +Note that this template is copied /modified from +https://github.com/OpenAPITools/openapi-generator/blob/640ef9d9448a4a008af90eca9cc84c8a78ec87ec/modules/openapi-generator/src/main/resources/JavaJaxRS/resteasy/pojo.mustache +It is updated to remove all swagger annotations and support builders and immutability +}} + +{{#description}}@ApiModel(description="{{{.}}}"){{/description}}{{>additionalModelTypeAnnotations}}{{>generatedAnnotation}}{{#discriminator}}{{>typeInfoAnnotation}}{{/discriminator}}{{#vendorExtensions.x-class-extra-annotation}} + {{{vendorExtensions.x-class-extra-annotation}}} +{{/vendorExtensions.x-class-extra-annotation}}public class {{classname}} {{#parent}}extends {{{.}}}{{/parent}} {{#vendorExtensions.x-implements}}{{#-first}}implements {{{.}}}{{/-first}}{{^-first}}, {{{.}}}{{/-first}}{{/vendorExtensions.x-implements}} { +{{#serializableModel}} + private static final long serialVersionUID = 1L; +{{/serializableModel}} +{{#vars}}{{#isEnum}}{{^isContainer}} + {{>enumClass}}{{/isContainer}}{{#isContainer}}{{#mostInnerItems}} + {{>enumClass}}{{/mostInnerItems}}{{/isContainer}}{{/isEnum}} +{{#vendorExtensions.x-field-extra-annotation}} + {{{vendorExtensions.x-field-extra-annotation}}} +{{/vendorExtensions.x-field-extra-annotation}} +{{#useBeanValidation}}{{>beanValidation}}{{^isPrimitiveType}}{{^isDate}}{{^isDateTime}}{{^isString}}{{^isFile}} @Valid +{{/isFile}}{{/isString}}{{/isDateTime}}{{/isDate}}{{/isPrimitiveType}}{{/useBeanValidation}} private final {{{datatypeWithEnum}}} {{name}};{{/vars}} +{{#vars}} + /** + {{#description}} + * {{.}} + {{/description}} + {{#minimum}} + * minimum: {{.}} + {{/minimum}} + {{#maximum}} + * maximum: {{.}} + {{/maximum}} + **/ + {{#vendorExtensions.x-extra-annotation}}{{{vendorExtensions.x-extra-annotation}}} + {{/vendorExtensions.x-extra-annotation}}@ApiModelProperty({{#example}}example = "{{{.}}}", {{/example}}{{#required}}required = {{required}}, {{/required}}value = "{{{description}}}") + @JsonProperty(value = "{{baseName}}"{{#required}}, required = true{{/required}}) + public {{{datatypeWithEnum}}} {{getter}}() { + return {{name}}; + } + +{{/vars}} + {{#vendorExtensions.x-java-all-args-constructor}} + @JsonCreator + public {{classname}}({{#vendorExtensions.x-java-all-args-constructor-vars}}@JsonProperty(value = "{{baseName}}"{{#required}}, required = true{{/required}}) {{{datatypeWithEnum}}} {{name}}{{^-last}}, {{/-last}}{{/vendorExtensions.x-java-all-args-constructor-vars}}) { + {{#parent}} + super({{#parentVars}}{{name}}{{^-last}}, {{/-last}}{{/parentVars}}); + {{/parent}} + {{#vars}} + this.{{name}} = {{#defaultValue}}Objects.requireNonNullElse({{name}}, {{{.}}}){{/defaultValue}}{{^defaultValue}}{{name}}{{/defaultValue}}; + {{/vars}} + } + {{/vendorExtensions.x-java-all-args-constructor}} + {{^vendorExtensions.x-java-all-args-constructor}} + @JsonCreator + public {{classname}}({{#allVars}}@JsonProperty("{{baseName}}") {{{datatypeWithEnum}}} {{name}}{{^-last}}, {{/-last}}{{/allVars}}) { + {{#parent}} + super({{#parentVars}}{{name}}{{^-last}}, {{/-last}}{{/parentVars}}); + {{/parent}} + {{#vars}} + this.{{name}} = {{#defaultValue}}Objects.requireNonNullElse({{name}}, {{{.}}}){{/defaultValue}}{{^defaultValue}}{{name}}{{/defaultValue}}; + {{/vars}} + } + {{/vendorExtensions.x-java-all-args-constructor}} + + + {{#hasOptional}} + {{#hasRequired}} + public {{classname}}({{#requiredVars}}{{{datatypeWithEnum}}} {{name}}{{^-last}}, {{/-last}}{{/requiredVars}}) { + {{#parent}} + super({{#parentRequiredVars}}{{name}}{{^-last}}, {{/-last}}{{/parentRequiredVars}}); + {{/parent}} + {{#vars}} + {{#required}} + this.{{name}} = {{#defaultValue}}Objects.requireNonNullElse({{name}}, {{{.}}}){{/defaultValue}}{{^defaultValue}}{{name}}{{/defaultValue}}; + {{/required}} + {{^required}} + this.{{name}} = {{#defaultValue}}{{{.}}}{{/defaultValue}}{{^defaultValue}}null{{/defaultValue}}; + {{/required}} + {{/vars}} + } + {{/hasRequired}} + {{/hasOptional}} + + {{^hasChildren}} + public static Builder builder() { + return new Builder(); + } + {{#hasRequired}} + public static Builder builder({{#requiredVars}}{{{datatypeWithEnum}}} {{name}}{{^-last}}, {{/-last}}{{/requiredVars}}) { + return new Builder({{#requiredVars}}{{name}}{{^-last}}, {{/-last}}{{/requiredVars}}); + } + {{/hasRequired}} + {{/hasChildren}} + + {{#additionalPropertiesType}} + @JsonValue + public Map toMap() { + Map map = new HashMap<>(this); + {{#vars}} + map.put("{{baseName}}", {{name}}); + {{/vars}} + return map; + } + {{/additionalPropertiesType}} + + public static final class Builder { + {{#vendorExtensions.x-java-all-args-constructor}} + {{#vendorExtensions.x-java-all-args-constructor-vars}} + private {{{datatypeWithEnum}}} {{name}}; + {{/vendorExtensions.x-java-all-args-constructor-vars}} + {{/vendorExtensions.x-java-all-args-constructor}} + {{^vendorExtensions.x-java-all-args-constructor}} + {{#allVars}} + private {{{datatypeWithEnum}}} {{name}}; + {{/allVars}} + {{/vendorExtensions.x-java-all-args-constructor}} + {{#additionalPropertiesType}} + private Map additionalProperties = new HashMap<>(); + {{/additionalPropertiesType}} + private Builder() { + } + {{#hasRequired}} + private Builder({{#requiredVars}}{{{datatypeWithEnum}}} {{name}}{{^-last}}, {{/-last}}{{/requiredVars}}) { + {{#requiredVars}} + this.{{name}} = {{#defaultValue}}Objects.requireNonNullElse({{name}}, {{{.}}}){{/defaultValue}}{{^defaultValue}}{{name}}{{/defaultValue}}; + {{/requiredVars}} + } + {{/hasRequired}} + +{{#vendorExtensions.x-java-all-args-constructor}} + {{#vendorExtensions.x-java-all-args-constructor-vars}} + public Builder {{setter}}({{{datatypeWithEnum}}} {{name}}) { + this.{{name}} = {{name}}; + return this; + } + {{/vendorExtensions.x-java-all-args-constructor-vars}} +{{/vendorExtensions.x-java-all-args-constructor}} +{{^vendorExtensions.x-java-all-args-constructor}} + {{#allVars}} + public Builder {{setter}}({{{datatypeWithEnum}}} {{name}}) { + this.{{name}} = {{name}}; + return this; + } + {{/allVars}} +{{/vendorExtensions.x-java-all-args-constructor}} +{{#additionalPropertiesType}} + public Builder addProperty(String key, {{additionalPropertiesType}} value) { + additionalProperties.put(key, value); + return this; + } + + public Builder putAll(Map values) { + additionalProperties.putAll(values); + return this; + } +{{/additionalPropertiesType}} + + + public {{classname}} build() { +{{#vendorExtensions.x-java-all-args-constructor}} + {{classname}} inst = new {{classname}}({{#vendorExtensions.x-java-all-args-constructor-vars}}{{name}}{{^-last}}, {{/-last}}{{/vendorExtensions.x-java-all-args-constructor-vars}}); + {{#additionalPropertiesType}} + inst.putAll(additionalProperties); + {{/additionalPropertiesType}} + return inst; +{{/vendorExtensions.x-java-all-args-constructor}} +{{^vendorExtensions.x-java-all-args-constructor}} + {{classname}} inst = new {{classname}}({{#allVars}}{{name}}{{^-last}}, {{/-last}}{{/allVars}}); + {{#additionalPropertiesType}} + inst.putAll(additionalProperties); + {{/additionalPropertiesType}} + return inst; +{{/vendorExtensions.x-java-all-args-constructor}} + } + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + {{classname}} {{classVarName}} = ({{classname}}) o;{{#hasVars}} + return {{#parent}}super.equals(o) && {{/parent}}{{#vars}}Objects.equals(this.{{name}}, {{classVarName}}.{{name}}){{^-last}} && + {{/-last}}{{#-last}};{{/-last}}{{/vars}}{{/hasVars}}{{^hasVars}}{{#parent}}return super.equals(o);{{/parent}}{{^parent}}return true;{{/parent}}{{/hasVars}} + } + + @Override + public int hashCode() { + return {{^hasVars}}{{#parent}}super.hashCode(){{/parent}}{{^parent}}1{{/parent}}{{/hasVars}}{{#hasVars}}Objects.hash({{#vars}}{{#parent}}super.hashCode(), {{/parent}}{{name}}{{^-last}}, {{/-last}}{{/vars}}){{/hasVars}}; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class {{classname}} {\n"); + {{#parent}}sb.append(" ").append(toIndentedString(super.toString())).append("\n");{{/parent}} + {{#vars}}sb.append(" {{name}}: ").append({{#isPassword}}"*"{{/isPassword}}{{^isPassword}}toIndentedString({{name}}){{/isPassword}}).append("\n"); + {{/vars}}sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } +} \ No newline at end of file diff --git a/server-templates/queryParams.mustache b/server-templates/queryParams.mustache new file mode 100644 index 0000000000..3bcbe0368b --- /dev/null +++ b/server-templates/queryParams.mustache @@ -0,0 +1,18 @@ +{{! +Note that this template is copied from https://github.com/OpenAPITools/openapi-generator/blob/783e68c7acbbdcbb2282d167d1644b069f12d486/modules/openapi-generator/src/main/resources/JavaJaxRS/resteasy/queryParams.mustache +It is here to remove some unsupported swagger annotations +}}{{#isQueryParam}}{{! + + }}{{^isContainer}}{{! + }}{{#defaultValue}}{{! + }} @DefaultValue("{{{defaultValue}}}"){{! + }}{{/defaultValue}}{{! + }}{{/isContainer}}{{! + + }} @QueryParam("{{baseName}}"){{! + + }}{{#useBeanValidation}} {{>beanValidation}}{{/useBeanValidation}}{{! + + }} {{{dataType}}} {{paramName}}{{! + +}}{{/isQueryParam}} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000000..84aa3101b5 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,8 @@ +rootProject.name = 'polaris' + +include 'polaris-core' +include 'polaris-service' +include 'extension:persistence:hibernate' +include 'extension:persistence:eclipselink' + +project(":extension:persistence:eclipselink").name = "polaris-eclipselink" diff --git a/setup.sh b/setup.sh new file mode 100755 index 0000000000..5bd0e16b4b --- /dev/null +++ b/setup.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +CURRENT_DIR=$(pwd) + +# deploy the registry +echo "Building Kind Registry..." +sh ./kind-registry.sh + +# build and deploy the server image +echo "Building polaris image..." +docker build -t localhost:5001/polaris -f Dockerfile . +echo "Pushing polaris image..." +docker push localhost:5001/polaris +echo "Loading polaris image to kind..." +kind load docker-image localhost:5001/polaris:latest + +echo "Applying kubernetes manifests..." +kubectl delete -f k8/deployment.yaml --ignore-not-found +kubectl apply -f k8/deployment.yaml diff --git a/spec/polaris-management-service.yml b/spec/polaris-management-service.yml new file mode 100644 index 0000000000..6bb3a25c44 --- /dev/null +++ b/spec/polaris-management-service.yml @@ -0,0 +1,1327 @@ +openapi: 3.0.3 +info: + title: Polaris Management Service + version: 0.0.1 + description: + Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals +servers: + - url: "{scheme}://{host}/api/management/v1" + description: Server URL when the port can be inferred from the scheme + variables: + scheme: + description: The scheme of the URI, either http or https. + default: https + host: + description: The host address for the specified server + default: localhost +# All routes are currently configured using an Authorization header. +security: + - OAuth2: [] + +paths: + /catalogs: + get: + operationId: listCatalogs + description: List all catalogs in this polaris service + responses: + 200: + description: List of catalogs in the polaris service + content: + application/json: + schema: + $ref: "#/components/schemas/Catalogs" + 403: + description: "The caller does not have permission to list catalog details" + post: + operationId: createCatalog + description: Add a new Catalog + requestBody: + description: The Catalog to create + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/CreateCatalogRequest" + responses: + 201: + description: "Successful response" + 403: + description: "The caller does not have permission to create a catalog" + 404: + description: "The catalog does not exist" + 409: + description: "A catalog with the specified name already exists" + + /catalogs/{catalogName}: + parameters: + - name: catalogName + in: path + description: The name of the catalog + required: true + schema: + type: string + minLength: 1 + maxLength: 256 + pattern: '^(?!\s*[s|S][y|Y][s|S][t|T][e|E][m|M]\$).*$' + get: + operationId: getCatalog + description: Get the details of a catalog + responses: + 200: + description: The catalog details + content: + application/json: + schema: + $ref: "#/components/schemas/Catalog" + 403: + description: "The caller does not have permission to read catalog details" + 404: + description: "The catalog does not exist" + + put: + operationId: updateCatalog + description: Update an existing catalog + requestBody: + description: The catalog details to use in the update + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/UpdateCatalogRequest" + responses: + 200: + description: The catalog details + content: + application/json: + schema: + $ref: "#/components/schemas/Catalog" + 403: + description: "The caller does not have permission to update catalog details" + 404: + description: "The catalog does not exist" + 409: + description: "The entity version doesn't match the currentEntityVersion; retry after fetching latest version" + + delete: + operationId: deleteCatalog + description: Delete an existing catalog. This is a cascading operation that deletes all metadata, including principals, + roles and grants. If the catalog is an internal catalog, all tables and namespaces are dropped without purge. + responses: + 204: + description: "Success, no content" + 403: + description: "The caller does not have permission to delete a catalog" + 404: + description: "The catalog does not exist" + + /principals: + get: + operationId: listPrincipals + description: List the principals for the current catalog + responses: + 200: + description: List of principals for this catalog + content: + application/json: + schema: + $ref: "#/components/schemas/Principals" + 403: + description: "The caller does not have permission to list catalog admins" + 404: + description: "The catalog does not exist" + + post: + operationId: createPrincipal + description: Create a principal + requestBody: + description: The principal to create + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/CreatePrincipalRequest" + responses: + 201: + description: "Successful response" + content: + application/json: + schema: + $ref: "#/components/schemas/PrincipalWithCredentials" + 403: + description: "The caller does not have permission to add a principal" + + /principals/{principalName}: + parameters: + - name: principalName + in: path + description: The principal name + required: true + schema: + type: string + minLength: 1 + maxLength: 256 + pattern: '^(?!\s*[s|S][y|Y][s|S][t|T][e|E][m|M]\$).*$' + get: + operationId: getPrincipal + description: Get the principal details + responses: + 200: + description: The requested principal + content: + application/json: + schema: + $ref: "#/components/schemas/Principal" + 403: + description: "The caller does not have permission to get principal details" + 404: + description: "The catalog or principal does not exist" + + put: + operationId: updatePrincipal + description: Update an existing principal + requestBody: + description: The principal details to use in the update + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/UpdatePrincipalRequest" + responses: + 200: + description: The updated principal + content: + application/json: + schema: + $ref: "#/components/schemas/Principal" + 403: + description: "The caller does not have permission to update principal details" + 404: + description: "The principal does not exist" + 409: + description: "The entity version doesn't match the currentEntityVersion; retry after fetching latest version" + + delete: + operationId: deletePrincipal + description: Remove a principal from polaris + responses: + 204: + description: "Success, no content" + 403: + description: "The caller does not have permission to delete a principal" + 404: + description: "The principal does not exist" + + /principals/{principalName}/rotate: + parameters: + - name: principalName + in: path + description: The user name + required: true + schema: + type: string + minLength: 1 + maxLength: 256 + pattern: '^(?!\s*[s|S][y|Y][s|S][t|T][e|E][m|M]\$).*$' + post: + operationId: rotateCredentials + description: Rotate a principal's credentials. The new credentials will be returned in the response. This is the only + API, aside from createPrincipal, that returns the user's credentials. This API is *not* idempotent. + responses: + 200: + description: The principal details along with the newly rotated credentials + content: + application/json: + schema: + $ref: "#/components/schemas/PrincipalWithCredentials" + 403: + description: "The caller does not have permission to rotate credentials" + 404: + description: "The principal does not exist" + + /principals/{principalName}/principal-roles: + parameters: + - name: principalName + in: path + description: The name of the target principal + required: true + schema: + type: string + minLength: 1 + maxLength: 256 + pattern: '^(?!\s*[s|S][y|Y][s|S][t|T][e|E][m|M]\$).*$' + get: + operationId: listPrincipalRolesAssigned + description: List the roles assigned to the principal + responses: + 200: + description: List of roles assigned to this principal + content: + application/json: + schema: + $ref: "#/components/schemas/PrincipalRoles" + 403: + description: "The caller does not have permission to list roles" + 404: + description: "The principal or catalog does not exist" + + put: + operationId: assignPrincipalRole + description: Add a role to the principal + requestBody: + description: The principal role to assign + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/GrantPrincipalRoleRequest" + responses: + 201: + description: "Successful response" + 403: + description: "The caller does not have permission to add assign a role to the principal" + 404: + description: "The catalog, the principal, or the role does not exist" + + /principals/{principalName}/principal-roles/{principalRoleName}: + parameters: + - name: principalName + in: path + description: The name of the target principal + required: true + schema: + type: string + minLength: 1 + maxLength: 256 + pattern: '^(?!\s*[s|S][y|Y][s|S][t|T][e|E][m|M]\$).*$' + - name: principalRoleName + in: path + description: The name of the role + required: true + schema: + type: string + minLength: 1 + maxLength: 256 + pattern: '^(?!\s*[s|S][y|Y][s|S][t|T][e|E][m|M]\$).*$' + delete: + operationId: revokePrincipalRole + description: Remove a role from a catalog principal + responses: + 204: + description: "Success, no content" + 403: + description: "The caller does not have permission to remove a role from the principal" + 404: + description: "The catalog or principal does not exist" + + /principal-roles: + get: + operationId: listPrincipalRoles + description: List the principal roles + responses: + 200: + description: List of principal roles + content: + application/json: + schema: + $ref: "#/components/schemas/PrincipalRoles" + 403: + description: "The caller does not have permission to list principal roles" + 404: + description: "The catalog does not exist" + + post: + operationId: createPrincipalRole + description: Create a principal role + requestBody: + description: The principal to create + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/CreatePrincipalRoleRequest" + responses: + 201: + description: "Successful response" + 403: + description: "The caller does not have permission to add a principal role" + + /principal-roles/{principalRoleName}: + parameters: + - name: principalRoleName + in: path + description: The principal role name + required: true + schema: + type: string + minLength: 1 + maxLength: 256 + pattern: '^(?!\s*[s|S][y|Y][s|S][t|T][e|E][m|M]\$).*$' + get: + operationId: getPrincipalRole + description: Get the principal role details + responses: + 200: + description: The requested principal role + content: + application/json: + schema: + $ref: "#/components/schemas/PrincipalRole" + 403: + description: "The caller does not have permission to get principal role details" + 404: + description: "The principal role does not exist" + + put: + operationId: updatePrincipalRole + description: Update an existing principalRole + requestBody: + description: The principalRole details to use in the update + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/UpdatePrincipalRoleRequest" + responses: + 200: + description: The updated principal role + content: + application/json: + schema: + $ref: "#/components/schemas/PrincipalRole" + 403: + description: "The caller does not have permission to update principal role details" + 404: + description: "The principal role does not exist" + 409: + description: "The entity version doesn't match the currentEntityVersion; retry after fetching latest version" + + delete: + operationId: deletePrincipalRole + description: Remove a principal role from polaris + responses: + 204: + description: "Success, no content" + 403: + description: "The caller does not have permission to delete a principal role" + 404: + description: "The principal role does not exist" + + /principal-roles/{principalRoleName}/principals: + parameters: + - name: principalRoleName + in: path + description: The principal role name + required: true + schema: + type: string + minLength: 1 + maxLength: 256 + pattern: '^(?!\s*[s|S][y|Y][s|S][t|T][e|E][m|M]\$).*$' + get: + operationId: listAssigneePrincipalsForPrincipalRole + description: List the Principals to whom the target principal role has been assigned + responses: + 200: + description: List the Principals to whom the target principal role has been assigned + content: + application/json: + schema: + $ref: "#/components/schemas/Principals" + 403: + description: "The caller does not have permission to list principals" + 404: + description: "The principal role does not exist" + + /principal-roles/{principalRoleName}/catalog-roles/{catalogName}: + parameters: + - name: principalRoleName + in: path + description: The principal role name + required: true + schema: + type: string + minLength: 1 + maxLength: 256 + pattern: '^(?!\s*[s|S][y|Y][s|S][t|T][e|E][m|M]\$).*$' + - name: catalogName + in: path + required: true + description: The name of the catalog where the catalogRoles reside + schema: + type: string + minLength: 1 + maxLength: 256 + pattern: '^(?!\s*[s|S][y|Y][s|S][t|T][e|E][m|M]\$).*$' + get: + operationId: listCatalogRolesForPrincipalRole + description: Get the catalog roles mapped to the principal role + responses: + 200: + description: The list of catalog roles mapped to the principal role + content: + application/json: + schema: + $ref: "#/components/schemas/CatalogRoles" + 403: + description: "The caller does not have permission to list catalog roles" + 404: + description: "The principal role does not exist" + + put: + operationId: assignCatalogRoleToPrincipalRole + description: Assign a catalog role to a principal role + requestBody: + description: The principal to create + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/GrantCatalogRoleRequest" + responses: + 201: + description: "Successful response" + 403: + description: "The caller does not have permission to assign a catalog role" + + /principal-roles/{principalRoleName}/catalog-roles/{catalogName}/{catalogRoleName}: + parameters: + - name: principalRoleName + in: path + description: The principal role name + required: true + schema: + type: string + minLength: 1 + maxLength: 256 + pattern: '^(?!\s*[s|S][y|Y][s|S][t|T][e|E][m|M]\$).*$' + - name: catalogName + in: path + description: The name of the catalog that contains the role to revoke + required: true + schema: + type: string + minLength: 1 + maxLength: 256 + pattern: '^(?!\s*[s|S][y|Y][s|S][t|T][e|E][m|M]\$).*$' + - name: catalogRoleName + in: path + description: The name of the catalog role that should be revoked + required: true + schema: + type: string + minLength: 1 + maxLength: 256 + pattern: '^(?!\s*[s|S][y|Y][s|S][t|T][e|E][m|M]\$).*$' + delete: + operationId: revokeCatalogRoleFromPrincipalRole + description: Remove a catalog role from a principal role + responses: + 204: + description: "Success, no content" + 403: + description: "The caller does not have permission to revoke a catalog role" + 404: + description: "The principal role does not exist" + + /catalogs/{catalogName}/catalog-roles: + parameters: + - name: catalogName + in: path + description: The catalog for which we are reading/updating roles + required: true + schema: + type: string + minLength: 1 + maxLength: 256 + pattern: '^(?!\s*[s|S][y|Y][s|S][t|T][e|E][m|M]\$).*$' + get: + operationId: listCatalogRoles + description: List existing roles in the catalog + responses: + 200: + description: The list of roles that exist in this catalog + content: + application/json: + schema: + $ref: "#/components/schemas/CatalogRoles" + post: + operationId: createCatalogRole + description: Create a new role in the catalog + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/CreateCatalogRoleRequest" + responses: + 201: + description: "Successful response" + 403: + description: "The principal is not authorized to create roles" + 404: + description: "The catalog does not exist" + + /catalogs/{catalogName}/catalog-roles/{catalogRoleName}: + parameters: + - name: catalogName + in: path + description: The catalog for which we are retrieving roles + required: true + schema: + type: string + minLength: 1 + maxLength: 256 + pattern: '^(?!\s*[s|S][y|Y][s|S][t|T][e|E][m|M]\$).*$' + - name: catalogRoleName + in: path + description: The name of the role + required: true + schema: + type: string + minLength: 1 + maxLength: 256 + pattern: '^(?!\s*[s|S][y|Y][s|S][t|T][e|E][m|M]\$).*$' + get: + operationId: getCatalogRole + description: Get the details of an existing role + responses: + 200: + description: The specified role details + content: + application/json: + schema: + $ref: "#/components/schemas/CatalogRole" + 403: + description: "The principal is not authorized to read role data" + 404: + description: "The catalog or the role does not exist" + + put: + operationId: updateCatalogRole + description: Update an existing role in the catalog + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/UpdateCatalogRoleRequest" + responses: + 200: + description: The specified role details + content: + application/json: + schema: + $ref: "#/components/schemas/CatalogRole" + 403: + description: "The principal is not authorized to update roles" + 404: + description: "The catalog or the role does not exist" + 409: + description: "The entity version doesn't match the currentEntityVersion; retry after fetching latest version" + + delete: + operationId: deleteCatalogRole + description: Delete an existing role from the catalog. All associated grants will also be deleted + responses: + 204: + description: "Success, no content" + 403: + description: "The principal is not authorized to delete roles" + 404: + description: "The catalog or the role does not exist" + + /catalogs/{catalogName}/catalog-roles/{catalogRoleName}/principal-roles: + parameters: + - name: catalogName + in: path + required: true + description: The name of the catalog where the catalog role resides + schema: + type: string + minLength: 1 + maxLength: 256 + pattern: '^(?!\s*[s|S][y|Y][s|S][t|T][e|E][m|M]\$).*$' + - name: catalogRoleName + in: path + required: true + description: The name of the catalog role + schema: + type: string + minLength: 1 + maxLength: 256 + pattern: '^(?!\s*[s|S][y|Y][s|S][t|T][e|E][m|M]\$).*$' + get: + operationId: listAssigneePrincipalRolesForCatalogRole + description: List the PrincipalRoles to which the target catalog role has been assigned + responses: + 200: + description: List the PrincipalRoles to which the target catalog role has been assigned + content: + application/json: + schema: + $ref: "#/components/schemas/PrincipalRoles" + 403: + description: "The caller does not have permission to list principal roles" + 404: + description: "The catalog or catalog role does not exist" + + /catalogs/{catalogName}/catalog-roles/{catalogRoleName}/grants: + parameters: + - name: catalogName + in: path + required: true + description: The name of the catalog where the role will receive the grant + schema: + type: string + minLength: 1 + maxLength: 256 + pattern: '^(?!\s*[s|S][y|Y][s|S][t|T][e|E][m|M]\$).*$' + - name: catalogRoleName + in: path + required: true + description: The name of the role receiving the grant (must exist) + schema: + type: string + minLength: 1 + maxLength: 256 + pattern: '^(?!\s*[s|S][y|Y][s|S][t|T][e|E][m|M]\$).*$' + get: + operationId: listGrantsForCatalogRole + description: List the grants the catalog role holds + responses: + 200: + description: List of all grants given to the role in this catalog + content: + application/json: + schema: + $ref: "#/components/schemas/GrantResources" + put: + operationId: addGrantToCatalogRole + description: Add a new grant to the catalog role + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/AddGrantRequest" + responses: + 201: + description: "Successful response" + 403: + description: "The principal is not authorized to create grants" + 404: + description: "The catalog or the role does not exist" + post: + operationId: revokeGrantFromCatalogRole + description: + Delete a specific grant from the role. This may be a subset or a superset of the grants the role has. In case of + a subset, the role will retain the grants not specified. If the `cascade` parameter is true, grant revocation + will have a cascading effect - that is, if a principal has specific grants on a subresource, and grants are revoked + on a parent resource, the grants present on the subresource will be revoked as well. By default, this behavior + is disabled and grant revocation only affects the specified resource. + parameters: + - name: cascade + in: query + schema: + type: boolean + default: false + description: If true, the grant revocation cascades to all subresources. + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/RevokeGrantRequest" + responses: + 201: + description: "Successful response" + 403: + description: "The principal is not authorized to create grants" + 404: + description: "The catalog or the role does not exist" + +components: + securitySchemes: + OAuth2: + type: oauth2 + description: Uses OAuth 2 with client credentials flow + flows: + implicit: + authorizationUrl: "{scheme}://{host}/api/v1/oauth/tokens" + scopes: {} + + schemas: + Catalogs: + type: object + description: A list of Catalog objects + properties: + catalogs: + type: array + items: + $ref: "#/components/schemas/Catalog" + required: + - catalogs + + CreateCatalogRequest: + type: object + description: Request to create a new catalog + properties: + catalog: + $ref: "#/components/schemas/Catalog" + required: + - catalog + + Catalog: + type: object + description: A catalog object. A catalog may be internal or external. Internal catalogs are managed entirely by + an external catalog interface. Third party catalogs may be other Iceberg REST implementations or other services + with their own proprietary APIs + properties: + type: + type: string + enum: + - INTERNAL + - EXTERNAL + description: the type of catalog - internal or external + default: INTERNAL + name: + type: string + minLength: 1 + maxLength: 256 + pattern: '^(?!\s*[s|S][y|Y][s|S][t|T][e|E][m|M]\$).*$' + description: The name of the catalog + properties: + type: object + properties: + default-base-location: + type: string + additionalProperties: + type: string + required: + - default-base-location + createTimestamp: + type: integer + format: "int64" + description: The creation time represented as unix epoch timestamp in milliseconds + lastUpdateTimestamp: + type: integer + format: "int64" + description: The last update time represented as unix epoch timestamp in milliseconds + entityVersion: + type: integer + description: The version of the catalog object used to determine if the catalog metadata has changed + storageConfigInfo: + $ref: "#/components/schemas/StorageConfigInfo" + required: + - name + - type + - storageConfigInfo + - properties + discriminator: + propertyName: type + mapping: + INTERNAL: "#/components/schemas/PolarisCatalog" + EXTERNAL: "#/components/schemas/ExternalCatalog" + + + PolarisCatalog: + type: object + allOf: + - $ref: "#/components/schemas/Catalog" + description: The base catalog type - this contains all the fields necessary to construct an INTERNAL catalog + + ExternalCatalog: + description: An externally managed catalog + type: object + allOf: + - $ref: "#/components/schemas/Catalog" + - type: object + properties: + remoteUrl: + type: string + description: URL to the remote catalog API + + StorageConfigInfo: + type: object + description: A storage configuration used by catalogs + properties: + storageType: + type: string + enum: + - S3 + - GCS + - AZURE + - FILE + description: The cloud provider type this storage is built on. FILE is supported for testing purposes only + allowedLocations: + type: array + items: + type: string + example: "For AWS [s3://bucketname/prefix/], for AZURE [abfss://container@storageaccount.blob.core.windows.net/prefix/], for GCP [gs://bucketname/prefix/]" + required: + - storageType + discriminator: + propertyName: storageType + mapping: + S3: "#/components/schemas/AwsStorageConfigInfo" + AZURE: "#/components/schemas/AzureStorageConfigInfo" + GCS: "#/components/schemas/GcpStorageConfigInfo" + FILE: "#/components/schemas/FileStorageConfigInfo" + + AwsStorageConfigInfo: + type: object + description: aws storage configuration info + allOf: + - $ref: '#/components/schemas/StorageConfigInfo' + properties: + roleArn: + type: string + description: the aws role arn that grants privileges on the S3 buckets + example: "arn:aws:iam::123456789001:principal/abc1-b-self1234" + externalId: + type: string + description: an optional external id used to establish a trust relationship with AWS in the trust policy + userArn: + type: string + description: the aws user arn used to assume the aws role + example: "arn:aws:iam::123456789001:user/abc1-b-self1234" + required: + - roleArn + + AzureStorageConfigInfo: + type: object + description: azure storage configuration info + allOf: + - $ref: '#/components/schemas/StorageConfigInfo' + properties: + tenantId: + type: string + description: the tenant id that the storage accounts belong to + multiTenantAppName: + type: string + description: the name of the azure client application + consentUrl: + type: string + description: URL to the Azure permissions request page + required: + - tenantId + + GcpStorageConfigInfo: + type: object + description: gcp storage configuration info + allOf: + - $ref: '#/components/schemas/StorageConfigInfo' + properties: + gcsServiceAccount: + type: string + description: a Google cloud storage service account + + FileStorageConfigInfo: + type: object + description: gcp storage configuration info + allOf: + - $ref: '#/components/schemas/StorageConfigInfo' + + UpdateCatalogRequest: + description: Updates to apply to a Catalog + type: object + properties: + currentEntityVersion: + type: integer + description: The version of the object onto which this update is applied; if the object changed, the update will fail and the caller should retry after fetching the latest version. + properties: + type: object + additionalProperties: + type: string + storageConfigInfo: + $ref: "#/components/schemas/StorageConfigInfo" + + Principals: + description: A list of Principals + type: object + properties: + principals: + type: array + items: + $ref: "#/components/schemas/Principal" + required: + - principals + + PrincipalWithCredentials: + description: A user with its client id and secret. This type is returned when a new principal is created or when its + credentials are rotated + type: object + properties: + principal: + $ref: "#/components/schemas/Principal" + credentials: + type: object + properties: + clientId: + type: string + clientSecret: + type: string + required: + - principal + - credentials + + CreatePrincipalRequest: + type: object + properties: + principal: + $ref: '#/components/schemas/Principal' + credentialRotationRequired: + type: boolean + description: If true, the initial credentials can only be used to call rotateCredentials + + Principal: + description: A Polaris principal. + type: object + properties: + name: + type: string + minLength: 1 + maxLength: 256 + pattern: '^(?!\s*[s|S][y|Y][s|S][t|T][e|E][m|M]\$).*$' + clientId: + type: string + description: The output-only OAuth clientId associated with this principal if applicable + properties: + type: object + additionalProperties: + type: string + createTimestamp: + type: integer + format: "int64" + lastUpdateTimestamp: + type: integer + format: "int64" + entityVersion: + type: integer + description: The version of the principal object used to determine if the principal metadata has changed + required: + - name + + UpdatePrincipalRequest: + description: Updates to apply to a Principal + type: object + properties: + currentEntityVersion: + type: integer + description: The version of the object onto which this update is applied; if the object changed, the update will fail and the caller should retry after fetching the latest version. + properties: + type: object + additionalProperties: + type: string + required: + - currentEntityVersion + - properties + + PrincipalRoles: + type: object + properties: + roles: + type: array + items: + $ref: "#/components/schemas/PrincipalRole" + required: + - roles + + GrantPrincipalRoleRequest: + type: object + properties: + principalRole: + $ref: '#/components/schemas/PrincipalRole' + + CreatePrincipalRoleRequest: + type: object + properties: + principalRole: + $ref: '#/components/schemas/PrincipalRole' + + PrincipalRole: + type: object + properties: + name: + type: string + minLength: 1 + maxLength: 256 + pattern: '^(?!\s*[s|S][y|Y][s|S][t|T][e|E][m|M]\$).*$' + description: The name of the role + properties: + type: object + additionalProperties: + type: string + createTimestamp: + type: integer + format: "int64" + lastUpdateTimestamp: + type: integer + format: "int64" + entityVersion: + type: integer + description: The version of the principal role object used to determine if the principal role metadata has changed + required: + - name + + UpdatePrincipalRoleRequest: + description: Updates to apply to a Principal Role + type: object + properties: + currentEntityVersion: + type: integer + description: The version of the object onto which this update is applied; if the object changed, the update will fail and the caller should retry after fetching the latest version. + properties: + type: object + additionalProperties: + type: string + required: + - currentEntityVersion + - properties + + CatalogRoles: + type: object + properties: + roles: + type: array + items: + $ref: "#/components/schemas/CatalogRole" + description: The list of catalog roles + required: + - roles + + GrantCatalogRoleRequest: + type: object + properties: + catalogRole: + $ref: '#/components/schemas/CatalogRole' + + CreateCatalogRoleRequest: + type: object + properties: + catalogRole: + $ref: '#/components/schemas/CatalogRole' + + CatalogRole: + type: object + properties: + name: + type: string + minLength: 1 + maxLength: 256 + pattern: '^(?!\s*[s|S][y|Y][s|S][t|T][e|E][m|M]\$).*$' + description: The name of the role + properties: + type: object + additionalProperties: + type: string + createTimestamp: + type: integer + format: "int64" + lastUpdateTimestamp: + type: integer + format: "int64" + entityVersion: + type: integer + description: The version of the catalog role object used to determine if the catalog role metadata has changed + required: + - name + + UpdateCatalogRoleRequest: + description: Updates to apply to a Catalog Role + type: object + properties: + currentEntityVersion: + type: integer + description: The version of the object onto which this update is applied; if the object changed, the update will fail and the caller should retry after fetching the latest version. + properties: + type: object + additionalProperties: + type: string + required: + - currentEntityVersion + - properties + + ViewPrivilege: + type: string + enum: + - CATALOG_MANAGE_ACCESS + - VIEW_CREATE + - VIEW_DROP + - VIEW_LIST + - VIEW_READ_PROPERTIES + - VIEW_WRITE_PROPERTIES + - VIEW_FULL_METADATA + + TablePrivilege: + type: string + enum: + - CATALOG_MANAGE_ACCESS + - TABLE_DROP + - TABLE_LIST + - TABLE_READ_PROPERTIES + - VIEW_READ_PROPERTIES + - TABLE_WRITE_PROPERTIES + - TABLE_READ_DATA + - TABLE_WRITE_DATA + - TABLE_FULL_METADATA + + NamespacePrivilege: + type: string + enum: + - CATALOG_MANAGE_ACCESS + - CATALOG_MANAGE_CONTENT + - CATALOG_MANAGE_METADATA + - NAMESPACE_CREATE + - TABLE_CREATE + - VIEW_CREATE + - NAMESPACE_DROP + - TABLE_DROP + - VIEW_DROP + - NAMESPACE_LIST + - TABLE_LIST + - VIEW_LIST + - NAMESPACE_READ_PROPERTIES + - TABLE_READ_PROPERTIES + - VIEW_READ_PROPERTIES + - NAMESPACE_WRITE_PROPERTIES + - TABLE_WRITE_PROPERTIES + - VIEW_WRITE_PROPERTIES + - TABLE_READ_DATA + - TABLE_WRITE_DATA + - NAMESPACE_FULL_METADATA + - TABLE_FULL_METADATA + - VIEW_FULL_METADATA + + CatalogPrivilege: + type: string + enum: + - CATALOG_MANAGE_ACCESS + - CATALOG_MANAGE_CONTENT + - CATALOG_MANAGE_METADATA + - CATALOG_READ_PROPERTIES + - CATALOG_WRITE_PROPERTIES + - NAMESPACE_CREATE + - TABLE_CREATE + - VIEW_CREATE + - NAMESPACE_DROP + - TABLE_DROP + - VIEW_DROP + - NAMESPACE_LIST + - TABLE_LIST + - VIEW_LIST + - NAMESPACE_READ_PROPERTIES + - TABLE_READ_PROPERTIES + - VIEW_READ_PROPERTIES + - NAMESPACE_WRITE_PROPERTIES + - TABLE_WRITE_PROPERTIES + - VIEW_WRITE_PROPERTIES + - TABLE_READ_DATA + - TABLE_WRITE_DATA + - NAMESPACE_FULL_METADATA + - TABLE_FULL_METADATA + - VIEW_FULL_METADATA + + AddGrantRequest: + type: object + properties: + grant: + $ref: '#/components/schemas/GrantResource' + + RevokeGrantRequest: + type: object + properties: + grant: + $ref: '#/components/schemas/GrantResource' + + ViewGrant: + allOf: + - $ref: '#/components/schemas/GrantResource' + - type: object + properties: + namespace: + type: array + items: + type: string + viewName: + type: string + minLength: 1 + maxLength: 256 + privilege: + $ref: '#/components/schemas/ViewPrivilege' + required: + - namespace + - viewName + - privilege + + TableGrant: + allOf: + - $ref: '#/components/schemas/GrantResource' + - type: object + properties: + namespace: + type: array + items: + type: string + tableName: + type: string + minLength: 1 + maxLength: 256 + privilege: + $ref: '#/components/schemas/TablePrivilege' + required: + - namespace + - tableName + - privilege + + NamespaceGrant: + allOf: + - $ref: '#/components/schemas/GrantResource' + - type: object + properties: + namespace: + type: array + items: + type: string + privilege: + $ref: '#/components/schemas/NamespacePrivilege' + required: + - namespace + - privilege + + + CatalogGrant: + allOf: + - $ref: '#/components/schemas/GrantResource' + - type: object + properties: + privilege: + $ref: '#/components/schemas/CatalogPrivilege' + required: + - privilege + + GrantResource: + type: object + discriminator: + propertyName: type + mapping: + catalog: '#/components/schemas/CatalogGrant' + namespace: '#/components/schemas/NamespaceGrant' + table: '#/components/schemas/TableGrant' + view: '#/components/schemas/ViewGrant' + properties: + type: + type: string + enum: + - catalog + - namespace + - table + - view + required: + - type + + GrantResources: + type: object + properties: + grants: + type: array + items: + $ref: "#/components/schemas/GrantResource" + required: + - grants diff --git a/spec/rest-catalog-open-api.yaml b/spec/rest-catalog-open-api.yaml new file mode 100644 index 0000000000..1b7ec745a2 --- /dev/null +++ b/spec/rest-catalog-open-api.yaml @@ -0,0 +1,4148 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# + +--- +openapi: 3.0.3 +info: + title: Apache Iceberg REST Catalog API + license: + name: Apache 2.0 + url: https://www.apache.org/licenses/LICENSE-2.0.html + version: 0.0.1 + description: + Defines the specification for the first version of the REST Catalog API. + Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. +servers: + - url: "{scheme}://{host}/{basePath}" + description: Server URL when the port can be inferred from the scheme + variables: + scheme: + description: The scheme of the URI, either http or https. + default: https + host: + description: The host address for the specified server + default: localhost + basePath: + description: Optional prefix to be appended to all routes + default: "" + - url: "{scheme}://{host}:{port}/{basePath}" + description: Generic base server URL, with all parts configurable + variables: + scheme: + description: The scheme of the URI, either http or https. + default: https + host: + description: The host address for the specified server + default: localhost + port: + description: The port used when addressing the host + default: "443" + basePath: + description: Optional prefix to be appended to all routes + default: "" +# All routes are currently configured using an Authorization header. +security: + - OAuth2: [catalog] + - BearerAuth: [] + +paths: + /v1/config: + + get: + tags: + - Configuration API + summary: List all catalog configuration settings + operationId: getConfig + parameters: + - name: warehouse + in: query + required: false + schema: + type: string + description: Warehouse location or identifier to request from the service + description: + " + All REST clients should first call this route to get catalog configuration + properties from the server to configure the catalog and its HTTP client. + Configuration from the server consists of two sets of key/value pairs. + + - defaults - properties that should be used as default configuration; applied before client configuration + + - overrides - properties that should be used to override client configuration; applied after defaults and client configuration + + + Catalog configuration is constructed by setting the defaults, then client- + provided configuration, and finally overrides. The final property set is then + used to configure the catalog. + + + For example, a default configuration property might set the size of the + client pool, which can be replaced with a client-specific setting. An + override might be used to set the warehouse location, which is stored + on the server rather than in client configuration. + + + Common catalog configuration settings are documented at + https://iceberg.apache.org/docs/latest/configuration/#catalog-properties + " + responses: + 200: + description: Server specified configuration values. + content: + application/json: + schema: + $ref: '#/components/schemas/CatalogConfig' + example: { + "overrides": { + "warehouse": "s3://bucket/warehouse/" + }, + "defaults": { + "clients": "4" + } + } + 400: + $ref: '#/components/responses/BadRequestErrorResponse' + 401: + $ref: '#/components/responses/UnauthorizedResponse' + 403: + $ref: '#/components/responses/ForbiddenResponse' + 419: + $ref: '#/components/responses/AuthenticationTimeoutResponse' + 503: + $ref: '#/components/responses/ServiceUnavailableResponse' + 5XX: + $ref: '#/components/responses/ServerErrorResponse' + + /v1/oauth/tokens: + + post: + tags: + - OAuth2 API + summary: Get a token using an OAuth2 flow + operationId: getToken + description: + Exchange credentials for a token using the OAuth2 client credentials flow or token exchange. + + + This endpoint is used for three purposes - + + 1. To exchange client credentials (client ID and secret) for an access token + This uses the client credentials flow. + + 2. To exchange a client token and an identity token for a more specific access token + This uses the token exchange flow. + + 3. To exchange an access token for one with the same claims and a refreshed expiration period + This uses the token exchange flow. + + + For example, a catalog client may be configured with client credentials from the OAuth2 + Authorization flow. This client would exchange its client ID and secret for an access token + using the client credentials request with this endpoint (1). Subsequent requests would then + use that access token. + + + Some clients may also handle sessions that have additional user context. These clients would + use the token exchange flow to exchange a user token (the "subject" token) from the session + for a more specific access token for that user, using the catalog's access token as the + "actor" token (2). The user ID token is the "subject" token and can be any token type + allowed by the OAuth2 token exchange flow, including a unsecured JWT token with a sub claim. + This request should use the catalog's bearer token in the "Authorization" header. + + + Clients may also use the token exchange flow to refresh a token that is about to expire by + sending a token exchange request (3). The request's "subject" token should be the expiring + token. This request should use the subject token in the "Authorization" header. + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/OAuthTokenRequest' + responses: + 200: + $ref: '#/components/responses/OAuthTokenResponse' + 400: + $ref: '#/components/responses/OAuthErrorResponse' + 401: + $ref: '#/components/responses/OAuthErrorResponse' + 5XX: + $ref: '#/components/responses/OAuthErrorResponse' + security: + - BearerAuth: [] + + /v1/{prefix}/namespaces: + parameters: + - $ref: '#/components/parameters/prefix' + + get: + tags: + - Catalog API + summary: List namespaces, optionally providing a parent namespace to list underneath + description: + List all namespaces at a certain level, optionally starting from a given parent namespace. + If table accounting.tax.paid.info exists, using 'SELECT NAMESPACE IN accounting' would + translate into `GET /namespaces?parent=accounting` and must return a namespace, ["accounting", "tax"] only. + Using 'SELECT NAMESPACE IN accounting.tax' would + translate into `GET /namespaces?parent=accounting%1Ftax` and must return a namespace, ["accounting", "tax", "paid"]. + If `parent` is not provided, all top-level namespaces should be listed. + operationId: listNamespaces + parameters: + - $ref: '#/components/parameters/page-token' + - $ref: '#/components/parameters/page-size' + - name: parent + in: query + description: + An optional namespace, underneath which to list namespaces. + If not provided or empty, all top-level namespaces should be listed. + If parent is a multipart namespace, the parts must be separated by the unit separator (`0x1F`) byte. + required: false + allowEmptyValue: true + schema: + type: string + example: "accounting%1Ftax" + responses: + 200: + $ref: '#/components/responses/ListNamespacesResponse' + 400: + $ref: '#/components/responses/BadRequestErrorResponse' + 401: + $ref: '#/components/responses/UnauthorizedResponse' + 403: + $ref: '#/components/responses/ForbiddenResponse' + 404: + description: Not Found - Namespace provided in the `parent` query parameter is not found. + content: + application/json: + schema: + $ref: '#/components/schemas/IcebergErrorResponse' + examples: + NoSuchNamespaceExample: + $ref: '#/components/examples/NoSuchNamespaceError' + 419: + $ref: '#/components/responses/AuthenticationTimeoutResponse' + 503: + $ref: '#/components/responses/ServiceUnavailableResponse' + 5XX: + $ref: '#/components/responses/ServerErrorResponse' + + post: + tags: + - Catalog API + summary: Create a namespace + description: + Create a namespace, with an optional set of properties. + The server might also add properties, such as `last_modified_time` etc. + operationId: createNamespace + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateNamespaceRequest' + responses: + 200: + $ref: '#/components/responses/CreateNamespaceResponse' + 400: + $ref: '#/components/responses/BadRequestErrorResponse' + 401: + $ref: '#/components/responses/UnauthorizedResponse' + 403: + $ref: '#/components/responses/ForbiddenResponse' + 406: + $ref: '#/components/responses/UnsupportedOperationResponse' + 409: + description: Conflict - The namespace already exists + content: + application/json: + schema: + $ref: '#/components/schemas/IcebergErrorResponse' + examples: + NamespaceAlreadyExists: + $ref: '#/components/examples/NamespaceAlreadyExistsError' + 419: + $ref: '#/components/responses/AuthenticationTimeoutResponse' + 503: + $ref: '#/components/responses/ServiceUnavailableResponse' + 5XX: + $ref: '#/components/responses/ServerErrorResponse' + + /v1/{prefix}/namespaces/{namespace}: + parameters: + - $ref: '#/components/parameters/prefix' + - $ref: '#/components/parameters/namespace' + + get: + tags: + - Catalog API + summary: Load the metadata properties for a namespace + operationId: loadNamespaceMetadata + description: Return all stored metadata properties for a given namespace + responses: + 200: + $ref: '#/components/responses/GetNamespaceResponse' + 400: + $ref: '#/components/responses/BadRequestErrorResponse' + 401: + $ref: '#/components/responses/UnauthorizedResponse' + 403: + $ref: '#/components/responses/ForbiddenResponse' + 404: + description: Not Found - Namespace not found + content: + application/json: + schema: + $ref: '#/components/schemas/IcebergErrorResponse' + examples: + NoSuchNamespaceExample: + $ref: '#/components/examples/NoSuchNamespaceError' + 419: + $ref: '#/components/responses/AuthenticationTimeoutResponse' + 503: + $ref: '#/components/responses/ServiceUnavailableResponse' + 5XX: + $ref: '#/components/responses/ServerErrorResponse' + + head: + tags: + - Catalog API + summary: Check if a namespace exists + operationId: namespaceExists + description: + Check if a namespace exists. The response does not contain a body. + responses: + 204: + description: Success, no content + 400: + $ref: '#/components/responses/BadRequestErrorResponse' + 401: + $ref: '#/components/responses/UnauthorizedResponse' + 403: + $ref: '#/components/responses/ForbiddenResponse' + 404: + description: Not Found - Namespace not found + content: + application/json: + schema: + $ref: '#/components/schemas/IcebergErrorResponse' + examples: + NoSuchNamespaceExample: + $ref: '#/components/examples/NoSuchNamespaceError' + 419: + $ref: '#/components/responses/AuthenticationTimeoutResponse' + 503: + $ref: '#/components/responses/ServiceUnavailableResponse' + 5XX: + $ref: '#/components/responses/ServerErrorResponse' + + delete: + tags: + - Catalog API + summary: Drop a namespace from the catalog. Namespace must be empty. + operationId: dropNamespace + responses: + 204: + description: Success, no content + 400: + $ref: '#/components/responses/BadRequestErrorResponse' + 401: + $ref: '#/components/responses/UnauthorizedResponse' + 403: + $ref: '#/components/responses/ForbiddenResponse' + 404: + description: Not Found - Namespace to delete does not exist. + content: + application/json: + schema: + $ref: '#/components/schemas/IcebergErrorResponse' + examples: + NoSuchNamespaceExample: + $ref: '#/components/examples/NoSuchNamespaceError' + 419: + $ref: '#/components/responses/AuthenticationTimeoutResponse' + 503: + $ref: '#/components/responses/ServiceUnavailableResponse' + 5XX: + $ref: '#/components/responses/ServerErrorResponse' + + /v1/{prefix}/namespaces/{namespace}/properties: + parameters: + - $ref: '#/components/parameters/prefix' + - $ref: '#/components/parameters/namespace' + + post: + tags: + - Catalog API + summary: Set or remove properties on a namespace + operationId: updateProperties + description: + Set and/or remove properties on a namespace. + The request body specifies a list of properties to remove and a map + of key value pairs to update. + + Properties that are not in the request are not modified or removed by this call. + + Server implementations are not required to support namespace properties. + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateNamespacePropertiesRequest' + examples: + UpdateAndRemoveProperties: + $ref: '#/components/examples/UpdateAndRemoveNamespacePropertiesRequest' + responses: + 200: + $ref: '#/components/responses/UpdateNamespacePropertiesResponse' + 400: + $ref: '#/components/responses/BadRequestErrorResponse' + 401: + $ref: '#/components/responses/UnauthorizedResponse' + 403: + $ref: '#/components/responses/ForbiddenResponse' + 404: + description: Not Found - Namespace not found + content: + application/json: + schema: + $ref: '#/components/schemas/IcebergErrorResponse' + examples: + NamespaceNotFound: + $ref: '#/components/examples/NoSuchNamespaceError' + 406: + $ref: '#/components/responses/UnsupportedOperationResponse' + 422: + description: Unprocessable Entity - A property key was included in both `removals` and `updates` + content: + application/json: + schema: + $ref: '#/components/schemas/IcebergErrorResponse' + examples: + UnprocessableEntityDuplicateKey: + $ref: '#/components/examples/UnprocessableEntityDuplicateKey' + 419: + $ref: '#/components/responses/AuthenticationTimeoutResponse' + 503: + $ref: '#/components/responses/ServiceUnavailableResponse' + 5XX: + $ref: '#/components/responses/ServerErrorResponse' + + /v1/{prefix}/namespaces/{namespace}/tables: + parameters: + - $ref: '#/components/parameters/prefix' + - $ref: '#/components/parameters/namespace' + + get: + tags: + - Catalog API + summary: List all table identifiers underneath a given namespace + description: Return all table identifiers under this namespace + operationId: listTables + parameters: + - $ref: '#/components/parameters/page-token' + - $ref: '#/components/parameters/page-size' + responses: + 200: + $ref: '#/components/responses/ListTablesResponse' + 400: + $ref: '#/components/responses/BadRequestErrorResponse' + 401: + $ref: '#/components/responses/UnauthorizedResponse' + 403: + $ref: '#/components/responses/ForbiddenResponse' + 404: + description: Not Found - The namespace specified does not exist + content: + application/json: + schema: + $ref: '#/components/schemas/IcebergErrorResponse' + examples: + NamespaceNotFound: + $ref: '#/components/examples/NoSuchNamespaceError' + 419: + $ref: '#/components/responses/AuthenticationTimeoutResponse' + 503: + $ref: '#/components/responses/ServiceUnavailableResponse' + 5XX: + $ref: '#/components/responses/ServerErrorResponse' + + post: + tags: + - Catalog API + summary: Create a table in the given namespace + description: + Create a table or start a create transaction, like atomic CTAS. + + + If `stage-create` is false, the table is created immediately. + + + If `stage-create` is true, the table is not created, but table metadata is initialized and returned. + The service should prepare as needed for a commit to the table commit endpoint to complete the create + transaction. The client uses the returned metadata to begin a transaction. To commit the transaction, + the client sends all create and subsequent changes to the table commit route. Changes from the table + create operation include changes like AddSchemaUpdate and SetCurrentSchemaUpdate that set the initial + table state. + operationId: createTable + parameters: + - $ref: '#/components/parameters/data-access' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateTableRequest' + responses: + 200: + $ref: '#/components/responses/CreateTableResponse' + 400: + $ref: '#/components/responses/BadRequestErrorResponse' + 401: + $ref: '#/components/responses/UnauthorizedResponse' + 403: + $ref: '#/components/responses/ForbiddenResponse' + 404: + description: Not Found - The namespace specified does not exist + content: + application/json: + schema: + $ref: '#/components/schemas/IcebergErrorResponse' + examples: + NamespaceNotFound: + $ref: '#/components/examples/NoSuchNamespaceError' + 409: + description: Conflict - The table already exists + content: + application/json: + schema: + $ref: '#/components/schemas/IcebergErrorResponse' + examples: + NamespaceAlreadyExists: + $ref: '#/components/examples/TableAlreadyExistsError' + 419: + $ref: '#/components/responses/AuthenticationTimeoutResponse' + 503: + $ref: '#/components/responses/ServiceUnavailableResponse' + 5XX: + $ref: '#/components/responses/ServerErrorResponse' + + /v1/{prefix}/namespaces/{namespace}/register: + parameters: + - $ref: '#/components/parameters/prefix' + - $ref: '#/components/parameters/namespace' + + post: + tags: + - Catalog API + summary: Register a table in the given namespace using given metadata file location + description: + Register a table using given metadata file location. + + operationId: registerTable + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/RegisterTableRequest' + responses: + 200: + $ref: '#/components/responses/LoadTableResponse' + 400: + $ref: '#/components/responses/BadRequestErrorResponse' + 401: + $ref: '#/components/responses/UnauthorizedResponse' + 403: + $ref: '#/components/responses/ForbiddenResponse' + 404: + description: Not Found - The namespace specified does not exist + content: + application/json: + schema: + $ref: '#/components/schemas/IcebergErrorResponse' + examples: + NamespaceNotFound: + $ref: '#/components/examples/NoSuchNamespaceError' + 409: + description: Conflict - The table already exists + content: + application/json: + schema: + $ref: '#/components/schemas/IcebergErrorResponse' + examples: + NamespaceAlreadyExists: + $ref: '#/components/examples/TableAlreadyExistsError' + 419: + $ref: '#/components/responses/AuthenticationTimeoutResponse' + 503: + $ref: '#/components/responses/ServiceUnavailableResponse' + 5XX: + $ref: '#/components/responses/ServerErrorResponse' + + /v1/{prefix}/namespaces/{namespace}/tables/{table}: + parameters: + - $ref: '#/components/parameters/prefix' + - $ref: '#/components/parameters/namespace' + - $ref: '#/components/parameters/table' + + get: + tags: + - Catalog API + summary: Load a table from the catalog + operationId: loadTable + description: + Load a table from the catalog. + + + The response contains both configuration and table metadata. The configuration, if non-empty is used + as additional configuration for the table that overrides catalog configuration. For example, this + configuration may change the FileIO implementation to be used for the table. + + + The response also contains the table's full metadata, matching the table metadata JSON file. + + + The catalog configuration may contain credentials that should be used for subsequent requests for the + table. The configuration key "token" is used to pass an access token to be used as a bearer token + for table requests. Otherwise, a token may be passed using a RFC 8693 token type as a configuration + key. For example, "urn:ietf:params:oauth:token-type:jwt=". + parameters: + - $ref: '#/components/parameters/data-access' + - in: query + name: snapshots + description: + The snapshots to return in the body of the metadata. Setting the value to `all` would + return the full set of snapshots currently valid for the table. Setting the value to + `refs` would load all snapshots referenced by branches or tags. + + Default if no param is provided is `all`. + required: false + schema: + type: string + enum: [all, refs] + responses: + 200: + $ref: '#/components/responses/LoadTableResponse' + 400: + $ref: '#/components/responses/BadRequestErrorResponse' + 401: + $ref: '#/components/responses/UnauthorizedResponse' + 403: + $ref: '#/components/responses/ForbiddenResponse' + 404: + description: + Not Found - NoSuchTableException, table to load does not exist + content: + application/json: + schema: + $ref: '#/components/schemas/IcebergErrorResponse' + examples: + TableToLoadDoesNotExist: + $ref: '#/components/examples/NoSuchTableError' + 419: + $ref: '#/components/responses/AuthenticationTimeoutResponse' + 503: + $ref: '#/components/responses/ServiceUnavailableResponse' + 5XX: + $ref: '#/components/responses/ServerErrorResponse' + + post: + tags: + - Catalog API + summary: Commit updates to a table + operationId: updateTable + description: + Commit updates to a table. + + + Commits have two parts, requirements and updates. Requirements are assertions that will be validated + before attempting to make and commit changes. For example, `assert-ref-snapshot-id` will check that a + named ref's snapshot ID has a certain value. + + + Updates are changes to make to table metadata. For example, after asserting that the current main ref + is at the expected snapshot, a commit may add a new child snapshot and set the ref to the new + snapshot id. + + + Create table transactions that are started by createTable with `stage-create` set to true are + committed using this route. Transactions should include all changes to the table, including table + initialization, like AddSchemaUpdate and SetCurrentSchemaUpdate. The `assert-create` requirement is + used to ensure that the table was not created concurrently. + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CommitTableRequest' + responses: + 200: + $ref: '#/components/responses/CommitTableResponse' + 400: + $ref: '#/components/responses/BadRequestErrorResponse' + 401: + $ref: '#/components/responses/UnauthorizedResponse' + 403: + $ref: '#/components/responses/ForbiddenResponse' + 404: + description: + Not Found - NoSuchTableException, table to load does not exist + content: + application/json: + schema: + $ref: '#/components/schemas/IcebergErrorResponse' + examples: + TableToUpdateDoesNotExist: + $ref: '#/components/examples/NoSuchTableError' + 409: + description: + Conflict - CommitFailedException, one or more requirements failed. The client may retry. + content: + application/json: + schema: + $ref: '#/components/schemas/IcebergErrorResponse' + 419: + $ref: '#/components/responses/AuthenticationTimeoutResponse' + 500: + description: + An unknown server-side problem occurred; the commit state is unknown. + content: + application/json: + schema: + $ref: '#/components/schemas/IcebergErrorResponse' + example: { + "error": { + "message": "Internal Server Error", + "type": "CommitStateUnknownException", + "code": 500 + } + } + 503: + $ref: '#/components/responses/ServiceUnavailableResponse' + 502: + description: + A gateway or proxy received an invalid response from the upstream server; the commit state is unknown. + content: + application/json: + schema: + $ref: '#/components/schemas/IcebergErrorResponse' + example: { + "error": { + "message": "Invalid response from the upstream server", + "type": "CommitStateUnknownException", + "code": 502 + } + } + 504: + description: + A server-side gateway timeout occurred; the commit state is unknown. + content: + application/json: + schema: + $ref: '#/components/schemas/IcebergErrorResponse' + example: { + "error": { + "message": "Gateway timed out during commit", + "type": "CommitStateUnknownException", + "code": 504 + } + } + 5XX: + description: + A server-side problem that might not be addressable on the client. + content: + application/json: + schema: + $ref: '#/components/schemas/IcebergErrorResponse' + example: { + "error": { + "message": "Bad Gateway", + "type": "InternalServerError", + "code": 502 + } + } + + delete: + tags: + - Catalog API + summary: Drop a table from the catalog + operationId: dropTable + description: Remove a table from the catalog + parameters: + - name: purgeRequested + in: query + required: false + description: Whether the user requested to purge the underlying table's data and metadata + schema: + type: boolean + default: false + responses: + 204: + description: Success, no content + 400: + $ref: '#/components/responses/BadRequestErrorResponse' + 401: + $ref: '#/components/responses/UnauthorizedResponse' + 403: + $ref: '#/components/responses/ForbiddenResponse' + 404: + description: + Not Found - NoSuchTableException, Table to drop does not exist + content: + application/json: + schema: + $ref: '#/components/schemas/IcebergErrorResponse' + examples: + TableToDeleteDoesNotExist: + $ref: '#/components/examples/NoSuchTableError' + 419: + $ref: '#/components/responses/AuthenticationTimeoutResponse' + 503: + $ref: '#/components/responses/ServiceUnavailableResponse' + 5XX: + $ref: '#/components/responses/ServerErrorResponse' + + head: + tags: + - Catalog API + summary: Check if a table exists + operationId: tableExists + description: + Check if a table exists within a given namespace. The response does not contain a body. + responses: + 204: + description: Success, no content + 400: + $ref: '#/components/responses/BadRequestErrorResponse' + 401: + $ref: '#/components/responses/UnauthorizedResponse' + 403: + $ref: '#/components/responses/ForbiddenResponse' + 404: + description: + Not Found - NoSuchTableException, Table not found + content: + application/json: + schema: + $ref: '#/components/schemas/IcebergErrorResponse' + examples: + TableToLoadDoesNotExist: + $ref: '#/components/examples/NoSuchTableError' + 419: + $ref: '#/components/responses/AuthenticationTimeoutResponse' + 503: + $ref: '#/components/responses/ServiceUnavailableResponse' + 5XX: + $ref: '#/components/responses/ServerErrorResponse' + + /v1/{prefix}/tables/rename: + parameters: + - $ref: '#/components/parameters/prefix' + + post: + tags: + - Catalog API + summary: Rename a table from its current name to a new name + description: + Rename a table from one identifier to another. It's valid to move a table + across namespaces, but the server implementation is not required to support it. + operationId: renameTable + requestBody: + description: Current table identifier to rename and new table identifier to rename to + content: + application/json: + schema: + $ref: '#/components/schemas/RenameTableRequest' + examples: + RenameTableSameNamespace: + $ref: '#/components/examples/RenameTableSameNamespace' + required: true + responses: + 204: + description: Success, no content + 400: + $ref: '#/components/responses/BadRequestErrorResponse' + 401: + $ref: '#/components/responses/UnauthorizedResponse' + 403: + $ref: '#/components/responses/ForbiddenResponse' + 404: + description: + Not Found + - NoSuchTableException, Table to rename does not exist + - NoSuchNamespaceException, The target namespace of the new table identifier does not exist + content: + application/json: + schema: + $ref: '#/components/schemas/IcebergErrorResponse' + examples: + TableToRenameDoesNotExist: + $ref: '#/components/examples/NoSuchTableError' + NamespaceToRenameToDoesNotExist: + $ref: '#/components/examples/NoSuchNamespaceError' + 406: + $ref: '#/components/responses/UnsupportedOperationResponse' + 409: + description: Conflict - The target identifier to rename to already exists as a table or view + content: + application/json: + schema: + $ref: '#/components/schemas/IcebergErrorResponse' + example: + $ref: '#/components/examples/TableAlreadyExistsError' + 419: + $ref: '#/components/responses/AuthenticationTimeoutResponse' + 503: + $ref: '#/components/responses/ServiceUnavailableResponse' + 5XX: + $ref: '#/components/responses/ServerErrorResponse' + + /v1/{prefix}/namespaces/{namespace}/tables/{table}/metrics: + parameters: + - $ref: '#/components/parameters/prefix' + - $ref: '#/components/parameters/namespace' + - $ref: '#/components/parameters/table' + + post: + tags: + - Catalog API + summary: Send a metrics report to this endpoint to be processed by the backend + operationId: reportMetrics + requestBody: + description: The request containing the metrics report to be sent + content: + application/json: + schema: + $ref: '#/components/schemas/ReportMetricsRequest' + required: true + responses: + 204: + description: Success, no content + 400: + $ref: '#/components/responses/BadRequestErrorResponse' + 401: + $ref: '#/components/responses/UnauthorizedResponse' + 403: + $ref: '#/components/responses/ForbiddenResponse' + 404: + description: + Not Found - NoSuchTableException, table to load does not exist + content: + application/json: + schema: + $ref: '#/components/schemas/IcebergErrorResponse' + examples: + TableToLoadDoesNotExist: + $ref: '#/components/examples/NoSuchTableError' + 419: + $ref: '#/components/responses/AuthenticationTimeoutResponse' + 503: + $ref: '#/components/responses/ServiceUnavailableResponse' + 5XX: + $ref: '#/components/responses/ServerErrorResponse' + + /v1/{prefix}/namespaces/{namespace}/tables/{table}/notifications: + parameters: + - $ref: '#/components/parameters/prefix' + - $ref: '#/components/parameters/namespace' + - $ref: '#/components/parameters/table' + + post: + tags: + - Catalog API + summary: Sends a notification to the table + operationId: sendNotification + requestBody: + description: The request containing the notification to be sent + content: + application/json: + schema: + $ref: '#/components/schemas/NotificationRequest' + required: true + responses: + 204: + description: Success, no content + 400: + $ref: '#/components/responses/BadRequestErrorResponse' + 401: + $ref: '#/components/responses/UnauthorizedResponse' + 403: + $ref: '#/components/responses/ForbiddenResponse' + 404: + description: + Not Found - NoSuchTableException, table to load does not exist + content: + application/json: + schema: + $ref: '#/components/schemas/IcebergErrorResponse' + examples: + TableToLoadDoesNotExist: + $ref: '#/components/examples/NoSuchTableError' + 419: + $ref: '#/components/responses/AuthenticationTimeoutResponse' + 503: + $ref: '#/components/responses/ServiceUnavailableResponse' + 5XX: + $ref: '#/components/responses/ServerErrorResponse' + + /v1/{prefix}/transactions/commit: + parameters: + - $ref: '#/components/parameters/prefix' + + post: + tags: + - Catalog API + summary: Commit updates to multiple tables in an atomic operation + operationId: commitTransaction + requestBody: + description: + Commit updates to multiple tables in an atomic operation + + + A commit for a single table consists of a table identifier with requirements and updates. + Requirements are assertions that will be validated before attempting to make and commit changes. + For example, `assert-ref-snapshot-id` will check that a named ref's snapshot ID has a certain value. + + + Updates are changes to make to table metadata. For example, after asserting that the current main ref + is at the expected snapshot, a commit may add a new child snapshot and set the ref to the new + snapshot id. + content: + application/json: + schema: + $ref: '#/components/schemas/CommitTransactionRequest' + required: true + responses: + 204: + description: Success, no content + 400: + $ref: '#/components/responses/BadRequestErrorResponse' + 401: + $ref: '#/components/responses/UnauthorizedResponse' + 403: + $ref: '#/components/responses/ForbiddenResponse' + 404: + description: + Not Found - NoSuchTableException, table to load does not exist + content: + application/json: + schema: + $ref: '#/components/schemas/IcebergErrorResponse' + examples: + TableToUpdateDoesNotExist: + $ref: '#/components/examples/NoSuchTableError' + 409: + description: + Conflict - CommitFailedException, one or more requirements failed. The client may retry. + content: + application/json: + schema: + $ref: '#/components/schemas/IcebergErrorResponse' + 419: + $ref: '#/components/responses/AuthenticationTimeoutResponse' + 500: + description: + An unknown server-side problem occurred; the commit state is unknown. + content: + application/json: + schema: + $ref: '#/components/schemas/IcebergErrorResponse' + example: { + "error": { + "message": "Internal Server Error", + "type": "CommitStateUnknownException", + "code": 500 + } + } + 503: + $ref: '#/components/responses/ServiceUnavailableResponse' + 502: + description: + A gateway or proxy received an invalid response from the upstream server; the commit state is unknown. + content: + application/json: + schema: + $ref: '#/components/schemas/IcebergErrorResponse' + example: { + "error": { + "message": "Invalid response from the upstream server", + "type": "CommitStateUnknownException", + "code": 502 + } + } + 504: + description: + A server-side gateway timeout occurred; the commit state is unknown. + content: + application/json: + schema: + $ref: '#/components/schemas/IcebergErrorResponse' + example: { + "error": { + "message": "Gateway timed out during commit", + "type": "CommitStateUnknownException", + "code": 504 + } + } + 5XX: + description: + A server-side problem that might not be addressable on the client. + content: + application/json: + schema: + $ref: '#/components/schemas/IcebergErrorResponse' + example: { + "error": { + "message": "Bad Gateway", + "type": "InternalServerError", + "code": 502 + } + } + + /v1/{prefix}/namespaces/{namespace}/views: + parameters: + - $ref: '#/components/parameters/prefix' + - $ref: '#/components/parameters/namespace' + + get: + tags: + - Catalog API + summary: List all view identifiers underneath a given namespace + description: Return all view identifiers under this namespace + operationId: listViews + parameters: + - $ref: '#/components/parameters/page-token' + - $ref: '#/components/parameters/page-size' + responses: + 200: + $ref: '#/components/responses/ListTablesResponse' + 400: + $ref: '#/components/responses/BadRequestErrorResponse' + 401: + $ref: '#/components/responses/UnauthorizedResponse' + 403: + $ref: '#/components/responses/ForbiddenResponse' + 404: + description: Not Found - The namespace specified does not exist + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorModel' + examples: + NamespaceNotFound: + $ref: '#/components/examples/NoSuchNamespaceError' + 419: + $ref: '#/components/responses/AuthenticationTimeoutResponse' + 503: + $ref: '#/components/responses/ServiceUnavailableResponse' + 5XX: + $ref: '#/components/responses/ServerErrorResponse' + + post: + tags: + - Catalog API + summary: Create a view in the given namespace + description: + Create a view in the given namespace. + operationId: createView + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateViewRequest' + responses: + 200: + $ref: '#/components/responses/LoadViewResponse' + 400: + $ref: '#/components/responses/BadRequestErrorResponse' + 401: + $ref: '#/components/responses/UnauthorizedResponse' + 403: + $ref: '#/components/responses/ForbiddenResponse' + 404: + description: Not Found - The namespace specified does not exist + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorModel' + examples: + NamespaceNotFound: + $ref: '#/components/examples/NoSuchNamespaceError' + 409: + description: Conflict - The view already exists + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorModel' + examples: + NamespaceAlreadyExists: + $ref: '#/components/examples/ViewAlreadyExistsError' + 419: + $ref: '#/components/responses/AuthenticationTimeoutResponse' + 503: + $ref: '#/components/responses/ServiceUnavailableResponse' + 5XX: + $ref: '#/components/responses/ServerErrorResponse' + + /v1/{prefix}/namespaces/{namespace}/views/{view}: + parameters: + - $ref: '#/components/parameters/prefix' + - $ref: '#/components/parameters/namespace' + - $ref: '#/components/parameters/view' + + get: + tags: + - Catalog API + summary: Load a view from the catalog + operationId: loadView + description: + Load a view from the catalog. + + + The response contains both configuration and view metadata. The configuration, if non-empty is used + as additional configuration for the view that overrides catalog configuration. + + + The response also contains the view's full metadata, matching the view metadata JSON file. + + + The catalog configuration may contain credentials that should be used for subsequent requests for the + view. The configuration key "token" is used to pass an access token to be used as a bearer token + for view requests. Otherwise, a token may be passed using a RFC 8693 token type as a configuration + key. For example, "urn:ietf:params:oauth:token-type:jwt=". + responses: + 200: + $ref: '#/components/responses/LoadViewResponse' + 400: + $ref: '#/components/responses/BadRequestErrorResponse' + 401: + $ref: '#/components/responses/UnauthorizedResponse' + 403: + $ref: '#/components/responses/ForbiddenResponse' + 404: + description: + Not Found - NoSuchViewException, view to load does not exist + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorModel' + examples: + ViewToLoadDoesNotExist: + $ref: '#/components/examples/NoSuchViewError' + 419: + $ref: '#/components/responses/AuthenticationTimeoutResponse' + 503: + $ref: '#/components/responses/ServiceUnavailableResponse' + 5XX: + $ref: '#/components/responses/ServerErrorResponse' + + post: + tags: + - Catalog API + summary: Replace a view + operationId: replaceView + description: + Commit updates to a view. + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CommitViewRequest' + responses: + 200: + $ref: '#/components/responses/LoadViewResponse' + 400: + $ref: '#/components/responses/BadRequestErrorResponse' + 401: + $ref: '#/components/responses/UnauthorizedResponse' + 403: + $ref: '#/components/responses/ForbiddenResponse' + 404: + description: + Not Found - NoSuchViewException, view to load does not exist + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorModel' + examples: + ViewToUpdateDoesNotExist: + $ref: '#/components/examples/NoSuchViewError' + 409: + description: + Conflict - CommitFailedException. The client may retry. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorModel' + 419: + $ref: '#/components/responses/AuthenticationTimeoutResponse' + 500: + description: + An unknown server-side problem occurred; the commit state is unknown. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorModel' + example: { + "error": { + "message": "Internal Server Error", + "type": "CommitStateUnknownException", + "code": 500 + } + } + 503: + $ref: '#/components/responses/ServiceUnavailableResponse' + 502: + description: + A gateway or proxy received an invalid response from the upstream server; the commit state is unknown. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorModel' + example: { + "error": { + "message": "Invalid response from the upstream server", + "type": "CommitStateUnknownException", + "code": 502 + } + } + 504: + description: + A server-side gateway timeout occurred; the commit state is unknown. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorModel' + example: { + "error": { + "message": "Gateway timed out during commit", + "type": "CommitStateUnknownException", + "code": 504 + } + } + 5XX: + description: + A server-side problem that might not be addressable on the client. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorModel' + example: { + "error": { + "message": "Bad Gateway", + "type": "InternalServerError", + "code": 502 + } + } + + delete: + tags: + - Catalog API + summary: Drop a view from the catalog + operationId: dropView + description: Remove a view from the catalog + responses: + 204: + description: Success, no content + 400: + $ref: '#/components/responses/BadRequestErrorResponse' + 401: + $ref: '#/components/responses/UnauthorizedResponse' + 403: + $ref: '#/components/responses/ForbiddenResponse' + 404: + description: + Not Found - NoSuchViewException, view to drop does not exist + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorModel' + examples: + ViewToDeleteDoesNotExist: + $ref: '#/components/examples/NoSuchViewError' + 419: + $ref: '#/components/responses/AuthenticationTimeoutResponse' + 503: + $ref: '#/components/responses/ServiceUnavailableResponse' + 5XX: + $ref: '#/components/responses/ServerErrorResponse' + + head: + tags: + - Catalog API + summary: Check if a view exists + operationId: viewExists + description: + Check if a view exists within a given namespace. This request does not return a response body. + responses: + 204: + description: Success, no content + 400: + description: Bad Request + 401: + description: Unauthorized + 404: + description: Not Found + 419: + $ref: '#/components/responses/AuthenticationTimeoutResponse' + 503: + $ref: '#/components/responses/ServiceUnavailableResponse' + 5XX: + $ref: '#/components/responses/ServerErrorResponse' + + /v1/{prefix}/views/rename: + parameters: + - $ref: '#/components/parameters/prefix' + + post: + tags: + - Catalog API + summary: Rename a view from its current name to a new name + description: + Rename a view from one identifier to another. It's valid to move a view + across namespaces, but the server implementation is not required to support it. + operationId: renameView + requestBody: + description: Current view identifier to rename and new view identifier to rename to + content: + application/json: + schema: + $ref: '#/components/schemas/RenameTableRequest' + examples: + RenameViewSameNamespace: + $ref: '#/components/examples/RenameViewSameNamespace' + required: true + responses: + 204: + description: Success, no content + 400: + $ref: '#/components/responses/BadRequestErrorResponse' + 401: + $ref: '#/components/responses/UnauthorizedResponse' + 403: + $ref: '#/components/responses/ForbiddenResponse' + 404: + description: + Not Found + - NoSuchViewException, view to rename does not exist + - NoSuchNamespaceException, The target namespace of the new identifier does not exist + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorModel' + examples: + ViewToRenameDoesNotExist: + $ref: '#/components/examples/NoSuchViewError' + NamespaceToRenameToDoesNotExist: + $ref: '#/components/examples/NoSuchNamespaceError' + 406: + $ref: '#/components/responses/UnsupportedOperationResponse' + 409: + description: Conflict - The target identifier to rename to already exists as a table or view + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorModel' + example: + $ref: '#/components/examples/ViewAlreadyExistsError' + 419: + $ref: '#/components/responses/AuthenticationTimeoutResponse' + 503: + $ref: '#/components/responses/ServiceUnavailableResponse' + 5XX: + $ref: '#/components/responses/ServerErrorResponse' + + +components: + ####################################################### + # Common Parameter Definitions Used In Several Routes # + ####################################################### + parameters: + namespace: + name: namespace + in: path + required: true + description: + A namespace identifier as a single string. + Multipart namespace parts should be separated by the unit separator (`0x1F`) byte. + schema: + type: string + examples: + singlepart_namespace: + value: "accounting" + multipart_namespace: + value: "accounting%1Ftax" + + prefix: + name: prefix + in: path + schema: + type: string + required: true + description: An optional prefix in the path + + table: + name: table + in: path + description: A table name + required: true + schema: + type: string + example: "sales" + + view: + name: view + in: path + description: A view name + required: true + schema: + type: string + example: "sales" + + data-access: + name: X-Iceberg-Access-Delegation + in: header + description: > + Optional signal to the server that the client supports delegated access + via a comma-separated list of access mechanisms. The server may choose + to supply access via any or none of the requested mechanisms. + + + Specific properties and handling for `vended-credentials` is documented + in the `LoadTableResult` schema section of this spec document. + + + The protocol and specification for `remote-signing` is documented in + the `s3-signer-open-api.yaml` OpenApi spec in the `aws` module. + + required: false + schema: + type: string + enum: + - vended-credentials + - remote-signing + style: simple + explode: false + example: "vended-credentials,remote-signing" + + page-token: + name: pageToken + in: query + required: false + allowEmptyValue: true + schema: + $ref: '#/components/schemas/PageToken' + + page-size: + name: pageSize + in: query + description: + For servers that support pagination, this signals an upper bound of the number of results that a client will receive. + For servers that do not support pagination, clients may receive results larger than the indicated `pageSize`. + required: false + schema: + type: integer + minimum: 1 + + ############################## + # Application Schema Objects # + ############################## + schemas: + + ErrorModel: + type: object + description: JSON error payload returned in a response with further details on the error + required: + - message + - type + - code + properties: + message: + type: string + description: Human-readable error message + type: + type: string + description: Internal type definition of the error + example: NoSuchNamespaceException + code: + type: integer + minimum: 400 + maximum: 600 + description: HTTP response code + example: 404 + stack: + type: array + items: + type: string + + CatalogConfig: + type: object + description: Server-provided configuration for the catalog. + required: + - defaults + - overrides + properties: + overrides: + type: object + additionalProperties: + type: string + description: + Properties that should be used to override client configuration; applied after defaults and client configuration. + defaults: + type: object + additionalProperties: + type: string + description: + Properties that should be used as default configuration; applied before client configuration. + + CreateNamespaceRequest: + type: object + required: + - namespace + properties: + namespace: + $ref: '#/components/schemas/Namespace' + properties: + type: object + description: Configured string to string map of properties for the namespace + example: {"owner": "Hank Bendickson"} + default: {} + additionalProperties: + type: string + + UpdateNamespacePropertiesRequest: + type: object + properties: + removals: + type: array + uniqueItems: true + items: + type: string + example: ["department", "access_group"] + updates: + type: object + example: {"owner": "Hank Bendickson"} + additionalProperties: + type: string + + RenameTableRequest: + type: object + required: + - source + - destination + properties: + source: + $ref: '#/components/schemas/TableIdentifier' + destination: + $ref: '#/components/schemas/TableIdentifier' + + Namespace: + description: Reference to one or more levels of a namespace + type: array + items: + type: string + example: ["accounting", "tax"] + + PageToken: + description: + An opaque token that allows clients to make use of pagination for list APIs + (e.g. ListTables). Clients may initiate the first paginated request by sending an empty + query parameter `pageToken` to the server. + + Servers that support pagination should identify the `pageToken` parameter and return a + `next-page-token` in the response if there are more results available. After the initial + request, the value of `next-page-token` from each response must be used as the `pageToken` + parameter value for the next request. The server must return `null` value for the + `next-page-token` in the last response. + + Servers that support pagination must return all results in a single response with the value + of `next-page-token` set to `null` if the query parameter `pageToken` is not set in the + request. + + Servers that do not support pagination should ignore the `pageToken` parameter and return + all results in a single response. The `next-page-token` must be omitted from the response. + + Clients must interpret either `null` or missing response value of `next-page-token` as + the end of the listing results. + + type: string + nullable: true + + TableIdentifier: + type: object + required: + - namespace + - name + properties: + namespace: + $ref: '#/components/schemas/Namespace' + name: + type: string + nullable: false + + PrimitiveType: + type: string + example: + - "long" + - "string" + - "fixed[16]" + - "decimal(10,2)" + + StructField: + type: object + required: + - id + - name + - type + - required + properties: + id: + type: integer + name: + type: string + type: + $ref: '#/components/schemas/Type' + required: + type: boolean + doc: + type: string + + StructType: + type: object + required: + - type + - fields + properties: + type: + type: string + enum: ["struct"] + fields: + type: array + items: + $ref: '#/components/schemas/StructField' + + ListType: + type: object + required: + - type + - element-id + - element + - element-required + properties: + type: + type: string + enum: ["list"] + element-id: + type: integer + element: + $ref: '#/components/schemas/Type' + element-required: + type: boolean + + MapType: + type: object + required: + - type + - key-id + - key + - value-id + - value + - value-required + properties: + type: + type: string + enum: ["map"] + key-id: + type: integer + key: + $ref: '#/components/schemas/Type' + value-id: + type: integer + value: + $ref: '#/components/schemas/Type' + value-required: + type: boolean + + Type: + oneOf: + - $ref: '#/components/schemas/PrimitiveType' + - $ref: '#/components/schemas/StructType' + - $ref: '#/components/schemas/ListType' + - $ref: '#/components/schemas/MapType' + + Schema: + allOf: + - $ref: '#/components/schemas/StructType' + - type: object + properties: + schema-id: + type: integer + readOnly: true + identifier-field-ids: + type: array + items: + type: integer + + Expression: + oneOf: + - $ref: '#/components/schemas/AndOrExpression' + - $ref: '#/components/schemas/NotExpression' + - $ref: '#/components/schemas/SetExpression' + - $ref: '#/components/schemas/LiteralExpression' + - $ref: '#/components/schemas/UnaryExpression' + + ExpressionType: + type: string + example: + - "eq" + - "and" + - "or" + - "not" + - "in" + - "not-in" + - "lt" + - "lt-eq" + - "gt" + - "gt-eq" + - "not-eq" + - "starts-with" + - "not-starts-with" + - "is-null" + - "not-null" + - "is-nan" + - "not-nan" + + AndOrExpression: + type: object + required: + - type + - left + - right + properties: + type: + $ref: '#/components/schemas/ExpressionType' + enum: ["and", "or"] + left: + $ref: '#/components/schemas/Expression' + right: + $ref: '#/components/schemas/Expression' + + NotExpression: + type: object + required: + - type + - child + properties: + type: + $ref: '#/components/schemas/ExpressionType' + enum: ["not"] + child: + $ref: '#/components/schemas/Expression' + + UnaryExpression: + type: object + required: + - type + - term + - value + properties: + type: + $ref: '#/components/schemas/ExpressionType' + enum: ["is-null", "not-null", "is-nan", "not-nan"] + term: + $ref: '#/components/schemas/Term' + value: + type: object + + LiteralExpression: + type: object + required: + - type + - term + - value + properties: + type: + $ref: '#/components/schemas/ExpressionType' + enum: ["lt", "lt-eq", "gt", "gt-eq", "eq", "not-eq", "starts-with", "not-starts-with"] + term: + $ref: '#/components/schemas/Term' + value: + type: object + + SetExpression: + type: object + required: + - type + - term + - values + properties: + type: + $ref: '#/components/schemas/ExpressionType' + enum: ["in", "not-in"] + term: + $ref: '#/components/schemas/Term' + values: + type: array + items: + type: object + + Term: + oneOf: + - $ref: '#/components/schemas/Reference' + - $ref: '#/components/schemas/TransformTerm' + + Reference: + type: string + example: + - "column-name" + + TransformTerm: + type: object + required: + - type + - transform + - term + properties: + type: + type: string + enum: ["transform"] + transform: + $ref: '#/components/schemas/Transform' + term: + $ref: '#/components/schemas/Reference' + + Transform: + type: string + example: + - "identity" + - "year" + - "month" + - "day" + - "hour" + - "bucket[256]" + - "truncate[16]" + + PartitionField: + type: object + required: + - source-id + - transform + - name + properties: + field-id: + type: integer + source-id: + type: integer + name: + type: string + transform: + $ref: '#/components/schemas/Transform' + + PartitionSpec: + type: object + required: + - fields + properties: + spec-id: + type: integer + readOnly: true + fields: + type: array + items: + $ref: '#/components/schemas/PartitionField' + + SortDirection: + type: string + enum: ["asc", "desc"] + + NullOrder: + type: string + enum: ["nulls-first", "nulls-last"] + + SortField: + type: object + required: + - source-id + - transform + - direction + - null-order + properties: + source-id: + type: integer + transform: + $ref: '#/components/schemas/Transform' + direction: + $ref: '#/components/schemas/SortDirection' + null-order: + $ref: '#/components/schemas/NullOrder' + + SortOrder: + type: object + required: + - order-id + - fields + properties: + order-id: + type: integer + readOnly: true + fields: + type: array + items: + $ref: '#/components/schemas/SortField' + + Snapshot: + type: object + required: + - snapshot-id + - timestamp-ms + - manifest-list + - summary + properties: + snapshot-id: + type: integer + format: int64 + parent-snapshot-id: + type: integer + format: int64 + sequence-number: + type: integer + format: int64 + timestamp-ms: + type: integer + format: int64 + manifest-list: + type: string + description: Location of the snapshot's manifest list file + summary: + type: object + required: + - operation + properties: + operation: + type: string + enum: ["append", "replace", "overwrite", "delete"] + additionalProperties: + type: string + schema-id: + type: integer + + SnapshotReference: + type: object + required: + - type + - snapshot-id + properties: + type: + type: string + enum: ["tag", "branch"] + snapshot-id: + type: integer + format: int64 + max-ref-age-ms: + type: integer + format: int64 + max-snapshot-age-ms: + type: integer + format: int64 + min-snapshots-to-keep: + type: integer + + SnapshotReferences: + type: object + additionalProperties: + $ref: '#/components/schemas/SnapshotReference' + + SnapshotLog: + type: array + items: + type: object + required: + - snapshot-id + - timestamp-ms + properties: + snapshot-id: + type: integer + format: int64 + timestamp-ms: + type: integer + format: int64 + + MetadataLog: + type: array + items: + type: object + required: + - metadata-file + - timestamp-ms + properties: + metadata-file: + type: string + timestamp-ms: + type: integer + format: int64 + + TableMetadata: + type: object + required: + - format-version + - table-uuid + properties: + format-version: + type: integer + minimum: 1 + maximum: 2 + table-uuid: + type: string + location: + type: string + last-updated-ms: + type: integer + format: int64 + properties: + type: object + additionalProperties: + type: string + # schema tracking + schemas: + type: array + items: + $ref: '#/components/schemas/Schema' + current-schema-id: + type: integer + last-column-id: + type: integer + # partition spec tracking + partition-specs: + type: array + items: + $ref: '#/components/schemas/PartitionSpec' + default-spec-id: + type: integer + last-partition-id: + type: integer + # sort order tracking + sort-orders: + type: array + items: + $ref: '#/components/schemas/SortOrder' + default-sort-order-id: + type: integer + # snapshot tracking + snapshots: + type: array + items: + $ref: '#/components/schemas/Snapshot' + refs: + $ref: '#/components/schemas/SnapshotReferences' + current-snapshot-id: + type: integer + format: int64 + last-sequence-number: + type: integer + format: int64 + # logs + snapshot-log: + $ref: '#/components/schemas/SnapshotLog' + metadata-log: + $ref: '#/components/schemas/MetadataLog' + # statistics + statistics-files: + type: array + items: + $ref: '#/components/schemas/StatisticsFile' + partition-statistics-files: + type: array + items: + $ref: '#/components/schemas/PartitionStatisticsFile' + + SQLViewRepresentation: + type: object + required: + - type + - sql + - dialect + properties: + type: + type: string + sql: + type: string + dialect: + type: string + + ViewRepresentation: + oneOf: + - $ref: '#/components/schemas/SQLViewRepresentation' + + ViewHistoryEntry: + type: object + required: + - version-id + - timestamp-ms + properties: + version-id: + type: integer + timestamp-ms: + type: integer + format: int64 + + ViewVersion: + type: object + required: + - version-id + - timestamp-ms + - schema-id + - summary + - representations + - default-namespace + properties: + version-id: + type: integer + timestamp-ms: + type: integer + format: int64 + schema-id: + type: integer + description: Schema ID to set as current, or -1 to set last added schema + summary: + type: object + additionalProperties: + type: string + representations: + type: array + items: + $ref: '#/components/schemas/ViewRepresentation' + default-catalog: + type: string + default-namespace: + $ref: '#/components/schemas/Namespace' + + ViewMetadata: + type: object + required: + - view-uuid + - format-version + - location + - current-version-id + - versions + - version-log + - schemas + properties: + view-uuid: + type: string + format-version: + type: integer + minimum: 1 + maximum: 1 + location: + type: string + current-version-id: + type: integer + versions: + type: array + items: + $ref: '#/components/schemas/ViewVersion' + version-log: + type: array + items: + $ref: '#/components/schemas/ViewHistoryEntry' + schemas: + type: array + items: + $ref: '#/components/schemas/Schema' + properties: + type: object + additionalProperties: + type: string + + BaseUpdate: + discriminator: + propertyName: action + mapping: + assign-uuid: '#/components/schemas/AssignUUIDUpdate' + upgrade-format-version: '#/components/schemas/UpgradeFormatVersionUpdate' + add-schema: '#/components/schemas/AddSchemaUpdate' + set-current-schema: '#/components/schemas/SetCurrentSchemaUpdate' + add-spec: '#/components/schemas/AddPartitionSpecUpdate' + set-default-spec: '#/components/schemas/SetDefaultSpecUpdate' + add-sort-order: '#/components/schemas/AddSortOrderUpdate' + set-default-sort-order: '#/components/schemas/SetDefaultSortOrderUpdate' + add-snapshot: '#/components/schemas/AddSnapshotUpdate' + set-snapshot-ref: '#/components/schemas/SetSnapshotRefUpdate' + remove-snapshots: '#/components/schemas/RemoveSnapshotsUpdate' + remove-snapshot-ref: '#/components/schemas/RemoveSnapshotRefUpdate' + set-location: '#/components/schemas/SetLocationUpdate' + set-properties: '#/components/schemas/SetPropertiesUpdate' + remove-properties: '#/components/schemas/RemovePropertiesUpdate' + add-view-version: '#/components/schemas/AddViewVersionUpdate' + set-current-view-version: '#/components/schemas/SetCurrentViewVersionUpdate' + set-statistics: '#/components/schemas/SetStatisticsUpdate' + remove-statistics: '#/components/schemas/RemoveStatisticsUpdate' + set-partition-statistics: '#/components/schemas/SetPartitionStatisticsUpdate' + remove-partition-statistics: '#/components/schemas/RemovePartitionStatisticsUpdate' + type: object + required: + - action + properties: + action: + type: string + + AssignUUIDUpdate: + description: Assigning a UUID to a table/view should only be done when creating the table/view. It is not safe to re-assign the UUID if a table/view already has a UUID assigned + allOf: + - $ref: '#/components/schemas/BaseUpdate' + required: + - action + - uuid + properties: + action: + type: string + enum: ["assign-uuid"] + uuid: + type: string + + UpgradeFormatVersionUpdate: + allOf: + - $ref: '#/components/schemas/BaseUpdate' + required: + - action + - format-version + properties: + action: + type: string + enum: ["upgrade-format-version"] + format-version: + type: integer + + AddSchemaUpdate: + allOf: + - $ref: '#/components/schemas/BaseUpdate' + required: + - action + - schema + properties: + action: + type: string + enum: ["add-schema"] + schema: + $ref: '#/components/schemas/Schema' + last-column-id: + type: integer + description: The highest assigned column ID for the table. This is used to ensure columns are always assigned an unused ID when evolving schemas. When omitted, it will be computed on the server side. + + SetCurrentSchemaUpdate: + allOf: + - $ref: '#/components/schemas/BaseUpdate' + required: + - action + - schema-id + properties: + action: + type: string + enum: ["set-current-schema"] + schema-id: + type: integer + description: Schema ID to set as current, or -1 to set last added schema + + AddPartitionSpecUpdate: + allOf: + - $ref: '#/components/schemas/BaseUpdate' + required: + - action + - spec + properties: + action: + type: string + enum: ["add-spec"] + spec: + $ref: '#/components/schemas/PartitionSpec' + + SetDefaultSpecUpdate: + allOf: + - $ref: '#/components/schemas/BaseUpdate' + required: + - action + - spec-id + properties: + action: + type: string + enum: ["set-default-spec"] + spec-id: + type: integer + description: Partition spec ID to set as the default, or -1 to set last added spec + + AddSortOrderUpdate: + allOf: + - $ref: '#/components/schemas/BaseUpdate' + required: + - action + - sort-order + properties: + action: + type: string + enum: ["add-sort-order"] + sort-order: + $ref: '#/components/schemas/SortOrder' + + SetDefaultSortOrderUpdate: + allOf: + - $ref: '#/components/schemas/BaseUpdate' + required: + - action + - sort-order-id + properties: + action: + type: string + enum: ["set-default-sort-order"] + sort-order-id: + type: integer + description: Sort order ID to set as the default, or -1 to set last added sort order + + AddSnapshotUpdate: + allOf: + - $ref: '#/components/schemas/BaseUpdate' + required: + - action + - snapshot + properties: + action: + type: string + enum: ["add-snapshot"] + snapshot: + $ref: '#/components/schemas/Snapshot' + + SetSnapshotRefUpdate: + allOf: + - $ref: '#/components/schemas/BaseUpdate' + - $ref: '#/components/schemas/SnapshotReference' + required: + - action + - ref-name + properties: + action: + type: string + enum: ["set-snapshot-ref"] + ref-name: + type: string + + RemoveSnapshotsUpdate: + allOf: + - $ref: '#/components/schemas/BaseUpdate' + required: + - action + - snapshot-ids + properties: + action: + type: string + enum: ["remove-snapshots"] + snapshot-ids: + type: array + items: + type: integer + format: int64 + + RemoveSnapshotRefUpdate: + allOf: + - $ref: '#/components/schemas/BaseUpdate' + required: + - action + - ref-name + properties: + action: + type: string + enum: ["remove-snapshot-ref"] + ref-name: + type: string + + SetLocationUpdate: + allOf: + - $ref: '#/components/schemas/BaseUpdate' + required: + - action + - location + properties: + action: + type: string + enum: ["set-location"] + location: + type: string + + SetPropertiesUpdate: + allOf: + - $ref: '#/components/schemas/BaseUpdate' + required: + - action + - updates + properties: + action: + type: string + enum: ["set-properties"] + updates: + type: object + additionalProperties: + type: string + + RemovePropertiesUpdate: + allOf: + - $ref: '#/components/schemas/BaseUpdate' + required: + - action + - removals + properties: + action: + type: string + enum: ["remove-properties"] + removals: + type: array + items: + type: string + + AddViewVersionUpdate: + allOf: + - $ref: '#/components/schemas/BaseUpdate' + required: + - action + - view-version + properties: + action: + type: string + enum: ["add-view-version"] + view-version: + $ref: '#/components/schemas/ViewVersion' + + SetCurrentViewVersionUpdate: + allOf: + - $ref: '#/components/schemas/BaseUpdate' + required: + - action + - view-version-id + properties: + action: + type: string + enum: ["set-current-view-version"] + view-version-id: + type: integer + description: The view version id to set as current, or -1 to set last added view version id + + SetStatisticsUpdate: + allOf: + - $ref: '#/components/schemas/BaseUpdate' + required: + - action + - snapshot-id + - statistics + properties: + action: + type: string + enum: ["set-statistics"] + snapshot-id: + type: integer + format: int64 + statistics: + $ref: '#/components/schemas/StatisticsFile' + + RemoveStatisticsUpdate: + allOf: + - $ref: '#/components/schemas/BaseUpdate' + required: + - action + - snapshot-id + properties: + action: + type: string + enum: ["remove-statistics"] + snapshot-id: + type: integer + format: int64 + + SetPartitionStatisticsUpdate: + allOf: + - $ref: '#/components/schemas/BaseUpdate' + required: + - action + - partition-statistics + properties: + action: + type: string + enum: ["set-partition-statistics"] + partition-statistics: + $ref: '#/components/schemas/PartitionStatisticsFile' + + RemovePartitionStatisticsUpdate: + allOf: + - $ref: '#/components/schemas/BaseUpdate' + required: + - action + - snapshot-id + properties: + action: + type: string + enum: ["remove-partition-statistics"] + snapshot-id: + type: integer + format: int64 + + TableUpdate: + anyOf: + - $ref: '#/components/schemas/AssignUUIDUpdate' + - $ref: '#/components/schemas/UpgradeFormatVersionUpdate' + - $ref: '#/components/schemas/AddSchemaUpdate' + - $ref: '#/components/schemas/SetCurrentSchemaUpdate' + - $ref: '#/components/schemas/AddPartitionSpecUpdate' + - $ref: '#/components/schemas/SetDefaultSpecUpdate' + - $ref: '#/components/schemas/AddSortOrderUpdate' + - $ref: '#/components/schemas/SetDefaultSortOrderUpdate' + - $ref: '#/components/schemas/AddSnapshotUpdate' + - $ref: '#/components/schemas/SetSnapshotRefUpdate' + - $ref: '#/components/schemas/RemoveSnapshotsUpdate' + - $ref: '#/components/schemas/RemoveSnapshotRefUpdate' + - $ref: '#/components/schemas/SetLocationUpdate' + - $ref: '#/components/schemas/SetPropertiesUpdate' + - $ref: '#/components/schemas/RemovePropertiesUpdate' + - $ref: '#/components/schemas/SetStatisticsUpdate' + - $ref: '#/components/schemas/RemoveStatisticsUpdate' + + ViewUpdate: + anyOf: + - $ref: '#/components/schemas/AssignUUIDUpdate' + - $ref: '#/components/schemas/UpgradeFormatVersionUpdate' + - $ref: '#/components/schemas/AddSchemaUpdate' + - $ref: '#/components/schemas/SetLocationUpdate' + - $ref: '#/components/schemas/SetPropertiesUpdate' + - $ref: '#/components/schemas/RemovePropertiesUpdate' + - $ref: '#/components/schemas/AddViewVersionUpdate' + - $ref: '#/components/schemas/SetCurrentViewVersionUpdate' + + TableRequirement: + discriminator: + propertyName: type + mapping: + assert-create: '#/components/schemas/AssertCreate' + assert-table-uuid: '#/components/schemas/AssertTableUUID' + assert-ref-snapshot-id: '#/components/schemas/AssertRefSnapshotId' + assert-last-assigned-field-id: '#/components/schemas/AssertLastAssignedFieldId' + assert-current-schema-id: '#/components/schemas/AssertCurrentSchemaId' + assert-last-assigned-partition-id: '#/components/schemas/AssertLastAssignedPartitionId' + assert-default-spec-id: '#/components/schemas/AssertDefaultSpecId' + assert-default-sort-order-id: '#/components/schemas/AssertDefaultSortOrderId' + type: object + required: + - type + properties: + type: + type: "string" + + AssertCreate: + allOf: + - $ref: "#/components/schemas/TableRequirement" + type: object + description: The table must not already exist; used for create transactions + required: + - type + properties: + type: + type: string + enum: ["assert-create"] + + AssertTableUUID: + allOf: + - $ref: "#/components/schemas/TableRequirement" + description: The table UUID must match the requirement's `uuid` + required: + - type + - uuid + properties: + type: + type: string + enum: ["assert-table-uuid"] + uuid: + type: string + + AssertRefSnapshotId: + allOf: + - $ref: "#/components/schemas/TableRequirement" + description: + The table branch or tag identified by the requirement's `ref` must reference the requirement's `snapshot-id`; + if `snapshot-id` is `null` or missing, the ref must not already exist + required: + - type + - ref + - snapshot-id + properties: + type: + type: string + enum: ["assert-ref-snapshot-id"] + ref: + type: string + snapshot-id: + type: integer + format: int64 + + AssertLastAssignedFieldId: + allOf: + - $ref: "#/components/schemas/TableRequirement" + description: + The table's last assigned column id must match the requirement's `last-assigned-field-id` + required: + - type + - last-assigned-field-id + properties: + type: + type: string + enum: ["assert-last-assigned-field-id"] + last-assigned-field-id: + type: integer + + AssertCurrentSchemaId: + allOf: + - $ref: "#/components/schemas/TableRequirement" + description: + The table's current schema id must match the requirement's `current-schema-id` + required: + - type + - current-schema-id + properties: + type: + type: string + enum: ["assert-current-schema-id"] + current-schema-id: + type: integer + + AssertLastAssignedPartitionId: + allOf: + - $ref: "#/components/schemas/TableRequirement" + description: + The table's last assigned partition id must match the requirement's `last-assigned-partition-id` + required: + - type + - last-assigned-partition-id + properties: + type: + type: string + enum: ["assert-last-assigned-partition-id"] + last-assigned-partition-id: + type: integer + + AssertDefaultSpecId: + allOf: + - $ref: "#/components/schemas/TableRequirement" + description: + The table's default spec id must match the requirement's `default-spec-id` + required: + - type + - default-spec-id + properties: + type: + type: string + enum: ["assert-default-spec-id"] + default-spec-id: + type: integer + + AssertDefaultSortOrderId: + allOf: + - $ref: "#/components/schemas/TableRequirement" + description: + The table's default sort order id must match the requirement's `default-sort-order-id` + required: + - type + - default-sort-order-id + properties: + type: + type: string + enum: ["assert-default-sort-order-id"] + default-sort-order-id: + type: integer + + ViewRequirement: + discriminator: + propertyName: type + mapping: + assert-view-uuid: '#/components/schemas/AssertViewUUID' + type: object + required: + - type + properties: + type: + type: "string" + + AssertViewUUID: + allOf: + - $ref: "#/components/schemas/ViewRequirement" + description: The view UUID must match the requirement's `uuid` + required: + - type + - uuid + properties: + type: + type: string + enum: ["assert-view-uuid"] + uuid: + type: string + + LoadTableResult: + description: | + Result used when a table is successfully loaded. + + + The table metadata JSON is returned in the `metadata` field. The corresponding file location of table metadata should be returned in the `metadata-location` field, unless the metadata is not yet committed. For example, a create transaction may return metadata that is staged but not committed. + Clients can check whether metadata has changed by comparing metadata locations after the table has been created. + + + The `config` map returns table-specific configuration for the table's resources, including its HTTP client and FileIO. For example, config may contain a specific FileIO implementation class for the table depending on its underlying storage. + + + The following configurations should be respected by clients: + + ## General Configurations + + - `token`: Authorization bearer token to use for table requests if OAuth2 security is enabled + + ## AWS Configurations + + The following configurations should be respected when working with tables stored in AWS S3 + - `client.region`: region to configure client for making requests to AWS + - `s3.access-key-id`: id for for credentials that provide access to the data in S3 + - `s3.secret-access-key`: secret for credentials that provide access to data in S3 + - `s3.session-token`: if present, this value should be used for as the session token + - `s3.remote-signing-enabled`: if `true` remote signing should be performed as described in the `s3-signer-open-api.yaml` specification + type: object + required: + - metadata + properties: + metadata-location: + type: string + description: May be null if the table is staged as part of a transaction + metadata: + $ref: '#/components/schemas/TableMetadata' + config: + type: object + additionalProperties: + type: string + + CommitTableRequest: + type: object + required: + - requirements + - updates + properties: + identifier: + description: Table identifier to update; must be present for CommitTransactionRequest + $ref: '#/components/schemas/TableIdentifier' + requirements: + type: array + items: + $ref: '#/components/schemas/TableRequirement' + updates: + type: array + items: + $ref: '#/components/schemas/TableUpdate' + + CommitViewRequest: + type: object + required: + - updates + properties: + identifier: + description: View identifier to update + $ref: '#/components/schemas/TableIdentifier' + requirements: + type: array + items: + $ref: '#/components/schemas/ViewRequirement' + updates: + type: array + items: + $ref: '#/components/schemas/ViewUpdate' + + CommitTransactionRequest: + type: object + required: + - table-changes + properties: + table-changes: + type: array + items: + description: Table commit request; must provide an `identifier` + $ref: '#/components/schemas/CommitTableRequest' + + CreateTableRequest: + type: object + required: + - name + - schema + properties: + name: + type: string + location: + type: string + schema: + $ref: '#/components/schemas/Schema' + partition-spec: + $ref: '#/components/schemas/PartitionSpec' + write-order: + $ref: '#/components/schemas/SortOrder' + stage-create: + type: boolean + properties: + type: object + additionalProperties: + type: string + + RegisterTableRequest: + type: object + required: + - name + - metadata-location + properties: + name: + type: string + metadata-location: + type: string + + CreateViewRequest: + type: object + required: + - name + - schema + - view-version + - properties + properties: + name: + type: string + location: + type: string + schema: + $ref: '#/components/schemas/Schema' + view-version: + $ref: '#/components/schemas/ViewVersion' + description: The view version to create, will replace the schema-id sent within the view-version with the id assigned to the provided schema + properties: + type: object + additionalProperties: + type: string + + LoadViewResult: + description: | + Result used when a view is successfully loaded. + + + The view metadata JSON is returned in the `metadata` field. The corresponding file location of view metadata is returned in the `metadata-location` field. + Clients can check whether metadata has changed by comparing metadata locations after the view has been created. + + The `config` map returns view-specific configuration for the view's resources. + + The following configurations should be respected by clients: + + ## General Configurations + + - `token`: Authorization bearer token to use for view requests if OAuth2 security is enabled + + type: object + required: + - metadata-location + - metadata + properties: + metadata-location: + type: string + metadata: + $ref: '#/components/schemas/ViewMetadata' + config: + type: object + additionalProperties: + type: string + + TokenType: + type: string + enum: + - urn:ietf:params:oauth:token-type:access_token + - urn:ietf:params:oauth:token-type:refresh_token + - urn:ietf:params:oauth:token-type:id_token + - urn:ietf:params:oauth:token-type:saml1 + - urn:ietf:params:oauth:token-type:saml2 + - urn:ietf:params:oauth:token-type:jwt + description: + Token type identifier, from RFC 8693 Section 3 + + + See https://datatracker.ietf.org/doc/html/rfc8693#section-3 + + OAuthClientCredentialsRequest: + description: + OAuth2 client credentials request + + + See https://datatracker.ietf.org/doc/html/rfc6749#section-4.4 + type: object + required: + - grant_type + - client_id + - client_secret + properties: + grant_type: + type: string + enum: + - client_credentials + scope: + type: string + client_id: + type: string + description: + Client ID + + + This can be sent in the request body, but OAuth2 recommends sending it in + a Basic Authorization header. + client_secret: + type: string + description: + Client secret + + + This can be sent in the request body, but OAuth2 recommends sending it in + a Basic Authorization header. + + OAuthTokenExchangeRequest: + description: + OAuth2 token exchange request + + + See https://datatracker.ietf.org/doc/html/rfc8693 + type: object + required: + - grant_type + - subject_token + - subject_token_type + properties: + grant_type: + type: string + enum: + - urn:ietf:params:oauth:grant-type:token-exchange + scope: + type: string + requested_token_type: + $ref: '#/components/schemas/TokenType' + subject_token: + type: string + description: Subject token for token exchange request + subject_token_type: + $ref: '#/components/schemas/TokenType' + actor_token: + type: string + description: Actor token for token exchange request + actor_token_type: + $ref: '#/components/schemas/TokenType' + + OAuthTokenRequest: + anyOf: + - $ref: '#/components/schemas/OAuthClientCredentialsRequest' + - $ref: '#/components/schemas/OAuthTokenExchangeRequest' + + CounterResult: + type: object + required: + - unit + - value + properties: + unit: + type: string + value: + type: integer + format: int64 + + TimerResult: + type: object + required: + - time-unit + - count + - total-duration + properties: + time-unit: + type: string + count: + type: integer + format: int64 + total-duration: + type: integer + format: int64 + + MetricResult: + anyOf: + - $ref: '#/components/schemas/CounterResult' + - $ref: '#/components/schemas/TimerResult' + + Metrics: + type: object + additionalProperties: + $ref: '#/components/schemas/MetricResult' + example: + "metrics": { + "total-planning-duration": { + "count": 1, + "time-unit": "nanoseconds", + "total-duration": 2644235116 + }, + "result-data-files": { + "unit": "count", + "value": 1, + }, + "result-delete-files": { + "unit": "count", + "value": 0, + }, + "total-data-manifests": { + "unit": "count", + "value": 1, + }, + "total-delete-manifests": { + "unit": "count", + "value": 0, + }, + "scanned-data-manifests": { + "unit": "count", + "value": 1, + }, + "skipped-data-manifests": { + "unit": "count", + "value": 0, + }, + "total-file-size-bytes": { + "unit": "bytes", + "value": 10, + }, + "total-delete-file-size-bytes": { + "unit": "bytes", + "value": 0, + } + } + + ReportMetricsRequest: + anyOf: + - $ref: '#/components/schemas/ScanReport' + - $ref: '#/components/schemas/CommitReport' + required: + - report-type + properties: + report-type: + type: string + + ScanReport: + type: object + required: + - table-name + - snapshot-id + - filter + - schema-id + - projected-field-ids + - projected-field-names + - metrics + properties: + table-name: + type: string + snapshot-id: + type: integer + format: int64 + filter: + $ref: '#/components/schemas/Expression' + schema-id: + type: integer + projected-field-ids: + type: array + items: + type: integer + projected-field-names: + type: array + items: + type: string + metrics: + $ref: '#/components/schemas/Metrics' + metadata: + type: object + additionalProperties: + type: string + + CommitReport: + type: object + required: + - table-name + - snapshot-id + - sequence-number + - operation + - metrics + properties: + table-name: + type: string + snapshot-id: + type: integer + format: int64 + sequence-number: + type: integer + format: int64 + operation: + type: string + metrics: + $ref: '#/components/schemas/Metrics' + metadata: + type: object + additionalProperties: + type: string + + NotificationRequest: + required: + - notification-type + properties: + notification-type: + $ref: '#/components/schemas/NotificationType' + payload: + $ref: '#/components/schemas/TableUpdateNotification' + + NotificationType: + type: string + enum: + - UNKNOWN + - CREATE + - UPDATE + - DROP + + TableUpdateNotification: + type: object + required: + - table-name + - timestamp + - table-uuid + - metadata-location + properties: + table-name: + type: string + timestamp: + type: integer + format: int64 + table-uuid: + type: string + metadata-location: + type: string + metadata: + $ref: '#/components/schemas/TableMetadata' + + OAuthError: + type: object + required: + - error + properties: + error: + type: string + enum: + - invalid_request + - invalid_client + - invalid_grant + - unauthorized_client + - unsupported_grant_type + - invalid_scope + error_description: + type: string + error_uri: + type: string + + OAuthTokenResponse: + type: object + required: + - access_token + - token_type + properties: + access_token: + type: string + description: + The access token, for client credentials or token exchange + token_type: + type: string + enum: + - bearer + - mac + - N_A + description: + Access token type for client credentials or token exchange + + + See https://datatracker.ietf.org/doc/html/rfc6749#section-7.1 + expires_in: + type: integer + description: + Lifetime of the access token in seconds for client credentials or token exchange + issued_token_type: + $ref: '#/components/schemas/TokenType' + refresh_token: + type: string + description: Refresh token for client credentials or token exchange + scope: + type: string + description: Authorization scope for client credentials or token exchange + + IcebergErrorResponse: + description: JSON wrapper for all error responses (non-2xx) + type: object + required: + - error + properties: + error: + $ref: '#/components/schemas/ErrorModel' + additionalProperties: false + example: + { + "error": { + "message": "The server does not support this operation", + "type": "UnsupportedOperationException", + "code": 406 + } + } + + CreateNamespaceResponse: + type: object + required: + - namespace + properties: + namespace: + $ref: '#/components/schemas/Namespace' + properties: + type: object + additionalProperties: + type: string + description: + Properties stored on the namespace, if supported by the server. + example: {"owner": "Ralph", "created_at": "1452120468"} + default: {} + + GetNamespaceResponse: + type: object + required: + - namespace + properties: + namespace: + $ref: '#/components/schemas/Namespace' + properties: + type: object + description: + Properties stored on the namespace, if supported by the server. + If the server does not support namespace properties, it should return null for this field. + If namespace properties are supported, but none are set, it should return an empty object. + additionalProperties: + type: string + example: {"owner": "Ralph", 'transient_lastDdlTime': '1452120468'} + default: {} + nullable: true + + ListTablesResponse: + type: object + properties: + next-page-token: + $ref: '#/components/schemas/PageToken' + identifiers: + type: array + uniqueItems: true + items: + $ref: '#/components/schemas/TableIdentifier' + + ListNamespacesResponse: + type: object + properties: + next-page-token: + $ref: '#/components/schemas/PageToken' + namespaces: + type: array + uniqueItems: true + items: + $ref: '#/components/schemas/Namespace' + + UpdateNamespacePropertiesResponse: + type: object + required: + - updated + - removed + properties: + updated: + description: List of property keys that were added or updated + type: array + uniqueItems: true + items: + type: string + removed: + description: List of properties that were removed + type: array + items: + type: string + missing: + type: array + items: + type: string + description: + List of properties requested for removal that were not found + in the namespace's properties. Represents a partial success response. + Server's do not need to implement this. + nullable: true + + CommitTableResponse: + type: object + required: + - metadata-location + - metadata + properties: + metadata-location: + type: string + metadata: + $ref: '#/components/schemas/TableMetadata' + + StatisticsFile: + type: object + required: + - snapshot-id + - statistics-path + - file-size-in-bytes + - file-footer-size-in-bytes + - blob-metadata + properties: + snapshot-id: + type: integer + format: int64 + statistics-path: + type: string + file-size-in-bytes: + type: integer + format: int64 + file-footer-size-in-bytes: + type: integer + format: int64 + blob-metadata: + type: array + items: + $ref: '#/components/schemas/BlobMetadata' + + BlobMetadata: + type: object + required: + - type + - snapshot-id + - sequence-number + - fields + properties: + type: + type: string + snapshot-id: + type: integer + format: int64 + sequence-number: + type: integer + format: int64 + fields: + type: array + items: + type: integer + properties: + type: object + + PartitionStatisticsFile: + type: object + required: + - snapshot-id + - statistics-path + - file-size-in-bytes + properties: + snapshot-id: + type: integer + format: int64 + statistics-path: + type: string + file-size-in-bytes: + type: integer + format: int64 + + BooleanTypeValue: + type: boolean + example: true + + IntegerTypeValue: + type: integer + example: 42 + + LongTypeValue: + type: integer + format: int64 + example: 9223372036854775807 + + FloatTypeValue: + type: number + format: float + example: 3.14 + + DoubleTypeValue: + type: number + format: double + example: 123.456 + + DecimalTypeValue: + type: string + description: + "Decimal type values are serialized as strings. Decimals with a positive scale serialize as numeric plain + text, while decimals with a negative scale use scientific notation and the exponent will be equal to the + negated scale. For instance, a decimal with a positive scale is '123.4500', with zero scale is '2', + and with a negative scale is '2E+20'" + example: "123.4500" + + StringTypeValue: + type: string + example: "hello" + + UUIDTypeValue: + type: string + format: uuid + pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$' + maxLength: 36 + minLength: 36 + description: + "UUID type values are serialized as a 36-character lowercase string in standard UUID format as specified + by RFC-4122" + example: "eb26bdb1-a1d8-4aa6-990e-da940875492c" + + DateTypeValue: + type: string + format: date + description: + "Date type values follow the 'YYYY-MM-DD' ISO-8601 standard date format" + example: "2007-12-03" + + TimeTypeValue: + type: string + description: + "Time type values follow the 'HH:MM:SS.ssssss' ISO-8601 format with microsecond precision" + example: "22:31:08.123456" + + TimestampTypeValue: + type: string + description: + "Timestamp type values follow the 'YYYY-MM-DDTHH:MM:SS.ssssss' ISO-8601 format with microsecond precision" + example: "2007-12-03T10:15:30.123456" + + TimestampTzTypeValue: + type: string + description: + "TimestampTz type values follow the 'YYYY-MM-DDTHH:MM:SS.ssssss+00:00' ISO-8601 format with microsecond precision, + and a timezone offset (+00:00 for UTC)" + example: "2007-12-03T10:15:30.123456+00:00" + + TimestampNanoTypeValue: + type: string + description: + "Timestamp_ns type values follow the 'YYYY-MM-DDTHH:MM:SS.sssssssss' ISO-8601 format with nanosecond precision" + example: "2007-12-03T10:15:30.123456789" + + TimestampTzNanoTypeValue: + type: string + description: + "Timestamp_ns type values follow the 'YYYY-MM-DDTHH:MM:SS.sssssssss+00:00' ISO-8601 format with nanosecond + precision, and a timezone offset (+00:00 for UTC)" + example: "2007-12-03T10:15:30.123456789+00:00" + + FixedTypeValue: + type: string + description: + "Fixed length type values are stored and serialized as an uppercase hexadecimal string + preserving the fixed length" + example: "78797A" + + BinaryTypeValue: + type: string + description: + "Binary type values are stored and serialized as an uppercase hexadecimal string" + example: "78797A" + + CountMap: + type: object + properties: + keys: + type: array + items: + $ref: '#/components/schemas/IntegerTypeValue' + description: "List of integer column ids for each corresponding value" + values: + type: array + items: + $ref: '#/components/schemas/LongTypeValue' + description: "List of Long values, matched to 'keys' by index" + example: + { + "keys": [1, 2], + "values": [100,200] + } + + ValueMap: + type: object + properties: + keys: + type: array + items: + $ref: '#/components/schemas/IntegerTypeValue' + description: "List of integer column ids for each corresponding value" + values: + type: array + items: + $ref: '#/components/schemas/PrimitiveTypeValue' + description: "List of primitive type values, matched to 'keys' by index" + example: + { + "keys": [1, 2], + "values": [100, "test"] + } + + PrimitiveTypeValue: + oneOf: + - $ref: '#/components/schemas/BooleanTypeValue' + - $ref: '#/components/schemas/IntegerTypeValue' + - $ref: '#/components/schemas/LongTypeValue' + - $ref: '#/components/schemas/FloatTypeValue' + - $ref: '#/components/schemas/DoubleTypeValue' + - $ref: '#/components/schemas/DecimalTypeValue' + - $ref: '#/components/schemas/StringTypeValue' + - $ref: '#/components/schemas/UUIDTypeValue' + - $ref: '#/components/schemas/DateTypeValue' + - $ref: '#/components/schemas/TimeTypeValue' + - $ref: '#/components/schemas/TimestampTypeValue' + - $ref: '#/components/schemas/TimestampTzTypeValue' + - $ref: '#/components/schemas/TimestampNanoTypeValue' + - $ref: '#/components/schemas/TimestampTzNanoTypeValue' + - $ref: '#/components/schemas/FixedTypeValue' + - $ref: '#/components/schemas/BinaryTypeValue' + + FileFormat: + type: string + enum: + - avro + - orc + - parquet + + ContentFile: + discriminator: + propertyName: content + mapping: + data: '#/components/schemas/DataFile' + position-deletes: '#/components/schemas/PositionDeleteFile' + equality-deletes: '#/components/schemas/EqualityDeleteFile' + type: object + required: + - spec-id + - content + - file-path + - file-format + - file-size-in-bytes + - record-count + properties: + content: + type: string + file-path: + type: string + file-format: + $ref: '#/components/schemas/FileFormat' + spec-id: + type: integer + partition: + type: array + items: + $ref: '#/components/schemas/PrimitiveTypeValue' + description: + "A list of partition field values ordered based on the fields of the partition spec specified by the + `spec-id`" + example: [1, "bar"] + file-size-in-bytes: + type: integer + format: int64 + description: "Total file size in bytes" + record-count: + type: integer + format: int64 + description: "Number of records in the file" + key-metadata: + allOf: + - $ref: '#/components/schemas/BinaryTypeValue' + description: "Encryption key metadata blob" + split-offsets: + type: array + items: + type: integer + format: int64 + description: "List of splittable offsets" + sort-order-id: + type: integer + + DataFile: + allOf: + - $ref: '#/components/schemas/ContentFile' + type: object + required: + - content + properties: + content: + type: string + enum: ["data"] + column-sizes: + allOf: + - $ref: '#/components/schemas/CountMap' + description: "Map of column id to total count, including null and NaN" + value-counts: + allOf: + - $ref: '#/components/schemas/CountMap' + description: "Map of column id to null value count" + null-value-counts: + allOf: + - $ref: '#/components/schemas/CountMap' + description: "Map of column id to null value count" + nan-value-counts: + allOf: + - $ref: '#/components/schemas/CountMap' + description: "Map of column id to number of NaN values in the column" + lower-bounds: + allOf: + - $ref: '#/components/schemas/ValueMap' + description: "Map of column id to lower bound primitive type values" + upper-bounds: + allOf: + - $ref: '#/components/schemas/ValueMap' + description: "Map of column id to upper bound primitive type values" + + PositionDeleteFile: + allOf: + - $ref: '#/components/schemas/ContentFile' + required: + - content + properties: + content: + type: string + enum: ["position-deletes"] + + EqualityDeleteFile: + allOf: + - $ref: '#/components/schemas/ContentFile' + required: + - content + properties: + content: + type: string + enum: ["equality-deletes"] + equality-ids: + type: array + items: + type: integer + description: "List of equality field IDs" + + ############################# + # Reusable Response Objects # + ############################# + responses: + + OAuthTokenResponse: + description: OAuth2 token response for client credentials or token exchange + content: + application/json: + schema: + $ref: '#/components/schemas/OAuthTokenResponse' + + OAuthErrorResponse: + description: OAuth2 error response + content: + application/json: + schema: + $ref: '#/components/schemas/OAuthError' + + BadRequestErrorResponse: + description: + Indicates a bad request error. It could be caused by an unexpected request + body format or other forms of request validation failure, such as invalid json. + Usually serves application/json content, although in some cases simple text/plain content might + be returned by the server's middleware. + content: + application/json: + schema: + $ref: '#/components/schemas/IcebergErrorResponse' + example: { + "error": { + "message": "Malformed request", + "type": "BadRequestException", + "code": 400 + } + } + + # Note that this is a representative example response for use as a shorthand in the spec. + # The fields `message` and `type` as indicated here are not presently prescriptive. + UnauthorizedResponse: + description: + Unauthorized. Authentication is required and has failed or has not yet been provided. + content: + application/json: + schema: + $ref: '#/components/schemas/IcebergErrorResponse' + example: { + "error": { + "message": "Not authorized to make this request", + "type": "NotAuthorizedException", + "code": 401 + } + } + + # Note that this is a representative example response for use as a shorthand in the spec. + # The fields `message` and `type` as indicated here are not presently prescriptive. + ForbiddenResponse: + description: Forbidden. Authenticated user does not have the necessary permissions. + content: + application/json: + schema: + $ref: '#/components/schemas/IcebergErrorResponse' + example: { + "error": { + "message": "Not authorized to make this request", + "type": "NotAuthorizedException", + "code": 403 + } + } + + # Note that this is a representative example response for use as a shorthand in the spec. + # The fields `message` and `type` as indicated here are not presently prescriptive. + UnsupportedOperationResponse: + description: Not Acceptable / Unsupported Operation. The server does not support this operation. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorModel' + example: { + "error": { + "message": "The server does not support this operation", + "type": "UnsupportedOperationException", + "code": 406 + } + } + + IcebergErrorResponse: + description: JSON wrapper for all error responses (non-2xx) + content: + application/json: + schema: + $ref: '#/components/schemas/IcebergErrorResponse' + example: { + "error": { + "message": "The server does not support this operation", + "type": "UnsupportedOperationException", + "code": 406 + }} + + CreateNamespaceResponse: + description: + Represents a successful call to create a namespace. + Returns the namespace created, as well as any properties that were stored for the namespace, + including those the server might have added. Implementations are not required to support namespace + properties. + content: + application/json: + schema: + $ref: '#/components/schemas/CreateNamespaceResponse' + example: { + "namespace": ["accounting", "tax"], + "properties": {"owner": "Ralph", "created_at": "1452120468"} + } + + GetNamespaceResponse: + description: + Returns a namespace, as well as any properties stored on the namespace if namespace properties + are supported by the server. + content: + application/json: + schema: + $ref: '#/components/schemas/GetNamespaceResponse' + + ListTablesResponse: + description: A list of table identifiers + content: + application/json: + schema: + $ref: '#/components/schemas/ListTablesResponse' + examples: + ListTablesResponseNonEmpty: + $ref: '#/components/examples/ListTablesNonEmptyExample' + ListTablesResponseEmpty: + $ref: '#/components/examples/ListTablesEmptyExample' + + ListNamespacesResponse: + description: A list of namespaces + content: + application/json: + schema: + $ref: '#/components/schemas/ListNamespacesResponse' + examples: + NonEmptyResponse: + $ref: '#/components/examples/ListNamespacesNonEmptyExample' + EmptyResponse: + $ref: '#/components/examples/ListNamespacesEmptyExample' + + AuthenticationTimeoutResponse: + description: + Credentials have timed out. If possible, the client should refresh credentials and retry. + content: + application/json: + schema: + $ref: '#/components/schemas/IcebergErrorResponse' + example: { + "error": { + "message": "Credentials have timed out", + "type": "AuthenticationTimeoutException", + "code": 419 + } + } + + ServiceUnavailableResponse: + description: + The service is not ready to handle the request. The client should wait and retry. + + + The service may additionally send a Retry-After header to indicate when to retry. + content: + application/json: + schema: + $ref: '#/components/schemas/IcebergErrorResponse' + example: { + "error": { + "message": "Slow down", + "type": "SlowDownException", + "code": 503 + } + } + + ServerErrorResponse: + description: + A server-side problem that might not be addressable from the client + side. Used for server 5xx errors without more specific documentation in + individual routes. + content: + application/json: + schema: + $ref: '#/components/schemas/IcebergErrorResponse' + example: { + "error": { + "message": "Internal Server Error", + "type": "InternalServerError", + "code": 500 + } + } + + UpdateNamespacePropertiesResponse: + description: JSON data response for a synchronous update properties request. + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateNamespacePropertiesResponse' + example: { + "updated": ["owner"], + "removed": ["foo"], + "missing": ["bar"] + } + + CreateTableResponse: + description: Table metadata result after creating a table + content: + application/json: + schema: + $ref: '#/components/schemas/LoadTableResult' + + LoadTableResponse: + description: Table metadata result when loading a table + content: + application/json: + schema: + $ref: '#/components/schemas/LoadTableResult' + + LoadViewResponse: + description: View metadata result when loading a view + content: + application/json: + schema: + $ref: '#/components/schemas/LoadViewResult' + + CommitTableResponse: + description: + Response used when a table is successfully updated. + + The table metadata JSON is returned in the metadata field. The corresponding file location of table metadata must be returned in the metadata-location field. Clients can check whether metadata has changed by comparing metadata locations. + content: + application/json: + schema: + $ref: '#/components/schemas/CommitTableResponse' + + ####################################### + # Common examples of different values # + ####################################### + examples: + + ListTablesEmptyExample: + summary: An empty list for a namespace with no tables + value: { + "identifiers": [] + } + + ListNamespacesEmptyExample: + summary: An empty list of namespaces + value: { + "namespaces": [] + } + + ListNamespacesNonEmptyExample: + summary: A non-empty list of namespaces + value: { + "namespaces": [ + ["accounting", "tax"], + ["accounting", "credits"] + ] + } + + ListTablesNonEmptyExample: + summary: A non-empty list of table identifiers + value: { + "identifiers": [ + {"namespace": ["accounting", "tax"], "name": "paid"}, + {"namespace": ["accounting", "tax"], "name": "owed"} + ] + } + + MultipartNamespaceAsPathVariable: + summary: A multi-part namespace, as represented in a path parameter + value: "accounting%1Ftax" + + NamespaceAsPathVariable: + summary: A single part namespace, as represented in a path paremeter + value: "accounting" + + NamespaceAlreadyExistsError: + summary: The requested namespace already exists + value: { + "error": { + "message": "The given namespace already exists", + "type": "AlreadyExistsException", + "code": 409 + } + } + + NoSuchTableError: + summary: The requested table does not exist + value: { + "error": { + "message": "The given table does not exist", + "type": "NoSuchTableException", + "code": 404 + } + } + + NoSuchViewError: + summary: The requested view does not exist + value: { + "error": { + "message": "The given view does not exist", + "type": "NoSuchViewException", + "code": 404 + } + } + + NoSuchNamespaceError: + summary: The requested namespace does not exist + value: { + "error": { + "message": "The given namespace does not exist", + "type": "NoSuchNamespaceException", + "code": 404 + } + } + + RenameTableSameNamespace: + summary: Rename a table in the same namespace + value: { + "source": {"namespace": ["accounting", "tax"], "name": "paid"}, + "destination": {"namespace": ["accounting", "tax"], "name": "owed"} + } + + RenameViewSameNamespace: + summary: Rename a view in the same namespace + value: { + "source": {"namespace": ["accounting", "tax"], "name": "paid-view"}, + "destination": {"namespace": ["accounting", "tax"], "name": "owed-view"} + } + + TableAlreadyExistsError: + summary: The requested table identifier already exists + value: { + "error": { + "message": "The given table already exists", + "type": "AlreadyExistsException", + "code": 409 + } + } + + ViewAlreadyExistsError: + summary: The requested view identifier already exists + value: { + "error": { + "message": "The given view already exists", + "type": "AlreadyExistsException", + "code": 409 + } + } + + # This is an example response and is not meant to be prescriptive regarding the message or type. + UnprocessableEntityDuplicateKey: + summary: + The request body either has the same key multiple times in what should be a map with unique keys + or the request body has keys in two or more fields which should be disjoint sets. + value: { + "error": { + "message": "The request cannot be processed as there is a key present multiple times", + "type": "UnprocessableEntityException", + "code": 422 + } + } + + UpdateAndRemoveNamespacePropertiesRequest: + summary: An update namespace properties request with both properties to remove and properties to upsert. + value: { + "removals": ["foo", "bar"], + "updates": {"owner": "Raoul"} + } + + securitySchemes: + OAuth2: + type: oauth2 + description: + This scheme is used for OAuth2 authorization. + + + For unauthorized requests, services should return an appropriate 401 or + 403 response. Implementations must not return altered success (200) + responses when a request is unauthenticated or unauthorized. + + If a separate authorization server is used, substitute the tokenUrl with + the full token path of the external authorization server, and use the + resulting token to access the resources defined in the spec. + flows: + clientCredentials: + tokenUrl: /v1/oauth/tokens + scopes: + catalog: Allows interacting with the Config and Catalog APIs + BearerAuth: + type: http + scheme: bearer \ No newline at end of file From 9505e37dc97682a7298f578e5bd60411400691f1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 26 Jul 2024 15:33:25 -0700 Subject: [PATCH 02/27] Bump gradle/actions from 3.4.2 to 3.5.0 (#14) Bumps [gradle/actions](https://github.com/gradle/actions) from 3.4.2 to 3.5.0. - [Release notes](https://github.com/gradle/actions/releases) - [Commits](https://github.com/gradle/actions/compare/dbbdc275be76ac10734476cc723d82dfe7ec6eda...d9c87d481d55275bb5441eef3fe0e46805f9ef70) --- updated-dependencies: - dependency-name: gradle/actions dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/gradle.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 64bd8be24c..4ca664b9d2 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -31,7 +31,7 @@ jobs: # Configure Gradle for optimal use in GiHub Actions, including caching of downloaded dependencies. # See: https://github.com/gradle/actions/blob/main/setup-gradle/README.md - name: Setup Gradle - uses: gradle/actions/setup-gradle@dbbdc275be76ac10734476cc723d82dfe7ec6eda # v3.4.2 + uses: gradle/actions/setup-gradle@d9c87d481d55275bb5441eef3fe0e46805f9ef70 # v3.5.0 - name: Check formatting run: ./gradlew check @@ -52,7 +52,7 @@ jobs: # If your project does not have the Gradle Wrapper configured, you can use the following configuration to run Gradle with a specified version. # # - name: Setup Gradle - # uses: gradle/actions/setup-gradle@dbbdc275be76ac10734476cc723d82dfe7ec6eda # v3.4.2 + # uses: gradle/actions/setup-gradle@d9c87d481d55275bb5441eef3fe0e46805f9ef70 # v3.5.0 # with: # gradle-version: '8.6' # From e1339e5fdf55c9af7fde8bfdbba9d7ee3c048f3c Mon Sep 17 00:00:00 2001 From: Anoop Johnson Date: Fri, 26 Jul 2024 15:39:33 -0700 Subject: [PATCH 03/27] Fix typo in the quickstart docs. (#17) Co-authored-by: Michael Collado <40346148+collado-mike@users.noreply.github.com> --- docs/quickstart.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/quickstart.md b/docs/quickstart.md index d42dcdcc34..27f8b28261 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -107,7 +107,7 @@ At this point, Polaris is running. ## Bootstrapping Polaris -For this tutortial, we'll launch an instance of Polaris that stores entities only in-memory. This means that any entities that you define will be destroyed when Polaris is shut down. It also means that Polaris will automatically bootstrap itself with root credentials. For more information on how to configure Polaris for production usage, see the [docs](./configuring-polaris-for-production.md). +For this tutorial, we'll launch an instance of Polaris that stores entities only in-memory. This means that any entities that you define will be destroyed when Polaris is shut down. It also means that Polaris will automatically bootstrap itself with root credentials. For more information on how to configure Polaris for production usage, see the [docs](./configuring-polaris-for-production.md). When Polaris is launched using in-memory mode the root `CLIENT_ID` and `CLIENT_SECRET` can be found in stdout on initial startup. For example: @@ -308,4 +308,4 @@ Spark will lose access to the table: spark.sql("SELECT * FROM quickstart_table").show(false) org.apache.iceberg.exceptions.ForbiddenException: Forbidden: Principal 'quickstart_user' with activated PrincipalRoles '[]' and activated ids '[6, 7]' is not authorized for op LOAD_TABLE_WITH_READ_DELEGATION -``` \ No newline at end of file +``` From 5685fcbe1893c62b15d6abfe9ecf41198216abee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?JB=20Onofr=C3=A9?= Date: Sat, 27 Jul 2024 00:56:47 +0200 Subject: [PATCH 04/27] Add base skeleton with updated LICENSE and NOTICE file (#8) Co-authored-by: Michael Collado <40346148+collado-mike@users.noreply.github.com> --- CODE_OF_CONDUCT.md | 102 +++++++++++++++++++++++++++++++++++++++++++++ CONTRIBUTING.md | 60 ++++++++++++++++++++++++++ LICENSE | 2 + NOTICE | 8 ++++ README.md | 16 +++++++ 5 files changed, 188 insertions(+) create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 NOTICE diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000000..866e1c0ffe --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,102 @@ + + +# Contributor Code of Conduct + +This is a copy of the [Contributor Covenant v2.1](https://www.contributor-covenant.org/version/2/1/code_of_conduct.html). No changes have been made. + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at . +All complaints will be reviewed and investigated promptly and fairly. +All community leaders are obligated to respect the privacy and security of the reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of actions. + +**Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.1, available at [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at [https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000..91ba713728 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,60 @@ + + +# Contributing to Polaris + +You want to contribute to Polaris: thank you! +Any contribution (code, test cases, documentation, use cases, ...) is valuable. + +This documentation will help you to start your contribution. + +## Report bugs and feature requests + +You can report an issue in Polaris [issue tracker](https://github.com/polaris-catalog/polaris-dev/issues). + +When reporting a bug make sure you document the steps to reproduce the issue and provide all necessary information (Apache Iceberg version, Catalog capabilities enabled, ...). +When creating a feature request document your requirements first. Please, try to not directly describe the solution. + +If you want to dive into development yourself then you can also browse for open issues or features that need to be implemented. Take ownership of an issue and try fix it. Before doing a bigger change, please describe the concept/design of what you plan to do. If unsure if the design is good or will be accepted, discuss it as issue comments. + +## Provide changes in a Pull Request + +The best way to provide changes is to fork Polaris repository on GitHub and provide a Pull Request with your changes. To make it easy to apply your changes please use the following conventions: + +* Every Pull Request should have a matching GitHub Issue. +* Create a branch that will house your change: + +```bash +git clone https://github.com/polaris/polaris-dev +cd polaris-dev +git fetch --all +git checkout -b my-branch origin/main +``` + + Don't forget to periodically rebase your branch: + +```bash +git pull --rebase +git push GitHubUser my-branch --force +``` + +* Pull Requests should be based on the `main` branch. +* Test that your changes works by adapting or adding tests. Verify the build passes (see `README.md` for build instructions). +* If your Pull Request has conflicts with the `main` branch, please rebase and fix the conflicts. + +## License + +When contributing to this project, you agree that your contributions use the Apache License version 2. Please ensure you have permission to do this if required by your employer. diff --git a/LICENSE b/LICENSE index 261eeb9e9f..e1816c9106 100644 --- a/LICENSE +++ b/LICENSE @@ -199,3 +199,5 @@ 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. + + Apache Iceberg diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000000..afa62e9a3d --- /dev/null +++ b/NOTICE @@ -0,0 +1,8 @@ +Polaris +Copyright 2024 Snowflake Computing Inc. + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). + +Apache Iceberg +Copyright 2017-2022 The Apache Software Foundation diff --git a/README.md b/README.md index bd2f49977c..b853c850b6 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,19 @@ + + # Polaris Catalog Polaris Catalog is an open source catalog for Apache Iceberg. Polaris Catalog implements Iceberg’s open REST API for multi-engine interoperability with Apache Doris, Apache Flink, Apache Spark, PyIceberg, StarRocks and Trino. From abaf2a9b7d0a01d8f713a3f6dd5deaf79560a114 Mon Sep 17 00:00:00 2001 From: Tyler Akidau Date: Sat, 27 Jul 2024 03:45:04 +0200 Subject: [PATCH 05/27] Suppress unchecked cast warning in DefaultConfigurationStore (#18) --- .../io/polaris/service/config/DefaultConfigurationStore.java | 1 + 1 file changed, 1 insertion(+) diff --git a/polaris-service/src/main/java/io/polaris/service/config/DefaultConfigurationStore.java b/polaris-service/src/main/java/io/polaris/service/config/DefaultConfigurationStore.java index fad9ec4177..389893292c 100644 --- a/polaris-service/src/main/java/io/polaris/service/config/DefaultConfigurationStore.java +++ b/polaris-service/src/main/java/io/polaris/service/config/DefaultConfigurationStore.java @@ -12,6 +12,7 @@ public DefaultConfigurationStore(Map properties) { this.properties = properties; } + @SuppressWarnings("unchecked") @Override public @Nullable T getConfiguration(PolarisCallContext ctx, String configName) { return (T) properties.get(configName); From 3c4ae7c16660489e0a42f629bf368d28b01036f7 Mon Sep 17 00:00:00 2001 From: Robert Stupp Date: Sat, 27 Jul 2024 10:44:58 +0200 Subject: [PATCH 06/27] Add some more exclusions to `.gitignore` (#20) --- .gitignore | 43 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 37 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index d18db69378..97f280ecce 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,11 @@ polaris-service/logs/ -regtests/derby.log -regtests/metastore_db regtests/output/ + +# Notebooks notebooks/.ipynb_checkpoints/ -.gradle -**/build/ -!src/**/build/ + +# Metastore +metastore_db/ # Ignore Gradle GUI config gradle-app.setting @@ -26,4 +26,35 @@ gradle-app.setting .classpath .env .java-version -**/*.iml + +# IntelliJ +/.idea +*.iml +*.ipr +*.iws + +# Gradle +/.gradle +**/build/ +!src/**/build/ + +# jenv +.java-version + +# Log files +*.log +logs/ + +# binary files +*.class +*.jar +*.zip +*.tar.gz +*.tgz + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +# macOS +*.DS_Store +.DS_Store From b7401c45ff087bf4ca09838a306038f72f4268a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?JB=20Onofr=C3=A9?= Date: Sun, 28 Jul 2024 07:20:09 +0200 Subject: [PATCH 07/27] Add Apache license header (#19) --- .github/CODEOWNERS | 16 ++++++++++++++++ .github/dependabot.yml | 16 ++++++++++++++++ .github/pull_request_template.md | 15 +++++++++++++++ .github/workflows/gradle.yml | 16 ++++++++++++++++ .github/workflows/regtest.yml | 16 ++++++++++++++++ .github/workflows/semgrep.yml | 16 ++++++++++++++++ .github/workflows/stale.yml | 16 ++++++++++++++++ Dockerfile | 15 +++++++++++++++ README.md | 4 +++- build.gradle | 16 ++++++++++++++++ docker-compose-jupyter.yml | 16 ++++++++++++++++ docker-compose.yml | 16 ++++++++++++++++ docs/entities.md | 16 ++++++++++++++++ docs/iceberg-rest/index.html | 19 +++++++++++++++++++ docs/index.html | 16 ++++++++++++++++ docs/polaris-management/index.html | 16 ++++++++++++++++ docs/quickstart.md | 16 ++++++++++++++++ .../persistence/eclipselink/build.gradle | 15 +++++++++++++++ ...pseLinkPolarisMetaStoreManagerFactory.java | 15 +++++++++++++++ ...olarisEclipseLinkMetaStoreSessionImpl.java | 15 +++++++++++++++ .../eclipselink/PolarisEclipseLinkStore.java | 15 +++++++++++++++ .../PolarisEclipseLinkMetaStoreTest.java | 17 +++++++++++++++-- gradle.properties | 15 +++++++++++++++ gradle/wrapper/gradle-wrapper.properties | 16 ++++++++++++++++ gradlew | 6 ++---- kind-registry.sh | 15 +++++++++++++++ notebooks/Dockerfile | 16 ++++++++++++++++ polaris | 16 +++++++++++++++- polaris-core/build.gradle | 16 ++++++++++++++++ .../io/polaris/core/PolarisCallContext.java | 15 +++++++++++++++ .../io/polaris/core/PolarisConfiguration.java | 15 +++++++++++++++ .../core/PolarisConfigurationStore.java | 15 +++++++++++++++ .../core/PolarisDefaultDiagServiceImpl.java | 15 +++++++++++++++ .../io/polaris/core/PolarisDiagnostics.java | 15 +++++++++++++++ .../auth/AuthenticatedPolarisPrincipal.java | 15 +++++++++++++++ .../auth/PolarisAuthorizableOperation.java | 15 +++++++++++++++ .../polaris/core/auth/PolarisAuthorizer.java | 15 +++++++++++++++ .../core/catalog/PolarisCatalogHelpers.java | 15 +++++++++++++++ .../io/polaris/core/context/CallContext.java | 15 +++++++++++++++ .../io/polaris/core/context/RealmContext.java | 15 +++++++++++++++ .../io/polaris/core/entity/AsyncTaskType.java | 15 +++++++++++++++ .../io/polaris/core/entity/CatalogEntity.java | 15 +++++++++++++++ .../core/entity/CatalogRoleEntity.java | 15 +++++++++++++++ .../polaris/core/entity/NamespaceEntity.java | 15 +++++++++++++++ .../core/entity/PolarisBaseEntity.java | 15 +++++++++++++-- .../entity/PolarisChangeTrackingVersions.java | 15 +++++++++++++++ .../core/entity/PolarisEntitiesActiveKey.java | 15 +++++++++++++-- .../io/polaris/core/entity/PolarisEntity.java | 15 +++++++++++++++ .../entity/PolarisEntityActiveRecord.java | 15 +++++++++++++-- .../core/entity/PolarisEntityConstants.java | 15 +++++++++++++-- .../core/entity/PolarisEntityCore.java | 15 +++++++++++++-- .../polaris/core/entity/PolarisEntityId.java | 15 +++++++++++++++ .../core/entity/PolarisEntitySubType.java | 15 +++++++++++++-- .../core/entity/PolarisEntityType.java | 15 +++++++++++++-- .../core/entity/PolarisGrantRecord.java | 15 +++++++++++++-- .../core/entity/PolarisPrincipalSecrets.java | 15 +++++++++++++-- .../polaris/core/entity/PolarisPrivilege.java | 15 +++++++++++++-- .../core/entity/PolarisTaskConstants.java | 15 +++++++++++++++ .../polaris/core/entity/PrincipalEntity.java | 15 +++++++++++++++ .../core/entity/PrincipalRoleEntity.java | 15 +++++++++++++++ .../polaris/core/entity/TableLikeEntity.java | 15 +++++++++++++++ .../io/polaris/core/entity/TaskEntity.java | 15 +++++++++++++++ .../core/monitor/PolarisMetricRegistry.java | 15 +++++++++++++++ .../LocalPolarisMetaStoreManagerFactory.java | 15 +++++++++++++++ .../persistence/MetaStoreManagerFactory.java | 15 +++++++++++++++ .../persistence/PolarisEntityManager.java | 15 +++++++++++++++ .../persistence/PolarisEntityResolver.java | 15 +++++++++++++-- .../persistence/PolarisMetaStoreManager.java | 15 +++++++++++++-- .../PolarisMetaStoreManagerImpl.java | 15 +++++++++++++++ .../persistence/PolarisMetaStoreSession.java | 15 +++++++++++++++ .../persistence/PolarisObjectMapperUtil.java | 15 +++++++++++++++ .../PolarisResolvedPathWrapper.java | 15 +++++++++++++++ .../PolarisTreeMapMetaStoreSessionImpl.java | 15 +++++++++++++++ .../core/persistence/PolarisTreeMapStore.java | 15 +++++++++++++++ .../persistence/ResolvedPolarisEntity.java | 15 +++++++++++++++ .../RetryOnConcurrencyException.java | 15 +++++++++++++++ .../core/persistence/cache/EntityCache.java | 15 +++++++++++++++ .../cache/EntityCacheByNameKey.java | 15 +++++++++++++++ .../persistence/cache/EntityCacheEntry.java | 15 +++++++++++++++ .../cache/EntityCacheLookupResult.java | 15 +++++++++++++++ .../persistence/cache/EntityCacheMode.java | 15 +++++++++++++++ .../core/persistence/models/ModelEntity.java | 15 +++++++++++++-- .../persistence/models/ModelEntityActive.java | 15 +++++++++++++-- .../models/ModelEntityChangeTracking.java | 15 +++++++++++++++ .../models/ModelEntityDropped.java | 15 +++++++++++++++ .../persistence/models/ModelGrantRecord.java | 15 +++++++++++++-- .../models/ModelPrincipalSecrets.java | 15 +++++++++++++-- .../persistence/models/ModelSequenceId.java | 15 +++++++++++++++ .../resolver/PolarisResolutionManifest.java | 15 +++++++++++++++ .../PolarisResolutionManifestCatalogView.java | 15 +++++++++++++++ .../core/persistence/resolver/Resolver.java | 15 +++++++++++++++ .../resolver/ResolverEntityName.java | 15 +++++++++++++++ .../persistence/resolver/ResolverPath.java | 15 +++++++++++++++ .../resolver/ResolverPrincipalRole.java | 15 +++++++++++++++ .../persistence/resolver/ResolverStatus.java | 15 +++++++++++++++ .../storage/FileStorageConfigurationInfo.java | 15 +++++++++++++++ .../storage/InMemoryStorageIntegration.java | 15 +++++++++++++++ .../storage/PolarisCredentialProperty.java | 15 +++++++++++++++ .../core/storage/PolarisStorageActions.java | 15 +++++++++++++++ .../PolarisStorageConfigurationInfo.java | 15 +++++++++++++++ .../storage/PolarisStorageIntegration.java | 15 +++++++++++++++ .../PolarisStorageIntegrationProvider.java | 15 +++++++++++++++ .../aws/AwsCredentialsStorageIntegration.java | 15 +++++++++++++++ .../aws/AwsStorageConfigurationInfo.java | 15 +++++++++++++++ .../aws/PolarisS3FileIOClientFactory.java | 15 +++++++++++++++ .../AzureCredentialsStorageIntegration.java | 15 +++++++++++++++ .../core/storage/azure/AzureLocation.java | 15 +++++++++++++++ .../azure/AzureStorageConfigurationInfo.java | 15 +++++++++++++++ .../storage/cache/StorageCredentialCache.java | 15 +++++++++++++++ .../cache/StorageCredentialCacheEntry.java | 15 +++++++++++++++ .../cache/StorageCredentialCacheKey.java | 15 +++++++++++++++ .../gcp/GcpCredentialsStorageIntegration.java | 15 +++++++++++++++ .../gcp/GcpStorageConfigurationInfo.java | 15 +++++++++++++++ .../core/persistence/EntityCacheTest.java | 15 +++++++++++++++ .../PolarisObjectMapperUtilTest.java | 15 +++++++++++++++ .../PolarisTreeMapMetaStoreManagerTest.java | 15 +++++++++++++++ .../core/persistence/ResolverTest.java | 15 +++++++++++++++ .../InMemoryStorageIntegrationTest.java | 15 +++++++++++++++ .../cache/StorageCredentialCacheTest.java | 15 +++++++++++++++ .../AwsCredentialsStorageIntegrationTest.java | 15 +++++++++++++++ ...AzureCredentialStorageIntegrationTest.java | 15 +++++++++++++++ .../storage/azure/AzureLocationTest.java | 15 +++++++++++++++ .../GcpCredentialsStorageIntegrationTest.java | 15 +++++++++++++++ .../PolarisMetaStoreManagerTest.java | 15 +++++++++++++-- .../PolarisTestMetaStoreManager.java | 15 +++++++++++++-- polaris-server.yml | 15 +++++++++++++++ polaris-service/build.gradle | 16 ++++++++++++++++ .../service/BootstrapRealmsCommand.java | 15 +++++++++++++++ .../service/IcebergExceptionMapper.java | 15 +++++++++++++++ ...IcebergJerseyViolationExceptionMapper.java | 15 +++++++++++++++ .../IcebergJsonProcessingExceptionMapper.java | 15 +++++++++++++++ .../polaris/service/PolarisApplication.java | 15 +++++++++++++++ .../polaris/service/PolarisHealthCheck.java | 15 +++++++++++++++ .../TimedApplicationEventListener.java | 15 +++++++++++++++ .../service/admin/PolarisAdminService.java | 15 +++++++++++++++ .../service/admin/PolarisServiceImpl.java | 15 +++++++++++++++ .../auth/BasePolarisAuthenticator.java | 15 +++++++++++++++ .../io/polaris/service/auth/DecodedToken.java | 15 +++++++++++++++ .../service/auth/DefaultOAuth2ApiService.java | 15 +++++++++++++++ .../auth/DefaultPolarisAuthenticator.java | 15 +++++++++++++++ .../auth/DiscoverableAuthenticator.java | 15 +++++++++++++++ .../io/polaris/service/auth/JWTBroker.java | 15 +++++++++++++++ .../polaris/service/auth/JWTRSAKeyPair.java | 15 +++++++++++++++ .../service/auth/JWTRSAKeyPairFactory.java | 15 +++++++++++++++ .../service/auth/JWTSymmetricKeyBroker.java | 15 +++++++++++++++ .../service/auth/JWTSymmetricKeyFactory.java | 15 +++++++++++++++ .../io/polaris/service/auth/KeyProvider.java | 15 +++++++++++++++ .../service/auth/LocalRSAKeyProvider.java | 15 +++++++++++++++ .../service/auth/OAuthTokenErrorResponse.java | 15 +++++++++++++++ .../io/polaris/service/auth/OAuthUtils.java | 15 +++++++++++++++ .../io/polaris/service/auth/PemUtils.java | 15 +++++++++++++++ ...InlineBearerTokenPolarisAuthenticator.java | 15 +++++++++++++++ .../service/auth/TestOAuth2ApiService.java | 15 +++++++++++++++ .../io/polaris/service/auth/TokenBroker.java | 15 +++++++++++++++ .../service/auth/TokenBrokerFactory.java | 15 +++++++++++++++ .../auth/TokenInfoExchangeResponse.java | 15 +++++++++++++++ .../service/auth/TokenRequestValidator.java | 15 +++++++++++++++ .../polaris/service/auth/TokenResponse.java | 15 +++++++++++++++ .../service/catalog/BasePolarisCatalog.java | 15 +++++++++++++++ .../catalog/IcebergCatalogAdapter.java | 15 +++++++++++++++ .../catalog/PolarisCatalogHandlerWrapper.java | 15 +++++++++++++++ .../catalog/SupportsCredentialDelegation.java | 15 +++++++++++++++ .../catalog/SupportsNotifications.java | 15 +++++++++++++++ .../config/ConfigurationStoreAware.java | 15 +++++++++++++++ .../service/config/CorsConfiguration.java | 15 +++++++++++++++ .../config/DefaultConfigurationStore.java | 15 +++++++++++++++ .../config/HasEntityManagerFactory.java | 15 +++++++++++++++ .../service/config/OAuth2ApiService.java | 15 +++++++++++++++ .../config/PolarisApplicationConfig.java | 15 +++++++++++++++ .../config/RealmEntityManagerFactory.java | 15 +++++++++++++++ .../polaris/service/config/Serializers.java | 15 +++++++++++++++ .../config/TaskHandlerConfiguration.java | 15 +++++++++++++++ .../context/CallContextCatalogFactory.java | 15 +++++++++++++++ .../service/context/CallContextResolver.java | 15 +++++++++++++++ .../context/DefaultContextResolver.java | 15 +++++++++++++++ .../PolarisCallContextCatalogFactory.java | 15 +++++++++++++++ .../service/context/RealmContextResolver.java | 15 +++++++++++++++ .../SqlliteCallContextCatalogFactory.java | 15 +++++++++++++++ .../logging/PolarisJsonLayoutFactory.java | 15 +++++++++++++++ ...nMemoryPolarisMetaStoreManagerFactory.java | 15 +++++++++++++++ .../io/polaris/service/resource/TimedApi.java | 15 +++++++++++++++ ...PolarisStorageIntegrationProviderImpl.java | 15 +++++++++++++++ .../task/ManifestFileCleanupTaskHandler.java | 15 +++++++++++++++ .../service/task/TableCleanupTaskHandler.java | 15 +++++++++++++++ .../io/polaris/service/task/TaskExecutor.java | 15 +++++++++++++++ .../service/task/TaskExecutorImpl.java | 15 +++++++++++++++ .../service/task/TaskFileIOSupplier.java | 15 +++++++++++++++ .../io/polaris/service/task/TaskHandler.java | 15 +++++++++++++++ .../io/polaris/service/task/TaskUtils.java | 15 +++++++++++++++ .../service/tracing/HeadersMapAccessor.java | 15 +++++++++++++++ .../service/tracing/OpenTelemetryAware.java | 15 +++++++++++++++ .../service/tracing/TracingFilter.java | 15 +++++++++++++++ .../service/types/CommitTableRequest.java | 15 +++++++++++++++ .../service/types/CommitViewRequest.java | 15 +++++++++++++++ .../service/types/NotificationRequest.java | 15 +++++++++++++++ .../service/types/NotificationType.java | 15 +++++++++++++++ .../types/TableUpdateNotification.java | 15 +++++++++++++++ .../io/polaris/service/types/TokenType.java | 15 +++++++++++++++ .../main/resources/META-INF/persistence.xml | 16 ++++++++++++++++ .../io.dropwizard.jackson.Discoverable | 16 ++++++++++++++++ ...ng.common.layout.DiscoverableLayoutFactory | 16 ++++++++++++++++ ...s.core.persistence.MetaStoreManagerFactory | 16 ++++++++++++++++ ...io.polaris.service.auth.TokenBrokerFactory | 17 ++++++++++++++++- ...io.polaris.service.config.OAuth2ApiService | 16 ++++++++++++++++ ...olaris.service.context.CallContextResolver | 16 ++++++++++++++++ ...laris.service.context.RealmContextResolver | 16 ++++++++++++++++ .../src/main/resources/log4j.properties | 16 ++++++++++++++++ .../PolarisApplicationIntegrationTest.java | 15 +++++++++++++++ .../admin/PolarisAdminServiceAuthzTest.java | 15 +++++++++++++++ .../service/admin/PolarisAuthzTestBase.java | 15 +++++++++++++++ .../PolarisServiceImplIntegrationTest.java | 15 +++++++++++++++ .../service/auth/JWTRSAKeyPairTest.java | 15 +++++++++++++++ .../auth/JWTSymmetricKeyGeneratorTest.java | 15 +++++++++++++++ .../auth/TokenRequestValidatorTest.java | 15 +++++++++++++++ .../io/polaris/service/auth/TokenUtils.java | 15 +++++++++++++++ .../catalog/BasePolarisCatalogTest.java | 15 +++++++++++++++ .../catalog/BasePolarisCatalogViewTest.java | 15 +++++++++++++++ ...PolarisCatalogHandlerWrapperAuthzTest.java | 15 +++++++++++++++ .../PolarisPassthroughResolutionView.java | 15 +++++++++++++++ .../PolarisRestCatalogIntegrationTest.java | 15 +++++++++++++++ ...PolarisRestCatalogViewIntegrationTest.java | 15 +++++++++++++++ .../catalog/PolarisSparkIntegrationTest.java | 15 +++++++++++++++ .../service/entity/CatalogEntityTest.java | 15 +++++++++++++++ .../ManifestFileCleanupTaskHandlerTest.java | 15 +++++++++++++++ .../task/TableCleanupTaskHandlerTest.java | 15 +++++++++++++++ .../polaris/service/task/TaskTestUtils.java | 15 +++++++++++++++ .../io/polaris/service/task/TestSnapshot.java | 15 +++++++++++++++ .../test/PolarisConnectionExtension.java | 15 +++++++++++++++ .../test/SnowmanCredentialsExtension.java | 15 +++++++++++++++ .../test/resources/META-INF/persistence.xml | 16 ++++++++++++++++ ...ris.service.auth.DiscoverableAuthenticator | 16 ++++++++++++++++ .../polaris-server-integrationtest.yml | 16 ++++++++++++++++ regtests/Dockerfile | 14 ++++++++++++++ regtests/README.md | 18 ++++++++++++++++++ .../python/.github/workflows/python.yml | 16 ++++++++++++++++ regtests/client/python/.gitlab-ci.yml | 15 +++++++++++++++ regtests/client/python/.travis.yml | 15 +++++++++++++++ regtests/client/python/README.md | 17 +++++++++++++++++ .../client/python/cli/command/__init__.py | 15 +++++++++++++++ .../python/cli/command/catalog_roles.py | 15 +++++++++++++++ .../client/python/cli/command/catalogs.py | 15 +++++++++++++++ .../python/cli/command/principal_roles.py | 15 +++++++++++++++ .../client/python/cli/command/principals.py | 15 +++++++++++++++ .../client/python/cli/command/privileges.py | 15 +++++++++++++++ regtests/client/python/cli/constants.py | 15 +++++++++++++++ .../client/python/cli/options/option_tree.py | 15 +++++++++++++++ regtests/client/python/cli/options/parser.py | 15 +++++++++++++++ regtests/client/python/cli/polaris_cli.py | 15 +++++++++++++++ .../client/python/docs/AddGrantRequest.md | 18 +++++++++++++++++- .../python/docs/AddPartitionSpecUpdate.md | 18 +++++++++++++++++- .../client/python/docs/AddSchemaUpdate.md | 18 +++++++++++++++++- .../client/python/docs/AddSnapshotUpdate.md | 18 +++++++++++++++++- .../client/python/docs/AddSortOrderUpdate.md | 18 +++++++++++++++++- .../python/docs/AddViewVersionUpdate.md | 18 +++++++++++++++++- .../client/python/docs/AndOrExpression.md | 18 +++++++++++++++++- regtests/client/python/docs/AssertCreate.md | 17 +++++++++++++++++ .../python/docs/AssertCurrentSchemaId.md | 17 +++++++++++++++++ .../python/docs/AssertDefaultSortOrderId.md | 17 +++++++++++++++++ .../client/python/docs/AssertDefaultSpecId.md | 17 +++++++++++++++++ .../python/docs/AssertLastAssignedFieldId.md | 17 +++++++++++++++++ .../docs/AssertLastAssignedPartitionId.md | 17 +++++++++++++++++ .../client/python/docs/AssertRefSnapshotId.md | 17 +++++++++++++++++ .../client/python/docs/AssertTableUUID.md | 17 +++++++++++++++++ regtests/client/python/docs/AssertViewUUID.md | 17 +++++++++++++++++ .../client/python/docs/AssignUUIDUpdate.md | 17 +++++++++++++++++ .../python/docs/AwsStorageConfigInfo.md | 17 +++++++++++++++++ .../python/docs/AzureStorageConfigInfo.md | 17 +++++++++++++++++ regtests/client/python/docs/BaseUpdate.md | 18 +++++++++++++++++- regtests/client/python/docs/BlobMetadata.md | 18 +++++++++++++++++- regtests/client/python/docs/Catalog.md | 17 +++++++++++++++++ regtests/client/python/docs/CatalogConfig.md | 17 +++++++++++++++++ regtests/client/python/docs/CatalogGrant.md | 18 +++++++++++++++++- .../client/python/docs/CatalogPrivilege.md | 18 +++++++++++++++++- .../client/python/docs/CatalogProperties.md | 18 +++++++++++++++++- regtests/client/python/docs/CatalogRole.md | 18 +++++++++++++++++- regtests/client/python/docs/CatalogRoles.md | 18 +++++++++++++++++- regtests/client/python/docs/Catalogs.md | 17 +++++++++++++++++ regtests/client/python/docs/CommitReport.md | 18 +++++++++++++++++- .../client/python/docs/CommitTableRequest.md | 18 +++++++++++++++++- .../client/python/docs/CommitTableResponse.md | 18 +++++++++++++++++- .../python/docs/CommitTransactionRequest.md | 18 +++++++++++++++++- .../client/python/docs/CommitViewRequest.md | 18 +++++++++++++++++- regtests/client/python/docs/ContentFile.md | 18 +++++++++++++++++- regtests/client/python/docs/CountMap.md | 18 +++++++++++++++++- regtests/client/python/docs/CounterResult.md | 18 +++++++++++++++++- .../python/docs/CreateCatalogRequest.md | 17 +++++++++++++++++ .../python/docs/CreateCatalogRoleRequest.md | 18 +++++++++++++++++- .../python/docs/CreateNamespaceRequest.md | 18 +++++++++++++++++- .../python/docs/CreateNamespaceResponse.md | 18 +++++++++++++++++- .../python/docs/CreatePrincipalRequest.md | 18 +++++++++++++++++- .../python/docs/CreatePrincipalRoleRequest.md | 18 +++++++++++++++++- .../client/python/docs/CreateTableRequest.md | 18 +++++++++++++++++- .../client/python/docs/CreateViewRequest.md | 18 +++++++++++++++++- regtests/client/python/docs/DataFile.md | 18 +++++++++++++++++- .../client/python/docs/EqualityDeleteFile.md | 18 +++++++++++++++++- regtests/client/python/docs/ErrorModel.md | 17 +++++++++++++++++ regtests/client/python/docs/Expression.md | 18 +++++++++++++++++- .../client/python/docs/ExternalCatalog.md | 17 +++++++++++++++++ regtests/client/python/docs/FileFormat.md | 18 +++++++++++++++++- .../python/docs/FileStorageConfigInfo.md | 17 +++++++++++++++++ .../python/docs/GcpStorageConfigInfo.md | 17 +++++++++++++++++ .../python/docs/GetNamespaceResponse.md | 18 +++++++++++++++++- .../python/docs/GrantCatalogRoleRequest.md | 18 +++++++++++++++++- .../python/docs/GrantPrincipalRoleRequest.md | 18 +++++++++++++++++- regtests/client/python/docs/GrantResource.md | 18 +++++++++++++++++- regtests/client/python/docs/GrantResources.md | 18 +++++++++++++++++- .../client/python/docs/IcebergCatalogAPI.md | 17 +++++++++++++++++ .../python/docs/IcebergConfigurationAPI.md | 17 +++++++++++++++++ .../python/docs/IcebergErrorResponse.md | 17 +++++++++++++++++ .../client/python/docs/IcebergOAuth2API.md | 17 +++++++++++++++++ .../python/docs/ListNamespacesResponse.md | 18 +++++++++++++++++- .../client/python/docs/ListTablesResponse.md | 18 +++++++++++++++++- regtests/client/python/docs/ListType.md | 18 +++++++++++++++++- .../client/python/docs/LiteralExpression.md | 18 +++++++++++++++++- .../client/python/docs/LoadTableResult.md | 17 +++++++++++++++++ regtests/client/python/docs/LoadViewResult.md | 17 +++++++++++++++++ regtests/client/python/docs/MapType.md | 18 +++++++++++++++++- .../client/python/docs/MetadataLogInner.md | 18 +++++++++++++++++- regtests/client/python/docs/MetricResult.md | 18 +++++++++++++++++- regtests/client/python/docs/ModelSchema.md | 18 +++++++++++++++++- regtests/client/python/docs/NamespaceGrant.md | 18 +++++++++++++++++- .../client/python/docs/NamespacePrivilege.md | 18 +++++++++++++++++- regtests/client/python/docs/NotExpression.md | 18 +++++++++++++++++- .../client/python/docs/NotificationRequest.md | 18 +++++++++++++++++- .../client/python/docs/NotificationType.md | 18 +++++++++++++++++- regtests/client/python/docs/NullOrder.md | 18 +++++++++++++++++- regtests/client/python/docs/OAuthError.md | 18 +++++++++++++++++- .../client/python/docs/OAuthTokenResponse.md | 18 +++++++++++++++++- regtests/client/python/docs/PartitionField.md | 18 +++++++++++++++++- regtests/client/python/docs/PartitionSpec.md | 18 +++++++++++++++++- .../python/docs/PartitionStatisticsFile.md | 18 +++++++++++++++++- regtests/client/python/docs/PolarisCatalog.md | 17 +++++++++++++++++ .../client/python/docs/PolarisDefaultApi.md | 17 +++++++++++++++++ .../client/python/docs/PositionDeleteFile.md | 18 +++++++++++++++++- .../client/python/docs/PrimitiveTypeValue.md | 18 +++++++++++++++++- regtests/client/python/docs/Principal.md | 17 +++++++++++++++++ regtests/client/python/docs/PrincipalRole.md | 18 +++++++++++++++++- regtests/client/python/docs/PrincipalRoles.md | 18 +++++++++++++++++- .../python/docs/PrincipalWithCredentials.md | 17 +++++++++++++++++ .../PrincipalWithCredentialsCredentials.md | 18 +++++++++++++++++- regtests/client/python/docs/Principals.md | 17 +++++++++++++++++ .../python/docs/RegisterTableRequest.md | 18 +++++++++++++++++- .../docs/RemovePartitionStatisticsUpdate.md | 18 +++++++++++++++++- .../python/docs/RemovePropertiesUpdate.md | 18 +++++++++++++++++- .../python/docs/RemoveSnapshotRefUpdate.md | 18 +++++++++++++++++- .../python/docs/RemoveSnapshotsUpdate.md | 18 +++++++++++++++++- .../python/docs/RemoveStatisticsUpdate.md | 18 +++++++++++++++++- .../client/python/docs/RenameTableRequest.md | 18 +++++++++++++++++- .../python/docs/ReportMetricsRequest.md | 18 +++++++++++++++++- .../client/python/docs/RevokeGrantRequest.md | 18 +++++++++++++++++- .../python/docs/SQLViewRepresentation.md | 18 +++++++++++++++++- regtests/client/python/docs/ScanReport.md | 18 +++++++++++++++++- .../python/docs/SetCurrentSchemaUpdate.md | 18 +++++++++++++++++- .../docs/SetCurrentViewVersionUpdate.md | 18 +++++++++++++++++- .../python/docs/SetDefaultSortOrderUpdate.md | 18 +++++++++++++++++- .../python/docs/SetDefaultSpecUpdate.md | 18 +++++++++++++++++- regtests/client/python/docs/SetExpression.md | 18 +++++++++++++++++- .../client/python/docs/SetLocationUpdate.md | 18 +++++++++++++++++- .../docs/SetPartitionStatisticsUpdate.md | 18 +++++++++++++++++- .../client/python/docs/SetPropertiesUpdate.md | 18 +++++++++++++++++- .../python/docs/SetSnapshotRefUpdate.md | 18 +++++++++++++++++- .../client/python/docs/SetStatisticsUpdate.md | 18 +++++++++++++++++- regtests/client/python/docs/Snapshot.md | 18 +++++++++++++++++- .../client/python/docs/SnapshotLogInner.md | 18 +++++++++++++++++- .../client/python/docs/SnapshotReference.md | 18 +++++++++++++++++- .../client/python/docs/SnapshotSummary.md | 18 +++++++++++++++++- regtests/client/python/docs/SortDirection.md | 18 +++++++++++++++++- regtests/client/python/docs/SortField.md | 18 +++++++++++++++++- regtests/client/python/docs/SortOrder.md | 18 +++++++++++++++++- regtests/client/python/docs/StatisticsFile.md | 18 +++++++++++++++++- .../client/python/docs/StorageConfigInfo.md | 17 +++++++++++++++++ regtests/client/python/docs/StructField.md | 18 +++++++++++++++++- regtests/client/python/docs/StructType.md | 18 +++++++++++++++++- regtests/client/python/docs/TableGrant.md | 18 +++++++++++++++++- .../client/python/docs/TableIdentifier.md | 18 +++++++++++++++++- regtests/client/python/docs/TableMetadata.md | 18 +++++++++++++++++- regtests/client/python/docs/TablePrivilege.md | 18 +++++++++++++++++- .../client/python/docs/TableRequirement.md | 18 +++++++++++++++++- regtests/client/python/docs/TableUpdate.md | 18 +++++++++++++++++- .../python/docs/TableUpdateNotification.md | 18 +++++++++++++++++- regtests/client/python/docs/Term.md | 18 +++++++++++++++++- regtests/client/python/docs/TimerResult.md | 18 +++++++++++++++++- regtests/client/python/docs/TokenType.md | 17 +++++++++++++++++ regtests/client/python/docs/TransformTerm.md | 18 +++++++++++++++++- regtests/client/python/docs/Type.md | 18 +++++++++++++++++- .../client/python/docs/UnaryExpression.md | 18 +++++++++++++++++- .../python/docs/UpdateCatalogRequest.md | 17 +++++++++++++++++ .../python/docs/UpdateCatalogRoleRequest.md | 17 +++++++++++++++++ .../docs/UpdateNamespacePropertiesRequest.md | 18 +++++++++++++++++- .../docs/UpdateNamespacePropertiesResponse.md | 18 +++++++++++++++++- .../python/docs/UpdatePrincipalRequest.md | 17 +++++++++++++++++ .../python/docs/UpdatePrincipalRoleRequest.md | 17 +++++++++++++++++ .../python/docs/UpgradeFormatVersionUpdate.md | 18 +++++++++++++++++- regtests/client/python/docs/ValueMap.md | 18 +++++++++++++++++- regtests/client/python/docs/ViewGrant.md | 18 +++++++++++++++++- .../client/python/docs/ViewHistoryEntry.md | 18 +++++++++++++++++- regtests/client/python/docs/ViewMetadata.md | 18 +++++++++++++++++- regtests/client/python/docs/ViewPrivilege.md | 18 +++++++++++++++++- .../client/python/docs/ViewRepresentation.md | 18 +++++++++++++++++- .../client/python/docs/ViewRequirement.md | 18 +++++++++++++++++- regtests/client/python/docs/ViewUpdate.md | 18 +++++++++++++++++- regtests/client/python/docs/ViewVersion.md | 18 +++++++++++++++++- regtests/client/python/git_push.sh | 15 +++++++++++++++ regtests/client/python/polaris/__init__.py | 15 +++++++++++++++ .../client/python/polaris/catalog/__init__.py | 15 +++++++++++++++ .../python/polaris/catalog/api/__init__.py | 15 +++++++++++++++ .../catalog/api/iceberg_catalog_api.py | 15 +++++++++++++++ .../catalog/api/iceberg_configuration_api.py | 15 +++++++++++++++ .../catalog/api/iceberg_o_auth2_api.py | 15 +++++++++++++++ .../python/polaris/catalog/api_client.py | 15 +++++++++++++++ .../python/polaris/catalog/api_response.py | 16 ++++++++++++++++ .../python/polaris/catalog/configuration.py | 15 +++++++++++++++ .../python/polaris/catalog/exceptions.py | 15 +++++++++++++++ .../python/polaris/catalog/models/__init__.py | 15 +++++++++++++++ .../models/add_partition_spec_update.py | 15 +++++++++++++++ .../catalog/models/add_schema_update.py | 15 +++++++++++++++ .../catalog/models/add_snapshot_update.py | 15 +++++++++++++++ .../catalog/models/add_sort_order_update.py | 15 +++++++++++++++ .../catalog/models/add_view_version_update.py | 15 +++++++++++++++ .../catalog/models/and_or_expression.py | 15 +++++++++++++++ .../polaris/catalog/models/assert_create.py | 15 +++++++++++++++ .../models/assert_current_schema_id.py | 15 +++++++++++++++ .../models/assert_default_sort_order_id.py | 15 +++++++++++++++ .../catalog/models/assert_default_spec_id.py | 15 +++++++++++++++ .../models/assert_last_assigned_field_id.py | 15 +++++++++++++++ .../assert_last_assigned_partition_id.py | 15 +++++++++++++++ .../catalog/models/assert_ref_snapshot_id.py | 15 +++++++++++++++ .../catalog/models/assert_table_uuid.py | 15 +++++++++++++++ .../catalog/models/assert_view_uuid.py | 15 +++++++++++++++ .../catalog/models/assign_uuid_update.py | 15 +++++++++++++++ .../polaris/catalog/models/base_update.py | 15 +++++++++++++++ .../polaris/catalog/models/blob_metadata.py | 15 +++++++++++++++ .../polaris/catalog/models/catalog_config.py | 15 +++++++++++++++ .../polaris/catalog/models/commit_report.py | 15 +++++++++++++++ .../catalog/models/commit_table_request.py | 15 +++++++++++++++ .../catalog/models/commit_table_response.py | 15 +++++++++++++++ .../models/commit_transaction_request.py | 15 +++++++++++++++ .../catalog/models/commit_view_request.py | 15 +++++++++++++++ .../polaris/catalog/models/content_file.py | 15 +++++++++++++++ .../polaris/catalog/models/count_map.py | 15 +++++++++++++++ .../polaris/catalog/models/counter_result.py | 15 +++++++++++++++ .../models/create_namespace_request.py | 15 +++++++++++++++ .../models/create_namespace_response.py | 15 +++++++++++++++ .../catalog/models/create_table_request.py | 15 +++++++++++++++ .../catalog/models/create_view_request.py | 15 +++++++++++++++ .../polaris/catalog/models/data_file.py | 15 +++++++++++++++ .../catalog/models/equality_delete_file.py | 15 +++++++++++++++ .../polaris/catalog/models/error_model.py | 15 +++++++++++++++ .../polaris/catalog/models/expression.py | 15 +++++++++++++++ .../polaris/catalog/models/file_format.py | 15 +++++++++++++++ .../catalog/models/get_namespace_response.py | 15 +++++++++++++++ .../catalog/models/iceberg_error_response.py | 15 +++++++++++++++ .../models/list_namespaces_response.py | 15 +++++++++++++++ .../catalog/models/list_tables_response.py | 15 +++++++++++++++ .../polaris/catalog/models/list_type.py | 15 +++++++++++++++ .../catalog/models/literal_expression.py | 15 +++++++++++++++ .../catalog/models/load_table_result.py | 15 +++++++++++++++ .../catalog/models/load_view_result.py | 15 +++++++++++++++ .../python/polaris/catalog/models/map_type.py | 15 +++++++++++++++ .../catalog/models/metadata_log_inner.py | 15 +++++++++++++++ .../polaris/catalog/models/metric_result.py | 15 +++++++++++++++ .../polaris/catalog/models/model_schema.py | 15 +++++++++++++++ .../polaris/catalog/models/not_expression.py | 15 +++++++++++++++ .../catalog/models/notification_request.py | 15 +++++++++++++++ .../catalog/models/notification_type.py | 15 +++++++++++++++ .../polaris/catalog/models/null_order.py | 15 +++++++++++++++ .../polaris/catalog/models/o_auth_error.py | 15 +++++++++++++++ .../catalog/models/o_auth_token_response.py | 15 +++++++++++++++ .../polaris/catalog/models/partition_field.py | 15 +++++++++++++++ .../polaris/catalog/models/partition_spec.py | 15 +++++++++++++++ .../models/partition_statistics_file.py | 15 +++++++++++++++ .../catalog/models/position_delete_file.py | 15 +++++++++++++++ .../catalog/models/primitive_type_value.py | 15 +++++++++++++++ .../catalog/models/register_table_request.py | 15 +++++++++++++++ .../remove_partition_statistics_update.py | 15 +++++++++++++++ .../models/remove_properties_update.py | 15 +++++++++++++++ .../models/remove_snapshot_ref_update.py | 15 +++++++++++++++ .../catalog/models/remove_snapshots_update.py | 15 +++++++++++++++ .../models/remove_statistics_update.py | 15 +++++++++++++++ .../catalog/models/rename_table_request.py | 15 +++++++++++++++ .../catalog/models/report_metrics_request.py | 15 +++++++++++++++ .../polaris/catalog/models/scan_report.py | 15 +++++++++++++++ .../models/set_current_schema_update.py | 15 +++++++++++++++ .../models/set_current_view_version_update.py | 15 +++++++++++++++ .../models/set_default_sort_order_update.py | 15 +++++++++++++++ .../catalog/models/set_default_spec_update.py | 15 +++++++++++++++ .../polaris/catalog/models/set_expression.py | 15 +++++++++++++++ .../catalog/models/set_location_update.py | 15 +++++++++++++++ .../models/set_partition_statistics_update.py | 15 +++++++++++++++ .../catalog/models/set_properties_update.py | 15 +++++++++++++++ .../catalog/models/set_snapshot_ref_update.py | 15 +++++++++++++++ .../catalog/models/set_statistics_update.py | 15 +++++++++++++++ .../python/polaris/catalog/models/snapshot.py | 15 +++++++++++++++ .../catalog/models/snapshot_log_inner.py | 15 +++++++++++++++ .../catalog/models/snapshot_reference.py | 15 +++++++++++++++ .../catalog/models/snapshot_summary.py | 15 +++++++++++++++ .../polaris/catalog/models/sort_direction.py | 15 +++++++++++++++ .../polaris/catalog/models/sort_field.py | 15 +++++++++++++++ .../polaris/catalog/models/sort_order.py | 15 +++++++++++++++ .../catalog/models/sql_view_representation.py | 15 +++++++++++++++ .../polaris/catalog/models/statistics_file.py | 15 +++++++++++++++ .../polaris/catalog/models/struct_field.py | 15 +++++++++++++++ .../polaris/catalog/models/struct_type.py | 15 +++++++++++++++ .../catalog/models/table_identifier.py | 15 +++++++++++++++ .../polaris/catalog/models/table_metadata.py | 15 +++++++++++++++ .../catalog/models/table_requirement.py | 15 +++++++++++++++ .../polaris/catalog/models/table_update.py | 15 +++++++++++++++ .../models/table_update_notification.py | 15 +++++++++++++++ .../python/polaris/catalog/models/term.py | 15 +++++++++++++++ .../polaris/catalog/models/timer_result.py | 15 +++++++++++++++ .../polaris/catalog/models/token_type.py | 15 +++++++++++++++ .../polaris/catalog/models/transform_term.py | 15 +++++++++++++++ .../python/polaris/catalog/models/type.py | 15 +++++++++++++++ .../catalog/models/unary_expression.py | 15 +++++++++++++++ .../update_namespace_properties_request.py | 15 +++++++++++++++ .../update_namespace_properties_response.py | 15 +++++++++++++++ .../models/upgrade_format_version_update.py | 15 +++++++++++++++ .../polaris/catalog/models/value_map.py | 15 +++++++++++++++ .../catalog/models/view_history_entry.py | 15 +++++++++++++++ .../polaris/catalog/models/view_metadata.py | 15 +++++++++++++++ .../catalog/models/view_representation.py | 15 +++++++++++++++ .../catalog/models/view_requirement.py | 15 +++++++++++++++ .../polaris/catalog/models/view_update.py | 15 +++++++++++++++ .../polaris/catalog/models/view_version.py | 15 +++++++++++++++ .../client/python/polaris/catalog/rest.py | 15 +++++++++++++++ .../python/polaris/management/__init__.py | 15 +++++++++++++++ .../python/polaris/management/api/__init__.py | 15 +++++++++++++++ .../management/api/polaris_default_api.py | 15 +++++++++++++++ .../python/polaris/management/api_client.py | 15 +++++++++++++++ .../python/polaris/management/api_response.py | 16 ++++++++++++++++ .../polaris/management/configuration.py | 15 +++++++++++++++ .../python/polaris/management/exceptions.py | 15 +++++++++++++++ .../polaris/management/models/__init__.py | 15 +++++++++++++++ .../management/models/add_grant_request.py | 15 +++++++++++++++ .../models/aws_storage_config_info.py | 15 +++++++++++++++ .../models/azure_storage_config_info.py | 15 +++++++++++++++ .../polaris/management/models/catalog.py | 15 +++++++++++++++ .../management/models/catalog_grant.py | 15 +++++++++++++++ .../management/models/catalog_privilege.py | 15 +++++++++++++++ .../management/models/catalog_properties.py | 15 +++++++++++++++ .../polaris/management/models/catalog_role.py | 15 +++++++++++++++ .../management/models/catalog_roles.py | 15 +++++++++++++++ .../polaris/management/models/catalogs.py | 15 +++++++++++++++ .../models/create_catalog_request.py | 15 +++++++++++++++ .../models/create_catalog_role_request.py | 15 +++++++++++++++ .../models/create_principal_request.py | 15 +++++++++++++++ .../models/create_principal_role_request.py | 15 +++++++++++++++ .../management/models/external_catalog.py | 15 +++++++++++++++ .../models/file_storage_config_info.py | 15 +++++++++++++++ .../models/gcp_storage_config_info.py | 15 +++++++++++++++ .../models/grant_catalog_role_request.py | 15 +++++++++++++++ .../models/grant_principal_role_request.py | 15 +++++++++++++++ .../management/models/grant_resource.py | 15 +++++++++++++++ .../management/models/grant_resources.py | 15 +++++++++++++++ .../management/models/namespace_grant.py | 15 +++++++++++++++ .../management/models/namespace_privilege.py | 15 +++++++++++++++ .../management/models/polaris_catalog.py | 15 +++++++++++++++ .../polaris/management/models/principal.py | 15 +++++++++++++++ .../management/models/principal_role.py | 15 +++++++++++++++ .../management/models/principal_roles.py | 15 +++++++++++++++ .../models/principal_with_credentials.py | 15 +++++++++++++++ .../principal_with_credentials_credentials.py | 15 +++++++++++++++ .../polaris/management/models/principals.py | 15 +++++++++++++++ .../management/models/revoke_grant_request.py | 15 +++++++++++++++ .../management/models/storage_config_info.py | 15 +++++++++++++++ .../polaris/management/models/table_grant.py | 15 +++++++++++++++ .../management/models/table_privilege.py | 15 +++++++++++++++ .../models/update_catalog_request.py | 15 +++++++++++++++ .../models/update_catalog_role_request.py | 15 +++++++++++++++ .../models/update_principal_request.py | 15 +++++++++++++++ .../models/update_principal_role_request.py | 15 +++++++++++++++ .../polaris/management/models/view_grant.py | 15 +++++++++++++++ .../management/models/view_privilege.py | 15 +++++++++++++++ .../client/python/polaris/management/rest.py | 15 +++++++++++++++ regtests/client/python/pyproject.toml | 16 ++++++++++++++++ regtests/client/python/setup.py | 15 +++++++++++++++ regtests/client/python/test/__init__.py | 15 +++++++++++++++ .../python/test/test_add_grant_request.py | 15 +++++++++++++++ .../test/test_add_partition_spec_update.py | 15 +++++++++++++++ .../python/test/test_add_schema_update.py | 15 +++++++++++++++ .../python/test/test_add_snapshot_update.py | 15 +++++++++++++++ .../python/test/test_add_sort_order_update.py | 15 +++++++++++++++ .../test/test_add_view_version_update.py | 15 +++++++++++++++ .../python/test/test_and_or_expression.py | 15 +++++++++++++++ .../client/python/test/test_assert_create.py | 15 +++++++++++++++ .../test/test_assert_current_schema_id.py | 15 +++++++++++++++ .../test/test_assert_default_sort_order_id.py | 15 +++++++++++++++ .../test/test_assert_default_spec_id.py | 15 +++++++++++++++ .../test_assert_last_assigned_field_id.py | 15 +++++++++++++++ .../test_assert_last_assigned_partition_id.py | 15 +++++++++++++++ .../test/test_assert_ref_snapshot_id.py | 15 +++++++++++++++ .../python/test/test_assert_table_uuid.py | 15 +++++++++++++++ .../python/test/test_assert_view_uuid.py | 15 +++++++++++++++ .../python/test/test_assign_uuid_update.py | 15 +++++++++++++++ .../test/test_aws_storage_config_info.py | 15 +++++++++++++++ .../test/test_azure_storage_config_info.py | 15 +++++++++++++++ .../client/python/test/test_base_update.py | 15 +++++++++++++++ .../client/python/test/test_blob_metadata.py | 15 +++++++++++++++ regtests/client/python/test/test_catalog.py | 15 +++++++++++++++ .../client/python/test/test_catalog_config.py | 15 +++++++++++++++ .../client/python/test/test_catalog_grant.py | 15 +++++++++++++++ .../python/test/test_catalog_privilege.py | 15 +++++++++++++++ .../python/test/test_catalog_properties.py | 15 +++++++++++++++ .../client/python/test/test_catalog_role.py | 15 +++++++++++++++ .../client/python/test/test_catalog_roles.py | 15 +++++++++++++++ regtests/client/python/test/test_catalogs.py | 15 +++++++++++++++ .../client/python/test/test_cli_parsing.py | 15 +++++++++++++++ .../client/python/test/test_commit_report.py | 15 +++++++++++++++ .../python/test/test_commit_table_request.py | 15 +++++++++++++++ .../python/test/test_commit_table_response.py | 15 +++++++++++++++ .../test/test_commit_transaction_request.py | 15 +++++++++++++++ .../python/test/test_commit_view_request.py | 15 +++++++++++++++ .../client/python/test/test_content_file.py | 15 +++++++++++++++ regtests/client/python/test/test_count_map.py | 15 +++++++++++++++ .../client/python/test/test_counter_result.py | 15 +++++++++++++++ .../test/test_create_catalog_request.py | 15 +++++++++++++++ .../test/test_create_catalog_role_request.py | 15 +++++++++++++++ .../test/test_create_namespace_request.py | 15 +++++++++++++++ .../test/test_create_namespace_response.py | 15 +++++++++++++++ .../test/test_create_principal_request.py | 15 +++++++++++++++ .../test_create_principal_role_request.py | 15 +++++++++++++++ .../python/test/test_create_table_request.py | 15 +++++++++++++++ .../python/test/test_create_view_request.py | 15 +++++++++++++++ regtests/client/python/test/test_data_file.py | 15 +++++++++++++++ .../python/test/test_equality_delete_file.py | 15 +++++++++++++++ .../client/python/test/test_error_model.py | 15 +++++++++++++++ .../client/python/test/test_expression.py | 15 +++++++++++++++ .../python/test/test_external_catalog.py | 15 +++++++++++++++ .../client/python/test/test_file_format.py | 15 +++++++++++++++ .../test/test_file_storage_config_info.py | 15 +++++++++++++++ .../test/test_gcp_storage_config_info.py | 15 +++++++++++++++ .../test/test_get_namespace_response.py | 15 +++++++++++++++ .../test/test_grant_catalog_role_request.py | 15 +++++++++++++++ .../test/test_grant_principal_role_request.py | 15 +++++++++++++++ .../client/python/test/test_grant_resource.py | 15 +++++++++++++++ .../python/test/test_grant_resources.py | 15 +++++++++++++++ .../python/test/test_iceberg_catalog_api.py | 15 +++++++++++++++ .../test/test_iceberg_configuration_api.py | 15 +++++++++++++++ .../test/test_iceberg_error_response.py | 15 +++++++++++++++ .../python/test/test_iceberg_o_auth2_api.py | 15 +++++++++++++++ .../test/test_list_namespaces_response.py | 15 +++++++++++++++ .../python/test/test_list_tables_response.py | 15 +++++++++++++++ regtests/client/python/test/test_list_type.py | 15 +++++++++++++++ .../python/test/test_literal_expression.py | 15 +++++++++++++++ .../python/test/test_load_table_result.py | 15 +++++++++++++++ .../python/test/test_load_view_result.py | 15 +++++++++++++++ regtests/client/python/test/test_map_type.py | 15 +++++++++++++++ .../python/test/test_metadata_log_inner.py | 15 +++++++++++++++ .../client/python/test/test_metric_result.py | 15 +++++++++++++++ .../client/python/test/test_model_schema.py | 15 +++++++++++++++ .../python/test/test_namespace_grant.py | 15 +++++++++++++++ .../python/test/test_namespace_privilege.py | 15 +++++++++++++++ .../client/python/test/test_not_expression.py | 15 +++++++++++++++ .../python/test/test_notification_request.py | 15 +++++++++++++++ .../python/test/test_notification_type.py | 15 +++++++++++++++ .../client/python/test/test_null_order.py | 15 +++++++++++++++ .../client/python/test/test_o_auth_error.py | 15 +++++++++++++++ .../python/test/test_o_auth_token_response.py | 15 +++++++++++++++ .../python/test/test_partition_field.py | 15 +++++++++++++++ .../client/python/test/test_partition_spec.py | 15 +++++++++++++++ .../test/test_partition_statistics_file.py | 15 +++++++++++++++ .../python/test/test_polaris_catalog.py | 15 +++++++++++++++ .../python/test/test_polaris_default_api.py | 15 +++++++++++++++ .../python/test/test_position_delete_file.py | 15 +++++++++++++++ .../python/test/test_primitive_type_value.py | 15 +++++++++++++++ regtests/client/python/test/test_principal.py | 15 +++++++++++++++ .../client/python/test/test_principal_role.py | 15 +++++++++++++++ .../python/test/test_principal_roles.py | 15 +++++++++++++++ .../test/test_principal_with_credentials.py | 15 +++++++++++++++ ..._principal_with_credentials_credentials.py | 15 +++++++++++++++ .../client/python/test/test_principals.py | 15 +++++++++++++++ .../test/test_register_table_request.py | 15 +++++++++++++++ ...test_remove_partition_statistics_update.py | 15 +++++++++++++++ .../test/test_remove_properties_update.py | 15 +++++++++++++++ .../test/test_remove_snapshot_ref_update.py | 15 +++++++++++++++ .../test/test_remove_snapshots_update.py | 15 +++++++++++++++ .../test/test_remove_statistics_update.py | 15 +++++++++++++++ .../python/test/test_rename_table_request.py | 15 +++++++++++++++ .../test/test_report_metrics_request.py | 15 +++++++++++++++ .../python/test/test_revoke_grant_request.py | 15 +++++++++++++++ .../client/python/test/test_scan_report.py | 15 +++++++++++++++ .../test/test_set_current_schema_update.py | 15 +++++++++++++++ .../test_set_current_view_version_update.py | 15 +++++++++++++++ .../test_set_default_sort_order_update.py | 15 +++++++++++++++ .../test/test_set_default_spec_update.py | 15 +++++++++++++++ .../client/python/test/test_set_expression.py | 15 +++++++++++++++ .../python/test/test_set_location_update.py | 15 +++++++++++++++ .../test_set_partition_statistics_update.py | 15 +++++++++++++++ .../python/test/test_set_properties_update.py | 15 +++++++++++++++ .../test/test_set_snapshot_ref_update.py | 15 +++++++++++++++ .../python/test/test_set_statistics_update.py | 15 +++++++++++++++ regtests/client/python/test/test_snapshot.py | 15 +++++++++++++++ .../python/test/test_snapshot_log_inner.py | 15 +++++++++++++++ .../python/test/test_snapshot_reference.py | 15 +++++++++++++++ .../python/test/test_snapshot_summary.py | 15 +++++++++++++++ .../client/python/test/test_sort_direction.py | 15 +++++++++++++++ .../client/python/test/test_sort_field.py | 15 +++++++++++++++ .../client/python/test/test_sort_order.py | 15 +++++++++++++++ .../test/test_sql_view_representation.py | 15 +++++++++++++++ .../python/test/test_statistics_file.py | 15 +++++++++++++++ .../python/test/test_storage_config_info.py | 15 +++++++++++++++ .../client/python/test/test_struct_field.py | 15 +++++++++++++++ .../client/python/test/test_struct_type.py | 15 +++++++++++++++ .../client/python/test/test_table_grant.py | 15 +++++++++++++++ .../python/test/test_table_identifier.py | 15 +++++++++++++++ .../client/python/test/test_table_metadata.py | 15 +++++++++++++++ .../python/test/test_table_privilege.py | 15 +++++++++++++++ .../python/test/test_table_requirement.py | 15 +++++++++++++++ .../client/python/test/test_table_update.py | 15 +++++++++++++++ .../test/test_table_update_notification.py | 15 +++++++++++++++ regtests/client/python/test/test_term.py | 15 +++++++++++++++ .../client/python/test/test_timer_result.py | 15 +++++++++++++++ .../client/python/test/test_token_type.py | 15 +++++++++++++++ .../client/python/test/test_transform_term.py | 15 +++++++++++++++ regtests/client/python/test/test_type.py | 15 +++++++++++++++ .../python/test/test_unary_expression.py | 15 +++++++++++++++ .../test/test_update_catalog_request.py | 15 +++++++++++++++ .../test/test_update_catalog_role_request.py | 15 +++++++++++++++ ...est_update_namespace_properties_request.py | 15 +++++++++++++++ ...st_update_namespace_properties_response.py | 15 +++++++++++++++ .../test/test_update_principal_request.py | 15 +++++++++++++++ .../test_update_principal_role_request.py | 17 ++++++++++++++++- .../test_upgrade_format_version_update.py | 15 +++++++++++++++ regtests/client/python/test/test_value_map.py | 15 +++++++++++++++ .../client/python/test/test_view_grant.py | 15 +++++++++++++++ .../python/test/test_view_history_entry.py | 15 +++++++++++++++ .../client/python/test/test_view_metadata.py | 15 +++++++++++++++ .../client/python/test/test_view_privilege.py | 15 +++++++++++++++ .../python/test/test_view_representation.py | 15 +++++++++++++++ .../python/test/test_view_requirement.py | 15 +++++++++++++++ .../client/python/test/test_view_update.py | 15 +++++++++++++++ .../client/python/test/test_view_version.py | 15 +++++++++++++++ regtests/pyspark-setup.sh | 15 +++++++++++++++ regtests/run.sh | 16 +++++++++++++++- regtests/run_spark_sql.sh | 15 +++++++++++++++ regtests/setup.sh | 16 +++++++++++++++- regtests/t_hello_world/src/hello_world.sh | 14 ++++++++++++++ regtests/t_oauth/test_oauth2_tokens.py | 14 ++++++++++++++ regtests/t_pyspark/src/conftest.py | 14 ++++++++++++++ regtests/t_pyspark/src/iceberg_spark.py | 14 ++++++++++++++ .../src/test_spark_sql_s3_with_privileges.py | 14 ++++++++++++++ .../t_spark_sql/src/spark_sql_azure_blob.sh | 14 ++++++++++++++ .../t_spark_sql/src/spark_sql_azure_dfs.sh | 14 ++++++++++++++ regtests/t_spark_sql/src/spark_sql_basic.sh | 14 ++++++++++++++ regtests/t_spark_sql/src/spark_sql_gcp.sh | 14 ++++++++++++++ regtests/t_spark_sql/src/spark_sql_s3.sh | 14 ++++++++++++++ .../src/spark_sql_s3_cross_region.sh | 14 ++++++++++++++ regtests/t_spark_sql/src/spark_sql_views.sh | 14 ++++++++++++++ server-templates/api.mustache | 15 +++++++++++++++ server-templates/apiService.mustache | 15 +++++++++++++++ server-templates/apiServiceImpl.mustache | 15 +++++++++++++++ server-templates/pojo.mustache | 15 +++++++++++++++ settings.gradle | 16 ++++++++++++++++ setup.sh | 16 ++++++++++++++++ spec/polaris-management-service.yml | 13 +++++++++++++ 755 files changed, 11602 insertions(+), 165 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 0200ca247d..d176d662e5 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1,17 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# + @polaris-catalog/polaris \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 23c4cb3b50..94a8501fd0 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,3 +1,19 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# + --- version: 2 updates: diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 0c23671569..41fa22e738 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1 +1,16 @@ + diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 4ca664b9d2..6fc1d4eb19 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -1,3 +1,19 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# + # This workflow uses actions that are not certified by GitHub. # They are provided by a third-party and are governed by # separate terms of service, privacy policy, and support diff --git a/.github/workflows/regtest.yml b/.github/workflows/regtest.yml index 0d65a9bd33..b81f80c62e 100644 --- a/.github/workflows/regtest.yml +++ b/.github/workflows/regtest.yml @@ -1,3 +1,19 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# + name: Regression Tests on: push: diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml index f1d7286d3b..7538486564 100644 --- a/.github/workflows/semgrep.yml +++ b/.github/workflows/semgrep.yml @@ -1,3 +1,19 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# + --- name: Run semgrep checks on: diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index e1b106fc65..6444414313 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -1,3 +1,19 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# + --- jobs: stale: diff --git a/Dockerfile b/Dockerfile index 2b595656b2..c83cdb0ca8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # Base Image FROM gradle:8.6-jdk21 as build diff --git a/README.md b/README.md index b853c850b6..5081e564cb 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ # Polaris Catalog diff --git a/build.gradle b/build.gradle index b92c2cbc99..6ac0b673d4 100644 --- a/build.gradle +++ b/build.gradle @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ + buildscript { repositories { maven { diff --git a/docker-compose-jupyter.yml b/docker-compose-jupyter.yml index 24da194183..758cc85668 100644 --- a/docker-compose-jupyter.yml +++ b/docker-compose-jupyter.yml @@ -1,3 +1,19 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# + services: polaris: build: diff --git a/docker-compose.yml b/docker-compose.yml index c15c6ddbbe..1c48429629 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,3 +1,19 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# + services: polaris: build: diff --git a/docs/entities.md b/docs/entities.md index 8d4d4968e0..c1d74d7977 100644 --- a/docs/entities.md +++ b/docs/entities.md @@ -1,3 +1,19 @@ + + # Polaris Entities This page documents various entities that can be managed in Polaris. diff --git a/docs/iceberg-rest/index.html b/docs/iceberg-rest/index.html index 1ae8c54d5c..e6e2ca8117 100644 --- a/docs/iceberg-rest/index.html +++ b/docs/iceberg-rest/index.html @@ -1,3 +1,22 @@ + + diff --git a/docs/index.html b/docs/index.html index b9ed858545..683725b549 100644 --- a/docs/index.html +++ b/docs/index.html @@ -1,3 +1,19 @@ + + diff --git a/docs/polaris-management/index.html b/docs/polaris-management/index.html index ebdbd1fa64..a1da3d8455 100644 --- a/docs/polaris-management/index.html +++ b/docs/polaris-management/index.html @@ -1,3 +1,19 @@ + + diff --git a/docs/quickstart.md b/docs/quickstart.md index 27f8b28261..172c299267 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -1,3 +1,19 @@ + + # Quick Start This guide serves as a introduction to several key entities that can be managed with Polaris, describes how to build and deploy Polaris locally, and finally includes examples of how to use Polaris with Spark and Trino. diff --git a/extension/persistence/eclipselink/build.gradle b/extension/persistence/eclipselink/build.gradle index ed4b0b4ed9..1bbad8223a 100644 --- a/extension/persistence/eclipselink/build.gradle +++ b/extension/persistence/eclipselink/build.gradle @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ dependencies { implementation project(":polaris-core") implementation project(":polaris-service") diff --git a/extension/persistence/eclipselink/src/main/java/io/polaris/extension/persistence/impl/eclipselink/EclipseLinkPolarisMetaStoreManagerFactory.java b/extension/persistence/eclipselink/src/main/java/io/polaris/extension/persistence/impl/eclipselink/EclipseLinkPolarisMetaStoreManagerFactory.java index b282a85229..5ca6c7b25c 100644 --- a/extension/persistence/eclipselink/src/main/java/io/polaris/extension/persistence/impl/eclipselink/EclipseLinkPolarisMetaStoreManagerFactory.java +++ b/extension/persistence/eclipselink/src/main/java/io/polaris/extension/persistence/impl/eclipselink/EclipseLinkPolarisMetaStoreManagerFactory.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.extension.persistence.impl.eclipselink; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/extension/persistence/eclipselink/src/main/java/io/polaris/extension/persistence/impl/eclipselink/PolarisEclipseLinkMetaStoreSessionImpl.java b/extension/persistence/eclipselink/src/main/java/io/polaris/extension/persistence/impl/eclipselink/PolarisEclipseLinkMetaStoreSessionImpl.java index 1aae23a107..43a78436f2 100644 --- a/extension/persistence/eclipselink/src/main/java/io/polaris/extension/persistence/impl/eclipselink/PolarisEclipseLinkMetaStoreSessionImpl.java +++ b/extension/persistence/eclipselink/src/main/java/io/polaris/extension/persistence/impl/eclipselink/PolarisEclipseLinkMetaStoreSessionImpl.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.extension.persistence.impl.eclipselink; import static org.eclipse.persistence.config.PersistenceUnitProperties.ECLIPSELINK_PERSISTENCE_XML; diff --git a/extension/persistence/eclipselink/src/main/java/io/polaris/extension/persistence/impl/eclipselink/PolarisEclipseLinkStore.java b/extension/persistence/eclipselink/src/main/java/io/polaris/extension/persistence/impl/eclipselink/PolarisEclipseLinkStore.java index 7bdeaf0163..a4a880894d 100644 --- a/extension/persistence/eclipselink/src/main/java/io/polaris/extension/persistence/impl/eclipselink/PolarisEclipseLinkStore.java +++ b/extension/persistence/eclipselink/src/main/java/io/polaris/extension/persistence/impl/eclipselink/PolarisEclipseLinkStore.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.extension.persistence.impl.eclipselink; import io.polaris.core.PolarisDiagnostics; diff --git a/extension/persistence/eclipselink/src/test/java/com/snowflake/polaris/persistence/impl/eclipselink/PolarisEclipseLinkMetaStoreTest.java b/extension/persistence/eclipselink/src/test/java/com/snowflake/polaris/persistence/impl/eclipselink/PolarisEclipseLinkMetaStoreTest.java index b9d1b1c7e9..c791b2b86f 100644 --- a/extension/persistence/eclipselink/src/test/java/com/snowflake/polaris/persistence/impl/eclipselink/PolarisEclipseLinkMetaStoreTest.java +++ b/extension/persistence/eclipselink/src/test/java/com/snowflake/polaris/persistence/impl/eclipselink/PolarisEclipseLinkMetaStoreTest.java @@ -1,7 +1,20 @@ /* - * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. + * + * Copyright (c) 2024 Snowflake Computing 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. + * */ - package com.snowflake.polaris.persistence.impl.eclipselink; import io.polaris.core.PolarisCallContext; diff --git a/gradle.properties b/gradle.properties index 97ecbda280..73637cefb5 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,17 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# group=io.polaris version=1.0.0 \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a80b22ce5c..fdbf626db8 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,3 +1,19 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# + distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip diff --git a/gradlew b/gradlew index 1aa94a4269..4c77f6cee8 100755 --- a/gradlew +++ b/gradlew @@ -1,13 +1,12 @@ #!/bin/sh - # -# Copyright © 2015-2021 the original authors. +# Copyright (c) 2024 Snowflake Computing 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 # -# https://www.apache.org/licenses/LICENSE-2.0 +# 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, @@ -15,7 +14,6 @@ # See the License for the specific language governing permissions and # limitations under the License. # - ############################################################################## # # Gradle start up script for POSIX generated by Gradle. diff --git a/kind-registry.sh b/kind-registry.sh index 9fe55a821a..f2e153499d 100755 --- a/kind-registry.sh +++ b/kind-registry.sh @@ -1,4 +1,19 @@ #!/bin/sh +# +# Copyright (c) 2024 Snowflake Computing 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. +# set -o errexit # 1. Create registry container unless it already exists diff --git a/notebooks/Dockerfile b/notebooks/Dockerfile index cb785b75de..01a4955a3f 100644 --- a/notebooks/Dockerfile +++ b/notebooks/Dockerfile @@ -1,3 +1,19 @@ +# +# Copyright (c) 2024 Snowflake Computing 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 jupyter/all-spark-notebook:spark-3.5.0 COPY --chown=jovyan regtests/client /home/jovyan/client diff --git a/polaris b/polaris index 234de1a7e6..fb71f5c81b 100755 --- a/polaris +++ b/polaris @@ -1,5 +1,19 @@ #!/bin/bash - +# +# Copyright (c) 2024 Snowflake Computing 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. +# SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) if [ ! -d ${SCRIPT_DIR}/polaris-venv ]; then diff --git a/polaris-core/build.gradle b/polaris-core/build.gradle index 1767982a4f..5ef84b5843 100644 --- a/polaris-core/build.gradle +++ b/polaris-core/build.gradle @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ + plugins { id 'org.openapi.generator' version '7.6.0' id("java-library") diff --git a/polaris-core/src/main/java/io/polaris/core/PolarisCallContext.java b/polaris-core/src/main/java/io/polaris/core/PolarisCallContext.java index 52e018c316..88209769dd 100644 --- a/polaris-core/src/main/java/io/polaris/core/PolarisCallContext.java +++ b/polaris-core/src/main/java/io/polaris/core/PolarisCallContext.java @@ -1,3 +1,18 @@ +/* + * Copyright (C) 2024 Snowflake Computing 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. + */ package io.polaris.core; import io.polaris.core.persistence.PolarisMetaStoreSession; diff --git a/polaris-core/src/main/java/io/polaris/core/PolarisConfiguration.java b/polaris-core/src/main/java/io/polaris/core/PolarisConfiguration.java index fae652bb57..a297f05181 100644 --- a/polaris-core/src/main/java/io/polaris/core/PolarisConfiguration.java +++ b/polaris-core/src/main/java/io/polaris/core/PolarisConfiguration.java @@ -1,3 +1,18 @@ +/* + * Copyright (C) 2024 Snowflake Computing 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. + */ package io.polaris.core; public class PolarisConfiguration { diff --git a/polaris-core/src/main/java/io/polaris/core/PolarisConfigurationStore.java b/polaris-core/src/main/java/io/polaris/core/PolarisConfigurationStore.java index bcb5084ad0..a4f48e9099 100644 --- a/polaris-core/src/main/java/io/polaris/core/PolarisConfigurationStore.java +++ b/polaris-core/src/main/java/io/polaris/core/PolarisConfigurationStore.java @@ -1,3 +1,18 @@ +/* + * Copyright (C) 2024 Snowflake Computing 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. + */ package io.polaris.core; import com.google.common.base.Preconditions; diff --git a/polaris-core/src/main/java/io/polaris/core/PolarisDefaultDiagServiceImpl.java b/polaris-core/src/main/java/io/polaris/core/PolarisDefaultDiagServiceImpl.java index d9cc339228..39c0d130a4 100644 --- a/polaris-core/src/main/java/io/polaris/core/PolarisDefaultDiagServiceImpl.java +++ b/polaris-core/src/main/java/io/polaris/core/PolarisDefaultDiagServiceImpl.java @@ -1,3 +1,18 @@ +/* + * Copyright (C) 2024 Snowflake Computing 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. + */ package io.polaris.core; import com.google.common.base.Preconditions; diff --git a/polaris-core/src/main/java/io/polaris/core/PolarisDiagnostics.java b/polaris-core/src/main/java/io/polaris/core/PolarisDiagnostics.java index 39d7e82414..6a5a0c39fd 100644 --- a/polaris-core/src/main/java/io/polaris/core/PolarisDiagnostics.java +++ b/polaris-core/src/main/java/io/polaris/core/PolarisDiagnostics.java @@ -1,3 +1,18 @@ +/* + * Copyright (C) 2024 Snowflake Computing 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. + */ package io.polaris.core; import org.jetbrains.annotations.Contract; diff --git a/polaris-core/src/main/java/io/polaris/core/auth/AuthenticatedPolarisPrincipal.java b/polaris-core/src/main/java/io/polaris/core/auth/AuthenticatedPolarisPrincipal.java index 6e18b9a9ab..d40149660d 100644 --- a/polaris-core/src/main/java/io/polaris/core/auth/AuthenticatedPolarisPrincipal.java +++ b/polaris-core/src/main/java/io/polaris/core/auth/AuthenticatedPolarisPrincipal.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.core.auth; import io.polaris.core.entity.PolarisEntity; diff --git a/polaris-core/src/main/java/io/polaris/core/auth/PolarisAuthorizableOperation.java b/polaris-core/src/main/java/io/polaris/core/auth/PolarisAuthorizableOperation.java index 4a77858f27..1b82ff2fb9 100644 --- a/polaris-core/src/main/java/io/polaris/core/auth/PolarisAuthorizableOperation.java +++ b/polaris-core/src/main/java/io/polaris/core/auth/PolarisAuthorizableOperation.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.core.auth; import static io.polaris.core.entity.PolarisPrivilege.CATALOG_CREATE; diff --git a/polaris-core/src/main/java/io/polaris/core/auth/PolarisAuthorizer.java b/polaris-core/src/main/java/io/polaris/core/auth/PolarisAuthorizer.java index 60dbf88f69..29f40daaba 100644 --- a/polaris-core/src/main/java/io/polaris/core/auth/PolarisAuthorizer.java +++ b/polaris-core/src/main/java/io/polaris/core/auth/PolarisAuthorizer.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.core.auth; import static io.polaris.core.entity.PolarisPrivilege.CATALOG_CREATE; diff --git a/polaris-core/src/main/java/io/polaris/core/catalog/PolarisCatalogHelpers.java b/polaris-core/src/main/java/io/polaris/core/catalog/PolarisCatalogHelpers.java index 44d47d2d3e..cf3e94052d 100644 --- a/polaris-core/src/main/java/io/polaris/core/catalog/PolarisCatalogHelpers.java +++ b/polaris-core/src/main/java/io/polaris/core/catalog/PolarisCatalogHelpers.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.core.catalog; import io.polaris.core.entity.PolarisEntity; diff --git a/polaris-core/src/main/java/io/polaris/core/context/CallContext.java b/polaris-core/src/main/java/io/polaris/core/context/CallContext.java index b6aa0c8804..39940b9340 100644 --- a/polaris-core/src/main/java/io/polaris/core/context/CallContext.java +++ b/polaris-core/src/main/java/io/polaris/core/context/CallContext.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.core.context; import io.polaris.core.PolarisCallContext; diff --git a/polaris-core/src/main/java/io/polaris/core/context/RealmContext.java b/polaris-core/src/main/java/io/polaris/core/context/RealmContext.java index 64db6af249..ad5f7445e0 100644 --- a/polaris-core/src/main/java/io/polaris/core/context/RealmContext.java +++ b/polaris-core/src/main/java/io/polaris/core/context/RealmContext.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.core.context; /** diff --git a/polaris-core/src/main/java/io/polaris/core/entity/AsyncTaskType.java b/polaris-core/src/main/java/io/polaris/core/entity/AsyncTaskType.java index 2b85ceb0ec..f710a4dbe9 100644 --- a/polaris-core/src/main/java/io/polaris/core/entity/AsyncTaskType.java +++ b/polaris-core/src/main/java/io/polaris/core/entity/AsyncTaskType.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.core.entity; import com.fasterxml.jackson.annotation.JsonCreator; diff --git a/polaris-core/src/main/java/io/polaris/core/entity/CatalogEntity.java b/polaris-core/src/main/java/io/polaris/core/entity/CatalogEntity.java index 1e16ec0c0d..1b01ab6559 100644 --- a/polaris-core/src/main/java/io/polaris/core/entity/CatalogEntity.java +++ b/polaris-core/src/main/java/io/polaris/core/entity/CatalogEntity.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.core.entity; import static io.polaris.core.admin.model.StorageConfigInfo.StorageTypeEnum.AZURE; diff --git a/polaris-core/src/main/java/io/polaris/core/entity/CatalogRoleEntity.java b/polaris-core/src/main/java/io/polaris/core/entity/CatalogRoleEntity.java index 1c4c247588..043e1bb285 100644 --- a/polaris-core/src/main/java/io/polaris/core/entity/CatalogRoleEntity.java +++ b/polaris-core/src/main/java/io/polaris/core/entity/CatalogRoleEntity.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.core.entity; import io.polaris.core.admin.model.CatalogRole; diff --git a/polaris-core/src/main/java/io/polaris/core/entity/NamespaceEntity.java b/polaris-core/src/main/java/io/polaris/core/entity/NamespaceEntity.java index 6b352834c2..8f5998b651 100644 --- a/polaris-core/src/main/java/io/polaris/core/entity/NamespaceEntity.java +++ b/polaris-core/src/main/java/io/polaris/core/entity/NamespaceEntity.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.core.entity; import io.polaris.core.catalog.PolarisCatalogHelpers; diff --git a/polaris-core/src/main/java/io/polaris/core/entity/PolarisBaseEntity.java b/polaris-core/src/main/java/io/polaris/core/entity/PolarisBaseEntity.java index 9e6ee28b60..0209c9951c 100644 --- a/polaris-core/src/main/java/io/polaris/core/entity/PolarisBaseEntity.java +++ b/polaris-core/src/main/java/io/polaris/core/entity/PolarisBaseEntity.java @@ -1,7 +1,18 @@ /* - * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. + * Copyright (c) 2024 Snowflake Computing 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. */ - package io.polaris.core.entity; import com.fasterxml.jackson.annotation.JsonIgnore; diff --git a/polaris-core/src/main/java/io/polaris/core/entity/PolarisChangeTrackingVersions.java b/polaris-core/src/main/java/io/polaris/core/entity/PolarisChangeTrackingVersions.java index 7b0c320c06..09808aa285 100644 --- a/polaris-core/src/main/java/io/polaris/core/entity/PolarisChangeTrackingVersions.java +++ b/polaris-core/src/main/java/io/polaris/core/entity/PolarisChangeTrackingVersions.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.core.entity; import com.fasterxml.jackson.annotation.JsonCreator; diff --git a/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntitiesActiveKey.java b/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntitiesActiveKey.java index 809e4e2180..2d8d3a8d64 100644 --- a/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntitiesActiveKey.java +++ b/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntitiesActiveKey.java @@ -1,7 +1,18 @@ /* - * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. + * Copyright (c) 2024 Snowflake Computing 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. */ - package io.polaris.core.entity; public class PolarisEntitiesActiveKey { diff --git a/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntity.java b/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntity.java index 1fb25e67ab..a421fc09cf 100644 --- a/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntity.java +++ b/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntity.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.core.entity; import com.fasterxml.jackson.annotation.JsonCreator; diff --git a/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntityActiveRecord.java b/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntityActiveRecord.java index e4d659fbc7..10461d6501 100644 --- a/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntityActiveRecord.java +++ b/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntityActiveRecord.java @@ -1,7 +1,18 @@ /* - * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. + * Copyright (c) 2024 Snowflake Computing 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. */ - package io.polaris.core.entity; import com.fasterxml.jackson.annotation.JsonCreator; diff --git a/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntityConstants.java b/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntityConstants.java index 0715c77452..b7031b3fb3 100644 --- a/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntityConstants.java +++ b/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntityConstants.java @@ -1,7 +1,18 @@ /* - * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. + * Copyright (c) 2024 Snowflake Computing 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. */ - package io.polaris.core.entity; public class PolarisEntityConstants { diff --git a/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntityCore.java b/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntityCore.java index 1b7054401f..f084f42f58 100644 --- a/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntityCore.java +++ b/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntityCore.java @@ -1,7 +1,18 @@ /* - * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. + * Copyright (c) 2024 Snowflake Computing 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. */ - package io.polaris.core.entity; import com.fasterxml.jackson.annotation.JsonIgnore; diff --git a/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntityId.java b/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntityId.java index 5f9b5f0ae6..74e2b57b21 100644 --- a/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntityId.java +++ b/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntityId.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.core.entity; import com.fasterxml.jackson.annotation.JsonCreator; diff --git a/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntitySubType.java b/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntitySubType.java index 0d1adca120..c36b75bdc0 100644 --- a/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntitySubType.java +++ b/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntitySubType.java @@ -1,7 +1,18 @@ /* - * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. + * Copyright (c) 2024 Snowflake Computing 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. */ - package io.polaris.core.entity; import com.fasterxml.jackson.annotation.JsonCreator; diff --git a/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntityType.java b/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntityType.java index b3a009a0a2..f920efbfd1 100644 --- a/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntityType.java +++ b/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntityType.java @@ -1,7 +1,18 @@ /* - * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. + * Copyright (c) 2024 Snowflake Computing 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. */ - package io.polaris.core.entity; import com.fasterxml.jackson.annotation.JsonCreator; diff --git a/polaris-core/src/main/java/io/polaris/core/entity/PolarisGrantRecord.java b/polaris-core/src/main/java/io/polaris/core/entity/PolarisGrantRecord.java index b7c8539284..7af2a2ee38 100644 --- a/polaris-core/src/main/java/io/polaris/core/entity/PolarisGrantRecord.java +++ b/polaris-core/src/main/java/io/polaris/core/entity/PolarisGrantRecord.java @@ -1,7 +1,18 @@ /* - * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. + * Copyright (c) 2024 Snowflake Computing 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. */ - package io.polaris.core.entity; import com.fasterxml.jackson.annotation.JsonCreator; diff --git a/polaris-core/src/main/java/io/polaris/core/entity/PolarisPrincipalSecrets.java b/polaris-core/src/main/java/io/polaris/core/entity/PolarisPrincipalSecrets.java index a32326e257..7efab8f530 100644 --- a/polaris-core/src/main/java/io/polaris/core/entity/PolarisPrincipalSecrets.java +++ b/polaris-core/src/main/java/io/polaris/core/entity/PolarisPrincipalSecrets.java @@ -1,7 +1,18 @@ /* - * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. + * Copyright (c) 2024 Snowflake Computing 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. */ - package io.polaris.core.entity; import com.fasterxml.jackson.annotation.JsonCreator; diff --git a/polaris-core/src/main/java/io/polaris/core/entity/PolarisPrivilege.java b/polaris-core/src/main/java/io/polaris/core/entity/PolarisPrivilege.java index f17b5356cd..3215825d76 100644 --- a/polaris-core/src/main/java/io/polaris/core/entity/PolarisPrivilege.java +++ b/polaris-core/src/main/java/io/polaris/core/entity/PolarisPrivilege.java @@ -1,7 +1,18 @@ /* - * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. + * Copyright (c) 2024 Snowflake Computing 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. */ - package io.polaris.core.entity; import com.fasterxml.jackson.annotation.JsonCreator; diff --git a/polaris-core/src/main/java/io/polaris/core/entity/PolarisTaskConstants.java b/polaris-core/src/main/java/io/polaris/core/entity/PolarisTaskConstants.java index 26ddad3708..36961eb9f8 100644 --- a/polaris-core/src/main/java/io/polaris/core/entity/PolarisTaskConstants.java +++ b/polaris-core/src/main/java/io/polaris/core/entity/PolarisTaskConstants.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.core.entity; /** Constants used to store task properties and configuration parameters */ diff --git a/polaris-core/src/main/java/io/polaris/core/entity/PrincipalEntity.java b/polaris-core/src/main/java/io/polaris/core/entity/PrincipalEntity.java index 8944bf0e51..eaa8bfc7e3 100644 --- a/polaris-core/src/main/java/io/polaris/core/entity/PrincipalEntity.java +++ b/polaris-core/src/main/java/io/polaris/core/entity/PrincipalEntity.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.core.entity; import io.polaris.core.admin.model.Principal; diff --git a/polaris-core/src/main/java/io/polaris/core/entity/PrincipalRoleEntity.java b/polaris-core/src/main/java/io/polaris/core/entity/PrincipalRoleEntity.java index 4049c8940c..44732e875b 100644 --- a/polaris-core/src/main/java/io/polaris/core/entity/PrincipalRoleEntity.java +++ b/polaris-core/src/main/java/io/polaris/core/entity/PrincipalRoleEntity.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.core.entity; import io.polaris.core.admin.model.PrincipalRole; diff --git a/polaris-core/src/main/java/io/polaris/core/entity/TableLikeEntity.java b/polaris-core/src/main/java/io/polaris/core/entity/TableLikeEntity.java index bf4fbbf20e..3cf80a4739 100644 --- a/polaris-core/src/main/java/io/polaris/core/entity/TableLikeEntity.java +++ b/polaris-core/src/main/java/io/polaris/core/entity/TableLikeEntity.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.core.entity; import com.fasterxml.jackson.annotation.JsonIgnore; diff --git a/polaris-core/src/main/java/io/polaris/core/entity/TaskEntity.java b/polaris-core/src/main/java/io/polaris/core/entity/TaskEntity.java index 2c335b8191..ca2d7d17c2 100644 --- a/polaris-core/src/main/java/io/polaris/core/entity/TaskEntity.java +++ b/polaris-core/src/main/java/io/polaris/core/entity/TaskEntity.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.core.entity; import io.polaris.core.PolarisCallContext; diff --git a/polaris-core/src/main/java/io/polaris/core/monitor/PolarisMetricRegistry.java b/polaris-core/src/main/java/io/polaris/core/monitor/PolarisMetricRegistry.java index eb2a219260..1bef08a247 100644 --- a/polaris-core/src/main/java/io/polaris/core/monitor/PolarisMetricRegistry.java +++ b/polaris-core/src/main/java/io/polaris/core/monitor/PolarisMetricRegistry.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.core.monitor; import io.micrometer.core.instrument.Counter; diff --git a/polaris-core/src/main/java/io/polaris/core/persistence/LocalPolarisMetaStoreManagerFactory.java b/polaris-core/src/main/java/io/polaris/core/persistence/LocalPolarisMetaStoreManagerFactory.java index e56829ed9b..3ff5d54cbf 100644 --- a/polaris-core/src/main/java/io/polaris/core/persistence/LocalPolarisMetaStoreManagerFactory.java +++ b/polaris-core/src/main/java/io/polaris/core/persistence/LocalPolarisMetaStoreManagerFactory.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.core.persistence; import io.micrometer.core.instrument.MeterRegistry; diff --git a/polaris-core/src/main/java/io/polaris/core/persistence/MetaStoreManagerFactory.java b/polaris-core/src/main/java/io/polaris/core/persistence/MetaStoreManagerFactory.java index 1b35787a9c..25a215c655 100644 --- a/polaris-core/src/main/java/io/polaris/core/persistence/MetaStoreManagerFactory.java +++ b/polaris-core/src/main/java/io/polaris/core/persistence/MetaStoreManagerFactory.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.core.persistence; import com.fasterxml.jackson.annotation.JsonTypeInfo; diff --git a/polaris-core/src/main/java/io/polaris/core/persistence/PolarisEntityManager.java b/polaris-core/src/main/java/io/polaris/core/persistence/PolarisEntityManager.java index 683966e28f..79739813da 100644 --- a/polaris-core/src/main/java/io/polaris/core/persistence/PolarisEntityManager.java +++ b/polaris-core/src/main/java/io/polaris/core/persistence/PolarisEntityManager.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.core.persistence; import io.polaris.core.auth.AuthenticatedPolarisPrincipal; diff --git a/polaris-core/src/main/java/io/polaris/core/persistence/PolarisEntityResolver.java b/polaris-core/src/main/java/io/polaris/core/persistence/PolarisEntityResolver.java index d054713829..a5a87731b5 100644 --- a/polaris-core/src/main/java/io/polaris/core/persistence/PolarisEntityResolver.java +++ b/polaris-core/src/main/java/io/polaris/core/persistence/PolarisEntityResolver.java @@ -1,7 +1,18 @@ /* - * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. + * Copyright (c) 2024 Snowflake Computing 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. */ - package io.polaris.core.persistence; import io.polaris.core.PolarisCallContext; diff --git a/polaris-core/src/main/java/io/polaris/core/persistence/PolarisMetaStoreManager.java b/polaris-core/src/main/java/io/polaris/core/persistence/PolarisMetaStoreManager.java index a0e6e0822e..278dea10af 100644 --- a/polaris-core/src/main/java/io/polaris/core/persistence/PolarisMetaStoreManager.java +++ b/polaris-core/src/main/java/io/polaris/core/persistence/PolarisMetaStoreManager.java @@ -1,7 +1,18 @@ /* - * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. + * Copyright (c) 2024 Snowflake Computing 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. */ - package io.polaris.core.persistence; import com.fasterxml.jackson.annotation.JsonCreator; diff --git a/polaris-core/src/main/java/io/polaris/core/persistence/PolarisMetaStoreManagerImpl.java b/polaris-core/src/main/java/io/polaris/core/persistence/PolarisMetaStoreManagerImpl.java index 3a4ecd05f5..710fbed1d3 100644 --- a/polaris-core/src/main/java/io/polaris/core/persistence/PolarisMetaStoreManagerImpl.java +++ b/polaris-core/src/main/java/io/polaris/core/persistence/PolarisMetaStoreManagerImpl.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.core.persistence; import com.fasterxml.jackson.core.JsonProcessingException; diff --git a/polaris-core/src/main/java/io/polaris/core/persistence/PolarisMetaStoreSession.java b/polaris-core/src/main/java/io/polaris/core/persistence/PolarisMetaStoreSession.java index 6e48198fd1..8034887155 100644 --- a/polaris-core/src/main/java/io/polaris/core/persistence/PolarisMetaStoreSession.java +++ b/polaris-core/src/main/java/io/polaris/core/persistence/PolarisMetaStoreSession.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.core.persistence; import io.polaris.core.PolarisCallContext; diff --git a/polaris-core/src/main/java/io/polaris/core/persistence/PolarisObjectMapperUtil.java b/polaris-core/src/main/java/io/polaris/core/persistence/PolarisObjectMapperUtil.java index 41aea3c205..35a3f05d56 100644 --- a/polaris-core/src/main/java/io/polaris/core/persistence/PolarisObjectMapperUtil.java +++ b/polaris-core/src/main/java/io/polaris/core/persistence/PolarisObjectMapperUtil.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.core.persistence; import com.fasterxml.jackson.core.JsonFactory; diff --git a/polaris-core/src/main/java/io/polaris/core/persistence/PolarisResolvedPathWrapper.java b/polaris-core/src/main/java/io/polaris/core/persistence/PolarisResolvedPathWrapper.java index fb7d408b44..cf3e99c49e 100644 --- a/polaris-core/src/main/java/io/polaris/core/persistence/PolarisResolvedPathWrapper.java +++ b/polaris-core/src/main/java/io/polaris/core/persistence/PolarisResolvedPathWrapper.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.core.persistence; import io.polaris.core.entity.PolarisEntity; diff --git a/polaris-core/src/main/java/io/polaris/core/persistence/PolarisTreeMapMetaStoreSessionImpl.java b/polaris-core/src/main/java/io/polaris/core/persistence/PolarisTreeMapMetaStoreSessionImpl.java index 8127b1b612..8374fab262 100644 --- a/polaris-core/src/main/java/io/polaris/core/persistence/PolarisTreeMapMetaStoreSessionImpl.java +++ b/polaris-core/src/main/java/io/polaris/core/persistence/PolarisTreeMapMetaStoreSessionImpl.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.core.persistence; import com.google.common.base.Predicates; diff --git a/polaris-core/src/main/java/io/polaris/core/persistence/PolarisTreeMapStore.java b/polaris-core/src/main/java/io/polaris/core/persistence/PolarisTreeMapStore.java index 43c7427994..366c04c277 100644 --- a/polaris-core/src/main/java/io/polaris/core/persistence/PolarisTreeMapStore.java +++ b/polaris-core/src/main/java/io/polaris/core/persistence/PolarisTreeMapStore.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.core.persistence; import io.polaris.core.PolarisCallContext; diff --git a/polaris-core/src/main/java/io/polaris/core/persistence/ResolvedPolarisEntity.java b/polaris-core/src/main/java/io/polaris/core/persistence/ResolvedPolarisEntity.java index bf451d4679..f5b5c674c5 100644 --- a/polaris-core/src/main/java/io/polaris/core/persistence/ResolvedPolarisEntity.java +++ b/polaris-core/src/main/java/io/polaris/core/persistence/ResolvedPolarisEntity.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.core.persistence; import com.google.common.collect.ImmutableList; diff --git a/polaris-core/src/main/java/io/polaris/core/persistence/RetryOnConcurrencyException.java b/polaris-core/src/main/java/io/polaris/core/persistence/RetryOnConcurrencyException.java index f5a6187742..d7081c0208 100644 --- a/polaris-core/src/main/java/io/polaris/core/persistence/RetryOnConcurrencyException.java +++ b/polaris-core/src/main/java/io/polaris/core/persistence/RetryOnConcurrencyException.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.core.persistence; import com.google.errorprone.annotations.FormatMethod; diff --git a/polaris-core/src/main/java/io/polaris/core/persistence/cache/EntityCache.java b/polaris-core/src/main/java/io/polaris/core/persistence/cache/EntityCache.java index eaf6f6e75f..85be4d4860 100644 --- a/polaris-core/src/main/java/io/polaris/core/persistence/cache/EntityCache.java +++ b/polaris-core/src/main/java/io/polaris/core/persistence/cache/EntityCache.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.core.persistence.cache; import com.github.benmanes.caffeine.cache.Cache; diff --git a/polaris-core/src/main/java/io/polaris/core/persistence/cache/EntityCacheByNameKey.java b/polaris-core/src/main/java/io/polaris/core/persistence/cache/EntityCacheByNameKey.java index d829d60b3d..d41581a8d7 100644 --- a/polaris-core/src/main/java/io/polaris/core/persistence/cache/EntityCacheByNameKey.java +++ b/polaris-core/src/main/java/io/polaris/core/persistence/cache/EntityCacheByNameKey.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.core.persistence.cache; import io.polaris.core.entity.PolarisBaseEntity; diff --git a/polaris-core/src/main/java/io/polaris/core/persistence/cache/EntityCacheEntry.java b/polaris-core/src/main/java/io/polaris/core/persistence/cache/EntityCacheEntry.java index 279ace675e..294d835b74 100644 --- a/polaris-core/src/main/java/io/polaris/core/persistence/cache/EntityCacheEntry.java +++ b/polaris-core/src/main/java/io/polaris/core/persistence/cache/EntityCacheEntry.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.core.persistence.cache; import com.google.common.collect.ImmutableList; diff --git a/polaris-core/src/main/java/io/polaris/core/persistence/cache/EntityCacheLookupResult.java b/polaris-core/src/main/java/io/polaris/core/persistence/cache/EntityCacheLookupResult.java index e551144618..c8fe77f953 100644 --- a/polaris-core/src/main/java/io/polaris/core/persistence/cache/EntityCacheLookupResult.java +++ b/polaris-core/src/main/java/io/polaris/core/persistence/cache/EntityCacheLookupResult.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.core.persistence.cache; import org.jetbrains.annotations.Nullable; diff --git a/polaris-core/src/main/java/io/polaris/core/persistence/cache/EntityCacheMode.java b/polaris-core/src/main/java/io/polaris/core/persistence/cache/EntityCacheMode.java index 0d20383b09..f2addb9622 100644 --- a/polaris-core/src/main/java/io/polaris/core/persistence/cache/EntityCacheMode.java +++ b/polaris-core/src/main/java/io/polaris/core/persistence/cache/EntityCacheMode.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.core.persistence.cache; /** Cache mode, the default is ENABLE. */ diff --git a/polaris-core/src/main/java/io/polaris/core/persistence/models/ModelEntity.java b/polaris-core/src/main/java/io/polaris/core/persistence/models/ModelEntity.java index e96ea5c853..4b85a15867 100644 --- a/polaris-core/src/main/java/io/polaris/core/persistence/models/ModelEntity.java +++ b/polaris-core/src/main/java/io/polaris/core/persistence/models/ModelEntity.java @@ -1,7 +1,18 @@ /* - * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. + * Copyright (c) 2024 Snowflake Computing 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. */ - package io.polaris.core.persistence.models; import io.polaris.core.entity.PolarisBaseEntity; diff --git a/polaris-core/src/main/java/io/polaris/core/persistence/models/ModelEntityActive.java b/polaris-core/src/main/java/io/polaris/core/persistence/models/ModelEntityActive.java index 732581eacc..58ed614556 100644 --- a/polaris-core/src/main/java/io/polaris/core/persistence/models/ModelEntityActive.java +++ b/polaris-core/src/main/java/io/polaris/core/persistence/models/ModelEntityActive.java @@ -1,7 +1,18 @@ /* - * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. + * Copyright (c) 2024 Snowflake Computing 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. */ - package io.polaris.core.persistence.models; import io.polaris.core.entity.PolarisEntityActiveRecord; diff --git a/polaris-core/src/main/java/io/polaris/core/persistence/models/ModelEntityChangeTracking.java b/polaris-core/src/main/java/io/polaris/core/persistence/models/ModelEntityChangeTracking.java index f7d31860f9..857d5a6e3a 100644 --- a/polaris-core/src/main/java/io/polaris/core/persistence/models/ModelEntityChangeTracking.java +++ b/polaris-core/src/main/java/io/polaris/core/persistence/models/ModelEntityChangeTracking.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.core.persistence.models; import io.polaris.core.entity.PolarisBaseEntity; diff --git a/polaris-core/src/main/java/io/polaris/core/persistence/models/ModelEntityDropped.java b/polaris-core/src/main/java/io/polaris/core/persistence/models/ModelEntityDropped.java index 504ef42a7a..44ada31438 100644 --- a/polaris-core/src/main/java/io/polaris/core/persistence/models/ModelEntityDropped.java +++ b/polaris-core/src/main/java/io/polaris/core/persistence/models/ModelEntityDropped.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.core.persistence.models; import io.polaris.core.entity.PolarisBaseEntity; diff --git a/polaris-core/src/main/java/io/polaris/core/persistence/models/ModelGrantRecord.java b/polaris-core/src/main/java/io/polaris/core/persistence/models/ModelGrantRecord.java index 022d8334b2..b464a4eead 100644 --- a/polaris-core/src/main/java/io/polaris/core/persistence/models/ModelGrantRecord.java +++ b/polaris-core/src/main/java/io/polaris/core/persistence/models/ModelGrantRecord.java @@ -1,7 +1,18 @@ /* - * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. + * Copyright (c) 2024 Snowflake Computing 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. */ - package io.polaris.core.persistence.models; import io.polaris.core.entity.PolarisGrantRecord; diff --git a/polaris-core/src/main/java/io/polaris/core/persistence/models/ModelPrincipalSecrets.java b/polaris-core/src/main/java/io/polaris/core/persistence/models/ModelPrincipalSecrets.java index b7658c9186..c0f35dec9a 100644 --- a/polaris-core/src/main/java/io/polaris/core/persistence/models/ModelPrincipalSecrets.java +++ b/polaris-core/src/main/java/io/polaris/core/persistence/models/ModelPrincipalSecrets.java @@ -1,7 +1,18 @@ /* - * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. + * Copyright (c) 2024 Snowflake Computing 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. */ - package io.polaris.core.persistence.models; import io.polaris.core.entity.PolarisPrincipalSecrets; diff --git a/polaris-core/src/main/java/io/polaris/core/persistence/models/ModelSequenceId.java b/polaris-core/src/main/java/io/polaris/core/persistence/models/ModelSequenceId.java index a48fefefb9..52e6c8f44b 100644 --- a/polaris-core/src/main/java/io/polaris/core/persistence/models/ModelSequenceId.java +++ b/polaris-core/src/main/java/io/polaris/core/persistence/models/ModelSequenceId.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.core.persistence.models; import jakarta.persistence.Entity; diff --git a/polaris-core/src/main/java/io/polaris/core/persistence/resolver/PolarisResolutionManifest.java b/polaris-core/src/main/java/io/polaris/core/persistence/resolver/PolarisResolutionManifest.java index 8287cbe6d4..7c50039411 100644 --- a/polaris-core/src/main/java/io/polaris/core/persistence/resolver/PolarisResolutionManifest.java +++ b/polaris-core/src/main/java/io/polaris/core/persistence/resolver/PolarisResolutionManifest.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.core.persistence.resolver; import com.google.common.collect.HashMultimap; diff --git a/polaris-core/src/main/java/io/polaris/core/persistence/resolver/PolarisResolutionManifestCatalogView.java b/polaris-core/src/main/java/io/polaris/core/persistence/resolver/PolarisResolutionManifestCatalogView.java index 335a9726be..08865dd80c 100644 --- a/polaris-core/src/main/java/io/polaris/core/persistence/resolver/PolarisResolutionManifestCatalogView.java +++ b/polaris-core/src/main/java/io/polaris/core/persistence/resolver/PolarisResolutionManifestCatalogView.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.core.persistence.resolver; import io.polaris.core.entity.PolarisEntitySubType; diff --git a/polaris-core/src/main/java/io/polaris/core/persistence/resolver/Resolver.java b/polaris-core/src/main/java/io/polaris/core/persistence/resolver/Resolver.java index 6ed75e4296..ca8d87dccf 100644 --- a/polaris-core/src/main/java/io/polaris/core/persistence/resolver/Resolver.java +++ b/polaris-core/src/main/java/io/polaris/core/persistence/resolver/Resolver.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.core.persistence.resolver; import io.polaris.core.PolarisCallContext; diff --git a/polaris-core/src/main/java/io/polaris/core/persistence/resolver/ResolverEntityName.java b/polaris-core/src/main/java/io/polaris/core/persistence/resolver/ResolverEntityName.java index e808e14d99..3ec5c0a5b3 100644 --- a/polaris-core/src/main/java/io/polaris/core/persistence/resolver/ResolverEntityName.java +++ b/polaris-core/src/main/java/io/polaris/core/persistence/resolver/ResolverEntityName.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.core.persistence.resolver; import io.polaris.core.entity.PolarisEntityType; diff --git a/polaris-core/src/main/java/io/polaris/core/persistence/resolver/ResolverPath.java b/polaris-core/src/main/java/io/polaris/core/persistence/resolver/ResolverPath.java index 66518fc3c4..2f926a5398 100644 --- a/polaris-core/src/main/java/io/polaris/core/persistence/resolver/ResolverPath.java +++ b/polaris-core/src/main/java/io/polaris/core/persistence/resolver/ResolverPath.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.core.persistence.resolver; import com.google.common.collect.ImmutableList; diff --git a/polaris-core/src/main/java/io/polaris/core/persistence/resolver/ResolverPrincipalRole.java b/polaris-core/src/main/java/io/polaris/core/persistence/resolver/ResolverPrincipalRole.java index 415b4b3620..2c34f89f41 100644 --- a/polaris-core/src/main/java/io/polaris/core/persistence/resolver/ResolverPrincipalRole.java +++ b/polaris-core/src/main/java/io/polaris/core/persistence/resolver/ResolverPrincipalRole.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.core.persistence.resolver; /** Expected principal type for the principal. Expectation depends on the REST request type */ diff --git a/polaris-core/src/main/java/io/polaris/core/persistence/resolver/ResolverStatus.java b/polaris-core/src/main/java/io/polaris/core/persistence/resolver/ResolverStatus.java index d144cb01f2..180043bbf3 100644 --- a/polaris-core/src/main/java/io/polaris/core/persistence/resolver/ResolverStatus.java +++ b/polaris-core/src/main/java/io/polaris/core/persistence/resolver/ResolverStatus.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.core.persistence.resolver; import io.polaris.core.entity.PolarisEntityType; diff --git a/polaris-core/src/main/java/io/polaris/core/storage/FileStorageConfigurationInfo.java b/polaris-core/src/main/java/io/polaris/core/storage/FileStorageConfigurationInfo.java index f8b7f1c0cb..1d0d6ea39e 100644 --- a/polaris-core/src/main/java/io/polaris/core/storage/FileStorageConfigurationInfo.java +++ b/polaris-core/src/main/java/io/polaris/core/storage/FileStorageConfigurationInfo.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.core.storage; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/polaris-core/src/main/java/io/polaris/core/storage/InMemoryStorageIntegration.java b/polaris-core/src/main/java/io/polaris/core/storage/InMemoryStorageIntegration.java index 7d389c69c2..5e107b144e 100644 --- a/polaris-core/src/main/java/io/polaris/core/storage/InMemoryStorageIntegration.java +++ b/polaris-core/src/main/java/io/polaris/core/storage/InMemoryStorageIntegration.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.core.storage; import io.polaris.core.context.CallContext; diff --git a/polaris-core/src/main/java/io/polaris/core/storage/PolarisCredentialProperty.java b/polaris-core/src/main/java/io/polaris/core/storage/PolarisCredentialProperty.java index 61f53967b2..9fb9aee03b 100644 --- a/polaris-core/src/main/java/io/polaris/core/storage/PolarisCredentialProperty.java +++ b/polaris-core/src/main/java/io/polaris/core/storage/PolarisCredentialProperty.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.core.storage; /** Enum of polaris supported credential properties */ diff --git a/polaris-core/src/main/java/io/polaris/core/storage/PolarisStorageActions.java b/polaris-core/src/main/java/io/polaris/core/storage/PolarisStorageActions.java index a367c9b95b..fe0f562b86 100644 --- a/polaris-core/src/main/java/io/polaris/core/storage/PolarisStorageActions.java +++ b/polaris-core/src/main/java/io/polaris/core/storage/PolarisStorageActions.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.core.storage; public enum PolarisStorageActions { diff --git a/polaris-core/src/main/java/io/polaris/core/storage/PolarisStorageConfigurationInfo.java b/polaris-core/src/main/java/io/polaris/core/storage/PolarisStorageConfigurationInfo.java index bc25d1febd..b705a61a33 100644 --- a/polaris-core/src/main/java/io/polaris/core/storage/PolarisStorageConfigurationInfo.java +++ b/polaris-core/src/main/java/io/polaris/core/storage/PolarisStorageConfigurationInfo.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.core.storage; import com.fasterxml.jackson.annotation.JsonInclude; diff --git a/polaris-core/src/main/java/io/polaris/core/storage/PolarisStorageIntegration.java b/polaris-core/src/main/java/io/polaris/core/storage/PolarisStorageIntegration.java index 7f83b13149..4f89382bc5 100644 --- a/polaris-core/src/main/java/io/polaris/core/storage/PolarisStorageIntegration.java +++ b/polaris-core/src/main/java/io/polaris/core/storage/PolarisStorageIntegration.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.core.storage; import io.polaris.core.PolarisDiagnostics; diff --git a/polaris-core/src/main/java/io/polaris/core/storage/PolarisStorageIntegrationProvider.java b/polaris-core/src/main/java/io/polaris/core/storage/PolarisStorageIntegrationProvider.java index 1e9fd85893..12e2fc39c9 100644 --- a/polaris-core/src/main/java/io/polaris/core/storage/PolarisStorageIntegrationProvider.java +++ b/polaris-core/src/main/java/io/polaris/core/storage/PolarisStorageIntegrationProvider.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.core.storage; import org.jetbrains.annotations.Nullable; diff --git a/polaris-core/src/main/java/io/polaris/core/storage/aws/AwsCredentialsStorageIntegration.java b/polaris-core/src/main/java/io/polaris/core/storage/aws/AwsCredentialsStorageIntegration.java index fe53a3f587..be3a3eab85 100644 --- a/polaris-core/src/main/java/io/polaris/core/storage/aws/AwsCredentialsStorageIntegration.java +++ b/polaris-core/src/main/java/io/polaris/core/storage/aws/AwsCredentialsStorageIntegration.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.core.storage.aws; import io.polaris.core.PolarisDiagnostics; diff --git a/polaris-core/src/main/java/io/polaris/core/storage/aws/AwsStorageConfigurationInfo.java b/polaris-core/src/main/java/io/polaris/core/storage/aws/AwsStorageConfigurationInfo.java index 5cfed26840..8e0aef0d13 100644 --- a/polaris-core/src/main/java/io/polaris/core/storage/aws/AwsStorageConfigurationInfo.java +++ b/polaris-core/src/main/java/io/polaris/core/storage/aws/AwsStorageConfigurationInfo.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.core.storage.aws; import com.fasterxml.jackson.annotation.JsonCreator; diff --git a/polaris-core/src/main/java/io/polaris/core/storage/aws/PolarisS3FileIOClientFactory.java b/polaris-core/src/main/java/io/polaris/core/storage/aws/PolarisS3FileIOClientFactory.java index f250bcc3f7..93afe887a2 100644 --- a/polaris-core/src/main/java/io/polaris/core/storage/aws/PolarisS3FileIOClientFactory.java +++ b/polaris-core/src/main/java/io/polaris/core/storage/aws/PolarisS3FileIOClientFactory.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.core.storage.aws; import java.util.Map; diff --git a/polaris-core/src/main/java/io/polaris/core/storage/azure/AzureCredentialsStorageIntegration.java b/polaris-core/src/main/java/io/polaris/core/storage/azure/AzureCredentialsStorageIntegration.java index dc342c0e7f..97bcbe0095 100644 --- a/polaris-core/src/main/java/io/polaris/core/storage/azure/AzureCredentialsStorageIntegration.java +++ b/polaris-core/src/main/java/io/polaris/core/storage/azure/AzureCredentialsStorageIntegration.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.core.storage.azure; import com.azure.core.credential.AccessToken; diff --git a/polaris-core/src/main/java/io/polaris/core/storage/azure/AzureLocation.java b/polaris-core/src/main/java/io/polaris/core/storage/azure/AzureLocation.java index 847c38a0b2..0e6fa0d641 100644 --- a/polaris-core/src/main/java/io/polaris/core/storage/azure/AzureLocation.java +++ b/polaris-core/src/main/java/io/polaris/core/storage/azure/AzureLocation.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.core.storage.azure; import java.util.regex.Matcher; diff --git a/polaris-core/src/main/java/io/polaris/core/storage/azure/AzureStorageConfigurationInfo.java b/polaris-core/src/main/java/io/polaris/core/storage/azure/AzureStorageConfigurationInfo.java index 4c50562994..47bf4d53b0 100644 --- a/polaris-core/src/main/java/io/polaris/core/storage/azure/AzureStorageConfigurationInfo.java +++ b/polaris-core/src/main/java/io/polaris/core/storage/azure/AzureStorageConfigurationInfo.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.core.storage.azure; import com.fasterxml.jackson.annotation.JsonCreator; diff --git a/polaris-core/src/main/java/io/polaris/core/storage/cache/StorageCredentialCache.java b/polaris-core/src/main/java/io/polaris/core/storage/cache/StorageCredentialCache.java index 45723daae9..a3130e4ba9 100644 --- a/polaris-core/src/main/java/io/polaris/core/storage/cache/StorageCredentialCache.java +++ b/polaris-core/src/main/java/io/polaris/core/storage/cache/StorageCredentialCache.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.core.storage.cache; import com.github.benmanes.caffeine.cache.CacheLoader; diff --git a/polaris-core/src/main/java/io/polaris/core/storage/cache/StorageCredentialCacheEntry.java b/polaris-core/src/main/java/io/polaris/core/storage/cache/StorageCredentialCacheEntry.java index 0d0f841343..4f0eefd7bf 100644 --- a/polaris-core/src/main/java/io/polaris/core/storage/cache/StorageCredentialCacheEntry.java +++ b/polaris-core/src/main/java/io/polaris/core/storage/cache/StorageCredentialCacheEntry.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.core.storage.cache; import io.polaris.core.persistence.PolarisMetaStoreManager; diff --git a/polaris-core/src/main/java/io/polaris/core/storage/cache/StorageCredentialCacheKey.java b/polaris-core/src/main/java/io/polaris/core/storage/cache/StorageCredentialCacheKey.java index 559425f802..1791ceac29 100644 --- a/polaris-core/src/main/java/io/polaris/core/storage/cache/StorageCredentialCacheKey.java +++ b/polaris-core/src/main/java/io/polaris/core/storage/cache/StorageCredentialCacheKey.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.core.storage.cache; import io.polaris.core.PolarisCallContext; diff --git a/polaris-core/src/main/java/io/polaris/core/storage/gcp/GcpCredentialsStorageIntegration.java b/polaris-core/src/main/java/io/polaris/core/storage/gcp/GcpCredentialsStorageIntegration.java index 7a6ca1fdec..88d322f64f 100644 --- a/polaris-core/src/main/java/io/polaris/core/storage/gcp/GcpCredentialsStorageIntegration.java +++ b/polaris-core/src/main/java/io/polaris/core/storage/gcp/GcpCredentialsStorageIntegration.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.core.storage.gcp; import com.fasterxml.jackson.core.JsonProcessingException; diff --git a/polaris-core/src/main/java/io/polaris/core/storage/gcp/GcpStorageConfigurationInfo.java b/polaris-core/src/main/java/io/polaris/core/storage/gcp/GcpStorageConfigurationInfo.java index 4cff55f82b..15daf1eac5 100644 --- a/polaris-core/src/main/java/io/polaris/core/storage/gcp/GcpStorageConfigurationInfo.java +++ b/polaris-core/src/main/java/io/polaris/core/storage/gcp/GcpStorageConfigurationInfo.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.core.storage.gcp; import com.fasterxml.jackson.annotation.JsonCreator; diff --git a/polaris-core/src/test/java/io/polaris/core/persistence/EntityCacheTest.java b/polaris-core/src/test/java/io/polaris/core/persistence/EntityCacheTest.java index 98627e29dd..4d486d5278 100644 --- a/polaris-core/src/test/java/io/polaris/core/persistence/EntityCacheTest.java +++ b/polaris-core/src/test/java/io/polaris/core/persistence/EntityCacheTest.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.core.persistence; import io.polaris.core.PolarisCallContext; diff --git a/polaris-core/src/test/java/io/polaris/core/persistence/PolarisObjectMapperUtilTest.java b/polaris-core/src/test/java/io/polaris/core/persistence/PolarisObjectMapperUtilTest.java index 27b13ea4ce..3a89b31d91 100644 --- a/polaris-core/src/test/java/io/polaris/core/persistence/PolarisObjectMapperUtilTest.java +++ b/polaris-core/src/test/java/io/polaris/core/persistence/PolarisObjectMapperUtilTest.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.core.persistence; import io.polaris.core.entity.PolarisBaseEntity; diff --git a/polaris-core/src/test/java/io/polaris/core/persistence/PolarisTreeMapMetaStoreManagerTest.java b/polaris-core/src/test/java/io/polaris/core/persistence/PolarisTreeMapMetaStoreManagerTest.java index 36df7b5d7a..f910fa6d4d 100644 --- a/polaris-core/src/test/java/io/polaris/core/persistence/PolarisTreeMapMetaStoreManagerTest.java +++ b/polaris-core/src/test/java/io/polaris/core/persistence/PolarisTreeMapMetaStoreManagerTest.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.core.persistence; import io.polaris.core.PolarisCallContext; diff --git a/polaris-core/src/test/java/io/polaris/core/persistence/ResolverTest.java b/polaris-core/src/test/java/io/polaris/core/persistence/ResolverTest.java index f409751db3..e077cd9d0e 100644 --- a/polaris-core/src/test/java/io/polaris/core/persistence/ResolverTest.java +++ b/polaris-core/src/test/java/io/polaris/core/persistence/ResolverTest.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.core.persistence; import io.polaris.core.PolarisCallContext; diff --git a/polaris-core/src/test/java/io/polaris/core/storage/InMemoryStorageIntegrationTest.java b/polaris-core/src/test/java/io/polaris/core/storage/InMemoryStorageIntegrationTest.java index 778437be28..3b1291f614 100644 --- a/polaris-core/src/test/java/io/polaris/core/storage/InMemoryStorageIntegrationTest.java +++ b/polaris-core/src/test/java/io/polaris/core/storage/InMemoryStorageIntegrationTest.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.core.storage; import io.polaris.core.PolarisCallContext; diff --git a/polaris-core/src/test/java/io/polaris/core/storage/cache/StorageCredentialCacheTest.java b/polaris-core/src/test/java/io/polaris/core/storage/cache/StorageCredentialCacheTest.java index 2c5e045950..efbaf19c82 100644 --- a/polaris-core/src/test/java/io/polaris/core/storage/cache/StorageCredentialCacheTest.java +++ b/polaris-core/src/test/java/io/polaris/core/storage/cache/StorageCredentialCacheTest.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.core.storage.cache; import io.polaris.core.PolarisCallContext; diff --git a/polaris-core/src/test/java/io/polaris/service/storage/aws/AwsCredentialsStorageIntegrationTest.java b/polaris-core/src/test/java/io/polaris/service/storage/aws/AwsCredentialsStorageIntegrationTest.java index 1d27614633..44acbb8807 100644 --- a/polaris-core/src/test/java/io/polaris/service/storage/aws/AwsCredentialsStorageIntegrationTest.java +++ b/polaris-core/src/test/java/io/polaris/service/storage/aws/AwsCredentialsStorageIntegrationTest.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.storage.aws; import static org.assertj.core.api.Assertions.assertThat; diff --git a/polaris-core/src/test/java/io/polaris/service/storage/azure/AzureCredentialStorageIntegrationTest.java b/polaris-core/src/test/java/io/polaris/service/storage/azure/AzureCredentialStorageIntegrationTest.java index 2c9200803c..d80957bdc4 100644 --- a/polaris-core/src/test/java/io/polaris/service/storage/azure/AzureCredentialStorageIntegrationTest.java +++ b/polaris-core/src/test/java/io/polaris/service/storage/azure/AzureCredentialStorageIntegrationTest.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.storage.azure; import com.azure.storage.blob.BlobClient; diff --git a/polaris-core/src/test/java/io/polaris/service/storage/azure/AzureLocationTest.java b/polaris-core/src/test/java/io/polaris/service/storage/azure/AzureLocationTest.java index 00e0b23f20..973357e3d2 100644 --- a/polaris-core/src/test/java/io/polaris/service/storage/azure/AzureLocationTest.java +++ b/polaris-core/src/test/java/io/polaris/service/storage/azure/AzureLocationTest.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.storage.azure; import io.polaris.core.storage.azure.AzureLocation; diff --git a/polaris-core/src/test/java/io/polaris/service/storage/gcp/GcpCredentialsStorageIntegrationTest.java b/polaris-core/src/test/java/io/polaris/service/storage/gcp/GcpCredentialsStorageIntegrationTest.java index 580c744ae2..8aedf9bcbc 100644 --- a/polaris-core/src/test/java/io/polaris/service/storage/gcp/GcpCredentialsStorageIntegrationTest.java +++ b/polaris-core/src/test/java/io/polaris/service/storage/gcp/GcpCredentialsStorageIntegrationTest.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.storage.gcp; import static org.assertj.core.api.Assertions.assertThat; diff --git a/polaris-core/src/testFixtures/java/io/polaris/core/persistence/PolarisMetaStoreManagerTest.java b/polaris-core/src/testFixtures/java/io/polaris/core/persistence/PolarisMetaStoreManagerTest.java index 9006e24863..afb5ab0244 100644 --- a/polaris-core/src/testFixtures/java/io/polaris/core/persistence/PolarisMetaStoreManagerTest.java +++ b/polaris-core/src/testFixtures/java/io/polaris/core/persistence/PolarisMetaStoreManagerTest.java @@ -1,7 +1,18 @@ /* - * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. + * Copyright (c) 2024 Snowflake Computing 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. */ - package io.polaris.core.persistence; import io.polaris.core.PolarisCallContext; diff --git a/polaris-core/src/testFixtures/java/io/polaris/core/persistence/PolarisTestMetaStoreManager.java b/polaris-core/src/testFixtures/java/io/polaris/core/persistence/PolarisTestMetaStoreManager.java index 5ff18af4b5..8ba30563df 100644 --- a/polaris-core/src/testFixtures/java/io/polaris/core/persistence/PolarisTestMetaStoreManager.java +++ b/polaris-core/src/testFixtures/java/io/polaris/core/persistence/PolarisTestMetaStoreManager.java @@ -1,7 +1,18 @@ /* - * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. + * Copyright (c) 2024 Snowflake Computing 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. */ - package io.polaris.core.persistence; import com.fasterxml.jackson.core.JsonProcessingException; diff --git a/polaris-server.yml b/polaris-server.yml index 0311b423c0..886221937e 100644 --- a/polaris-server.yml +++ b/polaris-server.yml @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# server: # Maximum number of threads. maxThreads: 200 diff --git a/polaris-service/build.gradle b/polaris-service/build.gradle index 689c80c772..3dc0dfb364 100644 --- a/polaris-service/build.gradle +++ b/polaris-service/build.gradle @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ + plugins { id 'com.github.johnrengelman.shadow' version '8.1.1' id 'org.openapi.generator' version '7.6.0' diff --git a/polaris-service/src/main/java/io/polaris/service/BootstrapRealmsCommand.java b/polaris-service/src/main/java/io/polaris/service/BootstrapRealmsCommand.java index e815a08b71..cf17edcf77 100644 --- a/polaris-service/src/main/java/io/polaris/service/BootstrapRealmsCommand.java +++ b/polaris-service/src/main/java/io/polaris/service/BootstrapRealmsCommand.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service; import io.dropwizard.core.cli.ConfiguredCommand; diff --git a/polaris-service/src/main/java/io/polaris/service/IcebergExceptionMapper.java b/polaris-service/src/main/java/io/polaris/service/IcebergExceptionMapper.java index 64bcdcb93d..a623ea3e81 100644 --- a/polaris-service/src/main/java/io/polaris/service/IcebergExceptionMapper.java +++ b/polaris-service/src/main/java/io/polaris/service/IcebergExceptionMapper.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service; import jakarta.ws.rs.WebApplicationException; diff --git a/polaris-service/src/main/java/io/polaris/service/IcebergJerseyViolationExceptionMapper.java b/polaris-service/src/main/java/io/polaris/service/IcebergJerseyViolationExceptionMapper.java index 166be1b278..f80369e107 100644 --- a/polaris-service/src/main/java/io/polaris/service/IcebergJerseyViolationExceptionMapper.java +++ b/polaris-service/src/main/java/io/polaris/service/IcebergJerseyViolationExceptionMapper.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service; import io.dropwizard.jersey.validation.JerseyViolationException; diff --git a/polaris-service/src/main/java/io/polaris/service/IcebergJsonProcessingExceptionMapper.java b/polaris-service/src/main/java/io/polaris/service/IcebergJsonProcessingExceptionMapper.java index b6cc1739ad..5db1aed9a2 100644 --- a/polaris-service/src/main/java/io/polaris/service/IcebergJsonProcessingExceptionMapper.java +++ b/polaris-service/src/main/java/io/polaris/service/IcebergJsonProcessingExceptionMapper.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service; import com.fasterxml.jackson.core.JsonGenerationException; diff --git a/polaris-service/src/main/java/io/polaris/service/PolarisApplication.java b/polaris-service/src/main/java/io/polaris/service/PolarisApplication.java index adb1c8cb74..c8e0c24489 100644 --- a/polaris-service/src/main/java/io/polaris/service/PolarisApplication.java +++ b/polaris-service/src/main/java/io/polaris/service/PolarisApplication.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service; import com.fasterxml.jackson.annotation.JsonAutoDetect; diff --git a/polaris-service/src/main/java/io/polaris/service/PolarisHealthCheck.java b/polaris-service/src/main/java/io/polaris/service/PolarisHealthCheck.java index ecca6887a1..3302f2b651 100644 --- a/polaris-service/src/main/java/io/polaris/service/PolarisHealthCheck.java +++ b/polaris-service/src/main/java/io/polaris/service/PolarisHealthCheck.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service; import com.codahale.metrics.health.HealthCheck; diff --git a/polaris-service/src/main/java/io/polaris/service/TimedApplicationEventListener.java b/polaris-service/src/main/java/io/polaris/service/TimedApplicationEventListener.java index 59d9fa54d0..9645636ebd 100644 --- a/polaris-service/src/main/java/io/polaris/service/TimedApplicationEventListener.java +++ b/polaris-service/src/main/java/io/polaris/service/TimedApplicationEventListener.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service; import com.google.common.base.Stopwatch; diff --git a/polaris-service/src/main/java/io/polaris/service/admin/PolarisAdminService.java b/polaris-service/src/main/java/io/polaris/service/admin/PolarisAdminService.java index a2d78ce1c6..4313df55b8 100644 --- a/polaris-service/src/main/java/io/polaris/service/admin/PolarisAdminService.java +++ b/polaris-service/src/main/java/io/polaris/service/admin/PolarisAdminService.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.admin; import io.polaris.core.PolarisCallContext; diff --git a/polaris-service/src/main/java/io/polaris/service/admin/PolarisServiceImpl.java b/polaris-service/src/main/java/io/polaris/service/admin/PolarisServiceImpl.java index 344b4fa145..f033e036a0 100644 --- a/polaris-service/src/main/java/io/polaris/service/admin/PolarisServiceImpl.java +++ b/polaris-service/src/main/java/io/polaris/service/admin/PolarisServiceImpl.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.admin; import io.polaris.core.PolarisCallContext; diff --git a/polaris-service/src/main/java/io/polaris/service/auth/BasePolarisAuthenticator.java b/polaris-service/src/main/java/io/polaris/service/auth/BasePolarisAuthenticator.java index 18c9554780..2a088c3f5c 100644 --- a/polaris-service/src/main/java/io/polaris/service/auth/BasePolarisAuthenticator.java +++ b/polaris-service/src/main/java/io/polaris/service/auth/BasePolarisAuthenticator.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.auth; import io.polaris.core.PolarisCallContext; diff --git a/polaris-service/src/main/java/io/polaris/service/auth/DecodedToken.java b/polaris-service/src/main/java/io/polaris/service/auth/DecodedToken.java index 2274ff12eb..20fde6eeb3 100644 --- a/polaris-service/src/main/java/io/polaris/service/auth/DecodedToken.java +++ b/polaris-service/src/main/java/io/polaris/service/auth/DecodedToken.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.auth; public interface DecodedToken { diff --git a/polaris-service/src/main/java/io/polaris/service/auth/DefaultOAuth2ApiService.java b/polaris-service/src/main/java/io/polaris/service/auth/DefaultOAuth2ApiService.java index a1091a8ac0..bb43301e63 100644 --- a/polaris-service/src/main/java/io/polaris/service/auth/DefaultOAuth2ApiService.java +++ b/polaris-service/src/main/java/io/polaris/service/auth/DefaultOAuth2ApiService.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.auth; import com.fasterxml.jackson.annotation.JsonTypeName; diff --git a/polaris-service/src/main/java/io/polaris/service/auth/DefaultPolarisAuthenticator.java b/polaris-service/src/main/java/io/polaris/service/auth/DefaultPolarisAuthenticator.java index 8d5c7b47a2..2d30522383 100644 --- a/polaris-service/src/main/java/io/polaris/service/auth/DefaultPolarisAuthenticator.java +++ b/polaris-service/src/main/java/io/polaris/service/auth/DefaultPolarisAuthenticator.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.auth; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/polaris-service/src/main/java/io/polaris/service/auth/DiscoverableAuthenticator.java b/polaris-service/src/main/java/io/polaris/service/auth/DiscoverableAuthenticator.java index c5c1f1b73a..d5a731336f 100644 --- a/polaris-service/src/main/java/io/polaris/service/auth/DiscoverableAuthenticator.java +++ b/polaris-service/src/main/java/io/polaris/service/auth/DiscoverableAuthenticator.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.auth; import com.fasterxml.jackson.annotation.JsonTypeInfo; diff --git a/polaris-service/src/main/java/io/polaris/service/auth/JWTBroker.java b/polaris-service/src/main/java/io/polaris/service/auth/JWTBroker.java index 4f653ca905..1fa708f9db 100644 --- a/polaris-service/src/main/java/io/polaris/service/auth/JWTBroker.java +++ b/polaris-service/src/main/java/io/polaris/service/auth/JWTBroker.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.auth; import com.auth0.jwt.JWT; diff --git a/polaris-service/src/main/java/io/polaris/service/auth/JWTRSAKeyPair.java b/polaris-service/src/main/java/io/polaris/service/auth/JWTRSAKeyPair.java index 9cf33cef04..76da383c55 100644 --- a/polaris-service/src/main/java/io/polaris/service/auth/JWTRSAKeyPair.java +++ b/polaris-service/src/main/java/io/polaris/service/auth/JWTRSAKeyPair.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.auth; import com.auth0.jwt.algorithms.Algorithm; diff --git a/polaris-service/src/main/java/io/polaris/service/auth/JWTRSAKeyPairFactory.java b/polaris-service/src/main/java/io/polaris/service/auth/JWTRSAKeyPairFactory.java index 26f5f0a70c..876d2c96c2 100644 --- a/polaris-service/src/main/java/io/polaris/service/auth/JWTRSAKeyPairFactory.java +++ b/polaris-service/src/main/java/io/polaris/service/auth/JWTRSAKeyPairFactory.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.auth; import com.fasterxml.jackson.annotation.JsonTypeName; diff --git a/polaris-service/src/main/java/io/polaris/service/auth/JWTSymmetricKeyBroker.java b/polaris-service/src/main/java/io/polaris/service/auth/JWTSymmetricKeyBroker.java index 467e33e1d8..02cdfb9af7 100644 --- a/polaris-service/src/main/java/io/polaris/service/auth/JWTSymmetricKeyBroker.java +++ b/polaris-service/src/main/java/io/polaris/service/auth/JWTSymmetricKeyBroker.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.auth; import com.auth0.jwt.algorithms.Algorithm; diff --git a/polaris-service/src/main/java/io/polaris/service/auth/JWTSymmetricKeyFactory.java b/polaris-service/src/main/java/io/polaris/service/auth/JWTSymmetricKeyFactory.java index a056c6bd74..0714662485 100644 --- a/polaris-service/src/main/java/io/polaris/service/auth/JWTSymmetricKeyFactory.java +++ b/polaris-service/src/main/java/io/polaris/service/auth/JWTSymmetricKeyFactory.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.auth; import com.fasterxml.jackson.annotation.JsonTypeName; diff --git a/polaris-service/src/main/java/io/polaris/service/auth/KeyProvider.java b/polaris-service/src/main/java/io/polaris/service/auth/KeyProvider.java index 8adb335bd4..e4b6dc64d8 100644 --- a/polaris-service/src/main/java/io/polaris/service/auth/KeyProvider.java +++ b/polaris-service/src/main/java/io/polaris/service/auth/KeyProvider.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.auth; import com.fasterxml.jackson.annotation.JsonTypeInfo; diff --git a/polaris-service/src/main/java/io/polaris/service/auth/LocalRSAKeyProvider.java b/polaris-service/src/main/java/io/polaris/service/auth/LocalRSAKeyProvider.java index ce055a967b..317f56f593 100644 --- a/polaris-service/src/main/java/io/polaris/service/auth/LocalRSAKeyProvider.java +++ b/polaris-service/src/main/java/io/polaris/service/auth/LocalRSAKeyProvider.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.auth; import io.polaris.core.PolarisCallContext; diff --git a/polaris-service/src/main/java/io/polaris/service/auth/OAuthTokenErrorResponse.java b/polaris-service/src/main/java/io/polaris/service/auth/OAuthTokenErrorResponse.java index c37d950d35..942ef67127 100644 --- a/polaris-service/src/main/java/io/polaris/service/auth/OAuthTokenErrorResponse.java +++ b/polaris-service/src/main/java/io/polaris/service/auth/OAuthTokenErrorResponse.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.auth; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/polaris-service/src/main/java/io/polaris/service/auth/OAuthUtils.java b/polaris-service/src/main/java/io/polaris/service/auth/OAuthUtils.java index 9ad5275fa5..f42a891877 100644 --- a/polaris-service/src/main/java/io/polaris/service/auth/OAuthUtils.java +++ b/polaris-service/src/main/java/io/polaris/service/auth/OAuthUtils.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.auth; import jakarta.ws.rs.core.Response; diff --git a/polaris-service/src/main/java/io/polaris/service/auth/PemUtils.java b/polaris-service/src/main/java/io/polaris/service/auth/PemUtils.java index f44b3a5d22..df9f052846 100644 --- a/polaris-service/src/main/java/io/polaris/service/auth/PemUtils.java +++ b/polaris-service/src/main/java/io/polaris/service/auth/PemUtils.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.auth; import java.io.File; diff --git a/polaris-service/src/main/java/io/polaris/service/auth/TestInlineBearerTokenPolarisAuthenticator.java b/polaris-service/src/main/java/io/polaris/service/auth/TestInlineBearerTokenPolarisAuthenticator.java index f541fcb378..16ea0dbc92 100644 --- a/polaris-service/src/main/java/io/polaris/service/auth/TestInlineBearerTokenPolarisAuthenticator.java +++ b/polaris-service/src/main/java/io/polaris/service/auth/TestInlineBearerTokenPolarisAuthenticator.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.auth; import com.google.common.base.Splitter; diff --git a/polaris-service/src/main/java/io/polaris/service/auth/TestOAuth2ApiService.java b/polaris-service/src/main/java/io/polaris/service/auth/TestOAuth2ApiService.java index 4ad1c270ae..ef2c1f99d0 100644 --- a/polaris-service/src/main/java/io/polaris/service/auth/TestOAuth2ApiService.java +++ b/polaris-service/src/main/java/io/polaris/service/auth/TestOAuth2ApiService.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.auth; import com.fasterxml.jackson.annotation.JsonTypeName; diff --git a/polaris-service/src/main/java/io/polaris/service/auth/TokenBroker.java b/polaris-service/src/main/java/io/polaris/service/auth/TokenBroker.java index 97bd6be8a5..980335a285 100644 --- a/polaris-service/src/main/java/io/polaris/service/auth/TokenBroker.java +++ b/polaris-service/src/main/java/io/polaris/service/auth/TokenBroker.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.auth; import io.polaris.core.PolarisCallContext; diff --git a/polaris-service/src/main/java/io/polaris/service/auth/TokenBrokerFactory.java b/polaris-service/src/main/java/io/polaris/service/auth/TokenBrokerFactory.java index 8a6fc0d0b7..90def62c72 100644 --- a/polaris-service/src/main/java/io/polaris/service/auth/TokenBrokerFactory.java +++ b/polaris-service/src/main/java/io/polaris/service/auth/TokenBrokerFactory.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.auth; import com.fasterxml.jackson.annotation.JsonTypeInfo; diff --git a/polaris-service/src/main/java/io/polaris/service/auth/TokenInfoExchangeResponse.java b/polaris-service/src/main/java/io/polaris/service/auth/TokenInfoExchangeResponse.java index d01b96750d..a3e6f016cb 100644 --- a/polaris-service/src/main/java/io/polaris/service/auth/TokenInfoExchangeResponse.java +++ b/polaris-service/src/main/java/io/polaris/service/auth/TokenInfoExchangeResponse.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.auth; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/polaris-service/src/main/java/io/polaris/service/auth/TokenRequestValidator.java b/polaris-service/src/main/java/io/polaris/service/auth/TokenRequestValidator.java index b36d105a4b..d13a6c430c 100644 --- a/polaris-service/src/main/java/io/polaris/service/auth/TokenRequestValidator.java +++ b/polaris-service/src/main/java/io/polaris/service/auth/TokenRequestValidator.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.auth; import java.util.Optional; diff --git a/polaris-service/src/main/java/io/polaris/service/auth/TokenResponse.java b/polaris-service/src/main/java/io/polaris/service/auth/TokenResponse.java index bb171f40e2..c7ca2ee8b6 100644 --- a/polaris-service/src/main/java/io/polaris/service/auth/TokenResponse.java +++ b/polaris-service/src/main/java/io/polaris/service/auth/TokenResponse.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.auth; import java.util.Optional; diff --git a/polaris-service/src/main/java/io/polaris/service/catalog/BasePolarisCatalog.java b/polaris-service/src/main/java/io/polaris/service/catalog/BasePolarisCatalog.java index 01df54615c..856bc1e53b 100644 --- a/polaris-service/src/main/java/io/polaris/service/catalog/BasePolarisCatalog.java +++ b/polaris-service/src/main/java/io/polaris/service/catalog/BasePolarisCatalog.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.catalog; import com.google.common.annotations.VisibleForTesting; diff --git a/polaris-service/src/main/java/io/polaris/service/catalog/IcebergCatalogAdapter.java b/polaris-service/src/main/java/io/polaris/service/catalog/IcebergCatalogAdapter.java index 662dd7e03d..87e8bf195e 100644 --- a/polaris-service/src/main/java/io/polaris/service/catalog/IcebergCatalogAdapter.java +++ b/polaris-service/src/main/java/io/polaris/service/catalog/IcebergCatalogAdapter.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.catalog; import com.google.common.base.Preconditions; diff --git a/polaris-service/src/main/java/io/polaris/service/catalog/PolarisCatalogHandlerWrapper.java b/polaris-service/src/main/java/io/polaris/service/catalog/PolarisCatalogHandlerWrapper.java index b3a291aacf..9abd71dd0c 100644 --- a/polaris-service/src/main/java/io/polaris/service/catalog/PolarisCatalogHandlerWrapper.java +++ b/polaris-service/src/main/java/io/polaris/service/catalog/PolarisCatalogHandlerWrapper.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.catalog; import com.google.common.collect.Maps; diff --git a/polaris-service/src/main/java/io/polaris/service/catalog/SupportsCredentialDelegation.java b/polaris-service/src/main/java/io/polaris/service/catalog/SupportsCredentialDelegation.java index 87fc55e09a..0e4aa1bc07 100644 --- a/polaris-service/src/main/java/io/polaris/service/catalog/SupportsCredentialDelegation.java +++ b/polaris-service/src/main/java/io/polaris/service/catalog/SupportsCredentialDelegation.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.catalog; import io.polaris.core.storage.PolarisStorageActions; diff --git a/polaris-service/src/main/java/io/polaris/service/catalog/SupportsNotifications.java b/polaris-service/src/main/java/io/polaris/service/catalog/SupportsNotifications.java index ca9059033d..fd22ee9a74 100644 --- a/polaris-service/src/main/java/io/polaris/service/catalog/SupportsNotifications.java +++ b/polaris-service/src/main/java/io/polaris/service/catalog/SupportsNotifications.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.catalog; import io.polaris.service.types.NotificationRequest; diff --git a/polaris-service/src/main/java/io/polaris/service/config/ConfigurationStoreAware.java b/polaris-service/src/main/java/io/polaris/service/config/ConfigurationStoreAware.java index a0a383e5a4..5dd737c7d6 100644 --- a/polaris-service/src/main/java/io/polaris/service/config/ConfigurationStoreAware.java +++ b/polaris-service/src/main/java/io/polaris/service/config/ConfigurationStoreAware.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.config; import io.polaris.core.PolarisConfigurationStore; diff --git a/polaris-service/src/main/java/io/polaris/service/config/CorsConfiguration.java b/polaris-service/src/main/java/io/polaris/service/config/CorsConfiguration.java index cef7c326cc..a85c3b5d25 100644 --- a/polaris-service/src/main/java/io/polaris/service/config/CorsConfiguration.java +++ b/polaris-service/src/main/java/io/polaris/service/config/CorsConfiguration.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.config; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/polaris-service/src/main/java/io/polaris/service/config/DefaultConfigurationStore.java b/polaris-service/src/main/java/io/polaris/service/config/DefaultConfigurationStore.java index 389893292c..bf5a3f91f9 100644 --- a/polaris-service/src/main/java/io/polaris/service/config/DefaultConfigurationStore.java +++ b/polaris-service/src/main/java/io/polaris/service/config/DefaultConfigurationStore.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.config; import io.polaris.core.PolarisCallContext; diff --git a/polaris-service/src/main/java/io/polaris/service/config/HasEntityManagerFactory.java b/polaris-service/src/main/java/io/polaris/service/config/HasEntityManagerFactory.java index 71a48fbd04..ab00e6b403 100644 --- a/polaris-service/src/main/java/io/polaris/service/config/HasEntityManagerFactory.java +++ b/polaris-service/src/main/java/io/polaris/service/config/HasEntityManagerFactory.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.config; public interface HasEntityManagerFactory { diff --git a/polaris-service/src/main/java/io/polaris/service/config/OAuth2ApiService.java b/polaris-service/src/main/java/io/polaris/service/config/OAuth2ApiService.java index 488c2c5f5c..f7ce88da8f 100644 --- a/polaris-service/src/main/java/io/polaris/service/config/OAuth2ApiService.java +++ b/polaris-service/src/main/java/io/polaris/service/config/OAuth2ApiService.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.config; import com.fasterxml.jackson.annotation.JsonTypeInfo; diff --git a/polaris-service/src/main/java/io/polaris/service/config/PolarisApplicationConfig.java b/polaris-service/src/main/java/io/polaris/service/config/PolarisApplicationConfig.java index 3bcc927714..25e65880dd 100644 --- a/polaris-service/src/main/java/io/polaris/service/config/PolarisApplicationConfig.java +++ b/polaris-service/src/main/java/io/polaris/service/config/PolarisApplicationConfig.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.config; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/polaris-service/src/main/java/io/polaris/service/config/RealmEntityManagerFactory.java b/polaris-service/src/main/java/io/polaris/service/config/RealmEntityManagerFactory.java index b95beb91f0..6828f685c0 100644 --- a/polaris-service/src/main/java/io/polaris/service/config/RealmEntityManagerFactory.java +++ b/polaris-service/src/main/java/io/polaris/service/config/RealmEntityManagerFactory.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.config; import io.polaris.core.context.RealmContext; diff --git a/polaris-service/src/main/java/io/polaris/service/config/Serializers.java b/polaris-service/src/main/java/io/polaris/service/config/Serializers.java index 59eb158710..a7050ee705 100644 --- a/polaris-service/src/main/java/io/polaris/service/config/Serializers.java +++ b/polaris-service/src/main/java/io/polaris/service/config/Serializers.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.config; import com.fasterxml.jackson.core.JacksonException; diff --git a/polaris-service/src/main/java/io/polaris/service/config/TaskHandlerConfiguration.java b/polaris-service/src/main/java/io/polaris/service/config/TaskHandlerConfiguration.java index 410145d05b..9c8ed527f3 100644 --- a/polaris-service/src/main/java/io/polaris/service/config/TaskHandlerConfiguration.java +++ b/polaris-service/src/main/java/io/polaris/service/config/TaskHandlerConfiguration.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.config; import com.google.common.util.concurrent.ThreadFactoryBuilder; diff --git a/polaris-service/src/main/java/io/polaris/service/context/CallContextCatalogFactory.java b/polaris-service/src/main/java/io/polaris/service/context/CallContextCatalogFactory.java index 0615f62bc3..17cd1e5654 100644 --- a/polaris-service/src/main/java/io/polaris/service/context/CallContextCatalogFactory.java +++ b/polaris-service/src/main/java/io/polaris/service/context/CallContextCatalogFactory.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.context; import io.polaris.core.context.CallContext; diff --git a/polaris-service/src/main/java/io/polaris/service/context/CallContextResolver.java b/polaris-service/src/main/java/io/polaris/service/context/CallContextResolver.java index ea541c97c2..850b2c2ca2 100644 --- a/polaris-service/src/main/java/io/polaris/service/context/CallContextResolver.java +++ b/polaris-service/src/main/java/io/polaris/service/context/CallContextResolver.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.context; import com.fasterxml.jackson.annotation.JsonTypeInfo; diff --git a/polaris-service/src/main/java/io/polaris/service/context/DefaultContextResolver.java b/polaris-service/src/main/java/io/polaris/service/context/DefaultContextResolver.java index 47b08ebd36..1a76e2027d 100644 --- a/polaris-service/src/main/java/io/polaris/service/context/DefaultContextResolver.java +++ b/polaris-service/src/main/java/io/polaris/service/context/DefaultContextResolver.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.context; import com.fasterxml.jackson.annotation.JsonTypeName; diff --git a/polaris-service/src/main/java/io/polaris/service/context/PolarisCallContextCatalogFactory.java b/polaris-service/src/main/java/io/polaris/service/context/PolarisCallContextCatalogFactory.java index ff7637f3fd..556c464f05 100644 --- a/polaris-service/src/main/java/io/polaris/service/context/PolarisCallContextCatalogFactory.java +++ b/polaris-service/src/main/java/io/polaris/service/context/PolarisCallContextCatalogFactory.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.context; import io.polaris.core.context.CallContext; diff --git a/polaris-service/src/main/java/io/polaris/service/context/RealmContextResolver.java b/polaris-service/src/main/java/io/polaris/service/context/RealmContextResolver.java index 389383f1d4..0a716c73ac 100644 --- a/polaris-service/src/main/java/io/polaris/service/context/RealmContextResolver.java +++ b/polaris-service/src/main/java/io/polaris/service/context/RealmContextResolver.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.context; import com.fasterxml.jackson.annotation.JsonTypeInfo; diff --git a/polaris-service/src/main/java/io/polaris/service/context/SqlliteCallContextCatalogFactory.java b/polaris-service/src/main/java/io/polaris/service/context/SqlliteCallContextCatalogFactory.java index bd2c39466d..546c021766 100644 --- a/polaris-service/src/main/java/io/polaris/service/context/SqlliteCallContextCatalogFactory.java +++ b/polaris-service/src/main/java/io/polaris/service/context/SqlliteCallContextCatalogFactory.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.context; import io.polaris.core.context.CallContext; diff --git a/polaris-service/src/main/java/io/polaris/service/logging/PolarisJsonLayoutFactory.java b/polaris-service/src/main/java/io/polaris/service/logging/PolarisJsonLayoutFactory.java index 94e8cc8aa4..68000e5cae 100644 --- a/polaris-service/src/main/java/io/polaris/service/logging/PolarisJsonLayoutFactory.java +++ b/polaris-service/src/main/java/io/polaris/service/logging/PolarisJsonLayoutFactory.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.logging; import ch.qos.logback.classic.LoggerContext; diff --git a/polaris-service/src/main/java/io/polaris/service/persistence/InMemoryPolarisMetaStoreManagerFactory.java b/polaris-service/src/main/java/io/polaris/service/persistence/InMemoryPolarisMetaStoreManagerFactory.java index dc77d5793b..d9f93dd196 100644 --- a/polaris-service/src/main/java/io/polaris/service/persistence/InMemoryPolarisMetaStoreManagerFactory.java +++ b/polaris-service/src/main/java/io/polaris/service/persistence/InMemoryPolarisMetaStoreManagerFactory.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.persistence; import com.fasterxml.jackson.annotation.JsonTypeName; diff --git a/polaris-service/src/main/java/io/polaris/service/resource/TimedApi.java b/polaris-service/src/main/java/io/polaris/service/resource/TimedApi.java index e99b186872..6372210a39 100644 --- a/polaris-service/src/main/java/io/polaris/service/resource/TimedApi.java +++ b/polaris-service/src/main/java/io/polaris/service/resource/TimedApi.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.resource; import java.lang.annotation.ElementType; diff --git a/polaris-service/src/main/java/io/polaris/service/storage/PolarisStorageIntegrationProviderImpl.java b/polaris-service/src/main/java/io/polaris/service/storage/PolarisStorageIntegrationProviderImpl.java index 56bff16b1b..afc2c46e1d 100644 --- a/polaris-service/src/main/java/io/polaris/service/storage/PolarisStorageIntegrationProviderImpl.java +++ b/polaris-service/src/main/java/io/polaris/service/storage/PolarisStorageIntegrationProviderImpl.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.storage; import com.google.api.client.http.javanet.NetHttpTransport; diff --git a/polaris-service/src/main/java/io/polaris/service/task/ManifestFileCleanupTaskHandler.java b/polaris-service/src/main/java/io/polaris/service/task/ManifestFileCleanupTaskHandler.java index 58c0a1bbea..8947869c50 100644 --- a/polaris-service/src/main/java/io/polaris/service/task/ManifestFileCleanupTaskHandler.java +++ b/polaris-service/src/main/java/io/polaris/service/task/ManifestFileCleanupTaskHandler.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.task; import io.polaris.core.entity.AsyncTaskType; diff --git a/polaris-service/src/main/java/io/polaris/service/task/TableCleanupTaskHandler.java b/polaris-service/src/main/java/io/polaris/service/task/TableCleanupTaskHandler.java index 97d321e666..d281ff13b3 100644 --- a/polaris-service/src/main/java/io/polaris/service/task/TableCleanupTaskHandler.java +++ b/polaris-service/src/main/java/io/polaris/service/task/TableCleanupTaskHandler.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.task; import io.polaris.core.PolarisCallContext; diff --git a/polaris-service/src/main/java/io/polaris/service/task/TaskExecutor.java b/polaris-service/src/main/java/io/polaris/service/task/TaskExecutor.java index c75c6fca8c..d93f03a22a 100644 --- a/polaris-service/src/main/java/io/polaris/service/task/TaskExecutor.java +++ b/polaris-service/src/main/java/io/polaris/service/task/TaskExecutor.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.task; import io.polaris.core.context.CallContext; diff --git a/polaris-service/src/main/java/io/polaris/service/task/TaskExecutorImpl.java b/polaris-service/src/main/java/io/polaris/service/task/TaskExecutorImpl.java index 315e4fefed..98734fab59 100644 --- a/polaris-service/src/main/java/io/polaris/service/task/TaskExecutorImpl.java +++ b/polaris-service/src/main/java/io/polaris/service/task/TaskExecutorImpl.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.task; import io.polaris.core.context.CallContext; diff --git a/polaris-service/src/main/java/io/polaris/service/task/TaskFileIOSupplier.java b/polaris-service/src/main/java/io/polaris/service/task/TaskFileIOSupplier.java index 49d6db2b3b..b8a1007298 100644 --- a/polaris-service/src/main/java/io/polaris/service/task/TaskFileIOSupplier.java +++ b/polaris-service/src/main/java/io/polaris/service/task/TaskFileIOSupplier.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.task; import io.polaris.core.context.CallContext; diff --git a/polaris-service/src/main/java/io/polaris/service/task/TaskHandler.java b/polaris-service/src/main/java/io/polaris/service/task/TaskHandler.java index af73423977..5be15212e3 100644 --- a/polaris-service/src/main/java/io/polaris/service/task/TaskHandler.java +++ b/polaris-service/src/main/java/io/polaris/service/task/TaskHandler.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.task; import io.polaris.core.entity.TaskEntity; diff --git a/polaris-service/src/main/java/io/polaris/service/task/TaskUtils.java b/polaris-service/src/main/java/io/polaris/service/task/TaskUtils.java index c4f431bb2b..c2a7d25ca9 100644 --- a/polaris-service/src/main/java/io/polaris/service/task/TaskUtils.java +++ b/polaris-service/src/main/java/io/polaris/service/task/TaskUtils.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.task; import java.io.IOException; diff --git a/polaris-service/src/main/java/io/polaris/service/tracing/HeadersMapAccessor.java b/polaris-service/src/main/java/io/polaris/service/tracing/HeadersMapAccessor.java index c3694cd8b3..4f1677d9b0 100644 --- a/polaris-service/src/main/java/io/polaris/service/tracing/HeadersMapAccessor.java +++ b/polaris-service/src/main/java/io/polaris/service/tracing/HeadersMapAccessor.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.tracing; import io.opentelemetry.context.propagation.TextMapGetter; diff --git a/polaris-service/src/main/java/io/polaris/service/tracing/OpenTelemetryAware.java b/polaris-service/src/main/java/io/polaris/service/tracing/OpenTelemetryAware.java index f05db40ee8..ffdfacdd46 100644 --- a/polaris-service/src/main/java/io/polaris/service/tracing/OpenTelemetryAware.java +++ b/polaris-service/src/main/java/io/polaris/service/tracing/OpenTelemetryAware.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.tracing; import io.opentelemetry.api.OpenTelemetry; diff --git a/polaris-service/src/main/java/io/polaris/service/tracing/TracingFilter.java b/polaris-service/src/main/java/io/polaris/service/tracing/TracingFilter.java index 1cde0c402d..4c9f06c464 100644 --- a/polaris-service/src/main/java/io/polaris/service/tracing/TracingFilter.java +++ b/polaris-service/src/main/java/io/polaris/service/tracing/TracingFilter.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.tracing; import io.opentelemetry.api.OpenTelemetry; diff --git a/polaris-service/src/main/java/io/polaris/service/types/CommitTableRequest.java b/polaris-service/src/main/java/io/polaris/service/types/CommitTableRequest.java index c3ccdcab52..55428e7b1c 100644 --- a/polaris-service/src/main/java/io/polaris/service/types/CommitTableRequest.java +++ b/polaris-service/src/main/java/io/polaris/service/types/CommitTableRequest.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.types; import org.apache.iceberg.rest.requests.UpdateTableRequest; diff --git a/polaris-service/src/main/java/io/polaris/service/types/CommitViewRequest.java b/polaris-service/src/main/java/io/polaris/service/types/CommitViewRequest.java index 3cc8f262e5..fa4ca39531 100644 --- a/polaris-service/src/main/java/io/polaris/service/types/CommitViewRequest.java +++ b/polaris-service/src/main/java/io/polaris/service/types/CommitViewRequest.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.types; import org.apache.iceberg.rest.requests.UpdateTableRequest; diff --git a/polaris-service/src/main/java/io/polaris/service/types/NotificationRequest.java b/polaris-service/src/main/java/io/polaris/service/types/NotificationRequest.java index 8c404cdb9a..c13c8993c3 100644 --- a/polaris-service/src/main/java/io/polaris/service/types/NotificationRequest.java +++ b/polaris-service/src/main/java/io/polaris/service/types/NotificationRequest.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.types; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/polaris-service/src/main/java/io/polaris/service/types/NotificationType.java b/polaris-service/src/main/java/io/polaris/service/types/NotificationType.java index 6b1fa2bb76..245e675295 100644 --- a/polaris-service/src/main/java/io/polaris/service/types/NotificationType.java +++ b/polaris-service/src/main/java/io/polaris/service/types/NotificationType.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.types; import java.util.Arrays; diff --git a/polaris-service/src/main/java/io/polaris/service/types/TableUpdateNotification.java b/polaris-service/src/main/java/io/polaris/service/types/TableUpdateNotification.java index 6b21f841c9..0966c8b60a 100644 --- a/polaris-service/src/main/java/io/polaris/service/types/TableUpdateNotification.java +++ b/polaris-service/src/main/java/io/polaris/service/types/TableUpdateNotification.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.types; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/polaris-service/src/main/java/io/polaris/service/types/TokenType.java b/polaris-service/src/main/java/io/polaris/service/types/TokenType.java index 2a6f218a3e..9709f1448d 100644 --- a/polaris-service/src/main/java/io/polaris/service/types/TokenType.java +++ b/polaris-service/src/main/java/io/polaris/service/types/TokenType.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.types; import com.fasterxml.jackson.annotation.JsonCreator; diff --git a/polaris-service/src/main/resources/META-INF/persistence.xml b/polaris-service/src/main/resources/META-INF/persistence.xml index 640fe1175a..59fb601130 100644 --- a/polaris-service/src/main/resources/META-INF/persistence.xml +++ b/polaris-service/src/main/resources/META-INF/persistence.xml @@ -1,5 +1,21 @@ + diff --git a/polaris-service/src/main/resources/META-INF/services/io.dropwizard.jackson.Discoverable b/polaris-service/src/main/resources/META-INF/services/io.dropwizard.jackson.Discoverable index 8aa3e90116..8354b3f36d 100644 --- a/polaris-service/src/main/resources/META-INF/services/io.dropwizard.jackson.Discoverable +++ b/polaris-service/src/main/resources/META-INF/services/io.dropwizard.jackson.Discoverable @@ -1,3 +1,19 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# + io.polaris.service.auth.DiscoverableAuthenticator io.polaris.core.persistence.MetaStoreManagerFactory io.polaris.service.config.OAuth2ApiService diff --git a/polaris-service/src/main/resources/META-INF/services/io.dropwizard.logging.common.layout.DiscoverableLayoutFactory b/polaris-service/src/main/resources/META-INF/services/io.dropwizard.logging.common.layout.DiscoverableLayoutFactory index cf8f596130..7035f1198d 100644 --- a/polaris-service/src/main/resources/META-INF/services/io.dropwizard.logging.common.layout.DiscoverableLayoutFactory +++ b/polaris-service/src/main/resources/META-INF/services/io.dropwizard.logging.common.layout.DiscoverableLayoutFactory @@ -1 +1,17 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# + io.polaris.service.logging.PolarisJsonLayoutFactory \ No newline at end of file diff --git a/polaris-service/src/main/resources/META-INF/services/io.polaris.core.persistence.MetaStoreManagerFactory b/polaris-service/src/main/resources/META-INF/services/io.polaris.core.persistence.MetaStoreManagerFactory index 80fb2d5d40..795cbfb8c2 100644 --- a/polaris-service/src/main/resources/META-INF/services/io.polaris.core.persistence.MetaStoreManagerFactory +++ b/polaris-service/src/main/resources/META-INF/services/io.polaris.core.persistence.MetaStoreManagerFactory @@ -1,3 +1,19 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# + io.polaris.extension.persistence.impl.hibernate.HibernatePolarisMetaStoreManagerFactory io.polaris.service.persistence.InMemoryPolarisMetaStoreManagerFactory com.snowflake.polaris.persistence.impl.remote.RemotePolarisMetaStoreManagerFactory diff --git a/polaris-service/src/main/resources/META-INF/services/io.polaris.service.auth.TokenBrokerFactory b/polaris-service/src/main/resources/META-INF/services/io.polaris.service.auth.TokenBrokerFactory index b5a14e5dcc..5ecc8a1fb8 100644 --- a/polaris-service/src/main/resources/META-INF/services/io.polaris.service.auth.TokenBrokerFactory +++ b/polaris-service/src/main/resources/META-INF/services/io.polaris.service.auth.TokenBrokerFactory @@ -1,3 +1,18 @@ -com.snowflake.polaris.auth.SnowflakeOAuth2TokenBrokerFactory +# +# Copyright (c) 2024 Snowflake Computing 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. +# + io.polaris.service.auth.JWTRSAKeyPairFactory io.polaris.service.auth.JWTSymmetricKeyFactory \ No newline at end of file diff --git a/polaris-service/src/main/resources/META-INF/services/io.polaris.service.config.OAuth2ApiService b/polaris-service/src/main/resources/META-INF/services/io.polaris.service.config.OAuth2ApiService index 4c74cd07e3..a629e73b32 100644 --- a/polaris-service/src/main/resources/META-INF/services/io.polaris.service.config.OAuth2ApiService +++ b/polaris-service/src/main/resources/META-INF/services/io.polaris.service.config.OAuth2ApiService @@ -1,2 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# + io.polaris.service.auth.TestOAuth2ApiService io.polaris.service.auth.DefaultOAuth2ApiService \ No newline at end of file diff --git a/polaris-service/src/main/resources/META-INF/services/io.polaris.service.context.CallContextResolver b/polaris-service/src/main/resources/META-INF/services/io.polaris.service.context.CallContextResolver index 4b0118d637..83fe0f1bf1 100644 --- a/polaris-service/src/main/resources/META-INF/services/io.polaris.service.context.CallContextResolver +++ b/polaris-service/src/main/resources/META-INF/services/io.polaris.service.context.CallContextResolver @@ -1 +1,17 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# + io.polaris.service.context.DefaultContextResolver \ No newline at end of file diff --git a/polaris-service/src/main/resources/META-INF/services/io.polaris.service.context.RealmContextResolver b/polaris-service/src/main/resources/META-INF/services/io.polaris.service.context.RealmContextResolver index 4b0118d637..83fe0f1bf1 100644 --- a/polaris-service/src/main/resources/META-INF/services/io.polaris.service.context.RealmContextResolver +++ b/polaris-service/src/main/resources/META-INF/services/io.polaris.service.context.RealmContextResolver @@ -1 +1,17 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# + io.polaris.service.context.DefaultContextResolver \ No newline at end of file diff --git a/polaris-service/src/main/resources/log4j.properties b/polaris-service/src/main/resources/log4j.properties index fc63d9a2e2..663f16b4e6 100644 --- a/polaris-service/src/main/resources/log4j.properties +++ b/polaris-service/src/main/resources/log4j.properties @@ -1,3 +1,19 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# + log4j.rootLogger=INFO, stdout log4j.appender.stdout=org.apache.log4j.ConsoleAppender log4j.appender.stdout.Target=System.out diff --git a/polaris-service/src/test/java/io/polaris/service/PolarisApplicationIntegrationTest.java b/polaris-service/src/test/java/io/polaris/service/PolarisApplicationIntegrationTest.java index d42753cddb..4b4f4eb492 100644 --- a/polaris-service/src/test/java/io/polaris/service/PolarisApplicationIntegrationTest.java +++ b/polaris-service/src/test/java/io/polaris/service/PolarisApplicationIntegrationTest.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service; import static io.polaris.service.context.DefaultContextResolver.REALM_PROPERTY_KEY; diff --git a/polaris-service/src/test/java/io/polaris/service/admin/PolarisAdminServiceAuthzTest.java b/polaris-service/src/test/java/io/polaris/service/admin/PolarisAdminServiceAuthzTest.java index e8a1b3be0f..ed3c786726 100644 --- a/polaris-service/src/test/java/io/polaris/service/admin/PolarisAdminServiceAuthzTest.java +++ b/polaris-service/src/test/java/io/polaris/service/admin/PolarisAdminServiceAuthzTest.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.admin; import io.polaris.core.admin.model.UpdateCatalogRequest; diff --git a/polaris-service/src/test/java/io/polaris/service/admin/PolarisAuthzTestBase.java b/polaris-service/src/test/java/io/polaris/service/admin/PolarisAuthzTestBase.java index 80ad0bdf1a..6cfa021a89 100644 --- a/polaris-service/src/test/java/io/polaris/service/admin/PolarisAuthzTestBase.java +++ b/polaris-service/src/test/java/io/polaris/service/admin/PolarisAuthzTestBase.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.admin; import static org.apache.iceberg.types.Types.NestedField.required; diff --git a/polaris-service/src/test/java/io/polaris/service/admin/PolarisServiceImplIntegrationTest.java b/polaris-service/src/test/java/io/polaris/service/admin/PolarisServiceImplIntegrationTest.java index 0be6a9990a..63d8168a2d 100644 --- a/polaris-service/src/test/java/io/polaris/service/admin/PolarisServiceImplIntegrationTest.java +++ b/polaris-service/src/test/java/io/polaris/service/admin/PolarisServiceImplIntegrationTest.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.admin; import static io.dropwizard.jackson.Jackson.newObjectMapper; diff --git a/polaris-service/src/test/java/io/polaris/service/auth/JWTRSAKeyPairTest.java b/polaris-service/src/test/java/io/polaris/service/auth/JWTRSAKeyPairTest.java index 1cf8927537..b045998d1c 100644 --- a/polaris-service/src/test/java/io/polaris/service/auth/JWTRSAKeyPairTest.java +++ b/polaris-service/src/test/java/io/polaris/service/auth/JWTRSAKeyPairTest.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.auth; import static org.assertj.core.api.Fail.fail; diff --git a/polaris-service/src/test/java/io/polaris/service/auth/JWTSymmetricKeyGeneratorTest.java b/polaris-service/src/test/java/io/polaris/service/auth/JWTSymmetricKeyGeneratorTest.java index 6176228fca..e80a86c025 100644 --- a/polaris-service/src/test/java/io/polaris/service/auth/JWTSymmetricKeyGeneratorTest.java +++ b/polaris-service/src/test/java/io/polaris/service/auth/JWTSymmetricKeyGeneratorTest.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.auth; import static org.junit.jupiter.api.Assertions.assertEquals; diff --git a/polaris-service/src/test/java/io/polaris/service/auth/TokenRequestValidatorTest.java b/polaris-service/src/test/java/io/polaris/service/auth/TokenRequestValidatorTest.java index 37217a40d7..a44ed5f041 100644 --- a/polaris-service/src/test/java/io/polaris/service/auth/TokenRequestValidatorTest.java +++ b/polaris-service/src/test/java/io/polaris/service/auth/TokenRequestValidatorTest.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.auth; import java.util.Arrays; diff --git a/polaris-service/src/test/java/io/polaris/service/auth/TokenUtils.java b/polaris-service/src/test/java/io/polaris/service/auth/TokenUtils.java index bab323aca6..32c5e51226 100644 --- a/polaris-service/src/test/java/io/polaris/service/auth/TokenUtils.java +++ b/polaris-service/src/test/java/io/polaris/service/auth/TokenUtils.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.auth; import static io.polaris.service.context.DefaultContextResolver.REALM_PROPERTY_KEY; diff --git a/polaris-service/src/test/java/io/polaris/service/catalog/BasePolarisCatalogTest.java b/polaris-service/src/test/java/io/polaris/service/catalog/BasePolarisCatalogTest.java index 9207ba50de..06ebcf1f2b 100644 --- a/polaris-service/src/test/java/io/polaris/service/catalog/BasePolarisCatalogTest.java +++ b/polaris-service/src/test/java/io/polaris/service/catalog/BasePolarisCatalogTest.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.catalog; import static org.apache.iceberg.types.Types.NestedField.required; diff --git a/polaris-service/src/test/java/io/polaris/service/catalog/BasePolarisCatalogViewTest.java b/polaris-service/src/test/java/io/polaris/service/catalog/BasePolarisCatalogViewTest.java index 0680995d5d..c173d25bc9 100644 --- a/polaris-service/src/test/java/io/polaris/service/catalog/BasePolarisCatalogViewTest.java +++ b/polaris-service/src/test/java/io/polaris/service/catalog/BasePolarisCatalogViewTest.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.catalog; import com.google.common.collect.ImmutableMap; diff --git a/polaris-service/src/test/java/io/polaris/service/catalog/PolarisCatalogHandlerWrapperAuthzTest.java b/polaris-service/src/test/java/io/polaris/service/catalog/PolarisCatalogHandlerWrapperAuthzTest.java index 2b45e11205..7bd3f752e1 100644 --- a/polaris-service/src/test/java/io/polaris/service/catalog/PolarisCatalogHandlerWrapperAuthzTest.java +++ b/polaris-service/src/test/java/io/polaris/service/catalog/PolarisCatalogHandlerWrapperAuthzTest.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.catalog; import com.google.common.collect.ImmutableMap; diff --git a/polaris-service/src/test/java/io/polaris/service/catalog/PolarisPassthroughResolutionView.java b/polaris-service/src/test/java/io/polaris/service/catalog/PolarisPassthroughResolutionView.java index aa52bbfa28..15b77f1614 100644 --- a/polaris-service/src/test/java/io/polaris/service/catalog/PolarisPassthroughResolutionView.java +++ b/polaris-service/src/test/java/io/polaris/service/catalog/PolarisPassthroughResolutionView.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.catalog; import io.polaris.core.auth.AuthenticatedPolarisPrincipal; diff --git a/polaris-service/src/test/java/io/polaris/service/catalog/PolarisRestCatalogIntegrationTest.java b/polaris-service/src/test/java/io/polaris/service/catalog/PolarisRestCatalogIntegrationTest.java index 3d47331a59..afe180dab5 100644 --- a/polaris-service/src/test/java/io/polaris/service/catalog/PolarisRestCatalogIntegrationTest.java +++ b/polaris-service/src/test/java/io/polaris/service/catalog/PolarisRestCatalogIntegrationTest.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.catalog; import static io.polaris.service.context.DefaultContextResolver.REALM_PROPERTY_KEY; diff --git a/polaris-service/src/test/java/io/polaris/service/catalog/PolarisRestCatalogViewIntegrationTest.java b/polaris-service/src/test/java/io/polaris/service/catalog/PolarisRestCatalogViewIntegrationTest.java index 1366aa7405..c0c24633e0 100644 --- a/polaris-service/src/test/java/io/polaris/service/catalog/PolarisRestCatalogViewIntegrationTest.java +++ b/polaris-service/src/test/java/io/polaris/service/catalog/PolarisRestCatalogViewIntegrationTest.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.catalog; import static io.polaris.service.context.DefaultContextResolver.REALM_PROPERTY_KEY; diff --git a/polaris-service/src/test/java/io/polaris/service/catalog/PolarisSparkIntegrationTest.java b/polaris-service/src/test/java/io/polaris/service/catalog/PolarisSparkIntegrationTest.java index 1106f7bd5b..4dc4162a9f 100644 --- a/polaris-service/src/test/java/io/polaris/service/catalog/PolarisSparkIntegrationTest.java +++ b/polaris-service/src/test/java/io/polaris/service/catalog/PolarisSparkIntegrationTest.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.catalog; import static io.polaris.service.context.DefaultContextResolver.REALM_PROPERTY_KEY; diff --git a/polaris-service/src/test/java/io/polaris/service/entity/CatalogEntityTest.java b/polaris-service/src/test/java/io/polaris/service/entity/CatalogEntityTest.java index 536d4f803e..89e7f73192 100644 --- a/polaris-service/src/test/java/io/polaris/service/entity/CatalogEntityTest.java +++ b/polaris-service/src/test/java/io/polaris/service/entity/CatalogEntityTest.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.entity; import io.polaris.core.admin.model.AwsStorageConfigInfo; diff --git a/polaris-service/src/test/java/io/polaris/service/task/ManifestFileCleanupTaskHandlerTest.java b/polaris-service/src/test/java/io/polaris/service/task/ManifestFileCleanupTaskHandlerTest.java index 5204f332e2..77a5dfe7a0 100644 --- a/polaris-service/src/test/java/io/polaris/service/task/ManifestFileCleanupTaskHandlerTest.java +++ b/polaris-service/src/test/java/io/polaris/service/task/ManifestFileCleanupTaskHandlerTest.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.task; import static org.assertj.core.api.Assertions.assertThatPredicate; diff --git a/polaris-service/src/test/java/io/polaris/service/task/TableCleanupTaskHandlerTest.java b/polaris-service/src/test/java/io/polaris/service/task/TableCleanupTaskHandlerTest.java index 91032fdf9f..19ccdd0410 100644 --- a/polaris-service/src/test/java/io/polaris/service/task/TableCleanupTaskHandlerTest.java +++ b/polaris-service/src/test/java/io/polaris/service/task/TableCleanupTaskHandlerTest.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.task; import static org.assertj.core.api.Assertions.assertThat; diff --git a/polaris-service/src/test/java/io/polaris/service/task/TaskTestUtils.java b/polaris-service/src/test/java/io/polaris/service/task/TaskTestUtils.java index 44abb0c0ac..709ad056b3 100644 --- a/polaris-service/src/test/java/io/polaris/service/task/TaskTestUtils.java +++ b/polaris-service/src/test/java/io/polaris/service/task/TaskTestUtils.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.task; import java.io.IOException; diff --git a/polaris-service/src/test/java/io/polaris/service/task/TestSnapshot.java b/polaris-service/src/test/java/io/polaris/service/task/TestSnapshot.java index a3f2ddcea3..9ac7c1d122 100644 --- a/polaris-service/src/test/java/io/polaris/service/task/TestSnapshot.java +++ b/polaris-service/src/test/java/io/polaris/service/task/TestSnapshot.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.task; import com.google.common.collect.Lists; diff --git a/polaris-service/src/test/java/io/polaris/service/test/PolarisConnectionExtension.java b/polaris-service/src/test/java/io/polaris/service/test/PolarisConnectionExtension.java index 683f7694bd..78b2388bea 100644 --- a/polaris-service/src/test/java/io/polaris/service/test/PolarisConnectionExtension.java +++ b/polaris-service/src/test/java/io/polaris/service/test/PolarisConnectionExtension.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.test; import static io.polaris.service.context.DefaultContextResolver.REALM_PROPERTY_KEY; diff --git a/polaris-service/src/test/java/io/polaris/service/test/SnowmanCredentialsExtension.java b/polaris-service/src/test/java/io/polaris/service/test/SnowmanCredentialsExtension.java index 3137f7d117..ec3728984f 100644 --- a/polaris-service/src/test/java/io/polaris/service/test/SnowmanCredentialsExtension.java +++ b/polaris-service/src/test/java/io/polaris/service/test/SnowmanCredentialsExtension.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.test; import static io.polaris.service.context.DefaultContextResolver.REALM_PROPERTY_KEY; diff --git a/polaris-service/src/test/resources/META-INF/persistence.xml b/polaris-service/src/test/resources/META-INF/persistence.xml index d6a1c5aa4e..db27cb8f45 100644 --- a/polaris-service/src/test/resources/META-INF/persistence.xml +++ b/polaris-service/src/test/resources/META-INF/persistence.xml @@ -1,5 +1,21 @@ + diff --git a/polaris-service/src/test/resources/META-INF/services/io.polaris.service.auth.DiscoverableAuthenticator b/polaris-service/src/test/resources/META-INF/services/io.polaris.service.auth.DiscoverableAuthenticator index c34535714a..32c21d7dd3 100644 --- a/polaris-service/src/test/resources/META-INF/services/io.polaris.service.auth.DiscoverableAuthenticator +++ b/polaris-service/src/test/resources/META-INF/services/io.polaris.service.auth.DiscoverableAuthenticator @@ -1 +1,17 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# + io.polaris.service.auth.TestInlineBearerTokenPolarisAuthenticator \ No newline at end of file diff --git a/polaris-service/src/test/resources/polaris-server-integrationtest.yml b/polaris-service/src/test/resources/polaris-server-integrationtest.yml index 64146096df..77008e80a1 100644 --- a/polaris-service/src/test/resources/polaris-server-integrationtest.yml +++ b/polaris-service/src/test/resources/polaris-server-integrationtest.yml @@ -1,3 +1,19 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# + server: # Maximum number of threads. maxThreads: 200 diff --git a/regtests/Dockerfile b/regtests/Dockerfile index 270315235b..99d762af51 100644 --- a/regtests/Dockerfile +++ b/regtests/Dockerfile @@ -1,3 +1,17 @@ +# Copyright (c) 2024 Snowflake Computing 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 apache/spark:3.5.1-python3 ARG POLARIS_HOST=polaris ENV POLARIS_HOST=$POLARIS_HOST diff --git a/regtests/README.md b/regtests/README.md index 783501e16d..590201ffac 100644 --- a/regtests/README.md +++ b/regtests/README.md @@ -1,3 +1,21 @@ + + # End-to-end regression tests ## Run Tests With Docker Compose diff --git a/regtests/client/python/.github/workflows/python.yml b/regtests/client/python/.github/workflows/python.yml index f5a230e07b..559ec9f909 100644 --- a/regtests/client/python/.github/workflows/python.yml +++ b/regtests/client/python/.github/workflows/python.yml @@ -1,3 +1,19 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# + # NOTE: This file is auto generated by OpenAPI Generator. # URL: https://openapi-generator.tech # diff --git a/regtests/client/python/.gitlab-ci.yml b/regtests/client/python/.gitlab-ci.yml index 3a2ed010b8..a034567142 100644 --- a/regtests/client/python/.gitlab-ci.yml +++ b/regtests/client/python/.gitlab-ci.yml @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # NOTE: This file is auto generated by OpenAPI Generator. # URL: https://openapi-generator.tech # diff --git a/regtests/client/python/.travis.yml b/regtests/client/python/.travis.yml index dabde49c17..a4f41c2226 100644 --- a/regtests/client/python/.travis.yml +++ b/regtests/client/python/.travis.yml @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # ref: https://docs.travis-ci.com/user/languages/python language: python python: diff --git a/regtests/client/python/README.md b/regtests/client/python/README.md index 553569f4d7..ecd80b31d3 100644 --- a/regtests/client/python/README.md +++ b/regtests/client/python/README.md @@ -1,3 +1,20 @@ + # polaris.catalog Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2. diff --git a/regtests/client/python/cli/command/__init__.py b/regtests/client/python/cli/command/__init__.py index f9887a4623..099e3bf0d7 100644 --- a/regtests/client/python/cli/command/__init__.py +++ b/regtests/client/python/cli/command/__init__.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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 argparse from abc import ABC diff --git a/regtests/client/python/cli/command/catalog_roles.py b/regtests/client/python/cli/command/catalog_roles.py index 033dcf6982..56b64dd96d 100644 --- a/regtests/client/python/cli/command/catalog_roles.py +++ b/regtests/client/python/cli/command/catalog_roles.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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 dataclasses import dataclass from typing import Dict, Optional diff --git a/regtests/client/python/cli/command/catalogs.py b/regtests/client/python/cli/command/catalogs.py index 9ce81f5714..e4b3410ef4 100644 --- a/regtests/client/python/cli/command/catalogs.py +++ b/regtests/client/python/cli/command/catalogs.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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 dataclasses import dataclass, field from typing import Dict, Optional, List diff --git a/regtests/client/python/cli/command/principal_roles.py b/regtests/client/python/cli/command/principal_roles.py index 4c3e27e5c6..cfbb440714 100644 --- a/regtests/client/python/cli/command/principal_roles.py +++ b/regtests/client/python/cli/command/principal_roles.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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 dataclasses import dataclass from typing import Dict, Optional diff --git a/regtests/client/python/cli/command/principals.py b/regtests/client/python/cli/command/principals.py index 25f8196de3..f8174a38a2 100644 --- a/regtests/client/python/cli/command/principals.py +++ b/regtests/client/python/cli/command/principals.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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 dataclasses import dataclass from typing import Dict, Optional diff --git a/regtests/client/python/cli/command/privileges.py b/regtests/client/python/cli/command/privileges.py index dbd3f0d264..92b876eef5 100644 --- a/regtests/client/python/cli/command/privileges.py +++ b/regtests/client/python/cli/command/privileges.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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 dataclasses import dataclass from typing import List diff --git a/regtests/client/python/cli/constants.py b/regtests/client/python/cli/constants.py index 310c8bcdfa..210debaacd 100644 --- a/regtests/client/python/cli/constants.py +++ b/regtests/client/python/cli/constants.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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 enum import Enum diff --git a/regtests/client/python/cli/options/option_tree.py b/regtests/client/python/cli/options/option_tree.py index c9b9efdd35..5c3a1f3b97 100644 --- a/regtests/client/python/cli/options/option_tree.py +++ b/regtests/client/python/cli/options/option_tree.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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 dataclasses import dataclass, field from typing import List diff --git a/regtests/client/python/cli/options/parser.py b/regtests/client/python/cli/options/parser.py index 0e0260da43..18a281bc93 100644 --- a/regtests/client/python/cli/options/parser.py +++ b/regtests/client/python/cli/options/parser.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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 argparse import sys from typing import List, Optional, Dict diff --git a/regtests/client/python/cli/polaris_cli.py b/regtests/client/python/cli/polaris_cli.py index 564879fc06..2b0d1ff1e0 100644 --- a/regtests/client/python/cli/polaris_cli.py +++ b/regtests/client/python/cli/polaris_cli.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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 cli.options.parser import Parser from polaris.management import ApiClient, Configuration, ApiException from polaris.management import PolarisDefaultApi diff --git a/regtests/client/python/docs/AddGrantRequest.md b/regtests/client/python/docs/AddGrantRequest.md index d36e73027b..7a05da1eac 100644 --- a/regtests/client/python/docs/AddGrantRequest.md +++ b/regtests/client/python/docs/AddGrantRequest.md @@ -1,6 +1,22 @@ + # AddGrantRequest - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/AddPartitionSpecUpdate.md b/regtests/client/python/docs/AddPartitionSpecUpdate.md index 31a8448c6d..1156779bc9 100644 --- a/regtests/client/python/docs/AddPartitionSpecUpdate.md +++ b/regtests/client/python/docs/AddPartitionSpecUpdate.md @@ -1,6 +1,22 @@ + # AddPartitionSpecUpdate - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/AddSchemaUpdate.md b/regtests/client/python/docs/AddSchemaUpdate.md index 0957686695..1c236a67f9 100644 --- a/regtests/client/python/docs/AddSchemaUpdate.md +++ b/regtests/client/python/docs/AddSchemaUpdate.md @@ -1,6 +1,22 @@ + # AddSchemaUpdate - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/AddSnapshotUpdate.md b/regtests/client/python/docs/AddSnapshotUpdate.md index e29a4f6b70..dc23e76b58 100644 --- a/regtests/client/python/docs/AddSnapshotUpdate.md +++ b/regtests/client/python/docs/AddSnapshotUpdate.md @@ -1,6 +1,22 @@ + # AddSnapshotUpdate - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/AddSortOrderUpdate.md b/regtests/client/python/docs/AddSortOrderUpdate.md index 21c2ec9831..39b2609420 100644 --- a/regtests/client/python/docs/AddSortOrderUpdate.md +++ b/regtests/client/python/docs/AddSortOrderUpdate.md @@ -1,6 +1,22 @@ + # AddSortOrderUpdate - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/AddViewVersionUpdate.md b/regtests/client/python/docs/AddViewVersionUpdate.md index 5c1c4ec3ae..f1219c3970 100644 --- a/regtests/client/python/docs/AddViewVersionUpdate.md +++ b/regtests/client/python/docs/AddViewVersionUpdate.md @@ -1,6 +1,22 @@ + # AddViewVersionUpdate - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/AndOrExpression.md b/regtests/client/python/docs/AndOrExpression.md index 44438c4d8a..4a5dc57fa2 100644 --- a/regtests/client/python/docs/AndOrExpression.md +++ b/regtests/client/python/docs/AndOrExpression.md @@ -1,6 +1,22 @@ + # AndOrExpression - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/AssertCreate.md b/regtests/client/python/docs/AssertCreate.md index 80e9ab1e89..c30e8d0f83 100644 --- a/regtests/client/python/docs/AssertCreate.md +++ b/regtests/client/python/docs/AssertCreate.md @@ -1,3 +1,20 @@ + # AssertCreate The table must not already exist; used for create transactions diff --git a/regtests/client/python/docs/AssertCurrentSchemaId.md b/regtests/client/python/docs/AssertCurrentSchemaId.md index f1e81ba30a..f99598b4f4 100644 --- a/regtests/client/python/docs/AssertCurrentSchemaId.md +++ b/regtests/client/python/docs/AssertCurrentSchemaId.md @@ -1,3 +1,20 @@ + # AssertCurrentSchemaId The table's current schema id must match the requirement's `current-schema-id` diff --git a/regtests/client/python/docs/AssertDefaultSortOrderId.md b/regtests/client/python/docs/AssertDefaultSortOrderId.md index 6a50a52e3b..060a481592 100644 --- a/regtests/client/python/docs/AssertDefaultSortOrderId.md +++ b/regtests/client/python/docs/AssertDefaultSortOrderId.md @@ -1,3 +1,20 @@ + # AssertDefaultSortOrderId The table's default sort order id must match the requirement's `default-sort-order-id` diff --git a/regtests/client/python/docs/AssertDefaultSpecId.md b/regtests/client/python/docs/AssertDefaultSpecId.md index 5d952d7a2a..dca5b16730 100644 --- a/regtests/client/python/docs/AssertDefaultSpecId.md +++ b/regtests/client/python/docs/AssertDefaultSpecId.md @@ -1,3 +1,20 @@ + # AssertDefaultSpecId The table's default spec id must match the requirement's `default-spec-id` diff --git a/regtests/client/python/docs/AssertLastAssignedFieldId.md b/regtests/client/python/docs/AssertLastAssignedFieldId.md index 55927c54d8..7ebb658cd1 100644 --- a/regtests/client/python/docs/AssertLastAssignedFieldId.md +++ b/regtests/client/python/docs/AssertLastAssignedFieldId.md @@ -1,3 +1,20 @@ + # AssertLastAssignedFieldId The table's last assigned column id must match the requirement's `last-assigned-field-id` diff --git a/regtests/client/python/docs/AssertLastAssignedPartitionId.md b/regtests/client/python/docs/AssertLastAssignedPartitionId.md index 381c62aa13..f9d2e32bc5 100644 --- a/regtests/client/python/docs/AssertLastAssignedPartitionId.md +++ b/regtests/client/python/docs/AssertLastAssignedPartitionId.md @@ -1,3 +1,20 @@ + # AssertLastAssignedPartitionId The table's last assigned partition id must match the requirement's `last-assigned-partition-id` diff --git a/regtests/client/python/docs/AssertRefSnapshotId.md b/regtests/client/python/docs/AssertRefSnapshotId.md index 924951e9fd..80a141df55 100644 --- a/regtests/client/python/docs/AssertRefSnapshotId.md +++ b/regtests/client/python/docs/AssertRefSnapshotId.md @@ -1,3 +1,20 @@ + # AssertRefSnapshotId The table branch or tag identified by the requirement's `ref` must reference the requirement's `snapshot-id`; if `snapshot-id` is `null` or missing, the ref must not already exist diff --git a/regtests/client/python/docs/AssertTableUUID.md b/regtests/client/python/docs/AssertTableUUID.md index 6875919f8f..0aa31a4a89 100644 --- a/regtests/client/python/docs/AssertTableUUID.md +++ b/regtests/client/python/docs/AssertTableUUID.md @@ -1,3 +1,20 @@ + # AssertTableUUID The table UUID must match the requirement's `uuid` diff --git a/regtests/client/python/docs/AssertViewUUID.md b/regtests/client/python/docs/AssertViewUUID.md index 3d9d79fc89..fc2858d83c 100644 --- a/regtests/client/python/docs/AssertViewUUID.md +++ b/regtests/client/python/docs/AssertViewUUID.md @@ -1,3 +1,20 @@ + # AssertViewUUID The view UUID must match the requirement's `uuid` diff --git a/regtests/client/python/docs/AssignUUIDUpdate.md b/regtests/client/python/docs/AssignUUIDUpdate.md index 89a97aab37..00d847539b 100644 --- a/regtests/client/python/docs/AssignUUIDUpdate.md +++ b/regtests/client/python/docs/AssignUUIDUpdate.md @@ -1,3 +1,20 @@ + # AssignUUIDUpdate Assigning a UUID to a table/view should only be done when creating the table/view. It is not safe to re-assign the UUID if a table/view already has a UUID assigned diff --git a/regtests/client/python/docs/AwsStorageConfigInfo.md b/regtests/client/python/docs/AwsStorageConfigInfo.md index 7b4d97d5b0..5aab847180 100644 --- a/regtests/client/python/docs/AwsStorageConfigInfo.md +++ b/regtests/client/python/docs/AwsStorageConfigInfo.md @@ -1,3 +1,20 @@ + # AwsStorageConfigInfo aws storage configuration info diff --git a/regtests/client/python/docs/AzureStorageConfigInfo.md b/regtests/client/python/docs/AzureStorageConfigInfo.md index 9b74f953d6..cdbd2cff10 100644 --- a/regtests/client/python/docs/AzureStorageConfigInfo.md +++ b/regtests/client/python/docs/AzureStorageConfigInfo.md @@ -1,3 +1,20 @@ + # AzureStorageConfigInfo azure storage configuration info diff --git a/regtests/client/python/docs/BaseUpdate.md b/regtests/client/python/docs/BaseUpdate.md index ba1241dd4f..ff2cde9a30 100644 --- a/regtests/client/python/docs/BaseUpdate.md +++ b/regtests/client/python/docs/BaseUpdate.md @@ -1,6 +1,22 @@ + # BaseUpdate - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/BlobMetadata.md b/regtests/client/python/docs/BlobMetadata.md index 5918824fac..a8c4cbd12c 100644 --- a/regtests/client/python/docs/BlobMetadata.md +++ b/regtests/client/python/docs/BlobMetadata.md @@ -1,6 +1,22 @@ + # BlobMetadata - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/Catalog.md b/regtests/client/python/docs/Catalog.md index 585ddc11d1..3f583ca427 100644 --- a/regtests/client/python/docs/Catalog.md +++ b/regtests/client/python/docs/Catalog.md @@ -1,3 +1,20 @@ + # Catalog A catalog object. A catalog may be internal or external. Internal catalogs are managed entirely by an external catalog interface. Third party catalogs may be other Iceberg REST implementations or other services with their own proprietary APIs diff --git a/regtests/client/python/docs/CatalogConfig.md b/regtests/client/python/docs/CatalogConfig.md index 77cfa5e9bd..e9c99992cd 100644 --- a/regtests/client/python/docs/CatalogConfig.md +++ b/regtests/client/python/docs/CatalogConfig.md @@ -1,3 +1,20 @@ + # CatalogConfig Server-provided configuration for the catalog. diff --git a/regtests/client/python/docs/CatalogGrant.md b/regtests/client/python/docs/CatalogGrant.md index f7dbd9bb9d..1cdc65865f 100644 --- a/regtests/client/python/docs/CatalogGrant.md +++ b/regtests/client/python/docs/CatalogGrant.md @@ -1,6 +1,22 @@ + # CatalogGrant - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/CatalogPrivilege.md b/regtests/client/python/docs/CatalogPrivilege.md index 12dd0bcb61..e6088cc2b9 100644 --- a/regtests/client/python/docs/CatalogPrivilege.md +++ b/regtests/client/python/docs/CatalogPrivilege.md @@ -1,6 +1,22 @@ + # CatalogPrivilege - ## Enum * `CATALOG_MANAGE_ACCESS` (value: `'CATALOG_MANAGE_ACCESS'`) diff --git a/regtests/client/python/docs/CatalogProperties.md b/regtests/client/python/docs/CatalogProperties.md index 464a3f89e0..5a4fe0b3d4 100644 --- a/regtests/client/python/docs/CatalogProperties.md +++ b/regtests/client/python/docs/CatalogProperties.md @@ -1,6 +1,22 @@ + # CatalogProperties - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/CatalogRole.md b/regtests/client/python/docs/CatalogRole.md index cd052ca7ec..d29a63418a 100644 --- a/regtests/client/python/docs/CatalogRole.md +++ b/regtests/client/python/docs/CatalogRole.md @@ -1,6 +1,22 @@ + # CatalogRole - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/CatalogRoles.md b/regtests/client/python/docs/CatalogRoles.md index d7be37d45d..b4cb392191 100644 --- a/regtests/client/python/docs/CatalogRoles.md +++ b/regtests/client/python/docs/CatalogRoles.md @@ -1,6 +1,22 @@ + # CatalogRoles - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/Catalogs.md b/regtests/client/python/docs/Catalogs.md index d1f0ebe58f..07cb83db1e 100644 --- a/regtests/client/python/docs/Catalogs.md +++ b/regtests/client/python/docs/Catalogs.md @@ -1,3 +1,20 @@ + # Catalogs A list of Catalog objects diff --git a/regtests/client/python/docs/CommitReport.md b/regtests/client/python/docs/CommitReport.md index 57b22e3271..c683f42121 100644 --- a/regtests/client/python/docs/CommitReport.md +++ b/regtests/client/python/docs/CommitReport.md @@ -1,6 +1,22 @@ + # CommitReport - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/CommitTableRequest.md b/regtests/client/python/docs/CommitTableRequest.md index 9d490cbd61..a00a388106 100644 --- a/regtests/client/python/docs/CommitTableRequest.md +++ b/regtests/client/python/docs/CommitTableRequest.md @@ -1,6 +1,22 @@ + # CommitTableRequest - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/CommitTableResponse.md b/regtests/client/python/docs/CommitTableResponse.md index 24430f2a46..6dd62fce11 100644 --- a/regtests/client/python/docs/CommitTableResponse.md +++ b/regtests/client/python/docs/CommitTableResponse.md @@ -1,6 +1,22 @@ + # CommitTableResponse - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/CommitTransactionRequest.md b/regtests/client/python/docs/CommitTransactionRequest.md index cc3323e4a5..5d3634322f 100644 --- a/regtests/client/python/docs/CommitTransactionRequest.md +++ b/regtests/client/python/docs/CommitTransactionRequest.md @@ -1,6 +1,22 @@ + # CommitTransactionRequest - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/CommitViewRequest.md b/regtests/client/python/docs/CommitViewRequest.md index 5867bed269..4d40043d9a 100644 --- a/regtests/client/python/docs/CommitViewRequest.md +++ b/regtests/client/python/docs/CommitViewRequest.md @@ -1,6 +1,22 @@ + # CommitViewRequest - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/ContentFile.md b/regtests/client/python/docs/ContentFile.md index fe06ffde2d..c14af53b27 100644 --- a/regtests/client/python/docs/ContentFile.md +++ b/regtests/client/python/docs/ContentFile.md @@ -1,6 +1,22 @@ + # ContentFile - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/CountMap.md b/regtests/client/python/docs/CountMap.md index ac4ce65cac..644095f4bd 100644 --- a/regtests/client/python/docs/CountMap.md +++ b/regtests/client/python/docs/CountMap.md @@ -1,6 +1,22 @@ + # CountMap - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/CounterResult.md b/regtests/client/python/docs/CounterResult.md index 4efe56d321..cc9c364c71 100644 --- a/regtests/client/python/docs/CounterResult.md +++ b/regtests/client/python/docs/CounterResult.md @@ -1,6 +1,22 @@ + # CounterResult - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/CreateCatalogRequest.md b/regtests/client/python/docs/CreateCatalogRequest.md index 160e873c01..aeb5bacaae 100644 --- a/regtests/client/python/docs/CreateCatalogRequest.md +++ b/regtests/client/python/docs/CreateCatalogRequest.md @@ -1,3 +1,20 @@ + # CreateCatalogRequest Request to create a new catalog diff --git a/regtests/client/python/docs/CreateCatalogRoleRequest.md b/regtests/client/python/docs/CreateCatalogRoleRequest.md index b10da6c54c..36a77eaabd 100644 --- a/regtests/client/python/docs/CreateCatalogRoleRequest.md +++ b/regtests/client/python/docs/CreateCatalogRoleRequest.md @@ -1,6 +1,22 @@ + # CreateCatalogRoleRequest - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/CreateNamespaceRequest.md b/regtests/client/python/docs/CreateNamespaceRequest.md index 8ab25bdb33..5bb68cd93e 100644 --- a/regtests/client/python/docs/CreateNamespaceRequest.md +++ b/regtests/client/python/docs/CreateNamespaceRequest.md @@ -1,6 +1,22 @@ + # CreateNamespaceRequest - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/CreateNamespaceResponse.md b/regtests/client/python/docs/CreateNamespaceResponse.md index 144cb7d17d..23de5ec427 100644 --- a/regtests/client/python/docs/CreateNamespaceResponse.md +++ b/regtests/client/python/docs/CreateNamespaceResponse.md @@ -1,6 +1,22 @@ + # CreateNamespaceResponse - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/CreatePrincipalRequest.md b/regtests/client/python/docs/CreatePrincipalRequest.md index 8b02f3c882..5d4cb414d6 100644 --- a/regtests/client/python/docs/CreatePrincipalRequest.md +++ b/regtests/client/python/docs/CreatePrincipalRequest.md @@ -1,6 +1,22 @@ + # CreatePrincipalRequest - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/CreatePrincipalRoleRequest.md b/regtests/client/python/docs/CreatePrincipalRoleRequest.md index 3990833023..5c7c574545 100644 --- a/regtests/client/python/docs/CreatePrincipalRoleRequest.md +++ b/regtests/client/python/docs/CreatePrincipalRoleRequest.md @@ -1,6 +1,22 @@ + # CreatePrincipalRoleRequest - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/CreateTableRequest.md b/regtests/client/python/docs/CreateTableRequest.md index d87f8aa294..afbef3db4a 100644 --- a/regtests/client/python/docs/CreateTableRequest.md +++ b/regtests/client/python/docs/CreateTableRequest.md @@ -1,6 +1,22 @@ + # CreateTableRequest - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/CreateViewRequest.md b/regtests/client/python/docs/CreateViewRequest.md index e0c53722de..58c617aa43 100644 --- a/regtests/client/python/docs/CreateViewRequest.md +++ b/regtests/client/python/docs/CreateViewRequest.md @@ -1,6 +1,22 @@ + # CreateViewRequest - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/DataFile.md b/regtests/client/python/docs/DataFile.md index 49cb15cf88..9714f7648c 100644 --- a/regtests/client/python/docs/DataFile.md +++ b/regtests/client/python/docs/DataFile.md @@ -1,6 +1,22 @@ + # DataFile - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/EqualityDeleteFile.md b/regtests/client/python/docs/EqualityDeleteFile.md index da8af51645..84d01a3f4a 100644 --- a/regtests/client/python/docs/EqualityDeleteFile.md +++ b/regtests/client/python/docs/EqualityDeleteFile.md @@ -1,6 +1,22 @@ + # EqualityDeleteFile - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/ErrorModel.md b/regtests/client/python/docs/ErrorModel.md index ee4280a5c8..cb55548281 100644 --- a/regtests/client/python/docs/ErrorModel.md +++ b/regtests/client/python/docs/ErrorModel.md @@ -1,3 +1,20 @@ + # ErrorModel JSON error payload returned in a response with further details on the error diff --git a/regtests/client/python/docs/Expression.md b/regtests/client/python/docs/Expression.md index b9ed48b908..21d643a234 100644 --- a/regtests/client/python/docs/Expression.md +++ b/regtests/client/python/docs/Expression.md @@ -1,6 +1,22 @@ + # Expression - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/ExternalCatalog.md b/regtests/client/python/docs/ExternalCatalog.md index 6db96d1b8e..4475418394 100644 --- a/regtests/client/python/docs/ExternalCatalog.md +++ b/regtests/client/python/docs/ExternalCatalog.md @@ -1,3 +1,20 @@ + # ExternalCatalog An externally managed catalog diff --git a/regtests/client/python/docs/FileFormat.md b/regtests/client/python/docs/FileFormat.md index 32d47c400d..ede219a42f 100644 --- a/regtests/client/python/docs/FileFormat.md +++ b/regtests/client/python/docs/FileFormat.md @@ -1,6 +1,22 @@ + # FileFormat - ## Enum * `AVRO` (value: `'avro'`) diff --git a/regtests/client/python/docs/FileStorageConfigInfo.md b/regtests/client/python/docs/FileStorageConfigInfo.md index 8433842625..b14db26422 100644 --- a/regtests/client/python/docs/FileStorageConfigInfo.md +++ b/regtests/client/python/docs/FileStorageConfigInfo.md @@ -1,3 +1,20 @@ + # FileStorageConfigInfo gcp storage configuration info diff --git a/regtests/client/python/docs/GcpStorageConfigInfo.md b/regtests/client/python/docs/GcpStorageConfigInfo.md index 0fa5f202f5..f76f3843aa 100644 --- a/regtests/client/python/docs/GcpStorageConfigInfo.md +++ b/regtests/client/python/docs/GcpStorageConfigInfo.md @@ -1,3 +1,20 @@ + # GcpStorageConfigInfo gcp storage configuration info diff --git a/regtests/client/python/docs/GetNamespaceResponse.md b/regtests/client/python/docs/GetNamespaceResponse.md index f751daeb09..494acd210d 100644 --- a/regtests/client/python/docs/GetNamespaceResponse.md +++ b/regtests/client/python/docs/GetNamespaceResponse.md @@ -1,6 +1,22 @@ + # GetNamespaceResponse - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/GrantCatalogRoleRequest.md b/regtests/client/python/docs/GrantCatalogRoleRequest.md index 74f95c1ee0..3b872921ec 100644 --- a/regtests/client/python/docs/GrantCatalogRoleRequest.md +++ b/regtests/client/python/docs/GrantCatalogRoleRequest.md @@ -1,6 +1,22 @@ + # GrantCatalogRoleRequest - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/GrantPrincipalRoleRequest.md b/regtests/client/python/docs/GrantPrincipalRoleRequest.md index 58c780072f..31661d5517 100644 --- a/regtests/client/python/docs/GrantPrincipalRoleRequest.md +++ b/regtests/client/python/docs/GrantPrincipalRoleRequest.md @@ -1,6 +1,22 @@ + # GrantPrincipalRoleRequest - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/GrantResource.md b/regtests/client/python/docs/GrantResource.md index 5f6b10e18b..5f86dfb157 100644 --- a/regtests/client/python/docs/GrantResource.md +++ b/regtests/client/python/docs/GrantResource.md @@ -1,6 +1,22 @@ + # GrantResource - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/GrantResources.md b/regtests/client/python/docs/GrantResources.md index 26708628a8..5fe5397328 100644 --- a/regtests/client/python/docs/GrantResources.md +++ b/regtests/client/python/docs/GrantResources.md @@ -1,6 +1,22 @@ + # GrantResources - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/IcebergCatalogAPI.md b/regtests/client/python/docs/IcebergCatalogAPI.md index 3630897c0a..6c50da3bd3 100644 --- a/regtests/client/python/docs/IcebergCatalogAPI.md +++ b/regtests/client/python/docs/IcebergCatalogAPI.md @@ -1,3 +1,20 @@ + # polaris.catalog.IcebergCatalogAPI All URIs are relative to *https://localhost* diff --git a/regtests/client/python/docs/IcebergConfigurationAPI.md b/regtests/client/python/docs/IcebergConfigurationAPI.md index f3b9007dbd..0ccc62e9a8 100644 --- a/regtests/client/python/docs/IcebergConfigurationAPI.md +++ b/regtests/client/python/docs/IcebergConfigurationAPI.md @@ -1,3 +1,20 @@ + # polaris.catalog.IcebergConfigurationAPI All URIs are relative to *https://localhost* diff --git a/regtests/client/python/docs/IcebergErrorResponse.md b/regtests/client/python/docs/IcebergErrorResponse.md index de5a9542fc..f4f9b5c69d 100644 --- a/regtests/client/python/docs/IcebergErrorResponse.md +++ b/regtests/client/python/docs/IcebergErrorResponse.md @@ -1,3 +1,20 @@ + # IcebergErrorResponse JSON wrapper for all error responses (non-2xx) diff --git a/regtests/client/python/docs/IcebergOAuth2API.md b/regtests/client/python/docs/IcebergOAuth2API.md index 20e8d297b9..b5d2ca0364 100644 --- a/regtests/client/python/docs/IcebergOAuth2API.md +++ b/regtests/client/python/docs/IcebergOAuth2API.md @@ -1,3 +1,20 @@ + # polaris.catalog.IcebergOAuth2API All URIs are relative to *https://localhost* diff --git a/regtests/client/python/docs/ListNamespacesResponse.md b/regtests/client/python/docs/ListNamespacesResponse.md index faafb191ef..87e868837f 100644 --- a/regtests/client/python/docs/ListNamespacesResponse.md +++ b/regtests/client/python/docs/ListNamespacesResponse.md @@ -1,6 +1,22 @@ + # ListNamespacesResponse - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/ListTablesResponse.md b/regtests/client/python/docs/ListTablesResponse.md index 24df6f7874..3b785b6a0f 100644 --- a/regtests/client/python/docs/ListTablesResponse.md +++ b/regtests/client/python/docs/ListTablesResponse.md @@ -1,6 +1,22 @@ + # ListTablesResponse - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/ListType.md b/regtests/client/python/docs/ListType.md index 63fbd215e9..d8e608923b 100644 --- a/regtests/client/python/docs/ListType.md +++ b/regtests/client/python/docs/ListType.md @@ -1,6 +1,22 @@ + # ListType - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/LiteralExpression.md b/regtests/client/python/docs/LiteralExpression.md index 5c414fafd2..ade9cb63b9 100644 --- a/regtests/client/python/docs/LiteralExpression.md +++ b/regtests/client/python/docs/LiteralExpression.md @@ -1,6 +1,22 @@ + # LiteralExpression - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/LoadTableResult.md b/regtests/client/python/docs/LoadTableResult.md index 82154d946d..01ad84349d 100644 --- a/regtests/client/python/docs/LoadTableResult.md +++ b/regtests/client/python/docs/LoadTableResult.md @@ -1,3 +1,20 @@ + # LoadTableResult Result used when a table is successfully loaded. The table metadata JSON is returned in the `metadata` field. The corresponding file location of table metadata should be returned in the `metadata-location` field, unless the metadata is not yet committed. For example, a create transaction may return metadata that is staged but not committed. Clients can check whether metadata has changed by comparing metadata locations after the table has been created. The `config` map returns table-specific configuration for the table's resources, including its HTTP client and FileIO. For example, config may contain a specific FileIO implementation class for the table depending on its underlying storage. The following configurations should be respected by clients: ## General Configurations - `token`: Authorization bearer token to use for table requests if OAuth2 security is enabled ## AWS Configurations The following configurations should be respected when working with tables stored in AWS S3 - `client.region`: region to configure client for making requests to AWS - `s3.access-key-id`: id for for credentials that provide access to the data in S3 - `s3.secret-access-key`: secret for credentials that provide access to data in S3 - `s3.session-token`: if present, this value should be used for as the session token - `s3.remote-signing-enabled`: if `true` remote signing should be performed as described in the `s3-signer-open-api.yaml` specification diff --git a/regtests/client/python/docs/LoadViewResult.md b/regtests/client/python/docs/LoadViewResult.md index 3e861f6b4c..8cd50f0d41 100644 --- a/regtests/client/python/docs/LoadViewResult.md +++ b/regtests/client/python/docs/LoadViewResult.md @@ -1,3 +1,20 @@ + # LoadViewResult Result used when a view is successfully loaded. The view metadata JSON is returned in the `metadata` field. The corresponding file location of view metadata is returned in the `metadata-location` field. Clients can check whether metadata has changed by comparing metadata locations after the view has been created. The `config` map returns view-specific configuration for the view's resources. The following configurations should be respected by clients: ## General Configurations - `token`: Authorization bearer token to use for view requests if OAuth2 security is enabled diff --git a/regtests/client/python/docs/MapType.md b/regtests/client/python/docs/MapType.md index 56bbc1db05..71b50893a3 100644 --- a/regtests/client/python/docs/MapType.md +++ b/regtests/client/python/docs/MapType.md @@ -1,6 +1,22 @@ + # MapType - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/MetadataLogInner.md b/regtests/client/python/docs/MetadataLogInner.md index 8b132cb185..1c3b46be55 100644 --- a/regtests/client/python/docs/MetadataLogInner.md +++ b/regtests/client/python/docs/MetadataLogInner.md @@ -1,6 +1,22 @@ + # MetadataLogInner - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/MetricResult.md b/regtests/client/python/docs/MetricResult.md index 8763169072..22f5e006e9 100644 --- a/regtests/client/python/docs/MetricResult.md +++ b/regtests/client/python/docs/MetricResult.md @@ -1,6 +1,22 @@ + # MetricResult - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/ModelSchema.md b/regtests/client/python/docs/ModelSchema.md index 66e58d077f..831f8311ac 100644 --- a/regtests/client/python/docs/ModelSchema.md +++ b/regtests/client/python/docs/ModelSchema.md @@ -1,6 +1,22 @@ + # ModelSchema - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/NamespaceGrant.md b/regtests/client/python/docs/NamespaceGrant.md index 6ec24670db..5a0fb3f62e 100644 --- a/regtests/client/python/docs/NamespaceGrant.md +++ b/regtests/client/python/docs/NamespaceGrant.md @@ -1,6 +1,22 @@ + # NamespaceGrant - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/NamespacePrivilege.md b/regtests/client/python/docs/NamespacePrivilege.md index 47756c10b2..f9ed3b2921 100644 --- a/regtests/client/python/docs/NamespacePrivilege.md +++ b/regtests/client/python/docs/NamespacePrivilege.md @@ -1,6 +1,22 @@ + # NamespacePrivilege - ## Enum * `CATALOG_MANAGE_ACCESS` (value: `'CATALOG_MANAGE_ACCESS'`) diff --git a/regtests/client/python/docs/NotExpression.md b/regtests/client/python/docs/NotExpression.md index 0d19c6e22d..283a3fb804 100644 --- a/regtests/client/python/docs/NotExpression.md +++ b/regtests/client/python/docs/NotExpression.md @@ -1,6 +1,22 @@ + # NotExpression - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/NotificationRequest.md b/regtests/client/python/docs/NotificationRequest.md index d6cbb8555f..ecd0a51b1d 100644 --- a/regtests/client/python/docs/NotificationRequest.md +++ b/regtests/client/python/docs/NotificationRequest.md @@ -1,6 +1,22 @@ + # NotificationRequest - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/NotificationType.md b/regtests/client/python/docs/NotificationType.md index 0e6f189949..7b332037bd 100644 --- a/regtests/client/python/docs/NotificationType.md +++ b/regtests/client/python/docs/NotificationType.md @@ -1,6 +1,22 @@ + # NotificationType - ## Enum * `UNKNOWN` (value: `'UNKNOWN'`) diff --git a/regtests/client/python/docs/NullOrder.md b/regtests/client/python/docs/NullOrder.md index 2558e8ad24..d5a236b1de 100644 --- a/regtests/client/python/docs/NullOrder.md +++ b/regtests/client/python/docs/NullOrder.md @@ -1,5 +1,21 @@ -# NullOrder + +# NullOrder ## Enum diff --git a/regtests/client/python/docs/OAuthError.md b/regtests/client/python/docs/OAuthError.md index b800a505ce..26416cebd1 100644 --- a/regtests/client/python/docs/OAuthError.md +++ b/regtests/client/python/docs/OAuthError.md @@ -1,6 +1,22 @@ + # OAuthError - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/OAuthTokenResponse.md b/regtests/client/python/docs/OAuthTokenResponse.md index 3505f49939..9c1a7802c7 100644 --- a/regtests/client/python/docs/OAuthTokenResponse.md +++ b/regtests/client/python/docs/OAuthTokenResponse.md @@ -1,6 +1,22 @@ + # OAuthTokenResponse - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/PartitionField.md b/regtests/client/python/docs/PartitionField.md index 1dfdd35a77..0fcd147d24 100644 --- a/regtests/client/python/docs/PartitionField.md +++ b/regtests/client/python/docs/PartitionField.md @@ -1,6 +1,22 @@ + # PartitionField - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/PartitionSpec.md b/regtests/client/python/docs/PartitionSpec.md index 2014c9e6a4..468493a974 100644 --- a/regtests/client/python/docs/PartitionSpec.md +++ b/regtests/client/python/docs/PartitionSpec.md @@ -1,6 +1,22 @@ + # PartitionSpec - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/PartitionStatisticsFile.md b/regtests/client/python/docs/PartitionStatisticsFile.md index db25c98708..dc8b8c2ded 100644 --- a/regtests/client/python/docs/PartitionStatisticsFile.md +++ b/regtests/client/python/docs/PartitionStatisticsFile.md @@ -1,6 +1,22 @@ + # PartitionStatisticsFile - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/PolarisCatalog.md b/regtests/client/python/docs/PolarisCatalog.md index 8f29559ec5..91050694a1 100644 --- a/regtests/client/python/docs/PolarisCatalog.md +++ b/regtests/client/python/docs/PolarisCatalog.md @@ -1,3 +1,20 @@ + # PolarisCatalog The base catalog type - this contains all the fields necessary to construct an INTERNAL catalog diff --git a/regtests/client/python/docs/PolarisDefaultApi.md b/regtests/client/python/docs/PolarisDefaultApi.md index cefe48e142..42b3fe74de 100644 --- a/regtests/client/python/docs/PolarisDefaultApi.md +++ b/regtests/client/python/docs/PolarisDefaultApi.md @@ -1,3 +1,20 @@ + # polaris.management.PolarisDefaultApi All URIs are relative to *https://localhost/api/management/v1* diff --git a/regtests/client/python/docs/PositionDeleteFile.md b/regtests/client/python/docs/PositionDeleteFile.md index 0c276d2cbf..dc39d2b5b3 100644 --- a/regtests/client/python/docs/PositionDeleteFile.md +++ b/regtests/client/python/docs/PositionDeleteFile.md @@ -1,6 +1,22 @@ + # PositionDeleteFile - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/PrimitiveTypeValue.md b/regtests/client/python/docs/PrimitiveTypeValue.md index d327eb4451..585370edc7 100644 --- a/regtests/client/python/docs/PrimitiveTypeValue.md +++ b/regtests/client/python/docs/PrimitiveTypeValue.md @@ -1,6 +1,22 @@ + # PrimitiveTypeValue - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/Principal.md b/regtests/client/python/docs/Principal.md index afa5f290b7..a47d5cf390 100644 --- a/regtests/client/python/docs/Principal.md +++ b/regtests/client/python/docs/Principal.md @@ -1,3 +1,20 @@ + # Principal A Polaris principal. diff --git a/regtests/client/python/docs/PrincipalRole.md b/regtests/client/python/docs/PrincipalRole.md index 519f06456a..6fb31653cd 100644 --- a/regtests/client/python/docs/PrincipalRole.md +++ b/regtests/client/python/docs/PrincipalRole.md @@ -1,6 +1,22 @@ + # PrincipalRole - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/PrincipalRoles.md b/regtests/client/python/docs/PrincipalRoles.md index de0bb73e97..a16037dd2b 100644 --- a/regtests/client/python/docs/PrincipalRoles.md +++ b/regtests/client/python/docs/PrincipalRoles.md @@ -1,6 +1,22 @@ + # PrincipalRoles - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/PrincipalWithCredentials.md b/regtests/client/python/docs/PrincipalWithCredentials.md index 0e8256276f..fe1006172a 100644 --- a/regtests/client/python/docs/PrincipalWithCredentials.md +++ b/regtests/client/python/docs/PrincipalWithCredentials.md @@ -1,3 +1,20 @@ + # PrincipalWithCredentials A user with its client id and secret. This type is returned when a new principal is created or when its credentials are rotated diff --git a/regtests/client/python/docs/PrincipalWithCredentialsCredentials.md b/regtests/client/python/docs/PrincipalWithCredentialsCredentials.md index 2cebdcd6af..f3cdb77d8d 100644 --- a/regtests/client/python/docs/PrincipalWithCredentialsCredentials.md +++ b/regtests/client/python/docs/PrincipalWithCredentialsCredentials.md @@ -1,6 +1,22 @@ + # PrincipalWithCredentialsCredentials - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/Principals.md b/regtests/client/python/docs/Principals.md index 3b58434721..dc45c78635 100644 --- a/regtests/client/python/docs/Principals.md +++ b/regtests/client/python/docs/Principals.md @@ -1,3 +1,20 @@ + # Principals A list of Principals diff --git a/regtests/client/python/docs/RegisterTableRequest.md b/regtests/client/python/docs/RegisterTableRequest.md index d834072053..9aab7a364e 100644 --- a/regtests/client/python/docs/RegisterTableRequest.md +++ b/regtests/client/python/docs/RegisterTableRequest.md @@ -1,6 +1,22 @@ + # RegisterTableRequest - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/RemovePartitionStatisticsUpdate.md b/regtests/client/python/docs/RemovePartitionStatisticsUpdate.md index 9e9dac4896..6d98532cf9 100644 --- a/regtests/client/python/docs/RemovePartitionStatisticsUpdate.md +++ b/regtests/client/python/docs/RemovePartitionStatisticsUpdate.md @@ -1,6 +1,22 @@ + # RemovePartitionStatisticsUpdate - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/RemovePropertiesUpdate.md b/regtests/client/python/docs/RemovePropertiesUpdate.md index 764eb76d6b..f7619f9117 100644 --- a/regtests/client/python/docs/RemovePropertiesUpdate.md +++ b/regtests/client/python/docs/RemovePropertiesUpdate.md @@ -1,6 +1,22 @@ + # RemovePropertiesUpdate - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/RemoveSnapshotRefUpdate.md b/regtests/client/python/docs/RemoveSnapshotRefUpdate.md index 67d2700e47..0640643fad 100644 --- a/regtests/client/python/docs/RemoveSnapshotRefUpdate.md +++ b/regtests/client/python/docs/RemoveSnapshotRefUpdate.md @@ -1,6 +1,22 @@ + # RemoveSnapshotRefUpdate - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/RemoveSnapshotsUpdate.md b/regtests/client/python/docs/RemoveSnapshotsUpdate.md index 4b50794347..cf0292caeb 100644 --- a/regtests/client/python/docs/RemoveSnapshotsUpdate.md +++ b/regtests/client/python/docs/RemoveSnapshotsUpdate.md @@ -1,6 +1,22 @@ + # RemoveSnapshotsUpdate - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/RemoveStatisticsUpdate.md b/regtests/client/python/docs/RemoveStatisticsUpdate.md index a47aa31441..a6c8e89aff 100644 --- a/regtests/client/python/docs/RemoveStatisticsUpdate.md +++ b/regtests/client/python/docs/RemoveStatisticsUpdate.md @@ -1,6 +1,22 @@ + # RemoveStatisticsUpdate - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/RenameTableRequest.md b/regtests/client/python/docs/RenameTableRequest.md index 28daddd1d8..ff757923a1 100644 --- a/regtests/client/python/docs/RenameTableRequest.md +++ b/regtests/client/python/docs/RenameTableRequest.md @@ -1,6 +1,22 @@ + # RenameTableRequest - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/ReportMetricsRequest.md b/regtests/client/python/docs/ReportMetricsRequest.md index 61ddcaebd0..9b8f351de0 100644 --- a/regtests/client/python/docs/ReportMetricsRequest.md +++ b/regtests/client/python/docs/ReportMetricsRequest.md @@ -1,6 +1,22 @@ + # ReportMetricsRequest - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/RevokeGrantRequest.md b/regtests/client/python/docs/RevokeGrantRequest.md index d6409f5d93..7271a48417 100644 --- a/regtests/client/python/docs/RevokeGrantRequest.md +++ b/regtests/client/python/docs/RevokeGrantRequest.md @@ -1,6 +1,22 @@ + # RevokeGrantRequest - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/SQLViewRepresentation.md b/regtests/client/python/docs/SQLViewRepresentation.md index 7cf648b696..00adb1c6c1 100644 --- a/regtests/client/python/docs/SQLViewRepresentation.md +++ b/regtests/client/python/docs/SQLViewRepresentation.md @@ -1,6 +1,22 @@ + # SQLViewRepresentation - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/ScanReport.md b/regtests/client/python/docs/ScanReport.md index d2ac51da53..9b44317f5a 100644 --- a/regtests/client/python/docs/ScanReport.md +++ b/regtests/client/python/docs/ScanReport.md @@ -1,6 +1,22 @@ + # ScanReport - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/SetCurrentSchemaUpdate.md b/regtests/client/python/docs/SetCurrentSchemaUpdate.md index 54a34de9e2..3edf744ffc 100644 --- a/regtests/client/python/docs/SetCurrentSchemaUpdate.md +++ b/regtests/client/python/docs/SetCurrentSchemaUpdate.md @@ -1,6 +1,22 @@ + # SetCurrentSchemaUpdate - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/SetCurrentViewVersionUpdate.md b/regtests/client/python/docs/SetCurrentViewVersionUpdate.md index 306a5f90d3..c5a461cd05 100644 --- a/regtests/client/python/docs/SetCurrentViewVersionUpdate.md +++ b/regtests/client/python/docs/SetCurrentViewVersionUpdate.md @@ -1,6 +1,22 @@ + # SetCurrentViewVersionUpdate - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/SetDefaultSortOrderUpdate.md b/regtests/client/python/docs/SetDefaultSortOrderUpdate.md index be60238f5e..4829080578 100644 --- a/regtests/client/python/docs/SetDefaultSortOrderUpdate.md +++ b/regtests/client/python/docs/SetDefaultSortOrderUpdate.md @@ -1,6 +1,22 @@ + # SetDefaultSortOrderUpdate - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/SetDefaultSpecUpdate.md b/regtests/client/python/docs/SetDefaultSpecUpdate.md index 762f014b10..b21a42bd8a 100644 --- a/regtests/client/python/docs/SetDefaultSpecUpdate.md +++ b/regtests/client/python/docs/SetDefaultSpecUpdate.md @@ -1,6 +1,22 @@ + # SetDefaultSpecUpdate - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/SetExpression.md b/regtests/client/python/docs/SetExpression.md index bb3ec5c5f7..6034024761 100644 --- a/regtests/client/python/docs/SetExpression.md +++ b/regtests/client/python/docs/SetExpression.md @@ -1,6 +1,22 @@ + # SetExpression - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/SetLocationUpdate.md b/regtests/client/python/docs/SetLocationUpdate.md index 7209a097e7..63c660f7f1 100644 --- a/regtests/client/python/docs/SetLocationUpdate.md +++ b/regtests/client/python/docs/SetLocationUpdate.md @@ -1,6 +1,22 @@ + # SetLocationUpdate - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/SetPartitionStatisticsUpdate.md b/regtests/client/python/docs/SetPartitionStatisticsUpdate.md index 2f38fe1574..0f8d785bfe 100644 --- a/regtests/client/python/docs/SetPartitionStatisticsUpdate.md +++ b/regtests/client/python/docs/SetPartitionStatisticsUpdate.md @@ -1,6 +1,22 @@ + # SetPartitionStatisticsUpdate - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/SetPropertiesUpdate.md b/regtests/client/python/docs/SetPropertiesUpdate.md index bca9117bdd..23e4f560ae 100644 --- a/regtests/client/python/docs/SetPropertiesUpdate.md +++ b/regtests/client/python/docs/SetPropertiesUpdate.md @@ -1,6 +1,22 @@ + # SetPropertiesUpdate - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/SetSnapshotRefUpdate.md b/regtests/client/python/docs/SetSnapshotRefUpdate.md index d83b36a881..f892420512 100644 --- a/regtests/client/python/docs/SetSnapshotRefUpdate.md +++ b/regtests/client/python/docs/SetSnapshotRefUpdate.md @@ -1,6 +1,22 @@ + # SetSnapshotRefUpdate - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/SetStatisticsUpdate.md b/regtests/client/python/docs/SetStatisticsUpdate.md index 1ebd049e8c..2efff01888 100644 --- a/regtests/client/python/docs/SetStatisticsUpdate.md +++ b/regtests/client/python/docs/SetStatisticsUpdate.md @@ -1,6 +1,22 @@ + # SetStatisticsUpdate - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/Snapshot.md b/regtests/client/python/docs/Snapshot.md index 8b9ec27997..a64649e8a4 100644 --- a/regtests/client/python/docs/Snapshot.md +++ b/regtests/client/python/docs/Snapshot.md @@ -1,6 +1,22 @@ + # Snapshot - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/SnapshotLogInner.md b/regtests/client/python/docs/SnapshotLogInner.md index a19edc4e4c..ec0256e700 100644 --- a/regtests/client/python/docs/SnapshotLogInner.md +++ b/regtests/client/python/docs/SnapshotLogInner.md @@ -1,6 +1,22 @@ + # SnapshotLogInner - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/SnapshotReference.md b/regtests/client/python/docs/SnapshotReference.md index 0128bfa075..facff3816a 100644 --- a/regtests/client/python/docs/SnapshotReference.md +++ b/regtests/client/python/docs/SnapshotReference.md @@ -1,6 +1,22 @@ + # SnapshotReference - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/SnapshotSummary.md b/regtests/client/python/docs/SnapshotSummary.md index 4c32e38ab4..5a60c043a2 100644 --- a/regtests/client/python/docs/SnapshotSummary.md +++ b/regtests/client/python/docs/SnapshotSummary.md @@ -1,6 +1,22 @@ + # SnapshotSummary - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/SortDirection.md b/regtests/client/python/docs/SortDirection.md index c5c2266788..7a2f6834b4 100644 --- a/regtests/client/python/docs/SortDirection.md +++ b/regtests/client/python/docs/SortDirection.md @@ -1,5 +1,21 @@ -# SortDirection + +# SortDirection ## Enum diff --git a/regtests/client/python/docs/SortField.md b/regtests/client/python/docs/SortField.md index bc5264e9e3..6f05e7699a 100644 --- a/regtests/client/python/docs/SortField.md +++ b/regtests/client/python/docs/SortField.md @@ -1,6 +1,22 @@ + # SortField - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/SortOrder.md b/regtests/client/python/docs/SortOrder.md index a54dc8a995..04751ed593 100644 --- a/regtests/client/python/docs/SortOrder.md +++ b/regtests/client/python/docs/SortOrder.md @@ -1,6 +1,22 @@ + # SortOrder - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/StatisticsFile.md b/regtests/client/python/docs/StatisticsFile.md index 7c6c8c293a..f429240883 100644 --- a/regtests/client/python/docs/StatisticsFile.md +++ b/regtests/client/python/docs/StatisticsFile.md @@ -1,6 +1,22 @@ + # StatisticsFile - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/StorageConfigInfo.md b/regtests/client/python/docs/StorageConfigInfo.md index 7a992db4ff..704520290f 100644 --- a/regtests/client/python/docs/StorageConfigInfo.md +++ b/regtests/client/python/docs/StorageConfigInfo.md @@ -1,3 +1,20 @@ + # StorageConfigInfo A storage configuration used by catalogs diff --git a/regtests/client/python/docs/StructField.md b/regtests/client/python/docs/StructField.md index ac95e884eb..9125b17ff5 100644 --- a/regtests/client/python/docs/StructField.md +++ b/regtests/client/python/docs/StructField.md @@ -1,6 +1,22 @@ + # StructField - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/StructType.md b/regtests/client/python/docs/StructType.md index 236fb4d7b9..25bfef569c 100644 --- a/regtests/client/python/docs/StructType.md +++ b/regtests/client/python/docs/StructType.md @@ -1,6 +1,22 @@ + # StructType - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/TableGrant.md b/regtests/client/python/docs/TableGrant.md index 50a4de9bbd..0bc1e53bc1 100644 --- a/regtests/client/python/docs/TableGrant.md +++ b/regtests/client/python/docs/TableGrant.md @@ -1,6 +1,22 @@ + # TableGrant - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/TableIdentifier.md b/regtests/client/python/docs/TableIdentifier.md index a3b7a2161b..f64a178f49 100644 --- a/regtests/client/python/docs/TableIdentifier.md +++ b/regtests/client/python/docs/TableIdentifier.md @@ -1,6 +1,22 @@ + # TableIdentifier - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/TableMetadata.md b/regtests/client/python/docs/TableMetadata.md index 3a1019d612..e7532867a0 100644 --- a/regtests/client/python/docs/TableMetadata.md +++ b/regtests/client/python/docs/TableMetadata.md @@ -1,5 +1,21 @@ -# TableMetadata + +# TableMetadata ## Properties diff --git a/regtests/client/python/docs/TablePrivilege.md b/regtests/client/python/docs/TablePrivilege.md index f4015e82d7..23ae745888 100644 --- a/regtests/client/python/docs/TablePrivilege.md +++ b/regtests/client/python/docs/TablePrivilege.md @@ -1,6 +1,22 @@ + # TablePrivilege - ## Enum * `CATALOG_MANAGE_ACCESS` (value: `'CATALOG_MANAGE_ACCESS'`) diff --git a/regtests/client/python/docs/TableRequirement.md b/regtests/client/python/docs/TableRequirement.md index f2b2460e62..9e7ed0d29b 100644 --- a/regtests/client/python/docs/TableRequirement.md +++ b/regtests/client/python/docs/TableRequirement.md @@ -1,6 +1,22 @@ + # TableRequirement - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/TableUpdate.md b/regtests/client/python/docs/TableUpdate.md index 6dca410f47..d7bda62d74 100644 --- a/regtests/client/python/docs/TableUpdate.md +++ b/regtests/client/python/docs/TableUpdate.md @@ -1,5 +1,21 @@ -# TableUpdate + +# TableUpdate ## Properties diff --git a/regtests/client/python/docs/TableUpdateNotification.md b/regtests/client/python/docs/TableUpdateNotification.md index b0a7b5600a..935381243c 100644 --- a/regtests/client/python/docs/TableUpdateNotification.md +++ b/regtests/client/python/docs/TableUpdateNotification.md @@ -1,6 +1,22 @@ + # TableUpdateNotification - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/Term.md b/regtests/client/python/docs/Term.md index cf1e3fb923..c627b9a22d 100644 --- a/regtests/client/python/docs/Term.md +++ b/regtests/client/python/docs/Term.md @@ -1,6 +1,22 @@ + # Term - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/TimerResult.md b/regtests/client/python/docs/TimerResult.md index 7a4c22a36f..abdccb2296 100644 --- a/regtests/client/python/docs/TimerResult.md +++ b/regtests/client/python/docs/TimerResult.md @@ -1,6 +1,22 @@ + # TimerResult - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/TokenType.md b/regtests/client/python/docs/TokenType.md index 8bb0dc046c..bf3e5a0cbe 100644 --- a/regtests/client/python/docs/TokenType.md +++ b/regtests/client/python/docs/TokenType.md @@ -1,3 +1,20 @@ + # TokenType Token type identifier, from RFC 8693 Section 3 See https://datatracker.ietf.org/doc/html/rfc8693#section-3 diff --git a/regtests/client/python/docs/TransformTerm.md b/regtests/client/python/docs/TransformTerm.md index 14653cdf36..9aa207be5e 100644 --- a/regtests/client/python/docs/TransformTerm.md +++ b/regtests/client/python/docs/TransformTerm.md @@ -1,6 +1,22 @@ + # TransformTerm - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/Type.md b/regtests/client/python/docs/Type.md index 4a7337c956..7ce5b32297 100644 --- a/regtests/client/python/docs/Type.md +++ b/regtests/client/python/docs/Type.md @@ -1,6 +1,22 @@ + # Type - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/UnaryExpression.md b/regtests/client/python/docs/UnaryExpression.md index 5a3e12c1bc..3721decd2a 100644 --- a/regtests/client/python/docs/UnaryExpression.md +++ b/regtests/client/python/docs/UnaryExpression.md @@ -1,6 +1,22 @@ + # UnaryExpression - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/UpdateCatalogRequest.md b/regtests/client/python/docs/UpdateCatalogRequest.md index 5cb5581c9e..68a5c97ff7 100644 --- a/regtests/client/python/docs/UpdateCatalogRequest.md +++ b/regtests/client/python/docs/UpdateCatalogRequest.md @@ -1,3 +1,20 @@ + # UpdateCatalogRequest Updates to apply to a Catalog diff --git a/regtests/client/python/docs/UpdateCatalogRoleRequest.md b/regtests/client/python/docs/UpdateCatalogRoleRequest.md index bc4ace54f2..0fbe5a8a88 100644 --- a/regtests/client/python/docs/UpdateCatalogRoleRequest.md +++ b/regtests/client/python/docs/UpdateCatalogRoleRequest.md @@ -1,3 +1,20 @@ + # UpdateCatalogRoleRequest Updates to apply to a Catalog Role diff --git a/regtests/client/python/docs/UpdateNamespacePropertiesRequest.md b/regtests/client/python/docs/UpdateNamespacePropertiesRequest.md index ea2a5a6722..eeb294f5b0 100644 --- a/regtests/client/python/docs/UpdateNamespacePropertiesRequest.md +++ b/regtests/client/python/docs/UpdateNamespacePropertiesRequest.md @@ -1,6 +1,22 @@ + # UpdateNamespacePropertiesRequest - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/UpdateNamespacePropertiesResponse.md b/regtests/client/python/docs/UpdateNamespacePropertiesResponse.md index 70ebd68baf..ed73879da7 100644 --- a/regtests/client/python/docs/UpdateNamespacePropertiesResponse.md +++ b/regtests/client/python/docs/UpdateNamespacePropertiesResponse.md @@ -1,6 +1,22 @@ + # UpdateNamespacePropertiesResponse - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/UpdatePrincipalRequest.md b/regtests/client/python/docs/UpdatePrincipalRequest.md index 586f60a5f9..bcb52c2aa3 100644 --- a/regtests/client/python/docs/UpdatePrincipalRequest.md +++ b/regtests/client/python/docs/UpdatePrincipalRequest.md @@ -1,3 +1,20 @@ + # UpdatePrincipalRequest Updates to apply to a Principal diff --git a/regtests/client/python/docs/UpdatePrincipalRoleRequest.md b/regtests/client/python/docs/UpdatePrincipalRoleRequest.md index ecb2f9cb25..a918f706c6 100644 --- a/regtests/client/python/docs/UpdatePrincipalRoleRequest.md +++ b/regtests/client/python/docs/UpdatePrincipalRoleRequest.md @@ -1,3 +1,20 @@ + # UpdatePrincipalRoleRequest Updates to apply to a Principal Role diff --git a/regtests/client/python/docs/UpgradeFormatVersionUpdate.md b/regtests/client/python/docs/UpgradeFormatVersionUpdate.md index 599995ab0f..a1a02ea47c 100644 --- a/regtests/client/python/docs/UpgradeFormatVersionUpdate.md +++ b/regtests/client/python/docs/UpgradeFormatVersionUpdate.md @@ -1,6 +1,22 @@ + # UpgradeFormatVersionUpdate - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/ValueMap.md b/regtests/client/python/docs/ValueMap.md index 391355735a..87a581adfd 100644 --- a/regtests/client/python/docs/ValueMap.md +++ b/regtests/client/python/docs/ValueMap.md @@ -1,6 +1,22 @@ + # ValueMap - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/ViewGrant.md b/regtests/client/python/docs/ViewGrant.md index f9e8030ce3..59a7da9907 100644 --- a/regtests/client/python/docs/ViewGrant.md +++ b/regtests/client/python/docs/ViewGrant.md @@ -1,6 +1,22 @@ + # ViewGrant - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/ViewHistoryEntry.md b/regtests/client/python/docs/ViewHistoryEntry.md index 4def78578a..2942b6e2bc 100644 --- a/regtests/client/python/docs/ViewHistoryEntry.md +++ b/regtests/client/python/docs/ViewHistoryEntry.md @@ -1,6 +1,22 @@ + # ViewHistoryEntry - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/ViewMetadata.md b/regtests/client/python/docs/ViewMetadata.md index 174ca820f1..397011799a 100644 --- a/regtests/client/python/docs/ViewMetadata.md +++ b/regtests/client/python/docs/ViewMetadata.md @@ -1,6 +1,22 @@ + # ViewMetadata - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/ViewPrivilege.md b/regtests/client/python/docs/ViewPrivilege.md index 27dd07d427..0bffea4187 100644 --- a/regtests/client/python/docs/ViewPrivilege.md +++ b/regtests/client/python/docs/ViewPrivilege.md @@ -1,6 +1,22 @@ + # ViewPrivilege - ## Enum * `CATALOG_MANAGE_ACCESS` (value: `'CATALOG_MANAGE_ACCESS'`) diff --git a/regtests/client/python/docs/ViewRepresentation.md b/regtests/client/python/docs/ViewRepresentation.md index 6c892c9719..a1753a8731 100644 --- a/regtests/client/python/docs/ViewRepresentation.md +++ b/regtests/client/python/docs/ViewRepresentation.md @@ -1,6 +1,22 @@ + # ViewRepresentation - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/ViewRequirement.md b/regtests/client/python/docs/ViewRequirement.md index ff869b4354..9a6713860e 100644 --- a/regtests/client/python/docs/ViewRequirement.md +++ b/regtests/client/python/docs/ViewRequirement.md @@ -1,6 +1,22 @@ + # ViewRequirement - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/ViewUpdate.md b/regtests/client/python/docs/ViewUpdate.md index 98d45169ca..6965ae05de 100644 --- a/regtests/client/python/docs/ViewUpdate.md +++ b/regtests/client/python/docs/ViewUpdate.md @@ -1,6 +1,22 @@ + # ViewUpdate - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/docs/ViewVersion.md b/regtests/client/python/docs/ViewVersion.md index 6ed58a5f04..8b6c713c69 100644 --- a/regtests/client/python/docs/ViewVersion.md +++ b/regtests/client/python/docs/ViewVersion.md @@ -1,6 +1,22 @@ + # ViewVersion - ## Properties Name | Type | Description | Notes diff --git a/regtests/client/python/git_push.sh b/regtests/client/python/git_push.sh index f53a75d4fa..7d770085b8 100644 --- a/regtests/client/python/git_push.sh +++ b/regtests/client/python/git_push.sh @@ -1,4 +1,19 @@ #!/bin/sh +# +# Copyright (c) 2024 Snowflake Computing 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. +# # ref: https://help.github.com/articles/adding-an-existing-project-to-github-using-the-command-line/ # # Usage example: /bin/sh ./git_push.sh wing328 openapi-petstore-perl "minor update" "gitlab.com" diff --git a/regtests/client/python/polaris/__init__.py b/regtests/client/python/polaris/__init__.py index e69de29bb2..8d220260f1 100644 --- a/regtests/client/python/polaris/__init__.py +++ b/regtests/client/python/polaris/__init__.py @@ -0,0 +1,15 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# \ No newline at end of file diff --git a/regtests/client/python/polaris/catalog/__init__.py b/regtests/client/python/polaris/catalog/__init__.py index fd3a10c008..25a4c14b29 100644 --- a/regtests/client/python/polaris/catalog/__init__.py +++ b/regtests/client/python/polaris/catalog/__init__.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 # flake8: noqa diff --git a/regtests/client/python/polaris/catalog/api/__init__.py b/regtests/client/python/polaris/catalog/api/__init__.py index 63e5ffafcc..fd8a960a30 100644 --- a/regtests/client/python/polaris/catalog/api/__init__.py +++ b/regtests/client/python/polaris/catalog/api/__init__.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # flake8: noqa # import apis into api package diff --git a/regtests/client/python/polaris/catalog/api/iceberg_catalog_api.py b/regtests/client/python/polaris/catalog/api/iceberg_catalog_api.py index e6ce3fc024..87fc6e5eb0 100644 --- a/regtests/client/python/polaris/catalog/api/iceberg_catalog_api.py +++ b/regtests/client/python/polaris/catalog/api/iceberg_catalog_api.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/api/iceberg_configuration_api.py b/regtests/client/python/polaris/catalog/api/iceberg_configuration_api.py index 4d409d8e1b..b504d6af5d 100644 --- a/regtests/client/python/polaris/catalog/api/iceberg_configuration_api.py +++ b/regtests/client/python/polaris/catalog/api/iceberg_configuration_api.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/api/iceberg_o_auth2_api.py b/regtests/client/python/polaris/catalog/api/iceberg_o_auth2_api.py index f1a163c9c8..2d5adaadeb 100644 --- a/regtests/client/python/polaris/catalog/api/iceberg_o_auth2_api.py +++ b/regtests/client/python/polaris/catalog/api/iceberg_o_auth2_api.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/api_client.py b/regtests/client/python/polaris/catalog/api_client.py index 434429d84b..a07a05d249 100644 --- a/regtests/client/python/polaris/catalog/api_client.py +++ b/regtests/client/python/polaris/catalog/api_client.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/api_response.py b/regtests/client/python/polaris/catalog/api_response.py index 9bc7c11f6b..e3a3bc42e0 100644 --- a/regtests/client/python/polaris/catalog/api_response.py +++ b/regtests/client/python/polaris/catalog/api_response.py @@ -1,3 +1,19 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# + """API response object.""" from __future__ import annotations diff --git a/regtests/client/python/polaris/catalog/configuration.py b/regtests/client/python/polaris/catalog/configuration.py index 52dea68e3d..aecac866a7 100644 --- a/regtests/client/python/polaris/catalog/configuration.py +++ b/regtests/client/python/polaris/catalog/configuration.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/exceptions.py b/regtests/client/python/polaris/catalog/exceptions.py index cf15eaa377..9da03d2d2b 100644 --- a/regtests/client/python/polaris/catalog/exceptions.py +++ b/regtests/client/python/polaris/catalog/exceptions.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/__init__.py b/regtests/client/python/polaris/catalog/models/__init__.py index 10a07c8f53..c2da2dc1f6 100644 --- a/regtests/client/python/polaris/catalog/models/__init__.py +++ b/regtests/client/python/polaris/catalog/models/__init__.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 # flake8: noqa diff --git a/regtests/client/python/polaris/catalog/models/add_partition_spec_update.py b/regtests/client/python/polaris/catalog/models/add_partition_spec_update.py index f163977cb9..080ccc3cea 100644 --- a/regtests/client/python/polaris/catalog/models/add_partition_spec_update.py +++ b/regtests/client/python/polaris/catalog/models/add_partition_spec_update.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/add_schema_update.py b/regtests/client/python/polaris/catalog/models/add_schema_update.py index 46a4999dff..f5f70a560c 100644 --- a/regtests/client/python/polaris/catalog/models/add_schema_update.py +++ b/regtests/client/python/polaris/catalog/models/add_schema_update.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/add_snapshot_update.py b/regtests/client/python/polaris/catalog/models/add_snapshot_update.py index 048a524739..4da8a2ded1 100644 --- a/regtests/client/python/polaris/catalog/models/add_snapshot_update.py +++ b/regtests/client/python/polaris/catalog/models/add_snapshot_update.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/add_sort_order_update.py b/regtests/client/python/polaris/catalog/models/add_sort_order_update.py index 3e2145b319..e2990a5e57 100644 --- a/regtests/client/python/polaris/catalog/models/add_sort_order_update.py +++ b/regtests/client/python/polaris/catalog/models/add_sort_order_update.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/add_view_version_update.py b/regtests/client/python/polaris/catalog/models/add_view_version_update.py index b45c3cd5ac..883ad3edff 100644 --- a/regtests/client/python/polaris/catalog/models/add_view_version_update.py +++ b/regtests/client/python/polaris/catalog/models/add_view_version_update.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/and_or_expression.py b/regtests/client/python/polaris/catalog/models/and_or_expression.py index 5688bd43d0..2648c43cc4 100644 --- a/regtests/client/python/polaris/catalog/models/and_or_expression.py +++ b/regtests/client/python/polaris/catalog/models/and_or_expression.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/assert_create.py b/regtests/client/python/polaris/catalog/models/assert_create.py index e281a37d22..c5aeaa3621 100644 --- a/regtests/client/python/polaris/catalog/models/assert_create.py +++ b/regtests/client/python/polaris/catalog/models/assert_create.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/assert_current_schema_id.py b/regtests/client/python/polaris/catalog/models/assert_current_schema_id.py index 6b2e75b318..1cd1604852 100644 --- a/regtests/client/python/polaris/catalog/models/assert_current_schema_id.py +++ b/regtests/client/python/polaris/catalog/models/assert_current_schema_id.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/assert_default_sort_order_id.py b/regtests/client/python/polaris/catalog/models/assert_default_sort_order_id.py index 85ec938151..694b96b7e9 100644 --- a/regtests/client/python/polaris/catalog/models/assert_default_sort_order_id.py +++ b/regtests/client/python/polaris/catalog/models/assert_default_sort_order_id.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/assert_default_spec_id.py b/regtests/client/python/polaris/catalog/models/assert_default_spec_id.py index 764bb12c29..69445be2a9 100644 --- a/regtests/client/python/polaris/catalog/models/assert_default_spec_id.py +++ b/regtests/client/python/polaris/catalog/models/assert_default_spec_id.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/assert_last_assigned_field_id.py b/regtests/client/python/polaris/catalog/models/assert_last_assigned_field_id.py index 56c1beca23..f6143aadbf 100644 --- a/regtests/client/python/polaris/catalog/models/assert_last_assigned_field_id.py +++ b/regtests/client/python/polaris/catalog/models/assert_last_assigned_field_id.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/assert_last_assigned_partition_id.py b/regtests/client/python/polaris/catalog/models/assert_last_assigned_partition_id.py index 7a6af9b9ad..2329c6de1d 100644 --- a/regtests/client/python/polaris/catalog/models/assert_last_assigned_partition_id.py +++ b/regtests/client/python/polaris/catalog/models/assert_last_assigned_partition_id.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/assert_ref_snapshot_id.py b/regtests/client/python/polaris/catalog/models/assert_ref_snapshot_id.py index 783dd49fef..d59d1cbc0e 100644 --- a/regtests/client/python/polaris/catalog/models/assert_ref_snapshot_id.py +++ b/regtests/client/python/polaris/catalog/models/assert_ref_snapshot_id.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/assert_table_uuid.py b/regtests/client/python/polaris/catalog/models/assert_table_uuid.py index a626003efa..befdaf9415 100644 --- a/regtests/client/python/polaris/catalog/models/assert_table_uuid.py +++ b/regtests/client/python/polaris/catalog/models/assert_table_uuid.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/assert_view_uuid.py b/regtests/client/python/polaris/catalog/models/assert_view_uuid.py index b449ea7ba9..a0fb33be82 100644 --- a/regtests/client/python/polaris/catalog/models/assert_view_uuid.py +++ b/regtests/client/python/polaris/catalog/models/assert_view_uuid.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/assign_uuid_update.py b/regtests/client/python/polaris/catalog/models/assign_uuid_update.py index 6c6dc08430..d58d8b8e65 100644 --- a/regtests/client/python/polaris/catalog/models/assign_uuid_update.py +++ b/regtests/client/python/polaris/catalog/models/assign_uuid_update.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/base_update.py b/regtests/client/python/polaris/catalog/models/base_update.py index b83a445c80..0f3044d0be 100644 --- a/regtests/client/python/polaris/catalog/models/base_update.py +++ b/regtests/client/python/polaris/catalog/models/base_update.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/blob_metadata.py b/regtests/client/python/polaris/catalog/models/blob_metadata.py index 5c30309790..878668220e 100644 --- a/regtests/client/python/polaris/catalog/models/blob_metadata.py +++ b/regtests/client/python/polaris/catalog/models/blob_metadata.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/catalog_config.py b/regtests/client/python/polaris/catalog/models/catalog_config.py index 4e4950846c..02cff71399 100644 --- a/regtests/client/python/polaris/catalog/models/catalog_config.py +++ b/regtests/client/python/polaris/catalog/models/catalog_config.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/commit_report.py b/regtests/client/python/polaris/catalog/models/commit_report.py index f7998de0da..44315d8b65 100644 --- a/regtests/client/python/polaris/catalog/models/commit_report.py +++ b/regtests/client/python/polaris/catalog/models/commit_report.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/commit_table_request.py b/regtests/client/python/polaris/catalog/models/commit_table_request.py index 8ea8169d75..8261edc5f7 100644 --- a/regtests/client/python/polaris/catalog/models/commit_table_request.py +++ b/regtests/client/python/polaris/catalog/models/commit_table_request.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/commit_table_response.py b/regtests/client/python/polaris/catalog/models/commit_table_response.py index c55c836d4b..a4133f9f6f 100644 --- a/regtests/client/python/polaris/catalog/models/commit_table_response.py +++ b/regtests/client/python/polaris/catalog/models/commit_table_response.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/commit_transaction_request.py b/regtests/client/python/polaris/catalog/models/commit_transaction_request.py index 5c1feaeb1d..f26a7c7c2a 100644 --- a/regtests/client/python/polaris/catalog/models/commit_transaction_request.py +++ b/regtests/client/python/polaris/catalog/models/commit_transaction_request.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/commit_view_request.py b/regtests/client/python/polaris/catalog/models/commit_view_request.py index a849135a88..aa31000342 100644 --- a/regtests/client/python/polaris/catalog/models/commit_view_request.py +++ b/regtests/client/python/polaris/catalog/models/commit_view_request.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/content_file.py b/regtests/client/python/polaris/catalog/models/content_file.py index ede346069f..9ef02722ea 100644 --- a/regtests/client/python/polaris/catalog/models/content_file.py +++ b/regtests/client/python/polaris/catalog/models/content_file.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/count_map.py b/regtests/client/python/polaris/catalog/models/count_map.py index a7c68df254..448cdfcf59 100644 --- a/regtests/client/python/polaris/catalog/models/count_map.py +++ b/regtests/client/python/polaris/catalog/models/count_map.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/counter_result.py b/regtests/client/python/polaris/catalog/models/counter_result.py index 852ddbd2e3..dde3687ae7 100644 --- a/regtests/client/python/polaris/catalog/models/counter_result.py +++ b/regtests/client/python/polaris/catalog/models/counter_result.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/create_namespace_request.py b/regtests/client/python/polaris/catalog/models/create_namespace_request.py index 68ff1570f2..1232839088 100644 --- a/regtests/client/python/polaris/catalog/models/create_namespace_request.py +++ b/regtests/client/python/polaris/catalog/models/create_namespace_request.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/create_namespace_response.py b/regtests/client/python/polaris/catalog/models/create_namespace_response.py index e202dcd5f7..f046409584 100644 --- a/regtests/client/python/polaris/catalog/models/create_namespace_response.py +++ b/regtests/client/python/polaris/catalog/models/create_namespace_response.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/create_table_request.py b/regtests/client/python/polaris/catalog/models/create_table_request.py index fc864b30a2..611f2b77aa 100644 --- a/regtests/client/python/polaris/catalog/models/create_table_request.py +++ b/regtests/client/python/polaris/catalog/models/create_table_request.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/create_view_request.py b/regtests/client/python/polaris/catalog/models/create_view_request.py index b5464a5bec..6f61479672 100644 --- a/regtests/client/python/polaris/catalog/models/create_view_request.py +++ b/regtests/client/python/polaris/catalog/models/create_view_request.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/data_file.py b/regtests/client/python/polaris/catalog/models/data_file.py index 0613e6a130..2445bc1c2c 100644 --- a/regtests/client/python/polaris/catalog/models/data_file.py +++ b/regtests/client/python/polaris/catalog/models/data_file.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/equality_delete_file.py b/regtests/client/python/polaris/catalog/models/equality_delete_file.py index de4dbc2074..3b83eb60be 100644 --- a/regtests/client/python/polaris/catalog/models/equality_delete_file.py +++ b/regtests/client/python/polaris/catalog/models/equality_delete_file.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/error_model.py b/regtests/client/python/polaris/catalog/models/error_model.py index f283d377e5..780ba7d47f 100644 --- a/regtests/client/python/polaris/catalog/models/error_model.py +++ b/regtests/client/python/polaris/catalog/models/error_model.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/expression.py b/regtests/client/python/polaris/catalog/models/expression.py index a22db3ef04..00b37f99a8 100644 --- a/regtests/client/python/polaris/catalog/models/expression.py +++ b/regtests/client/python/polaris/catalog/models/expression.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/file_format.py b/regtests/client/python/polaris/catalog/models/file_format.py index f6c209036b..f82d4cccee 100644 --- a/regtests/client/python/polaris/catalog/models/file_format.py +++ b/regtests/client/python/polaris/catalog/models/file_format.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/get_namespace_response.py b/regtests/client/python/polaris/catalog/models/get_namespace_response.py index 8e0d3629d1..3510306ffb 100644 --- a/regtests/client/python/polaris/catalog/models/get_namespace_response.py +++ b/regtests/client/python/polaris/catalog/models/get_namespace_response.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/iceberg_error_response.py b/regtests/client/python/polaris/catalog/models/iceberg_error_response.py index fce685d03e..e51e83315d 100644 --- a/regtests/client/python/polaris/catalog/models/iceberg_error_response.py +++ b/regtests/client/python/polaris/catalog/models/iceberg_error_response.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/list_namespaces_response.py b/regtests/client/python/polaris/catalog/models/list_namespaces_response.py index 89ea46d218..772fb37eab 100644 --- a/regtests/client/python/polaris/catalog/models/list_namespaces_response.py +++ b/regtests/client/python/polaris/catalog/models/list_namespaces_response.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/list_tables_response.py b/regtests/client/python/polaris/catalog/models/list_tables_response.py index 734e3bb5e4..38ee5dedbd 100644 --- a/regtests/client/python/polaris/catalog/models/list_tables_response.py +++ b/regtests/client/python/polaris/catalog/models/list_tables_response.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/list_type.py b/regtests/client/python/polaris/catalog/models/list_type.py index 886b1ff0f8..c54c322e1a 100644 --- a/regtests/client/python/polaris/catalog/models/list_type.py +++ b/regtests/client/python/polaris/catalog/models/list_type.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/literal_expression.py b/regtests/client/python/polaris/catalog/models/literal_expression.py index cf5d8c5cc0..10d006c090 100644 --- a/regtests/client/python/polaris/catalog/models/literal_expression.py +++ b/regtests/client/python/polaris/catalog/models/literal_expression.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/load_table_result.py b/regtests/client/python/polaris/catalog/models/load_table_result.py index 21e17ca4d2..e9e7a715f1 100644 --- a/regtests/client/python/polaris/catalog/models/load_table_result.py +++ b/regtests/client/python/polaris/catalog/models/load_table_result.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/load_view_result.py b/regtests/client/python/polaris/catalog/models/load_view_result.py index 7e87afbd57..fdf1a6ab56 100644 --- a/regtests/client/python/polaris/catalog/models/load_view_result.py +++ b/regtests/client/python/polaris/catalog/models/load_view_result.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/map_type.py b/regtests/client/python/polaris/catalog/models/map_type.py index 92d8b76bed..f59f558163 100644 --- a/regtests/client/python/polaris/catalog/models/map_type.py +++ b/regtests/client/python/polaris/catalog/models/map_type.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/metadata_log_inner.py b/regtests/client/python/polaris/catalog/models/metadata_log_inner.py index a2d4f0350a..acbb2a9d0d 100644 --- a/regtests/client/python/polaris/catalog/models/metadata_log_inner.py +++ b/regtests/client/python/polaris/catalog/models/metadata_log_inner.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/metric_result.py b/regtests/client/python/polaris/catalog/models/metric_result.py index f6659adb3a..94b8fd86a1 100644 --- a/regtests/client/python/polaris/catalog/models/metric_result.py +++ b/regtests/client/python/polaris/catalog/models/metric_result.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/model_schema.py b/regtests/client/python/polaris/catalog/models/model_schema.py index 15a4b1bec1..bc2c82e1af 100644 --- a/regtests/client/python/polaris/catalog/models/model_schema.py +++ b/regtests/client/python/polaris/catalog/models/model_schema.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/not_expression.py b/regtests/client/python/polaris/catalog/models/not_expression.py index 5cfc26440b..8bc6e8cc1a 100644 --- a/regtests/client/python/polaris/catalog/models/not_expression.py +++ b/regtests/client/python/polaris/catalog/models/not_expression.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/notification_request.py b/regtests/client/python/polaris/catalog/models/notification_request.py index 6add546be3..b408c32b99 100644 --- a/regtests/client/python/polaris/catalog/models/notification_request.py +++ b/regtests/client/python/polaris/catalog/models/notification_request.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/notification_type.py b/regtests/client/python/polaris/catalog/models/notification_type.py index 64b0d4b2af..c5b8491782 100644 --- a/regtests/client/python/polaris/catalog/models/notification_type.py +++ b/regtests/client/python/polaris/catalog/models/notification_type.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/null_order.py b/regtests/client/python/polaris/catalog/models/null_order.py index 91ae75ebeb..f05c35ae9c 100644 --- a/regtests/client/python/polaris/catalog/models/null_order.py +++ b/regtests/client/python/polaris/catalog/models/null_order.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/o_auth_error.py b/regtests/client/python/polaris/catalog/models/o_auth_error.py index 0399be5b0e..df82791b09 100644 --- a/regtests/client/python/polaris/catalog/models/o_auth_error.py +++ b/regtests/client/python/polaris/catalog/models/o_auth_error.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/o_auth_token_response.py b/regtests/client/python/polaris/catalog/models/o_auth_token_response.py index c7786e539a..369ce4a9b6 100644 --- a/regtests/client/python/polaris/catalog/models/o_auth_token_response.py +++ b/regtests/client/python/polaris/catalog/models/o_auth_token_response.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/partition_field.py b/regtests/client/python/polaris/catalog/models/partition_field.py index b5d626ad95..7d06fdaec3 100644 --- a/regtests/client/python/polaris/catalog/models/partition_field.py +++ b/regtests/client/python/polaris/catalog/models/partition_field.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/partition_spec.py b/regtests/client/python/polaris/catalog/models/partition_spec.py index 13d7bd2259..3f22124d32 100644 --- a/regtests/client/python/polaris/catalog/models/partition_spec.py +++ b/regtests/client/python/polaris/catalog/models/partition_spec.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/partition_statistics_file.py b/regtests/client/python/polaris/catalog/models/partition_statistics_file.py index 2a74c9e78d..265d1f5be6 100644 --- a/regtests/client/python/polaris/catalog/models/partition_statistics_file.py +++ b/regtests/client/python/polaris/catalog/models/partition_statistics_file.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/position_delete_file.py b/regtests/client/python/polaris/catalog/models/position_delete_file.py index 8ea4417ec5..548166d078 100644 --- a/regtests/client/python/polaris/catalog/models/position_delete_file.py +++ b/regtests/client/python/polaris/catalog/models/position_delete_file.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/primitive_type_value.py b/regtests/client/python/polaris/catalog/models/primitive_type_value.py index 36e79b0e0a..0632ea1dab 100644 --- a/regtests/client/python/polaris/catalog/models/primitive_type_value.py +++ b/regtests/client/python/polaris/catalog/models/primitive_type_value.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/register_table_request.py b/regtests/client/python/polaris/catalog/models/register_table_request.py index 39fe8057c6..332f8cb7d6 100644 --- a/regtests/client/python/polaris/catalog/models/register_table_request.py +++ b/regtests/client/python/polaris/catalog/models/register_table_request.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/remove_partition_statistics_update.py b/regtests/client/python/polaris/catalog/models/remove_partition_statistics_update.py index 6f8a3a4ea5..d23fcfe50f 100644 --- a/regtests/client/python/polaris/catalog/models/remove_partition_statistics_update.py +++ b/regtests/client/python/polaris/catalog/models/remove_partition_statistics_update.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/remove_properties_update.py b/regtests/client/python/polaris/catalog/models/remove_properties_update.py index 4042243bb0..ac88e9087a 100644 --- a/regtests/client/python/polaris/catalog/models/remove_properties_update.py +++ b/regtests/client/python/polaris/catalog/models/remove_properties_update.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/remove_snapshot_ref_update.py b/regtests/client/python/polaris/catalog/models/remove_snapshot_ref_update.py index 1825964862..c4d2e7b749 100644 --- a/regtests/client/python/polaris/catalog/models/remove_snapshot_ref_update.py +++ b/regtests/client/python/polaris/catalog/models/remove_snapshot_ref_update.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/remove_snapshots_update.py b/regtests/client/python/polaris/catalog/models/remove_snapshots_update.py index b1ec7c2ee7..f4727cb6af 100644 --- a/regtests/client/python/polaris/catalog/models/remove_snapshots_update.py +++ b/regtests/client/python/polaris/catalog/models/remove_snapshots_update.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/remove_statistics_update.py b/regtests/client/python/polaris/catalog/models/remove_statistics_update.py index 48fd2549fb..ba684f4561 100644 --- a/regtests/client/python/polaris/catalog/models/remove_statistics_update.py +++ b/regtests/client/python/polaris/catalog/models/remove_statistics_update.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/rename_table_request.py b/regtests/client/python/polaris/catalog/models/rename_table_request.py index 3a58b6157f..f1e3c68525 100644 --- a/regtests/client/python/polaris/catalog/models/rename_table_request.py +++ b/regtests/client/python/polaris/catalog/models/rename_table_request.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/report_metrics_request.py b/regtests/client/python/polaris/catalog/models/report_metrics_request.py index a9e4926c5f..481613e48d 100644 --- a/regtests/client/python/polaris/catalog/models/report_metrics_request.py +++ b/regtests/client/python/polaris/catalog/models/report_metrics_request.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/scan_report.py b/regtests/client/python/polaris/catalog/models/scan_report.py index 6ab453199b..41c3b96f5d 100644 --- a/regtests/client/python/polaris/catalog/models/scan_report.py +++ b/regtests/client/python/polaris/catalog/models/scan_report.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/set_current_schema_update.py b/regtests/client/python/polaris/catalog/models/set_current_schema_update.py index 39891ca779..5aeb3ddc88 100644 --- a/regtests/client/python/polaris/catalog/models/set_current_schema_update.py +++ b/regtests/client/python/polaris/catalog/models/set_current_schema_update.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/set_current_view_version_update.py b/regtests/client/python/polaris/catalog/models/set_current_view_version_update.py index e4623dff7e..7767f13bc6 100644 --- a/regtests/client/python/polaris/catalog/models/set_current_view_version_update.py +++ b/regtests/client/python/polaris/catalog/models/set_current_view_version_update.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/set_default_sort_order_update.py b/regtests/client/python/polaris/catalog/models/set_default_sort_order_update.py index 40f552cc98..25edad166d 100644 --- a/regtests/client/python/polaris/catalog/models/set_default_sort_order_update.py +++ b/regtests/client/python/polaris/catalog/models/set_default_sort_order_update.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/set_default_spec_update.py b/regtests/client/python/polaris/catalog/models/set_default_spec_update.py index da150c9a5b..e4daeca6de 100644 --- a/regtests/client/python/polaris/catalog/models/set_default_spec_update.py +++ b/regtests/client/python/polaris/catalog/models/set_default_spec_update.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/set_expression.py b/regtests/client/python/polaris/catalog/models/set_expression.py index d2aa53a611..ee3718c16a 100644 --- a/regtests/client/python/polaris/catalog/models/set_expression.py +++ b/regtests/client/python/polaris/catalog/models/set_expression.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/set_location_update.py b/regtests/client/python/polaris/catalog/models/set_location_update.py index d7e9c19708..cefc437bec 100644 --- a/regtests/client/python/polaris/catalog/models/set_location_update.py +++ b/regtests/client/python/polaris/catalog/models/set_location_update.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/set_partition_statistics_update.py b/regtests/client/python/polaris/catalog/models/set_partition_statistics_update.py index 9c872dfc53..37073914a4 100644 --- a/regtests/client/python/polaris/catalog/models/set_partition_statistics_update.py +++ b/regtests/client/python/polaris/catalog/models/set_partition_statistics_update.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/set_properties_update.py b/regtests/client/python/polaris/catalog/models/set_properties_update.py index 1f04486e1a..abebb40cf7 100644 --- a/regtests/client/python/polaris/catalog/models/set_properties_update.py +++ b/regtests/client/python/polaris/catalog/models/set_properties_update.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/set_snapshot_ref_update.py b/regtests/client/python/polaris/catalog/models/set_snapshot_ref_update.py index fafee48029..5957ace507 100644 --- a/regtests/client/python/polaris/catalog/models/set_snapshot_ref_update.py +++ b/regtests/client/python/polaris/catalog/models/set_snapshot_ref_update.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/set_statistics_update.py b/regtests/client/python/polaris/catalog/models/set_statistics_update.py index 5ce65734d9..55ad85a2a4 100644 --- a/regtests/client/python/polaris/catalog/models/set_statistics_update.py +++ b/regtests/client/python/polaris/catalog/models/set_statistics_update.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/snapshot.py b/regtests/client/python/polaris/catalog/models/snapshot.py index 888ab24518..406b7e7cd7 100644 --- a/regtests/client/python/polaris/catalog/models/snapshot.py +++ b/regtests/client/python/polaris/catalog/models/snapshot.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/snapshot_log_inner.py b/regtests/client/python/polaris/catalog/models/snapshot_log_inner.py index f61efbfcb4..0ac7547901 100644 --- a/regtests/client/python/polaris/catalog/models/snapshot_log_inner.py +++ b/regtests/client/python/polaris/catalog/models/snapshot_log_inner.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/snapshot_reference.py b/regtests/client/python/polaris/catalog/models/snapshot_reference.py index b7e81a7b12..7a3ceb9806 100644 --- a/regtests/client/python/polaris/catalog/models/snapshot_reference.py +++ b/regtests/client/python/polaris/catalog/models/snapshot_reference.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/snapshot_summary.py b/regtests/client/python/polaris/catalog/models/snapshot_summary.py index dcdc05f6bf..c26e86609d 100644 --- a/regtests/client/python/polaris/catalog/models/snapshot_summary.py +++ b/regtests/client/python/polaris/catalog/models/snapshot_summary.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/sort_direction.py b/regtests/client/python/polaris/catalog/models/sort_direction.py index 799cdc96d3..f1306e9227 100644 --- a/regtests/client/python/polaris/catalog/models/sort_direction.py +++ b/regtests/client/python/polaris/catalog/models/sort_direction.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/sort_field.py b/regtests/client/python/polaris/catalog/models/sort_field.py index 0fb3b98765..d162b22167 100644 --- a/regtests/client/python/polaris/catalog/models/sort_field.py +++ b/regtests/client/python/polaris/catalog/models/sort_field.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/sort_order.py b/regtests/client/python/polaris/catalog/models/sort_order.py index 07a68ce734..8612e694ac 100644 --- a/regtests/client/python/polaris/catalog/models/sort_order.py +++ b/regtests/client/python/polaris/catalog/models/sort_order.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/sql_view_representation.py b/regtests/client/python/polaris/catalog/models/sql_view_representation.py index 0ad32c7f37..fd8422b1be 100644 --- a/regtests/client/python/polaris/catalog/models/sql_view_representation.py +++ b/regtests/client/python/polaris/catalog/models/sql_view_representation.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/statistics_file.py b/regtests/client/python/polaris/catalog/models/statistics_file.py index cd07fb7ceb..ba500a90e0 100644 --- a/regtests/client/python/polaris/catalog/models/statistics_file.py +++ b/regtests/client/python/polaris/catalog/models/statistics_file.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/struct_field.py b/regtests/client/python/polaris/catalog/models/struct_field.py index 65ca86605c..411962b82b 100644 --- a/regtests/client/python/polaris/catalog/models/struct_field.py +++ b/regtests/client/python/polaris/catalog/models/struct_field.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/struct_type.py b/regtests/client/python/polaris/catalog/models/struct_type.py index c65b50c9a0..5107cce271 100644 --- a/regtests/client/python/polaris/catalog/models/struct_type.py +++ b/regtests/client/python/polaris/catalog/models/struct_type.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/table_identifier.py b/regtests/client/python/polaris/catalog/models/table_identifier.py index d8df0b64e2..6c25d2e92d 100644 --- a/regtests/client/python/polaris/catalog/models/table_identifier.py +++ b/regtests/client/python/polaris/catalog/models/table_identifier.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/table_metadata.py b/regtests/client/python/polaris/catalog/models/table_metadata.py index ba92e3574d..c55a0b96f4 100644 --- a/regtests/client/python/polaris/catalog/models/table_metadata.py +++ b/regtests/client/python/polaris/catalog/models/table_metadata.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/table_requirement.py b/regtests/client/python/polaris/catalog/models/table_requirement.py index 994fa69aaa..087aa5d9dc 100644 --- a/regtests/client/python/polaris/catalog/models/table_requirement.py +++ b/regtests/client/python/polaris/catalog/models/table_requirement.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/table_update.py b/regtests/client/python/polaris/catalog/models/table_update.py index b3d758fef6..9ca03eafdc 100644 --- a/regtests/client/python/polaris/catalog/models/table_update.py +++ b/regtests/client/python/polaris/catalog/models/table_update.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/table_update_notification.py b/regtests/client/python/polaris/catalog/models/table_update_notification.py index dbd7687485..61053b0bd6 100644 --- a/regtests/client/python/polaris/catalog/models/table_update_notification.py +++ b/regtests/client/python/polaris/catalog/models/table_update_notification.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/term.py b/regtests/client/python/polaris/catalog/models/term.py index 52e23ca9e7..68135ef6f5 100644 --- a/regtests/client/python/polaris/catalog/models/term.py +++ b/regtests/client/python/polaris/catalog/models/term.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/timer_result.py b/regtests/client/python/polaris/catalog/models/timer_result.py index 1de1deb866..39f82396c9 100644 --- a/regtests/client/python/polaris/catalog/models/timer_result.py +++ b/regtests/client/python/polaris/catalog/models/timer_result.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/token_type.py b/regtests/client/python/polaris/catalog/models/token_type.py index 68f5ef272f..734ea6c677 100644 --- a/regtests/client/python/polaris/catalog/models/token_type.py +++ b/regtests/client/python/polaris/catalog/models/token_type.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/transform_term.py b/regtests/client/python/polaris/catalog/models/transform_term.py index b5b70e8922..591538becd 100644 --- a/regtests/client/python/polaris/catalog/models/transform_term.py +++ b/regtests/client/python/polaris/catalog/models/transform_term.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/type.py b/regtests/client/python/polaris/catalog/models/type.py index 06d416ce54..8e0a716a6c 100644 --- a/regtests/client/python/polaris/catalog/models/type.py +++ b/regtests/client/python/polaris/catalog/models/type.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/unary_expression.py b/regtests/client/python/polaris/catalog/models/unary_expression.py index f91f5ae41d..c400a8f753 100644 --- a/regtests/client/python/polaris/catalog/models/unary_expression.py +++ b/regtests/client/python/polaris/catalog/models/unary_expression.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/update_namespace_properties_request.py b/regtests/client/python/polaris/catalog/models/update_namespace_properties_request.py index 79d6164ebb..27ccfe7f30 100644 --- a/regtests/client/python/polaris/catalog/models/update_namespace_properties_request.py +++ b/regtests/client/python/polaris/catalog/models/update_namespace_properties_request.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/update_namespace_properties_response.py b/regtests/client/python/polaris/catalog/models/update_namespace_properties_response.py index 5758b03ddd..2c17214b9c 100644 --- a/regtests/client/python/polaris/catalog/models/update_namespace_properties_response.py +++ b/regtests/client/python/polaris/catalog/models/update_namespace_properties_response.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/upgrade_format_version_update.py b/regtests/client/python/polaris/catalog/models/upgrade_format_version_update.py index 03f4607c94..05e6789b3f 100644 --- a/regtests/client/python/polaris/catalog/models/upgrade_format_version_update.py +++ b/regtests/client/python/polaris/catalog/models/upgrade_format_version_update.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/value_map.py b/regtests/client/python/polaris/catalog/models/value_map.py index e3c478e86a..4e27b8face 100644 --- a/regtests/client/python/polaris/catalog/models/value_map.py +++ b/regtests/client/python/polaris/catalog/models/value_map.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/view_history_entry.py b/regtests/client/python/polaris/catalog/models/view_history_entry.py index f8b61abefe..432e9dc5bd 100644 --- a/regtests/client/python/polaris/catalog/models/view_history_entry.py +++ b/regtests/client/python/polaris/catalog/models/view_history_entry.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/view_metadata.py b/regtests/client/python/polaris/catalog/models/view_metadata.py index 01e95c9874..4ac6cd2d3f 100644 --- a/regtests/client/python/polaris/catalog/models/view_metadata.py +++ b/regtests/client/python/polaris/catalog/models/view_metadata.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/view_representation.py b/regtests/client/python/polaris/catalog/models/view_representation.py index 4dd8bac363..68ed5ad528 100644 --- a/regtests/client/python/polaris/catalog/models/view_representation.py +++ b/regtests/client/python/polaris/catalog/models/view_representation.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/view_requirement.py b/regtests/client/python/polaris/catalog/models/view_requirement.py index 6b3ac6ec62..69645ffb09 100644 --- a/regtests/client/python/polaris/catalog/models/view_requirement.py +++ b/regtests/client/python/polaris/catalog/models/view_requirement.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/view_update.py b/regtests/client/python/polaris/catalog/models/view_update.py index 0924487823..74fb2a834a 100644 --- a/regtests/client/python/polaris/catalog/models/view_update.py +++ b/regtests/client/python/polaris/catalog/models/view_update.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/models/view_version.py b/regtests/client/python/polaris/catalog/models/view_version.py index 5f61d23e1e..4e5fa47735 100644 --- a/regtests/client/python/polaris/catalog/models/view_version.py +++ b/regtests/client/python/polaris/catalog/models/view_version.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/catalog/rest.py b/regtests/client/python/polaris/catalog/rest.py index 31d6a92dd7..7d1b969c2c 100644 --- a/regtests/client/python/polaris/catalog/rest.py +++ b/regtests/client/python/polaris/catalog/rest.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/management/__init__.py b/regtests/client/python/polaris/management/__init__.py index 400517acd7..aca9584611 100644 --- a/regtests/client/python/polaris/management/__init__.py +++ b/regtests/client/python/polaris/management/__init__.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 # flake8: noqa diff --git a/regtests/client/python/polaris/management/api/__init__.py b/regtests/client/python/polaris/management/api/__init__.py index 53bae63ba1..9856835dad 100644 --- a/regtests/client/python/polaris/management/api/__init__.py +++ b/regtests/client/python/polaris/management/api/__init__.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # flake8: noqa # import apis into api package diff --git a/regtests/client/python/polaris/management/api/polaris_default_api.py b/regtests/client/python/polaris/management/api/polaris_default_api.py index fdf69e3ff9..f8b6d70fdc 100644 --- a/regtests/client/python/polaris/management/api/polaris_default_api.py +++ b/regtests/client/python/polaris/management/api/polaris_default_api.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/management/api_client.py b/regtests/client/python/polaris/management/api_client.py index d558f9c8dc..15e99856ed 100644 --- a/regtests/client/python/polaris/management/api_client.py +++ b/regtests/client/python/polaris/management/api_client.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/management/api_response.py b/regtests/client/python/polaris/management/api_response.py index 9bc7c11f6b..e3a3bc42e0 100644 --- a/regtests/client/python/polaris/management/api_response.py +++ b/regtests/client/python/polaris/management/api_response.py @@ -1,3 +1,19 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# + """API response object.""" from __future__ import annotations diff --git a/regtests/client/python/polaris/management/configuration.py b/regtests/client/python/polaris/management/configuration.py index a2c20d54ce..08c49869bf 100644 --- a/regtests/client/python/polaris/management/configuration.py +++ b/regtests/client/python/polaris/management/configuration.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/management/exceptions.py b/regtests/client/python/polaris/management/exceptions.py index 6be8f29051..fb71e432c1 100644 --- a/regtests/client/python/polaris/management/exceptions.py +++ b/regtests/client/python/polaris/management/exceptions.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/management/models/__init__.py b/regtests/client/python/polaris/management/models/__init__.py index 5b5c133836..6c61f415df 100644 --- a/regtests/client/python/polaris/management/models/__init__.py +++ b/regtests/client/python/polaris/management/models/__init__.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 # flake8: noqa diff --git a/regtests/client/python/polaris/management/models/add_grant_request.py b/regtests/client/python/polaris/management/models/add_grant_request.py index b09e215ac3..b83f88754b 100644 --- a/regtests/client/python/polaris/management/models/add_grant_request.py +++ b/regtests/client/python/polaris/management/models/add_grant_request.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/management/models/aws_storage_config_info.py b/regtests/client/python/polaris/management/models/aws_storage_config_info.py index bec41d775e..b7441165da 100644 --- a/regtests/client/python/polaris/management/models/aws_storage_config_info.py +++ b/regtests/client/python/polaris/management/models/aws_storage_config_info.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/management/models/azure_storage_config_info.py b/regtests/client/python/polaris/management/models/azure_storage_config_info.py index 9f79f0c7a2..f03320e444 100644 --- a/regtests/client/python/polaris/management/models/azure_storage_config_info.py +++ b/regtests/client/python/polaris/management/models/azure_storage_config_info.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/management/models/catalog.py b/regtests/client/python/polaris/management/models/catalog.py index 3d71762758..66fa7242b4 100644 --- a/regtests/client/python/polaris/management/models/catalog.py +++ b/regtests/client/python/polaris/management/models/catalog.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/management/models/catalog_grant.py b/regtests/client/python/polaris/management/models/catalog_grant.py index e6bbd9e75e..628b32b9e7 100644 --- a/regtests/client/python/polaris/management/models/catalog_grant.py +++ b/regtests/client/python/polaris/management/models/catalog_grant.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/management/models/catalog_privilege.py b/regtests/client/python/polaris/management/models/catalog_privilege.py index df29cb62e8..03404a10bc 100644 --- a/regtests/client/python/polaris/management/models/catalog_privilege.py +++ b/regtests/client/python/polaris/management/models/catalog_privilege.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/management/models/catalog_properties.py b/regtests/client/python/polaris/management/models/catalog_properties.py index c6f0b16a89..8669c57743 100644 --- a/regtests/client/python/polaris/management/models/catalog_properties.py +++ b/regtests/client/python/polaris/management/models/catalog_properties.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/management/models/catalog_role.py b/regtests/client/python/polaris/management/models/catalog_role.py index 3dd7d9a649..1823dfffa6 100644 --- a/regtests/client/python/polaris/management/models/catalog_role.py +++ b/regtests/client/python/polaris/management/models/catalog_role.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/management/models/catalog_roles.py b/regtests/client/python/polaris/management/models/catalog_roles.py index f944b332c4..5072c191a3 100644 --- a/regtests/client/python/polaris/management/models/catalog_roles.py +++ b/regtests/client/python/polaris/management/models/catalog_roles.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/management/models/catalogs.py b/regtests/client/python/polaris/management/models/catalogs.py index f47f029aae..66698aec16 100644 --- a/regtests/client/python/polaris/management/models/catalogs.py +++ b/regtests/client/python/polaris/management/models/catalogs.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/management/models/create_catalog_request.py b/regtests/client/python/polaris/management/models/create_catalog_request.py index d0ecbe00e8..4136ac7522 100644 --- a/regtests/client/python/polaris/management/models/create_catalog_request.py +++ b/regtests/client/python/polaris/management/models/create_catalog_request.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/management/models/create_catalog_role_request.py b/regtests/client/python/polaris/management/models/create_catalog_role_request.py index 0c9be75d6b..189cef6e93 100644 --- a/regtests/client/python/polaris/management/models/create_catalog_role_request.py +++ b/regtests/client/python/polaris/management/models/create_catalog_role_request.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/management/models/create_principal_request.py b/regtests/client/python/polaris/management/models/create_principal_request.py index f7091fb995..89031d58e5 100644 --- a/regtests/client/python/polaris/management/models/create_principal_request.py +++ b/regtests/client/python/polaris/management/models/create_principal_request.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/management/models/create_principal_role_request.py b/regtests/client/python/polaris/management/models/create_principal_role_request.py index 0aea403c47..4678dfe04f 100644 --- a/regtests/client/python/polaris/management/models/create_principal_role_request.py +++ b/regtests/client/python/polaris/management/models/create_principal_role_request.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/management/models/external_catalog.py b/regtests/client/python/polaris/management/models/external_catalog.py index 768451cdc6..bbcfa05478 100644 --- a/regtests/client/python/polaris/management/models/external_catalog.py +++ b/regtests/client/python/polaris/management/models/external_catalog.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/management/models/file_storage_config_info.py b/regtests/client/python/polaris/management/models/file_storage_config_info.py index 87d6fbbbb7..7179236bc7 100644 --- a/regtests/client/python/polaris/management/models/file_storage_config_info.py +++ b/regtests/client/python/polaris/management/models/file_storage_config_info.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/management/models/gcp_storage_config_info.py b/regtests/client/python/polaris/management/models/gcp_storage_config_info.py index cc5401f354..7b9e06ec82 100644 --- a/regtests/client/python/polaris/management/models/gcp_storage_config_info.py +++ b/regtests/client/python/polaris/management/models/gcp_storage_config_info.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/management/models/grant_catalog_role_request.py b/regtests/client/python/polaris/management/models/grant_catalog_role_request.py index ccd47ab25c..acef99731e 100644 --- a/regtests/client/python/polaris/management/models/grant_catalog_role_request.py +++ b/regtests/client/python/polaris/management/models/grant_catalog_role_request.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/management/models/grant_principal_role_request.py b/regtests/client/python/polaris/management/models/grant_principal_role_request.py index d0d4cf5ae8..7033179ef0 100644 --- a/regtests/client/python/polaris/management/models/grant_principal_role_request.py +++ b/regtests/client/python/polaris/management/models/grant_principal_role_request.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/management/models/grant_resource.py b/regtests/client/python/polaris/management/models/grant_resource.py index f603d30a46..666fffd06f 100644 --- a/regtests/client/python/polaris/management/models/grant_resource.py +++ b/regtests/client/python/polaris/management/models/grant_resource.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/management/models/grant_resources.py b/regtests/client/python/polaris/management/models/grant_resources.py index 03803e417b..23d9a99442 100644 --- a/regtests/client/python/polaris/management/models/grant_resources.py +++ b/regtests/client/python/polaris/management/models/grant_resources.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/management/models/namespace_grant.py b/regtests/client/python/polaris/management/models/namespace_grant.py index c30e473cc1..44238a7822 100644 --- a/regtests/client/python/polaris/management/models/namespace_grant.py +++ b/regtests/client/python/polaris/management/models/namespace_grant.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/management/models/namespace_privilege.py b/regtests/client/python/polaris/management/models/namespace_privilege.py index 1866363099..79b785449a 100644 --- a/regtests/client/python/polaris/management/models/namespace_privilege.py +++ b/regtests/client/python/polaris/management/models/namespace_privilege.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/management/models/polaris_catalog.py b/regtests/client/python/polaris/management/models/polaris_catalog.py index d8b8672dfc..8e44d2a7da 100644 --- a/regtests/client/python/polaris/management/models/polaris_catalog.py +++ b/regtests/client/python/polaris/management/models/polaris_catalog.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/management/models/principal.py b/regtests/client/python/polaris/management/models/principal.py index d047520364..e7af4e7605 100644 --- a/regtests/client/python/polaris/management/models/principal.py +++ b/regtests/client/python/polaris/management/models/principal.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/management/models/principal_role.py b/regtests/client/python/polaris/management/models/principal_role.py index b6367a6339..4e23d86ecb 100644 --- a/regtests/client/python/polaris/management/models/principal_role.py +++ b/regtests/client/python/polaris/management/models/principal_role.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/management/models/principal_roles.py b/regtests/client/python/polaris/management/models/principal_roles.py index ad9048be9c..92e52074a0 100644 --- a/regtests/client/python/polaris/management/models/principal_roles.py +++ b/regtests/client/python/polaris/management/models/principal_roles.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/management/models/principal_with_credentials.py b/regtests/client/python/polaris/management/models/principal_with_credentials.py index 9cc87b7880..69a2e2b4bf 100644 --- a/regtests/client/python/polaris/management/models/principal_with_credentials.py +++ b/regtests/client/python/polaris/management/models/principal_with_credentials.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/management/models/principal_with_credentials_credentials.py b/regtests/client/python/polaris/management/models/principal_with_credentials_credentials.py index e95d33d6a3..be6d57d1b2 100644 --- a/regtests/client/python/polaris/management/models/principal_with_credentials_credentials.py +++ b/regtests/client/python/polaris/management/models/principal_with_credentials_credentials.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/management/models/principals.py b/regtests/client/python/polaris/management/models/principals.py index 5faff0916f..746608eb85 100644 --- a/regtests/client/python/polaris/management/models/principals.py +++ b/regtests/client/python/polaris/management/models/principals.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/management/models/revoke_grant_request.py b/regtests/client/python/polaris/management/models/revoke_grant_request.py index ff963e7118..681706990d 100644 --- a/regtests/client/python/polaris/management/models/revoke_grant_request.py +++ b/regtests/client/python/polaris/management/models/revoke_grant_request.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/management/models/storage_config_info.py b/regtests/client/python/polaris/management/models/storage_config_info.py index dc669b2261..7b36a47e4b 100644 --- a/regtests/client/python/polaris/management/models/storage_config_info.py +++ b/regtests/client/python/polaris/management/models/storage_config_info.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/management/models/table_grant.py b/regtests/client/python/polaris/management/models/table_grant.py index 175e4b6ec3..a2d9367d0a 100644 --- a/regtests/client/python/polaris/management/models/table_grant.py +++ b/regtests/client/python/polaris/management/models/table_grant.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/management/models/table_privilege.py b/regtests/client/python/polaris/management/models/table_privilege.py index 8aacfc8acb..4303296b6b 100644 --- a/regtests/client/python/polaris/management/models/table_privilege.py +++ b/regtests/client/python/polaris/management/models/table_privilege.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/management/models/update_catalog_request.py b/regtests/client/python/polaris/management/models/update_catalog_request.py index 8f12d8218c..d91fa23840 100644 --- a/regtests/client/python/polaris/management/models/update_catalog_request.py +++ b/regtests/client/python/polaris/management/models/update_catalog_request.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/management/models/update_catalog_role_request.py b/regtests/client/python/polaris/management/models/update_catalog_role_request.py index bbbd1762e2..d95e279383 100644 --- a/regtests/client/python/polaris/management/models/update_catalog_role_request.py +++ b/regtests/client/python/polaris/management/models/update_catalog_role_request.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/management/models/update_principal_request.py b/regtests/client/python/polaris/management/models/update_principal_request.py index e3000f7fba..60ac283d1e 100644 --- a/regtests/client/python/polaris/management/models/update_principal_request.py +++ b/regtests/client/python/polaris/management/models/update_principal_request.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/management/models/update_principal_role_request.py b/regtests/client/python/polaris/management/models/update_principal_role_request.py index 1b229ac150..1d0abd887c 100644 --- a/regtests/client/python/polaris/management/models/update_principal_role_request.py +++ b/regtests/client/python/polaris/management/models/update_principal_role_request.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/management/models/view_grant.py b/regtests/client/python/polaris/management/models/view_grant.py index 469458c59e..3089ece07e 100644 --- a/regtests/client/python/polaris/management/models/view_grant.py +++ b/regtests/client/python/polaris/management/models/view_grant.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/management/models/view_privilege.py b/regtests/client/python/polaris/management/models/view_privilege.py index 32390f9d2e..e2f268b204 100644 --- a/regtests/client/python/polaris/management/models/view_privilege.py +++ b/regtests/client/python/polaris/management/models/view_privilege.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/polaris/management/rest.py b/regtests/client/python/polaris/management/rest.py index 994871bb89..516566cd5e 100644 --- a/regtests/client/python/polaris/management/rest.py +++ b/regtests/client/python/polaris/management/rest.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/pyproject.toml b/regtests/client/python/pyproject.toml index 2783814afb..5263e89871 100644 --- a/regtests/client/python/pyproject.toml +++ b/regtests/client/python/pyproject.toml @@ -1,3 +1,19 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# + [tool.poetry] name = "polaris" version = "1.0.0" diff --git a/regtests/client/python/setup.py b/regtests/client/python/setup.py index 8fb1409ca2..48eab69197 100644 --- a/regtests/client/python/setup.py +++ b/regtests/client/python/setup.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/__init__.py b/regtests/client/python/test/__init__.py index e69de29bb2..8d220260f1 100644 --- a/regtests/client/python/test/__init__.py +++ b/regtests/client/python/test/__init__.py @@ -0,0 +1,15 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# \ No newline at end of file diff --git a/regtests/client/python/test/test_add_grant_request.py b/regtests/client/python/test/test_add_grant_request.py index 81ea2592ae..765fd48a39 100644 --- a/regtests/client/python/test/test_add_grant_request.py +++ b/regtests/client/python/test/test_add_grant_request.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_add_partition_spec_update.py b/regtests/client/python/test/test_add_partition_spec_update.py index 3450cccbb6..bd6ac6e9e1 100644 --- a/regtests/client/python/test/test_add_partition_spec_update.py +++ b/regtests/client/python/test/test_add_partition_spec_update.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_add_schema_update.py b/regtests/client/python/test/test_add_schema_update.py index f78d8e0654..9799c78d65 100644 --- a/regtests/client/python/test/test_add_schema_update.py +++ b/regtests/client/python/test/test_add_schema_update.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_add_snapshot_update.py b/regtests/client/python/test/test_add_snapshot_update.py index d721250f28..4640737823 100644 --- a/regtests/client/python/test/test_add_snapshot_update.py +++ b/regtests/client/python/test/test_add_snapshot_update.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_add_sort_order_update.py b/regtests/client/python/test/test_add_sort_order_update.py index 4a107cdf2d..ca436a697a 100644 --- a/regtests/client/python/test/test_add_sort_order_update.py +++ b/regtests/client/python/test/test_add_sort_order_update.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_add_view_version_update.py b/regtests/client/python/test/test_add_view_version_update.py index 49d5af5fe0..be145b9e94 100644 --- a/regtests/client/python/test/test_add_view_version_update.py +++ b/regtests/client/python/test/test_add_view_version_update.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_and_or_expression.py b/regtests/client/python/test/test_and_or_expression.py index 6a9f2aa339..c396e62514 100644 --- a/regtests/client/python/test/test_and_or_expression.py +++ b/regtests/client/python/test/test_and_or_expression.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_assert_create.py b/regtests/client/python/test/test_assert_create.py index 60d5ebc777..7128a928da 100644 --- a/regtests/client/python/test/test_assert_create.py +++ b/regtests/client/python/test/test_assert_create.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_assert_current_schema_id.py b/regtests/client/python/test/test_assert_current_schema_id.py index 776bbe8e00..1dd77a0f15 100644 --- a/regtests/client/python/test/test_assert_current_schema_id.py +++ b/regtests/client/python/test/test_assert_current_schema_id.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_assert_default_sort_order_id.py b/regtests/client/python/test/test_assert_default_sort_order_id.py index a7ab462f1e..0d52104659 100644 --- a/regtests/client/python/test/test_assert_default_sort_order_id.py +++ b/regtests/client/python/test/test_assert_default_sort_order_id.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_assert_default_spec_id.py b/regtests/client/python/test/test_assert_default_spec_id.py index b3b042150a..8fceeb5643 100644 --- a/regtests/client/python/test/test_assert_default_spec_id.py +++ b/regtests/client/python/test/test_assert_default_spec_id.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_assert_last_assigned_field_id.py b/regtests/client/python/test/test_assert_last_assigned_field_id.py index b1a7be799d..35d12109ba 100644 --- a/regtests/client/python/test/test_assert_last_assigned_field_id.py +++ b/regtests/client/python/test/test_assert_last_assigned_field_id.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_assert_last_assigned_partition_id.py b/regtests/client/python/test/test_assert_last_assigned_partition_id.py index 80c5ffdb84..e9cb23a9d5 100644 --- a/regtests/client/python/test/test_assert_last_assigned_partition_id.py +++ b/regtests/client/python/test/test_assert_last_assigned_partition_id.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_assert_ref_snapshot_id.py b/regtests/client/python/test/test_assert_ref_snapshot_id.py index 86e4e881fc..98b1889230 100644 --- a/regtests/client/python/test/test_assert_ref_snapshot_id.py +++ b/regtests/client/python/test/test_assert_ref_snapshot_id.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_assert_table_uuid.py b/regtests/client/python/test/test_assert_table_uuid.py index 58cff6f684..685a71f28b 100644 --- a/regtests/client/python/test/test_assert_table_uuid.py +++ b/regtests/client/python/test/test_assert_table_uuid.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_assert_view_uuid.py b/regtests/client/python/test/test_assert_view_uuid.py index 43a85d1512..b746a89918 100644 --- a/regtests/client/python/test/test_assert_view_uuid.py +++ b/regtests/client/python/test/test_assert_view_uuid.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_assign_uuid_update.py b/regtests/client/python/test/test_assign_uuid_update.py index cea2b51cee..2f80f9b997 100644 --- a/regtests/client/python/test/test_assign_uuid_update.py +++ b/regtests/client/python/test/test_assign_uuid_update.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_aws_storage_config_info.py b/regtests/client/python/test/test_aws_storage_config_info.py index c2dbb884fb..683c9dd512 100644 --- a/regtests/client/python/test/test_aws_storage_config_info.py +++ b/regtests/client/python/test/test_aws_storage_config_info.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_azure_storage_config_info.py b/regtests/client/python/test/test_azure_storage_config_info.py index bca4296847..bffd6f3208 100644 --- a/regtests/client/python/test/test_azure_storage_config_info.py +++ b/regtests/client/python/test/test_azure_storage_config_info.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_base_update.py b/regtests/client/python/test/test_base_update.py index a4c263be3a..2d2fc86b05 100644 --- a/regtests/client/python/test/test_base_update.py +++ b/regtests/client/python/test/test_base_update.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_blob_metadata.py b/regtests/client/python/test/test_blob_metadata.py index ad2108f421..ef03ab7c67 100644 --- a/regtests/client/python/test/test_blob_metadata.py +++ b/regtests/client/python/test/test_blob_metadata.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_catalog.py b/regtests/client/python/test/test_catalog.py index 4621a8645a..0b021e96e4 100644 --- a/regtests/client/python/test/test_catalog.py +++ b/regtests/client/python/test/test_catalog.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_catalog_config.py b/regtests/client/python/test/test_catalog_config.py index 1f32966dc8..9cd70b2905 100644 --- a/regtests/client/python/test/test_catalog_config.py +++ b/regtests/client/python/test/test_catalog_config.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_catalog_grant.py b/regtests/client/python/test/test_catalog_grant.py index 3dd5ced200..a0ab0e341c 100644 --- a/regtests/client/python/test/test_catalog_grant.py +++ b/regtests/client/python/test/test_catalog_grant.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_catalog_privilege.py b/regtests/client/python/test/test_catalog_privilege.py index 4aec06aed8..5ba2bfe556 100644 --- a/regtests/client/python/test/test_catalog_privilege.py +++ b/regtests/client/python/test/test_catalog_privilege.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_catalog_properties.py b/regtests/client/python/test/test_catalog_properties.py index 75d7a03a97..e9730b172d 100644 --- a/regtests/client/python/test/test_catalog_properties.py +++ b/regtests/client/python/test/test_catalog_properties.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_catalog_role.py b/regtests/client/python/test/test_catalog_role.py index f55011539c..f74d138a67 100644 --- a/regtests/client/python/test/test_catalog_role.py +++ b/regtests/client/python/test/test_catalog_role.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_catalog_roles.py b/regtests/client/python/test/test_catalog_roles.py index dd348af3d7..bcc1847053 100644 --- a/regtests/client/python/test/test_catalog_roles.py +++ b/regtests/client/python/test/test_catalog_roles.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_catalogs.py b/regtests/client/python/test/test_catalogs.py index cd5fc8e41c..27aef6a689 100644 --- a/regtests/client/python/test/test_catalogs.py +++ b/regtests/client/python/test/test_catalogs.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_cli_parsing.py b/regtests/client/python/test/test_cli_parsing.py index b52d30feed..073c8ecd5d 100644 --- a/regtests/client/python/test/test_cli_parsing.py +++ b/regtests/client/python/test/test_cli_parsing.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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 unittest import io diff --git a/regtests/client/python/test/test_commit_report.py b/regtests/client/python/test/test_commit_report.py index 122b669b7a..bf049e5afe 100644 --- a/regtests/client/python/test/test_commit_report.py +++ b/regtests/client/python/test/test_commit_report.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_commit_table_request.py b/regtests/client/python/test/test_commit_table_request.py index 156d93c01f..c94e0f3e19 100644 --- a/regtests/client/python/test/test_commit_table_request.py +++ b/regtests/client/python/test/test_commit_table_request.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_commit_table_response.py b/regtests/client/python/test/test_commit_table_response.py index 4f679a31ec..9fde1a54c8 100644 --- a/regtests/client/python/test/test_commit_table_response.py +++ b/regtests/client/python/test/test_commit_table_response.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_commit_transaction_request.py b/regtests/client/python/test/test_commit_transaction_request.py index ced46110b5..b20c19dc57 100644 --- a/regtests/client/python/test/test_commit_transaction_request.py +++ b/regtests/client/python/test/test_commit_transaction_request.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_commit_view_request.py b/regtests/client/python/test/test_commit_view_request.py index 2600455358..346c9363c0 100644 --- a/regtests/client/python/test/test_commit_view_request.py +++ b/regtests/client/python/test/test_commit_view_request.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_content_file.py b/regtests/client/python/test/test_content_file.py index 37b59a878c..72763c7866 100644 --- a/regtests/client/python/test/test_content_file.py +++ b/regtests/client/python/test/test_content_file.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_count_map.py b/regtests/client/python/test/test_count_map.py index c203cc0351..db330f129b 100644 --- a/regtests/client/python/test/test_count_map.py +++ b/regtests/client/python/test/test_count_map.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_counter_result.py b/regtests/client/python/test/test_counter_result.py index 46a487cc0c..1fa21fed14 100644 --- a/regtests/client/python/test/test_counter_result.py +++ b/regtests/client/python/test/test_counter_result.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_create_catalog_request.py b/regtests/client/python/test/test_create_catalog_request.py index efdfd1c413..884711c98a 100644 --- a/regtests/client/python/test/test_create_catalog_request.py +++ b/regtests/client/python/test/test_create_catalog_request.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_create_catalog_role_request.py b/regtests/client/python/test/test_create_catalog_role_request.py index 2465059c6c..be4dc47522 100644 --- a/regtests/client/python/test/test_create_catalog_role_request.py +++ b/regtests/client/python/test/test_create_catalog_role_request.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_create_namespace_request.py b/regtests/client/python/test/test_create_namespace_request.py index cc2259a04c..1d7e3db006 100644 --- a/regtests/client/python/test/test_create_namespace_request.py +++ b/regtests/client/python/test/test_create_namespace_request.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_create_namespace_response.py b/regtests/client/python/test/test_create_namespace_response.py index ed31dbaab9..462da06423 100644 --- a/regtests/client/python/test/test_create_namespace_response.py +++ b/regtests/client/python/test/test_create_namespace_response.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_create_principal_request.py b/regtests/client/python/test/test_create_principal_request.py index b0cb65d033..6f06415452 100644 --- a/regtests/client/python/test/test_create_principal_request.py +++ b/regtests/client/python/test/test_create_principal_request.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_create_principal_role_request.py b/regtests/client/python/test/test_create_principal_role_request.py index 43a7f4987f..4f07d8b07c 100644 --- a/regtests/client/python/test/test_create_principal_role_request.py +++ b/regtests/client/python/test/test_create_principal_role_request.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_create_table_request.py b/regtests/client/python/test/test_create_table_request.py index a1bc40c406..c7b2d5910d 100644 --- a/regtests/client/python/test/test_create_table_request.py +++ b/regtests/client/python/test/test_create_table_request.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_create_view_request.py b/regtests/client/python/test/test_create_view_request.py index ddfe2c178b..aefd3d03ac 100644 --- a/regtests/client/python/test/test_create_view_request.py +++ b/regtests/client/python/test/test_create_view_request.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_data_file.py b/regtests/client/python/test/test_data_file.py index d972ed9f66..3e6d633b89 100644 --- a/regtests/client/python/test/test_data_file.py +++ b/regtests/client/python/test/test_data_file.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_equality_delete_file.py b/regtests/client/python/test/test_equality_delete_file.py index 71417c944f..1f89bb9d40 100644 --- a/regtests/client/python/test/test_equality_delete_file.py +++ b/regtests/client/python/test/test_equality_delete_file.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_error_model.py b/regtests/client/python/test/test_error_model.py index b5a20cf44e..0213e8dd73 100644 --- a/regtests/client/python/test/test_error_model.py +++ b/regtests/client/python/test/test_error_model.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_expression.py b/regtests/client/python/test/test_expression.py index 9a2cde5d5f..a7a39b32cb 100644 --- a/regtests/client/python/test/test_expression.py +++ b/regtests/client/python/test/test_expression.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_external_catalog.py b/regtests/client/python/test/test_external_catalog.py index 37007bf268..be381c7b6a 100644 --- a/regtests/client/python/test/test_external_catalog.py +++ b/regtests/client/python/test/test_external_catalog.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_file_format.py b/regtests/client/python/test/test_file_format.py index 42f52233af..1f3f906778 100644 --- a/regtests/client/python/test/test_file_format.py +++ b/regtests/client/python/test/test_file_format.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_file_storage_config_info.py b/regtests/client/python/test/test_file_storage_config_info.py index 599c095532..a113b5f3df 100644 --- a/regtests/client/python/test/test_file_storage_config_info.py +++ b/regtests/client/python/test/test_file_storage_config_info.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_gcp_storage_config_info.py b/regtests/client/python/test/test_gcp_storage_config_info.py index 426574508a..ec3510c788 100644 --- a/regtests/client/python/test/test_gcp_storage_config_info.py +++ b/regtests/client/python/test/test_gcp_storage_config_info.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_get_namespace_response.py b/regtests/client/python/test/test_get_namespace_response.py index 6151839db7..4a8ca64f86 100644 --- a/regtests/client/python/test/test_get_namespace_response.py +++ b/regtests/client/python/test/test_get_namespace_response.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_grant_catalog_role_request.py b/regtests/client/python/test/test_grant_catalog_role_request.py index b953b70973..d345cba824 100644 --- a/regtests/client/python/test/test_grant_catalog_role_request.py +++ b/regtests/client/python/test/test_grant_catalog_role_request.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_grant_principal_role_request.py b/regtests/client/python/test/test_grant_principal_role_request.py index 74555e8202..1340bf3ddc 100644 --- a/regtests/client/python/test/test_grant_principal_role_request.py +++ b/regtests/client/python/test/test_grant_principal_role_request.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_grant_resource.py b/regtests/client/python/test/test_grant_resource.py index 8b0852af56..c779c99f02 100644 --- a/regtests/client/python/test/test_grant_resource.py +++ b/regtests/client/python/test/test_grant_resource.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_grant_resources.py b/regtests/client/python/test/test_grant_resources.py index f621c6c4bc..9b81e27e98 100644 --- a/regtests/client/python/test/test_grant_resources.py +++ b/regtests/client/python/test/test_grant_resources.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_iceberg_catalog_api.py b/regtests/client/python/test/test_iceberg_catalog_api.py index a6bafd37fc..4e3238644c 100644 --- a/regtests/client/python/test/test_iceberg_catalog_api.py +++ b/regtests/client/python/test/test_iceberg_catalog_api.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_iceberg_configuration_api.py b/regtests/client/python/test/test_iceberg_configuration_api.py index db36b459e2..4ef1757b66 100644 --- a/regtests/client/python/test/test_iceberg_configuration_api.py +++ b/regtests/client/python/test/test_iceberg_configuration_api.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_iceberg_error_response.py b/regtests/client/python/test/test_iceberg_error_response.py index 24375dd5a5..a2c0f54b38 100644 --- a/regtests/client/python/test/test_iceberg_error_response.py +++ b/regtests/client/python/test/test_iceberg_error_response.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_iceberg_o_auth2_api.py b/regtests/client/python/test/test_iceberg_o_auth2_api.py index b3c6903475..d90869611e 100644 --- a/regtests/client/python/test/test_iceberg_o_auth2_api.py +++ b/regtests/client/python/test/test_iceberg_o_auth2_api.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_list_namespaces_response.py b/regtests/client/python/test/test_list_namespaces_response.py index 9ac72ef0e5..79ce0a09f5 100644 --- a/regtests/client/python/test/test_list_namespaces_response.py +++ b/regtests/client/python/test/test_list_namespaces_response.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_list_tables_response.py b/regtests/client/python/test/test_list_tables_response.py index 94ba6afcd7..8a3dc64f32 100644 --- a/regtests/client/python/test/test_list_tables_response.py +++ b/regtests/client/python/test/test_list_tables_response.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_list_type.py b/regtests/client/python/test/test_list_type.py index ff9306a9f4..406ac62f04 100644 --- a/regtests/client/python/test/test_list_type.py +++ b/regtests/client/python/test/test_list_type.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_literal_expression.py b/regtests/client/python/test/test_literal_expression.py index ccd9ac88a1..2a524a9406 100644 --- a/regtests/client/python/test/test_literal_expression.py +++ b/regtests/client/python/test/test_literal_expression.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_load_table_result.py b/regtests/client/python/test/test_load_table_result.py index b10866d79c..e05f20a7b2 100644 --- a/regtests/client/python/test/test_load_table_result.py +++ b/regtests/client/python/test/test_load_table_result.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_load_view_result.py b/regtests/client/python/test/test_load_view_result.py index b42144b1da..73e446917b 100644 --- a/regtests/client/python/test/test_load_view_result.py +++ b/regtests/client/python/test/test_load_view_result.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_map_type.py b/regtests/client/python/test/test_map_type.py index b9fbb17d5f..f9c795a150 100644 --- a/regtests/client/python/test/test_map_type.py +++ b/regtests/client/python/test/test_map_type.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_metadata_log_inner.py b/regtests/client/python/test/test_metadata_log_inner.py index 6668edffb2..603ec1b406 100644 --- a/regtests/client/python/test/test_metadata_log_inner.py +++ b/regtests/client/python/test/test_metadata_log_inner.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_metric_result.py b/regtests/client/python/test/test_metric_result.py index 0a1ba72d9a..5e1dbe6443 100644 --- a/regtests/client/python/test/test_metric_result.py +++ b/regtests/client/python/test/test_metric_result.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_model_schema.py b/regtests/client/python/test/test_model_schema.py index e1629f5d39..334a902ea9 100644 --- a/regtests/client/python/test/test_model_schema.py +++ b/regtests/client/python/test/test_model_schema.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_namespace_grant.py b/regtests/client/python/test/test_namespace_grant.py index 76be3edbd2..8bb3d317f9 100644 --- a/regtests/client/python/test/test_namespace_grant.py +++ b/regtests/client/python/test/test_namespace_grant.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_namespace_privilege.py b/regtests/client/python/test/test_namespace_privilege.py index 5dbd90e7be..256b1b5326 100644 --- a/regtests/client/python/test/test_namespace_privilege.py +++ b/regtests/client/python/test/test_namespace_privilege.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_not_expression.py b/regtests/client/python/test/test_not_expression.py index a33577d6bc..deef692bdb 100644 --- a/regtests/client/python/test/test_not_expression.py +++ b/regtests/client/python/test/test_not_expression.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_notification_request.py b/regtests/client/python/test/test_notification_request.py index 326637b3f8..f77601b3cf 100644 --- a/regtests/client/python/test/test_notification_request.py +++ b/regtests/client/python/test/test_notification_request.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_notification_type.py b/regtests/client/python/test/test_notification_type.py index a936754eab..a2323c26f9 100644 --- a/regtests/client/python/test/test_notification_type.py +++ b/regtests/client/python/test/test_notification_type.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_null_order.py b/regtests/client/python/test/test_null_order.py index 883bf472b9..d354a8cbd9 100644 --- a/regtests/client/python/test/test_null_order.py +++ b/regtests/client/python/test/test_null_order.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_o_auth_error.py b/regtests/client/python/test/test_o_auth_error.py index d4018a6e2d..b7cadb98f9 100644 --- a/regtests/client/python/test/test_o_auth_error.py +++ b/regtests/client/python/test/test_o_auth_error.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_o_auth_token_response.py b/regtests/client/python/test/test_o_auth_token_response.py index 1c47e0fcd7..ffb250c0e7 100644 --- a/regtests/client/python/test/test_o_auth_token_response.py +++ b/regtests/client/python/test/test_o_auth_token_response.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_partition_field.py b/regtests/client/python/test/test_partition_field.py index 7b9344e92d..c4f0bf98f4 100644 --- a/regtests/client/python/test/test_partition_field.py +++ b/regtests/client/python/test/test_partition_field.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_partition_spec.py b/regtests/client/python/test/test_partition_spec.py index 1e8b23b9cd..7d3871d58c 100644 --- a/regtests/client/python/test/test_partition_spec.py +++ b/regtests/client/python/test/test_partition_spec.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_partition_statistics_file.py b/regtests/client/python/test/test_partition_statistics_file.py index 7e446786df..1f49af4e30 100644 --- a/regtests/client/python/test/test_partition_statistics_file.py +++ b/regtests/client/python/test/test_partition_statistics_file.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_polaris_catalog.py b/regtests/client/python/test/test_polaris_catalog.py index 65bdebff22..24eacdbf6b 100644 --- a/regtests/client/python/test/test_polaris_catalog.py +++ b/regtests/client/python/test/test_polaris_catalog.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_polaris_default_api.py b/regtests/client/python/test/test_polaris_default_api.py index 63d295ffc5..bf2dcd4579 100644 --- a/regtests/client/python/test/test_polaris_default_api.py +++ b/regtests/client/python/test/test_polaris_default_api.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_position_delete_file.py b/regtests/client/python/test/test_position_delete_file.py index 5f2cde2d32..f190a98ca6 100644 --- a/regtests/client/python/test/test_position_delete_file.py +++ b/regtests/client/python/test/test_position_delete_file.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_primitive_type_value.py b/regtests/client/python/test/test_primitive_type_value.py index 13498f5e71..f3299a5f72 100644 --- a/regtests/client/python/test/test_primitive_type_value.py +++ b/regtests/client/python/test/test_primitive_type_value.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_principal.py b/regtests/client/python/test/test_principal.py index 2ccfacc30d..4c26b6d133 100644 --- a/regtests/client/python/test/test_principal.py +++ b/regtests/client/python/test/test_principal.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_principal_role.py b/regtests/client/python/test/test_principal_role.py index 7d32a66fd6..67a06d2945 100644 --- a/regtests/client/python/test/test_principal_role.py +++ b/regtests/client/python/test/test_principal_role.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_principal_roles.py b/regtests/client/python/test/test_principal_roles.py index 46f5193aa1..2ebf00d23c 100644 --- a/regtests/client/python/test/test_principal_roles.py +++ b/regtests/client/python/test/test_principal_roles.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_principal_with_credentials.py b/regtests/client/python/test/test_principal_with_credentials.py index b497b71286..ac4bfa40d5 100644 --- a/regtests/client/python/test/test_principal_with_credentials.py +++ b/regtests/client/python/test/test_principal_with_credentials.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_principal_with_credentials_credentials.py b/regtests/client/python/test/test_principal_with_credentials_credentials.py index ca0585c019..5e729f9810 100644 --- a/regtests/client/python/test/test_principal_with_credentials_credentials.py +++ b/regtests/client/python/test/test_principal_with_credentials_credentials.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_principals.py b/regtests/client/python/test/test_principals.py index 628714b539..693d99e0fd 100644 --- a/regtests/client/python/test/test_principals.py +++ b/regtests/client/python/test/test_principals.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_register_table_request.py b/regtests/client/python/test/test_register_table_request.py index 3267b200f6..00a96a6c51 100644 --- a/regtests/client/python/test/test_register_table_request.py +++ b/regtests/client/python/test/test_register_table_request.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_remove_partition_statistics_update.py b/regtests/client/python/test/test_remove_partition_statistics_update.py index 7028034d80..40eed1c10f 100644 --- a/regtests/client/python/test/test_remove_partition_statistics_update.py +++ b/regtests/client/python/test/test_remove_partition_statistics_update.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_remove_properties_update.py b/regtests/client/python/test/test_remove_properties_update.py index 8c78854400..886c606839 100644 --- a/regtests/client/python/test/test_remove_properties_update.py +++ b/regtests/client/python/test/test_remove_properties_update.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_remove_snapshot_ref_update.py b/regtests/client/python/test/test_remove_snapshot_ref_update.py index 91dd203c5e..75442c3208 100644 --- a/regtests/client/python/test/test_remove_snapshot_ref_update.py +++ b/regtests/client/python/test/test_remove_snapshot_ref_update.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_remove_snapshots_update.py b/regtests/client/python/test/test_remove_snapshots_update.py index 6bb43297f9..97460e2751 100644 --- a/regtests/client/python/test/test_remove_snapshots_update.py +++ b/regtests/client/python/test/test_remove_snapshots_update.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_remove_statistics_update.py b/regtests/client/python/test/test_remove_statistics_update.py index d3fe59a079..d47e09f7e9 100644 --- a/regtests/client/python/test/test_remove_statistics_update.py +++ b/regtests/client/python/test/test_remove_statistics_update.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_rename_table_request.py b/regtests/client/python/test/test_rename_table_request.py index 2d0256b3e7..3059ffb8ff 100644 --- a/regtests/client/python/test/test_rename_table_request.py +++ b/regtests/client/python/test/test_rename_table_request.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_report_metrics_request.py b/regtests/client/python/test/test_report_metrics_request.py index 4fa51dd2f9..8bcf61a321 100644 --- a/regtests/client/python/test/test_report_metrics_request.py +++ b/regtests/client/python/test/test_report_metrics_request.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_revoke_grant_request.py b/regtests/client/python/test/test_revoke_grant_request.py index 50f013350c..a54a9a61d9 100644 --- a/regtests/client/python/test/test_revoke_grant_request.py +++ b/regtests/client/python/test/test_revoke_grant_request.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_scan_report.py b/regtests/client/python/test/test_scan_report.py index 82ba62bb76..be8858897e 100644 --- a/regtests/client/python/test/test_scan_report.py +++ b/regtests/client/python/test/test_scan_report.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_set_current_schema_update.py b/regtests/client/python/test/test_set_current_schema_update.py index 8dbb4c95f8..b1e9138064 100644 --- a/regtests/client/python/test/test_set_current_schema_update.py +++ b/regtests/client/python/test/test_set_current_schema_update.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_set_current_view_version_update.py b/regtests/client/python/test/test_set_current_view_version_update.py index 492b19a9e6..1ca4398273 100644 --- a/regtests/client/python/test/test_set_current_view_version_update.py +++ b/regtests/client/python/test/test_set_current_view_version_update.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_set_default_sort_order_update.py b/regtests/client/python/test/test_set_default_sort_order_update.py index a9a333fc09..3ff82c16cc 100644 --- a/regtests/client/python/test/test_set_default_sort_order_update.py +++ b/regtests/client/python/test/test_set_default_sort_order_update.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_set_default_spec_update.py b/regtests/client/python/test/test_set_default_spec_update.py index 61f074e51e..53827fb68b 100644 --- a/regtests/client/python/test/test_set_default_spec_update.py +++ b/regtests/client/python/test/test_set_default_spec_update.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_set_expression.py b/regtests/client/python/test/test_set_expression.py index 7f91657382..4adeb762f4 100644 --- a/regtests/client/python/test/test_set_expression.py +++ b/regtests/client/python/test/test_set_expression.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_set_location_update.py b/regtests/client/python/test/test_set_location_update.py index 6ff145f61b..47922065cd 100644 --- a/regtests/client/python/test/test_set_location_update.py +++ b/regtests/client/python/test/test_set_location_update.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_set_partition_statistics_update.py b/regtests/client/python/test/test_set_partition_statistics_update.py index 004ce9bbfd..36411ed591 100644 --- a/regtests/client/python/test/test_set_partition_statistics_update.py +++ b/regtests/client/python/test/test_set_partition_statistics_update.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_set_properties_update.py b/regtests/client/python/test/test_set_properties_update.py index ca25bcdf5a..9fb951b788 100644 --- a/regtests/client/python/test/test_set_properties_update.py +++ b/regtests/client/python/test/test_set_properties_update.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_set_snapshot_ref_update.py b/regtests/client/python/test/test_set_snapshot_ref_update.py index 3e7dc74bdc..8cc7e29101 100644 --- a/regtests/client/python/test/test_set_snapshot_ref_update.py +++ b/regtests/client/python/test/test_set_snapshot_ref_update.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_set_statistics_update.py b/regtests/client/python/test/test_set_statistics_update.py index 066e88ee3b..777caade1c 100644 --- a/regtests/client/python/test/test_set_statistics_update.py +++ b/regtests/client/python/test/test_set_statistics_update.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_snapshot.py b/regtests/client/python/test/test_snapshot.py index 1b57912764..ba0bdae198 100644 --- a/regtests/client/python/test/test_snapshot.py +++ b/regtests/client/python/test/test_snapshot.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_snapshot_log_inner.py b/regtests/client/python/test/test_snapshot_log_inner.py index 0b14e15882..31e98d7156 100644 --- a/regtests/client/python/test/test_snapshot_log_inner.py +++ b/regtests/client/python/test/test_snapshot_log_inner.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_snapshot_reference.py b/regtests/client/python/test/test_snapshot_reference.py index 5266837c87..9d5fe1bf8a 100644 --- a/regtests/client/python/test/test_snapshot_reference.py +++ b/regtests/client/python/test/test_snapshot_reference.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_snapshot_summary.py b/regtests/client/python/test/test_snapshot_summary.py index 0d985f7ce3..9039af1a34 100644 --- a/regtests/client/python/test/test_snapshot_summary.py +++ b/regtests/client/python/test/test_snapshot_summary.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_sort_direction.py b/regtests/client/python/test/test_sort_direction.py index 9e1e62832a..7880429837 100644 --- a/regtests/client/python/test/test_sort_direction.py +++ b/regtests/client/python/test/test_sort_direction.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_sort_field.py b/regtests/client/python/test/test_sort_field.py index 50ab4463a3..8c1b296f0a 100644 --- a/regtests/client/python/test/test_sort_field.py +++ b/regtests/client/python/test/test_sort_field.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_sort_order.py b/regtests/client/python/test/test_sort_order.py index 31357142a4..bd1fc93371 100644 --- a/regtests/client/python/test/test_sort_order.py +++ b/regtests/client/python/test/test_sort_order.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_sql_view_representation.py b/regtests/client/python/test/test_sql_view_representation.py index cf926ed455..5909feb856 100644 --- a/regtests/client/python/test/test_sql_view_representation.py +++ b/regtests/client/python/test/test_sql_view_representation.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_statistics_file.py b/regtests/client/python/test/test_statistics_file.py index 5a458aa6c6..9bb196d833 100644 --- a/regtests/client/python/test/test_statistics_file.py +++ b/regtests/client/python/test/test_statistics_file.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_storage_config_info.py b/regtests/client/python/test/test_storage_config_info.py index 5293afa4e8..0e6c2c7f88 100644 --- a/regtests/client/python/test/test_storage_config_info.py +++ b/regtests/client/python/test/test_storage_config_info.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_struct_field.py b/regtests/client/python/test/test_struct_field.py index ced146839d..e4dc64b236 100644 --- a/regtests/client/python/test/test_struct_field.py +++ b/regtests/client/python/test/test_struct_field.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_struct_type.py b/regtests/client/python/test/test_struct_type.py index abce7ed757..0d8c132aed 100644 --- a/regtests/client/python/test/test_struct_type.py +++ b/regtests/client/python/test/test_struct_type.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_table_grant.py b/regtests/client/python/test/test_table_grant.py index 361c69eb30..e6d1fb898d 100644 --- a/regtests/client/python/test/test_table_grant.py +++ b/regtests/client/python/test/test_table_grant.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_table_identifier.py b/regtests/client/python/test/test_table_identifier.py index bbbb700a43..fe685a038b 100644 --- a/regtests/client/python/test/test_table_identifier.py +++ b/regtests/client/python/test/test_table_identifier.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_table_metadata.py b/regtests/client/python/test/test_table_metadata.py index 63af8e44b2..bc714a4fea 100644 --- a/regtests/client/python/test/test_table_metadata.py +++ b/regtests/client/python/test/test_table_metadata.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_table_privilege.py b/regtests/client/python/test/test_table_privilege.py index e64b60286b..42ac331443 100644 --- a/regtests/client/python/test/test_table_privilege.py +++ b/regtests/client/python/test/test_table_privilege.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_table_requirement.py b/regtests/client/python/test/test_table_requirement.py index 1d51bfe1d7..938b4d1952 100644 --- a/regtests/client/python/test/test_table_requirement.py +++ b/regtests/client/python/test/test_table_requirement.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_table_update.py b/regtests/client/python/test/test_table_update.py index acbbe95da9..3f7a126f9e 100644 --- a/regtests/client/python/test/test_table_update.py +++ b/regtests/client/python/test/test_table_update.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_table_update_notification.py b/regtests/client/python/test/test_table_update_notification.py index 81f5e59898..a767194896 100644 --- a/regtests/client/python/test/test_table_update_notification.py +++ b/regtests/client/python/test/test_table_update_notification.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_term.py b/regtests/client/python/test/test_term.py index 7b715146b3..7ff7479516 100644 --- a/regtests/client/python/test/test_term.py +++ b/regtests/client/python/test/test_term.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_timer_result.py b/regtests/client/python/test/test_timer_result.py index c22c1d49d0..c18feb09d4 100644 --- a/regtests/client/python/test/test_timer_result.py +++ b/regtests/client/python/test/test_timer_result.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_token_type.py b/regtests/client/python/test/test_token_type.py index 3bb7c123aa..1e9b4d1e59 100644 --- a/regtests/client/python/test/test_token_type.py +++ b/regtests/client/python/test/test_token_type.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_transform_term.py b/regtests/client/python/test/test_transform_term.py index 8ab517bbfa..72da2f129b 100644 --- a/regtests/client/python/test/test_transform_term.py +++ b/regtests/client/python/test/test_transform_term.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_type.py b/regtests/client/python/test/test_type.py index 21ff2b6b68..ca80486463 100644 --- a/regtests/client/python/test/test_type.py +++ b/regtests/client/python/test/test_type.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_unary_expression.py b/regtests/client/python/test/test_unary_expression.py index 93be78ba34..417b18eb2f 100644 --- a/regtests/client/python/test/test_unary_expression.py +++ b/regtests/client/python/test/test_unary_expression.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_update_catalog_request.py b/regtests/client/python/test/test_update_catalog_request.py index 50276de2b2..8a04260ab4 100644 --- a/regtests/client/python/test/test_update_catalog_request.py +++ b/regtests/client/python/test/test_update_catalog_request.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_update_catalog_role_request.py b/regtests/client/python/test/test_update_catalog_role_request.py index 6a4247320e..5114bf8507 100644 --- a/regtests/client/python/test/test_update_catalog_role_request.py +++ b/regtests/client/python/test/test_update_catalog_role_request.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_update_namespace_properties_request.py b/regtests/client/python/test/test_update_namespace_properties_request.py index c27f6dd3ab..3604e94368 100644 --- a/regtests/client/python/test/test_update_namespace_properties_request.py +++ b/regtests/client/python/test/test_update_namespace_properties_request.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_update_namespace_properties_response.py b/regtests/client/python/test/test_update_namespace_properties_response.py index 958f8012aa..d9249ea08d 100644 --- a/regtests/client/python/test/test_update_namespace_properties_response.py +++ b/regtests/client/python/test/test_update_namespace_properties_response.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_update_principal_request.py b/regtests/client/python/test/test_update_principal_request.py index 870e7de3a0..6c6174c580 100644 --- a/regtests/client/python/test/test_update_principal_request.py +++ b/regtests/client/python/test/test_update_principal_request.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_update_principal_role_request.py b/regtests/client/python/test/test_update_principal_role_request.py index 03d1084f7b..36b0b4cb65 100644 --- a/regtests/client/python/test/test_update_principal_role_request.py +++ b/regtests/client/python/test/test_update_principal_role_request.py @@ -1,5 +1,20 @@ -# coding: utf-8 +# coding: utf-8 +# +# Copyright (c) 2024 Snowflake Computing 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. +# """ Polaris Management Service diff --git a/regtests/client/python/test/test_upgrade_format_version_update.py b/regtests/client/python/test/test_upgrade_format_version_update.py index d6cdbc32fc..f3afed16b7 100644 --- a/regtests/client/python/test/test_upgrade_format_version_update.py +++ b/regtests/client/python/test/test_upgrade_format_version_update.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_value_map.py b/regtests/client/python/test/test_value_map.py index 4348f07dc4..83d17c6f12 100644 --- a/regtests/client/python/test/test_value_map.py +++ b/regtests/client/python/test/test_value_map.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_view_grant.py b/regtests/client/python/test/test_view_grant.py index 40e54843c2..995361fb0e 100644 --- a/regtests/client/python/test/test_view_grant.py +++ b/regtests/client/python/test/test_view_grant.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_view_history_entry.py b/regtests/client/python/test/test_view_history_entry.py index 2d4120fad4..c33fc9047e 100644 --- a/regtests/client/python/test/test_view_history_entry.py +++ b/regtests/client/python/test/test_view_history_entry.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_view_metadata.py b/regtests/client/python/test/test_view_metadata.py index d06b5e482c..54caeea83d 100644 --- a/regtests/client/python/test/test_view_metadata.py +++ b/regtests/client/python/test/test_view_metadata.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_view_privilege.py b/regtests/client/python/test/test_view_privilege.py index 07aa2eb1c6..8990a9f128 100644 --- a/regtests/client/python/test/test_view_privilege.py +++ b/regtests/client/python/test/test_view_privilege.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_view_representation.py b/regtests/client/python/test/test_view_representation.py index 38db50ed6e..38053032dd 100644 --- a/regtests/client/python/test/test_view_representation.py +++ b/regtests/client/python/test/test_view_representation.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_view_requirement.py b/regtests/client/python/test/test_view_requirement.py index b9dbb86549..a787a2cdb0 100644 --- a/regtests/client/python/test/test_view_requirement.py +++ b/regtests/client/python/test/test_view_requirement.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_view_update.py b/regtests/client/python/test/test_view_update.py index 5d028557e0..a82eb8f113 100644 --- a/regtests/client/python/test/test_view_update.py +++ b/regtests/client/python/test/test_view_update.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/client/python/test/test_view_version.py b/regtests/client/python/test/test_view_version.py index 909181a1b5..7c068369de 100644 --- a/regtests/client/python/test/test_view_version.py +++ b/regtests/client/python/test/test_view_version.py @@ -1,3 +1,18 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# # coding: utf-8 """ diff --git a/regtests/pyspark-setup.sh b/regtests/pyspark-setup.sh index a9c3be9efa..7940037472 100755 --- a/regtests/pyspark-setup.sh +++ b/regtests/pyspark-setup.sh @@ -1,4 +1,19 @@ #!/bin/bash +# +# Copyright (c) 2024 Snowflake Computing 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. +# if [ ! -d ~/polaris-venv ]; then python3 -m venv ~/polaris-venv diff --git a/regtests/run.sh b/regtests/run.sh index 4bc2a3a31a..eef12b622a 100755 --- a/regtests/run.sh +++ b/regtests/run.sh @@ -1,5 +1,19 @@ #!/bin/bash - +# +# Copyright (c) 2024 Snowflake Computing 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. +# # Run without args to run all tests, or single arg for single test. if [ -z "${SPARK_HOME}"]; then diff --git a/regtests/run_spark_sql.sh b/regtests/run_spark_sql.sh index 4b9ca1f39b..94ce7039c6 100755 --- a/regtests/run_spark_sql.sh +++ b/regtests/run_spark_sql.sh @@ -1,5 +1,20 @@ #!/bin/bash # +# Copyright (c) 2024 Snowflake Computing 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. +# +# # Run this to open an interactive spark-sql shell talking to a catalog named "manual_spark" # # You must run 'use polaris;' as your first query in the spark-sql shell. diff --git a/regtests/setup.sh b/regtests/setup.sh index 9dbeb7dd1d..be80478c7f 100755 --- a/regtests/setup.sh +++ b/regtests/setup.sh @@ -1,5 +1,19 @@ #!/bin/bash - +# +# Copyright (c) 2024 Snowflake Computing 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. +# # Idempotent setup for regression tests. Run manually or let run.sh auto-run. # # Warning - first time setup may download large amounts of files diff --git a/regtests/t_hello_world/src/hello_world.sh b/regtests/t_hello_world/src/hello_world.sh index 485e2f637a..5c880a51f5 100755 --- a/regtests/t_hello_world/src/hello_world.sh +++ b/regtests/t_hello_world/src/hello_world.sh @@ -1,3 +1,17 @@ #!/bin/bash +# Copyright (c) 2024 Snowflake Computing 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. + echo "Hello world!" diff --git a/regtests/t_oauth/test_oauth2_tokens.py b/regtests/t_oauth/test_oauth2_tokens.py index 699db9050b..02b3839a3a 100644 --- a/regtests/t_oauth/test_oauth2_tokens.py +++ b/regtests/t_oauth/test_oauth2_tokens.py @@ -1,3 +1,17 @@ +# Copyright (c) 2024 Snowflake Computing 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. + """ Simple class to test OAuth endpoints in the Polaris Service. """ diff --git a/regtests/t_pyspark/src/conftest.py b/regtests/t_pyspark/src/conftest.py index e75bbf0970..9bea00f06d 100644 --- a/regtests/t_pyspark/src/conftest.py +++ b/regtests/t_pyspark/src/conftest.py @@ -1,3 +1,17 @@ +# Copyright (c) 2024 Snowflake Computing 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 codecs import os from typing import List diff --git a/regtests/t_pyspark/src/iceberg_spark.py b/regtests/t_pyspark/src/iceberg_spark.py index 3993faaaea..0e22749ca4 100644 --- a/regtests/t_pyspark/src/iceberg_spark.py +++ b/regtests/t_pyspark/src/iceberg_spark.py @@ -1,3 +1,17 @@ +# Copyright (c) 2024 Snowflake Computing 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. + """Spark connector with different catalog types.""" from typing import Any, Dict, List, Optional, Union diff --git a/regtests/t_pyspark/src/test_spark_sql_s3_with_privileges.py b/regtests/t_pyspark/src/test_spark_sql_s3_with_privileges.py index 8e67e6feb0..22b078b374 100644 --- a/regtests/t_pyspark/src/test_spark_sql_s3_with_privileges.py +++ b/regtests/t_pyspark/src/test_spark_sql_s3_with_privileges.py @@ -1,3 +1,17 @@ +# Copyright (c) 2024 Snowflake Computing 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 codecs import os import time diff --git a/regtests/t_spark_sql/src/spark_sql_azure_blob.sh b/regtests/t_spark_sql/src/spark_sql_azure_blob.sh index 787e6b415e..ccf4d0cd2c 100755 --- a/regtests/t_spark_sql/src/spark_sql_azure_blob.sh +++ b/regtests/t_spark_sql/src/spark_sql_azure_blob.sh @@ -1,5 +1,19 @@ #!/bin/bash +# Copyright (c) 2024 Snowflake Computing 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. + SPARK_BEARER_TOKEN="${REGTEST_ROOT_BEARER_TOKEN:-principal:root;realm:realm1}" curl -i -X POST -H "Authorization: Bearer ${SPARK_BEARER_TOKEN}" -H 'Accepts: application/json' -H 'Content-Type: application/json' \ diff --git a/regtests/t_spark_sql/src/spark_sql_azure_dfs.sh b/regtests/t_spark_sql/src/spark_sql_azure_dfs.sh index 2e270732ec..d183f7eec3 100755 --- a/regtests/t_spark_sql/src/spark_sql_azure_dfs.sh +++ b/regtests/t_spark_sql/src/spark_sql_azure_dfs.sh @@ -1,5 +1,19 @@ #!/bin/bash +# Copyright (c) 2024 Snowflake Computing 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. + SPARK_BEARER_TOKEN="${REGTEST_ROOT_BEARER_TOKEN:-principal:root;realm:realm1}" curl -i -X POST -H "Authorization: Bearer ${SPARK_BEARER_TOKEN}" -H 'Accepts: application/json' -H 'Content-Type: application/json' \ diff --git a/regtests/t_spark_sql/src/spark_sql_basic.sh b/regtests/t_spark_sql/src/spark_sql_basic.sh index dda952e8c2..c53628bb45 100755 --- a/regtests/t_spark_sql/src/spark_sql_basic.sh +++ b/regtests/t_spark_sql/src/spark_sql_basic.sh @@ -1,5 +1,19 @@ #!/bin/bash +# Copyright (c) 2024 Snowflake Computing 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. + SPARK_BEARER_TOKEN="${REGTEST_ROOT_BEARER_TOKEN:-principal:root;realm:realm1}" curl -i -X POST -H "Authorization: Bearer ${SPARK_BEARER_TOKEN}" -H 'Accepts: application/json' -H 'Content-Type: application/json' \ diff --git a/regtests/t_spark_sql/src/spark_sql_gcp.sh b/regtests/t_spark_sql/src/spark_sql_gcp.sh index 76a4cb2f85..b7bd6e03c4 100755 --- a/regtests/t_spark_sql/src/spark_sql_gcp.sh +++ b/regtests/t_spark_sql/src/spark_sql_gcp.sh @@ -1,5 +1,19 @@ #!/bin/bash +# Copyright (c) 2024 Snowflake Computing 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. + SPARK_BEARER_TOKEN="${REGTEST_ROOT_BEARER_TOKEN:-principal:root;realm:realm1}" curl -i -X POST -H "Authorization: Bearer ${SPARK_BEARER_TOKEN}" -H 'Accepts: application/json' -H 'Content-Type: application/json' \ diff --git a/regtests/t_spark_sql/src/spark_sql_s3.sh b/regtests/t_spark_sql/src/spark_sql_s3.sh index c4a7fdbad9..922567ff45 100755 --- a/regtests/t_spark_sql/src/spark_sql_s3.sh +++ b/regtests/t_spark_sql/src/spark_sql_s3.sh @@ -1,5 +1,19 @@ #!/bin/bash +# Copyright (c) 2024 Snowflake Computing 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. + if [ -z "$AWS_TEST_ENABLED" ] || [ "$AWS_TEST_ENABLED" != "true" ]; then echo "AWS_TEST_ENABLED is not set to 'true'. Skipping test." exit 0 diff --git a/regtests/t_spark_sql/src/spark_sql_s3_cross_region.sh b/regtests/t_spark_sql/src/spark_sql_s3_cross_region.sh index a9e3fb868f..3e75831dcc 100644 --- a/regtests/t_spark_sql/src/spark_sql_s3_cross_region.sh +++ b/regtests/t_spark_sql/src/spark_sql_s3_cross_region.sh @@ -1,5 +1,19 @@ #!/bin/bash +# Copyright (c) 2024 Snowflake Computing 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. + if [ -z "$AWS_CROSS_REGION_TEST_ENABLED" ] || [ "$AWS_CROSS_REGION_TEST_ENABLED" != "true" ]; then echo "AWS_CROSS_REGION_TEST_ENABLED is not set to 'true'. Skipping test." exit 0 diff --git a/regtests/t_spark_sql/src/spark_sql_views.sh b/regtests/t_spark_sql/src/spark_sql_views.sh index c9b74eec09..a6a50c47e2 100755 --- a/regtests/t_spark_sql/src/spark_sql_views.sh +++ b/regtests/t_spark_sql/src/spark_sql_views.sh @@ -1,5 +1,19 @@ #!/bin/bash +# Copyright (c) 2024 Snowflake Computing 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. + SPARK_BEARER_TOKEN="${REGTEST_ROOT_BEARER_TOKEN:-principal:root;realm:default-realm}" curl -i -X POST -H "Authorization: Bearer ${SPARK_BEARER_TOKEN}" -H 'Accepts: application/json' -H 'Content-Type: application/json' \ diff --git a/server-templates/api.mustache b/server-templates/api.mustache index 4a4faa7803..49064a11ab 100644 --- a/server-templates/api.mustache +++ b/server-templates/api.mustache @@ -1,3 +1,18 @@ +/* + * Copyright (C) 2024 Snowflake Computing 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. + */ package {{package}}; {{#imports}} diff --git a/server-templates/apiService.mustache b/server-templates/apiService.mustache index 2d199fb8e0..176cc6426a 100644 --- a/server-templates/apiService.mustache +++ b/server-templates/apiService.mustache @@ -1,3 +1,18 @@ +/* + * Copyright (C) 2024 Snowflake Computing 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. + */ package {{package}}; {{#operations}}{{#operation}}{{#isMultipart}}import org.jboss.resteasy.plugins.providers.multipart.MultipartFormDataInput; diff --git a/server-templates/apiServiceImpl.mustache b/server-templates/apiServiceImpl.mustache index 4d146d17b8..33534e434f 100644 --- a/server-templates/apiServiceImpl.mustache +++ b/server-templates/apiServiceImpl.mustache @@ -1,3 +1,18 @@ +/* + * Copyright (C) 2024 Snowflake Computing 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. + */ package {{package}}.impl; {{#operations}}{{#operation}}{{#isMultipart}}import org.jboss.resteasy.plugins.providers.multipart.MultipartFormDataInput; diff --git a/server-templates/pojo.mustache b/server-templates/pojo.mustache index 56849730d3..a274de17c0 100644 --- a/server-templates/pojo.mustache +++ b/server-templates/pojo.mustache @@ -1,3 +1,18 @@ +/* + * Copyright (C) 2024 Snowflake Computing 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 io.swagger.annotations.*; {{#useBeanValidation}}import jakarta.validation.Valid;{{/useBeanValidation}} {{#additionalPropertiesType}} diff --git a/settings.gradle b/settings.gradle index 84aa3101b5..96d678a095 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ + rootProject.name = 'polaris' include 'polaris-core' diff --git a/setup.sh b/setup.sh index 5bd0e16b4b..f2a5be377f 100755 --- a/setup.sh +++ b/setup.sh @@ -1,5 +1,21 @@ #!/bin/bash +# +# Copyright (c) 2024 Snowflake Computing 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. +# + CURRENT_DIR=$(pwd) # deploy the registry diff --git a/spec/polaris-management-service.yml b/spec/polaris-management-service.yml index 6bb3a25c44..9dd1a7e85d 100644 --- a/spec/polaris-management-service.yml +++ b/spec/polaris-management-service.yml @@ -1,3 +1,16 @@ +# Copyright (c) 2024 Snowflake Computing 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. openapi: 3.0.3 info: title: Polaris Management Service From 77becc8c894a7db4cfac533031bed8e2bcd727b8 Mon Sep 17 00:00:00 2001 From: Robert Stupp Date: Sun, 28 Jul 2024 07:31:51 +0200 Subject: [PATCH 08/27] Prominently warn for Java version < 21 (#22) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ... and add some notes to `CONTRIBUTING.md`. Co-authored-by: Anna Filippova <7892219+annafil@users.noreply.github.com> Co-authored-by: JB Onofré --- CONTRIBUTING.md | 7 +++++++ README.md | 2 +- settings.gradle | 11 +++++++++++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 91ba713728..5402c68808 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -55,6 +55,13 @@ git push GitHubUser my-branch --force * Test that your changes works by adapting or adding tests. Verify the build passes (see `README.md` for build instructions). * If your Pull Request has conflicts with the `main` branch, please rebase and fix the conflicts. +## Java version requirements + +The Polaris build currently requires Java 21 or later. There are a few tools that help you running the right Java version: + +* [SDKMAN!](https://sdkman.io/) follow the installation instructions, then run `sdk list java` to see the available distributions and versions, then run `sdk install java ` using the identifier for the distribution and version (>= 21) of your choice. +* [jenv](https://www.jenv.be/) If on a Mac you can use jenv to set the appropriate SDK. + ## License When contributing to this project, you agree that your contributions use the Apache License version 2. Please ensure you have permission to do this if required by your employer. diff --git a/README.md b/README.md index 5081e564cb..ad7e5bbedc 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ docker run -p 8080:80 -v ${PWD}:/spec redocly/cli build-docs spec/rest-catalog-o ## Requirements / Setup -- Java JDK >= 21 . If on a Mac you can use [jenv](https://www.jenv.be/) to set the appropriate SDK. +- Java JDK >= 21, see [CONTRIBUTING.md](./CONTRIBUTING.md#java-version-requirements). - Gradle 8.6 - This is included in the project and can be run using `./gradlew` in the project root. - Docker - If you want to run the project in a containerized environment. diff --git a/settings.gradle b/settings.gradle index 96d678a095..45a92275ec 100644 --- a/settings.gradle +++ b/settings.gradle @@ -14,6 +14,17 @@ * limitations under the License. */ +if (!JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_21)) { + throw new GradleException(""" + + Build aborted... + + The Apache Polaris build requires Java 21. + + + """) +} + rootProject.name = 'polaris' include 'polaris-core' From 8534cf446cc4c6061d8e1daaec135baf9226d035 Mon Sep 17 00:00:00 2001 From: Robert Stupp Date: Sun, 28 Jul 2024 07:47:07 +0200 Subject: [PATCH 09/27] Set project name in IDE (#23) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sets the project name to `Polaris ` for IntelliJ. It _should_ do the same for Eclipse (not tested). Co-authored-by: Anna Filippova <7892219+annafil@users.noreply.github.com> Co-authored-by: JB Onofré --- build.gradle | 14 ++++++++++++++ ide-name.txt | 1 + 2 files changed, 15 insertions(+) create mode 100644 ide-name.txt diff --git a/build.gradle b/build.gradle index 6ac0b673d4..bdbf596b7c 100644 --- a/build.gradle +++ b/build.gradle @@ -27,6 +27,7 @@ buildscript { plugins { id 'idea' + id 'eclipse' } allprojects { @@ -110,3 +111,16 @@ subprojects { } } } + +def projectName = rootProject.file("ide-name.txt").text.trim() +def ideName = "$projectName ${rootProject.version.toString().replace("^([0-9.]+).*", "\1")}" + +if (System.getProperty("idea.sync.active").asBoolean()) { + // There's no proper way to set the name of the IDEA project (when "just importing" or + // syncing the Gradle project) + def ideaDir = rootProject.layout.projectDirectory.dir(".idea") + ideaDir.asFile.mkdirs() + ideaDir.file(".name").asFile.text = ideName +} + +eclipse { project { name = ideName } } diff --git a/ide-name.txt b/ide-name.txt new file mode 100644 index 0000000000..fa950b0fa1 --- /dev/null +++ b/ide-name.txt @@ -0,0 +1 @@ +Polaris \ No newline at end of file From f1c4f851ce1f17cfa87f14289bf783c6b63aedb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?JB=20Onofr=C3=A9?= Date: Mon, 29 Jul 2024 07:22:13 +0200 Subject: [PATCH 10/27] Upgrade to Jackson 2.17.2 (#30) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index bdbf596b7c..d949f62f41 100644 --- a/build.gradle +++ b/build.gradle @@ -50,7 +50,7 @@ subprojects { apply plugin: 'jacoco-report-aggregation' apply plugin: 'groovy' ext { - jacksonVersion = '2.17.1' + jacksonVersion = '2.17.2' icebergVersion = '1.5.0' hadoopVersion = '3.3.6' dropwizardVersion = '4.0.7' From 43126c7b12ffe000318e782b0fd03feb3fe8aeca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?JB=20Onofr=C3=A9?= Date: Mon, 29 Jul 2024 08:18:17 +0200 Subject: [PATCH 11/27] Upgrade to Gradle 8.9 (#29) --- .gitignore | 4 ++++ README.md | 2 +- gradle/wrapper/gradle-wrapper.properties | 4 +++- gradlew | 4 ++++ 4 files changed, 12 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 97f280ecce..6218ac895f 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,10 @@ gradle-app.setting # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) !gradle-wrapper.jar +# Ignore Gradle wrapper jar file +gradle/wrapper/gradle-wrapper.jar +gradle/wrapper/gradle-wrapper-*.sha256 + # Avoid ignore Gradle wrappper properties !gradle-wrapper.properties diff --git a/README.md b/README.md index ad7e5bbedc..1b6fde1128 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ docker run -p 8080:80 -v ${PWD}:/spec redocly/cli build-docs spec/rest-catalog-o ## Requirements / Setup - Java JDK >= 21, see [CONTRIBUTING.md](./CONTRIBUTING.md#java-version-requirements). -- Gradle 8.6 - This is included in the project and can be run using `./gradlew` in the project root. +- Gradle - This is included in the project and can be run using `./gradlew` in the project root. - Docker - If you want to run the project in a containerized environment. Command-Line getting started diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index fdbf626db8..f24d7559ef 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -16,7 +16,9 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip +# See https://gradle.org/release-checksums/ for valid checksums +distributionSha256Sum=d725d707bfabd4dfdc958c624003b3c80accc03f7037b5122c4b1d0ef15cecab +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index 4c77f6cee8..549a8d3898 100755 --- a/gradlew +++ b/gradlew @@ -84,6 +84,10 @@ APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit +if [ ! -e $APP_HOME/gradle/wrapper/gradle-wrapper.jar ]; then + curl -o $APP_HOME/gradle/wrapper/gradle-wrapper.jar https://raw.githubusercontent.com/gradle/gradle/v8.9.0/gradle/wrapper/gradle-wrapper.jar +fi + # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum From d2bf3941dacb78a9f1b3e93b4e6b5e0dacf99895 Mon Sep 17 00:00:00 2001 From: Robert Stupp Date: Mon, 29 Jul 2024 09:13:26 +0200 Subject: [PATCH 12/27] No copyright in PR template (#33) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit No need to have the HTML escaped copyright header in the PR description Co-authored-by: JB Onofré --- .github/pull_request_template.md | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 41fa22e738..0c23671569 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,16 +1 @@ - From c273107cb1f436db96fba3fa586d8e2005796346 Mon Sep 17 00:00:00 2001 From: Robert Stupp Date: Mon, 29 Jul 2024 16:34:09 +0200 Subject: [PATCH 13/27] Gradle wrapper - download and verify (#32) This change automatically downloads the `gradle-wrapper.jar` that matches the Gradle version mentioned in `gradle-wrapper.properties`, while ensuring the integrity of it. Future Gradle version bumps don't need to do anything wrt `gradle-wrapper.jar`. --- .gitignore | 5 ++-- gradle/gradlew-include.sh | 37 ++++++++++++++++++++++++++++++ gradle/wrapper/gradle-wrapper.jar | Bin 43462 -> 0 bytes gradlew | 4 +--- 4 files changed, 41 insertions(+), 5 deletions(-) create mode 100644 gradle/gradlew-include.sh delete mode 100644 gradle/wrapper/gradle-wrapper.jar diff --git a/.gitignore b/.gitignore index 6218ac895f..04af1c3247 100644 --- a/.gitignore +++ b/.gitignore @@ -10,8 +10,9 @@ metastore_db/ # Ignore Gradle GUI config gradle-app.setting -# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) -!gradle-wrapper.jar +# Ignore Gradle wrapper jar file +gradle/wrapper/gradle-wrapper.jar +gradle/wrapper/gradle-wrapper-*.sha256 # Ignore Gradle wrapper jar file gradle/wrapper/gradle-wrapper.jar diff --git a/gradle/gradlew-include.sh b/gradle/gradlew-include.sh new file mode 100644 index 0000000000..815e5d17c5 --- /dev/null +++ b/gradle/gradlew-include.sh @@ -0,0 +1,37 @@ +# Downloads the gradle-wrapper.jar if necessary and verifies its integrity. +# Included from /.gradlew + +# Extract the Gradle version from gradle-wrapper.properties. +GRADLE_DIST_VERSION="$(grep distributionUrl= "$APP_HOME/gradle/wrapper/gradle-wrapper.properties" | sed 's/^.*gradle-\([0-9.]*\)-[a-z]*.zip$/\1/')" +GRADLE_WRAPPER_SHA256="$APP_HOME/gradle/wrapper/gradle-wrapper-${GRADLE_DIST_VERSION}.jar.sha256" +GRADLE_WRAPPER_JAR="$APP_HOME/gradle/wrapper/gradle-wrapper.jar" +if [ ! -e "${GRADLE_WRAPPER_SHA256}" ]; then + # Delete the wrapper jar, if the checksum file does not exist. + rm -f "${GRADLE_WRAPPER_JAR}" +fi +if [ -e "${GRADLE_WRAPPER_JAR}" ]; then + # Verify the wrapper jar, if it exists, delete wrapper jar and checksum file, if the checksums + # do not match. + JAR_CHECKSUM="$(sha256sum "${GRADLE_WRAPPER_JAR}" | cut -d\ -f1)" + EXPECTED="$(cat "${GRADLE_WRAPPER_SHA256}")" + if [ "${JAR_CHECKSUM}" != "${EXPECTED}" ]; then + rm -f "${GRADLE_WRAPPER_JAR}" "${GRADLE_WRAPPER_SHA256}" + fi +fi +if [ ! -e "${GRADLE_WRAPPER_SHA256}" ]; then + curl --location --output "${GRADLE_WRAPPER_SHA256}" https://services.gradle.org/distributions/gradle-${GRADLE_DIST_VERSION}-wrapper.jar.sha256 || exit 1 +fi +if [ ! -e "${GRADLE_WRAPPER_JAR}" ]; then + # The Gradle version extracted from the `distributionUrl` property does not contain ".0" patch + # versions. Need to append a ".0" in that case to download the wrapper jar. + GRADLE_VERSION="$(echo "$GRADLE_DIST_VERSION" | sed 's/^\([0-9]*[.][0-9]*\)$/\1.0/')" + curl --location --output "${GRADLE_WRAPPER_JAR}" https://raw.githubusercontent.com/gradle/gradle/v${GRADLE_VERSION}/gradle/wrapper/gradle-wrapper.jar || exit 1 + JAR_CHECKSUM="$(sha256sum "${GRADLE_WRAPPER_JAR}" | cut -d\ -f1)" + EXPECTED="$(cat "${GRADLE_WRAPPER_SHA256}")" + if [ "${JAR_CHECKSUM}" != "${EXPECTED}" ]; then + # If the (just downloaded) checksum and the downloaded wrapper jar do not match, something + # really bad is going on. + echo "Expected sha256 of the downloaded gradle-wrapper.jar does not match the downloaded sha256!" + exit 1 + fi +fi diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index d64cd4917707c1f8861d8cb53dd15194d4248596..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 43462 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 diff --git a/gradlew b/gradlew index 549a8d3898..7eb63c535d 100755 --- a/gradlew +++ b/gradlew @@ -84,9 +84,7 @@ APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit -if [ ! -e $APP_HOME/gradle/wrapper/gradle-wrapper.jar ]; then - curl -o $APP_HOME/gradle/wrapper/gradle-wrapper.jar https://raw.githubusercontent.com/gradle/gradle/v8.9.0/gradle/wrapper/gradle-wrapper.jar -fi +. ${APP_HOME}/gradle/gradlew-include.sh # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum From c3e540b691e2e487d08835c235404ebfee01c0a7 Mon Sep 17 00:00:00 2001 From: Robert Stupp Date: Mon, 29 Jul 2024 20:28:40 +0200 Subject: [PATCH 14/27] Simplify spotless-java expression (#24) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: JB Onofré Co-authored-by: Michael Collado <40346148+collado-mike@users.noreply.github.com> --- build.gradle | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index d949f62f41..b6d01be0f9 100644 --- a/build.gradle +++ b/build.gradle @@ -102,10 +102,9 @@ subprojects { } } java { - target 'src/main/java/**/*.java', 'src/testFixtures/java/**/*.java', 'src/test/java/**/*.java' + target 'src/*/java/**/*.java' + targetExclude 'build/**' googleJavaFormat() - indentWithSpaces(2) - removeUnusedImports() endWithNewline() custom 'disallowWildcardImports', disallowWildcardImports } From a6b83fd517b3ef096e49f45c727ae0fbcde4d6af Mon Sep 17 00:00:00 2001 From: Robert Stupp Date: Mon, 29 Jul 2024 21:12:59 +0200 Subject: [PATCH 15/27] Retain original copyright in `gradlew` (#43) --- gradlew | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/gradlew b/gradlew index 7eb63c535d..61ec480bca 100755 --- a/gradlew +++ b/gradlew @@ -1,12 +1,13 @@ #!/bin/sh + # -# Copyright (c) 2024 Snowflake Computing Inc. +# Copyright © 2015-2021 the original authors. # # 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 +# https://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, @@ -14,6 +15,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # + ############################################################################## # # Gradle start up script for POSIX generated by Gradle. From 8eccee837be8fe1b67d61d9bc5a052c728c9d78b Mon Sep 17 00:00:00 2001 From: Anna Filippova <7892219+annafil@users.noreply.github.com> Date: Mon, 29 Jul 2024 12:37:18 -0700 Subject: [PATCH 16/27] Add issue + PR templates (#27) * Add issue templates * Add color to PR template --- .github/ISSUE_TEMPLATE/bug_report.md | 35 +++++++++++++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 20 +++++++++++ .github/pull_request_template.md | 42 ++++++++++++++++++++++- 3 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000000..01e549df6e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,35 @@ +--- +name: Bug report +about: Create a report to help us improve +title: "[BUG]" +labels: bug +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**Is this a possible security vulnerability?** +- [ ] yes -- if yes, stop here and contact security@polaris.io instead +- [ ] no + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**System info (please complete the following information):** + - OS: [e.g. Windows] + - Polaris Catalog Version [e.g. 0.3.0] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000000..813747531d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: "[FEATURE REQUEST]" +labels: enhancement +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 0c23671569..5759e92308 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1 +1,41 @@ - +# Description + +Please include a summary of the changes and the related issue. Please also include relevant motivation and context. List any dependencies that are required for this change. + +Fixes # (issue) + +## Type of change + +Please delete options that are not relevant. + +- [ ] Bug fix (non-breaking change which fixes an issue) +- [ ] New feature (non-breaking change which adds functionality) +- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) +- [ ] This change requires a documentation update + +# How Has This Been Tested? + +Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration + +- [ ] Test A +- [ ] Test B + +**Test Configuration**: +* Firmware version: +* Hardware: +* Toolchain: +* SDK: + +# Checklist: + +Please delete options that are not relevant. + +- [ ] I have performed a self-review of my code +- [ ] I have commented my code, particularly in hard-to-understand areas +- [ ] I have made corresponding changes to the documentation +- [ ] My changes generate no new warnings +- [ ] I have added tests that prove my fix is effective or that my feature works +- [ ] New and existing unit tests pass locally with my changes +- [ ] Any dependent changes have been merged and published in downstream modules +- [ ] If adding new functionality, I have discussed my implementation with the community using the linked GitHub issue +- [ ] I have signed and submitted the [ICLA](../ICLA.md) and if needed, the [CCLA](../CCLA.md). See [Contributing](../CONTRIBUTING.md) for details. From 4fbe230cdfaf0410619bf5fadbfd5c82cb163235 Mon Sep 17 00:00:00 2001 From: Dennis Huo <7410123+dennishuo@users.noreply.github.com> Date: Mon, 29 Jul 2024 14:37:17 -0700 Subject: [PATCH 17/27] Add ascii art banner after initialization. (#45) * Add ascii art banner after initialization. --- .../polaris/service/PolarisApplication.java | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/polaris-service/src/main/java/io/polaris/service/PolarisApplication.java b/polaris-service/src/main/java/io/polaris/service/PolarisApplication.java index c8e0c24489..b903fc8b4a 100644 --- a/polaris-service/src/main/java/io/polaris/service/PolarisApplication.java +++ b/polaris-service/src/main/java/io/polaris/service/PolarisApplication.java @@ -106,6 +106,33 @@ public class PolarisApplication extends Application { public static void main(final String[] args) throws Exception { new PolarisApplication().run(args); + printAsciiArt(); + } + + private static void printAsciiArt() { + String bannerArt = + String.join( + "\n", + " @@@@ @@@ @ @ @@@@ @ @@@@ @@@@ @ @@@@@ @ @ @@@ @@@@ ", + " @ @ @ @ @ @ @ @ @ @ @@ @ @ @ @ @ @ @ @ @ @ ", + " @@@@ @ @ @ @@@@@ @@@@ @ @@ @ @@@@@ @ @@@@@ @ @ @ @ @@@", + " @ @@@ @@@@ @ @ @ @@ @ @@@@ @@@@ @ @ @ @@ @@ @@@@ @@@ @@@@ ", + " ", + " ", + " ", + " ", + " /////| ", + " //||///T||| ", + " ///|||////|||||| ", + " //||||T////||||||||| ", + " /T| //|||||T///T||//T|||||| ", + " //|||/////T||////||/////||||||| //|| ", + " //||||||T///////////////////T|||||||T||||| ", + " //||||/////T|//////////|///////T|||||T|||||||| ", + " //|||||/////|||T////////////////||||||/||||||||| ", + ",,..,,,..,,,..,//||||////////||||||||||/////////|||||///||||||||||,,,..,,..,,,..,,,.", + ",,..,,,..,,,..,,,..,,,..,,,..,,,..,,,..,,,..,,,..,,,..,,,..,,,..,,,..,,,.,,,..,,,..,"); + System.out.println(bannerArt.replaceAll("\\|", "\\\\")); } @Override From 0bc3c59965a10801abf4dfc7a027cb51330b1299 Mon Sep 17 00:00:00 2001 From: Anna Filippova <7892219+annafil@users.noreply.github.com> Date: Mon, 29 Jul 2024 17:14:11 -0700 Subject: [PATCH 18/27] Update + consolidate markdown + API docs (#26) * Catch up markdown docs and consolidate all docs * Update how to generate docs * Remove unnecessary files and make sure logo works * Update logo location * make logo relative to docs directory * add license headers --- README.md | 12 +- docs/access-control.md | 188 + docs/entities.md | 2 - docs/iceberg-rest/index.html | 1326 ---- docs/img/example-workflow.svg | 1 + ...laris-Catalog-BLOG-symmetrical-subhead.png | Bin 0 -> 856695 bytes docs/img/logos/polaris-brandmark.png | Bin 0 -> 283351 bytes .../logos/polaris-catalog-stacked-logo.svg | 17 + docs/img/logos/polaris-favicon.png | Bin 0 -> 12969 bytes docs/img/overview.svg | 1 + docs/img/rbac-example.svg | 1 + docs/img/rbac-model.svg | 1 + docs/img/sample-catalog-structure.svg | 1 + docs/index.html | 3844 ++++++++- docs/overview.md | 213 + docs/polaris-management/index.html | 877 --- spec/docs.yaml | 27 + spec/index.yaml | 6860 +++++++++++++++++ spec/redocly.yaml | 11 + 19 files changed, 10791 insertions(+), 2591 deletions(-) create mode 100644 docs/access-control.md delete mode 100644 docs/iceberg-rest/index.html create mode 100644 docs/img/example-workflow.svg create mode 100644 docs/img/logos/Polaris-Catalog-BLOG-symmetrical-subhead.png create mode 100644 docs/img/logos/polaris-brandmark.png create mode 100644 docs/img/logos/polaris-catalog-stacked-logo.svg create mode 100644 docs/img/logos/polaris-favicon.png create mode 100644 docs/img/overview.svg create mode 100644 docs/img/rbac-example.svg create mode 100644 docs/img/rbac-model.svg create mode 100644 docs/img/sample-catalog-structure.svg create mode 100644 docs/overview.md delete mode 100644 docs/polaris-management/index.html create mode 100644 spec/docs.yaml create mode 100644 spec/index.yaml create mode 100644 spec/redocly.yaml diff --git a/README.md b/README.md index 1b6fde1128..b68f86cbc1 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Polaris Catalog is an open source catalog for Apache Iceberg. Polaris Catalog implements Iceberg’s open REST API for multi-engine interoperability with Apache Doris, Apache Flink, Apache Spark, PyIceberg, StarRocks and Trino. -1200x500_DCS24_PR-Banner-Polaris Catalog-02@2x +![Polaris Catalog Header](docs/img/logos/Polaris-Catalog-BLOG-symmetrical-subhead.png) ## Status @@ -34,16 +34,16 @@ Polaris Catalog is open source under an Apache 2.0 license. API docs are hosted via Github Pages at https://polaris-catalog.github.io/polaris. All updates to the main branch update the hosted docs. -The Polaris management API docs are found [here](docs%2Fpolaris-management%2Findex.html) +The Polaris management API docs are found [here](https://polaris-catalog.github.io/polaris/index.html#tag/polaris-management-service_other) -The open source Iceberg REST API docs are at [index.html](docs%2Ficeberg-rest%2Findex.html) +The open source Iceberg REST API docs are found [here](https://polaris-catalog.github.io/polaris/index.html#tag/Configuration-API) -Docs are generated using Redocly. They can be regenerated by running the following commands +Docs are generated using [Redocly](https://redocly.com/docs/cli/installation). They can be regenerated by running the following commands from the project root directory ```bash -docker run -p 8080:80 -v ${PWD}:/spec redocly/cli build-docs spec/polaris-management-service.yml --output=docs/polaris-management/index.html -docker run -p 8080:80 -v ${PWD}:/spec redocly/cli build-docs spec/rest-catalog-open-api.yaml --output=docs/iceberg-rest/index.html +docker run -p 8080:80 -v ${PWD}:/spec redocly/cli join spec/docs.yaml spec/polaris-management-service.yml spec/rest-catalog-open-api.yaml -o spec/index.yaml --prefix-components-with-info-prop title +docker run -p 8080:80 -v ${PWD}:/spec redocly/cli build-docs spec/index.yaml --output=docs/index.html --config=spec/redocly.yaml ``` # Setup diff --git a/docs/access-control.md b/docs/access-control.md new file mode 100644 index 0000000000..fe5ceb1316 --- /dev/null +++ b/docs/access-control.md @@ -0,0 +1,188 @@ + + +This section provides information about how access control works for Polaris Catalog. + +Polaris Catalog uses a role-based access control (RBAC) model, in which the Polaris administrator assigns access privileges to catalog roles, +and then grants service principals access to resources by assigning catalog roles to principal roles. + +The key concepts to understanding access control in Polaris are: + +- **Securable object** +- **Principal role** +- **Catalog role** +- **Privilege** + +## Securable object + +A securable object is an object to which access can be granted. Polaris +has the following securable objects: + +- Catalog +- Namespace +- Iceberg table +- View + +## Principal role + +A principal role is a resource in Polaris that you can use to logically group Polaris service principals together and grant privileges on +securable objects. + +Polaris supports a many-to-one relationship between service principals and principal roles. For example, to grant the same privileges to +multiple service principals, you can grant a single principal role to those service principals. A service principal can be granted one +principal role. When registering a service connection, the Polaris administrator specifies the principal role that is granted to the +service principal. + +You don't grant privileges directly to a principal role. Instead, you configure object permissions at the catalog role level, and then grant +catalog roles to a principal role. + +The following table shows examples of principal roles that you might configure in Polaris: + +| Principal role name | Description | +| -----------------------| ----------- | +| Data_engineer | A role that is granted to multiple service principals for running data engineering jobs. | +| Data_scientist | A role that is granted to multiple service principals for running data science or AI jobs. | + +## Catalog role + +A catalog role belongs to a particular catalog resource in Polaris and specifies a set of permissions for actions on the catalog, or on objects +in the catalog, such as catalog namespaces or tables. You can create one or more catalog roles for a catalog. + +You grant privileges to a catalog role, and then grant the catalog role to a principal role to bestow the privileges to one or more service +principals. + +**Note** + +If you update the privileges bestowed to a service principal, the updates won\'t take effect for up to one hour. This means that if you +revoke or grant some privileges for a catalog, the updated privileges won\'t take effect on any service principal with access to that catalog +for up to one hour. + +Polaris also supports a many-to-many relationship between catalog roles and principal roles. You can grant the same catalog role to one or more +principal roles. Likewise, a principal role can be granted to one or more catalog roles. + +The following table displays examples of catalog roles that you might +configure in Polaris: + +| Example Catalog role | Description | +| -----------------------| ----------- | +| Catalog administrators | A role that has been granted multiple privileges to emulate full access to the catalog.

Principal roles that have been granted this role are permitted to create, alter, read, write, and drop tables in the catalog. | +| Catalog readers | A role that has been granted read-only privileges to tables in the catalog.

Principal roles that have been granted this role are allowed to read from tables in the catalog. | +| Catalog contributor | A role that has been granted read and write access privileges to all tables that belong to the catalog.

Principal roles that have been granted this role are allowed to perform read and write operations on tables in the catalog. | + +## RBAC model + +The following diagram illustrates the RBAC model used by Polaris Catalog. For each catalog, the Polaris administrator assigns access +privileges to catalog roles, and then grants service principals access to resources by assigning catalog roles to principal roles. Polaris +supports a many-to-one relationship between service principals and principal roles. + +![Diagram that shows the RBAC model for Polaris Catalog.](./img/rbac-model.svg "Polaris Catalog RBAC model") + +## Access control privileges + +This section describes the privileges that are available in the Polaris access control model. Privileges are granted to catalog roles, catalog +roles are granted to principal roles, and principal roles are granted to service principals to specify the operations that service principals can +perform on objects in Polaris. + +To grant the full set of privileges (drop, list, read, write, etc.) on an object, you can use the *full privilege* option. + +### Table privileges + +**Note** + +The TABLE_FULL_METADATA full privilege doesn't grant access to the TABLE_READ_DATA or TABLE_WRITE_DATA individual privileges. + +| Full privilege | Individual privilege | Description | +| -----------------------| ----------- | ---- | +| TABLE_FULL_METADATA | TABLE_CREATE | Enables registering a table with the catalog. | +| | TABLE_DROP | Enables dropping a table from the catalog. | +| | TABLE_LIST | Enables listing any tables in the catalog. | +| | TABLE_READ_PROPERTIES | Enables reading [properties](https://iceberg.apache.org/docs/nightly/configuration/#table-properties) of the table. | +| | TABLE_WRITE_PROPERTIES | Enables configuring [properties](https://iceberg.apache.org/docs/nightly/configuration/#table-properties) for the table. | +| N/A | TABLE_READ_DATA | Enables reading data from the table by receiving short-lived read-only storage credentials from the catalog. | +| N/A | TABLE_WRITE_DATA | Enables writing data to the table by receiving short-lived read+write storage credentials from the catalog. | + +### View privileges + +| Full privilege | Individual privilege | Description | +| -----------------------| ----------- | ---- | +| VIEW_FULL_METADATA | VIEW_CREATE | Enables registering a view with the catalog. | +| | VIEW_DROP | Enables dropping a view from the catalog. | +| | VIEW_LIST | Enables listing any views in the catalog. | +| | VIEW_READ_PROPERTIES | Enables reading all the view properties. | +| | VIEW_WRITE_PROPERTIES | Enables configuring view properties. | + +### Namespace privileges + +| Full privilege | Individual privilege | Description | +| -----------------------| ----------- | ---- | +| NAMESPACE_FULL_METADATA | NAMESPACE_CREATE | Enables creating a namespace in a catalog. | +| | NAMESPACE_DROP | Enables dropping the namespace from the catalog. | +| | NAMESPACE_LIST | Enables listing any object in the namespace, including nested namespaces and tables. | +| | NAMESPACE_READ_PROPERTIES | Enables reading all the namespace properties. | +| | NAMESPACE_WRITE_PROPERTIES | Enables configuring namespace properties. | + +### Catalog privileges + +| Privilege | Description | +| -----------------------| ----------- | +| CATALOG_MANAGE_ACCESS | Includes the ability to grant or revoke privileges on objects in a catalog to catalog roles, and the ability to grant or revoke catalog roles to or from principal roles. | +| CATALOG_MANAGE_CONTENT | Enables full management of content for the catalog. This privilege encompasses the following privileges:

  • CATALOG_MANAGE_METADATA
  • TABLE_FULL_METADATA
  • NAMESPACE_FULL_METADATA
  • VIEW_FULL_METADATA
  • TABLE_WRITE_DATA
  • TABLE_READ_DATA
  • CATALOG_READ_PROPERTIES
  • CATALOG_WRITE_PROPERTIES
| +| CATALOG_MANAGE_METADATA | Enables full management of the catalog, as well as catalog roles, namespaces, and tables. | +| CATALOG_READ_PROPERTIES | Enables listing catalogs and reading properties of the catalog. | +| CATALOG_WRITE_PROPERTIES | Enables configuring catalog properties. | + +## RBAC example + +The following diagram illustrates how RBAC works in Polaris, and +includes the following users: + +- **Alice**: A service admin who signs up for Polaris. Alice can + create service principals. She can also create catalogs and + namespaces, and configure access control for Polaris resources. + +> **Note** +> +> The service principal for Alice is not visible in the Polaris Catalog +> user interface. + +- **Bob**: A data engineer who uses Snowpipe Streaming (in Snowflake) + and Apache Spark connections to interact with Polaris. + + - Alice has created a service principal for Bob. It has been + granted the Data_engineer principal role, which in turn has been + granted the following catalog roles: Catalog contributor and + Data administrator (for both the Silver and Gold zone catalogs + in the following diagram). + + - The Catalog contributor role grants permission to create + namespaces and tables in the Bronze zone catalog. + + - The Data administrator roles grant full administrative rights to + the Silver zone catalog and Gold zone catalog. + +- **Mark**: A data scientist who uses Snowflake AI services to + interact with Polaris. + + - Alice has created a service principal for Mark. It has been + granted the Data_scientist principal role, which in turn has + been granted the catalog role named Catalog reader. + + - The Catalog reader role grants read-only access for a catalog + named Gold zone catalog. + +![Diagram that shows an example of how RBAC works in Polaris Catalog.](./img/rbac-example.svg "Polaris Catalog RBAC example") diff --git a/docs/entities.md b/docs/entities.md index c1d74d7977..c0cf1d650f 100644 --- a/docs/entities.md +++ b/docs/entities.md @@ -14,8 +14,6 @@ limitations under the License. --> -# Polaris Entities - This page documents various entities that can be managed in Polaris. ## Catalog diff --git a/docs/iceberg-rest/index.html b/docs/iceberg-rest/index.html deleted file mode 100644 index e6e2ca8117..0000000000 --- a/docs/iceberg-rest/index.html +++ /dev/null @@ -1,1326 +0,0 @@ - - - - - - - - Apache Iceberg REST Catalog API - - - - - - - - - -

Apache Iceberg REST Catalog API (0.0.1)

Download OpenAPI specification:Download

License: Apache 2.0

Defines the specification for the first version of the REST Catalog API. Implementations should ideally support both Iceberg table specs v1 and v2, with priority given to v2.

-

Configuration API

List all catalog configuration settings

All REST clients should first call this route to get catalog configuration properties from the server to configure the catalog and its HTTP client. Configuration from the server consists of two sets of key/value pairs.

-
    -
  • defaults - properties that should be used as default configuration; applied before client configuration
  • -
  • overrides - properties that should be used to override client configuration; applied after defaults and client configuration
  • -
-

Catalog configuration is constructed by setting the defaults, then client- provided configuration, and finally overrides. The final property set is then used to configure the catalog.

-

For example, a default configuration property might set the size of the client pool, which can be replaced with a client-specific setting. An override might be used to set the warehouse location, which is stored on the server rather than in client configuration.

-

Common catalog configuration settings are documented at https://iceberg.apache.org/docs/latest/configuration/#catalog-properties

-
Authorizations:
OAuth2BearerAuth
query Parameters
warehouse
string

Warehouse location or identifier to request from the service

-

Responses

Response samples

Content type
application/json
{
  • "overrides": {
    },
  • "defaults": {
    }
}

OAuth2 API

Get a token using an OAuth2 flow

Exchange credentials for a token using the OAuth2 client credentials flow or token exchange.

-

This endpoint is used for three purposes -

-
    -
  1. To exchange client credentials (client ID and secret) for an access token This uses the client credentials flow.
  2. -
  3. To exchange a client token and an identity token for a more specific access token This uses the token exchange flow.
  4. -
  5. To exchange an access token for one with the same claims and a refreshed expiration period This uses the token exchange flow.
  6. -
-

For example, a catalog client may be configured with client credentials from the OAuth2 Authorization flow. This client would exchange its client ID and secret for an access token using the client credentials request with this endpoint (1). Subsequent requests would then use that access token.

-

Some clients may also handle sessions that have additional user context. These clients would use the token exchange flow to exchange a user token (the "subject" token) from the session for a more specific access token for that user, using the catalog's access token as the "actor" token (2). The user ID token is the "subject" token and can be any token type allowed by the OAuth2 token exchange flow, including a unsecured JWT token with a sub claim. This request should use the catalog's bearer token in the "Authorization" header.

-

Clients may also use the token exchange flow to refresh a token that is about to expire by sending a token exchange request (3). The request's "subject" token should be the expiring token. This request should use the subject token in the "Authorization" header.

-
Authorizations:
BearerAuth
Request Body schema: application/x-www-form-urlencoded
required
Any of
grant_type
required
string
Value: "client_credentials"
scope
string
client_id
required
string

Client ID

-

This can be sent in the request body, but OAuth2 recommends sending it in a Basic Authorization header.

-
client_secret
required
string

Client secret

-

This can be sent in the request body, but OAuth2 recommends sending it in a Basic Authorization header.

-

Responses

Response samples

Content type
application/json
{
  • "access_token": "string",
  • "token_type": "bearer",
  • "expires_in": 0,
  • "issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
  • "refresh_token": "string",
  • "scope": "string"
}

Catalog API

List namespaces, optionally providing a parent namespace to list underneath

List all namespaces at a certain level, optionally starting from a given parent namespace. If table accounting.tax.paid.info exists, using 'SELECT NAMESPACE IN accounting' would translate into GET /namespaces?parent=accounting and must return a namespace, ["accounting", "tax"] only. Using 'SELECT NAMESPACE IN accounting.tax' would translate into GET /namespaces?parent=accounting%1Ftax and must return a namespace, ["accounting", "tax", "paid"]. If parent is not provided, all top-level namespaces should be listed.

-
Authorizations:
OAuth2BearerAuth
path Parameters
prefix
required
string

An optional prefix in the path

-
query Parameters
pageToken
string or null (PageToken)

An opaque token that allows clients to make use of pagination for list APIs (e.g. ListTables). Clients may initiate the first paginated request by sending an empty query parameter pageToken to the server. -Servers that support pagination should identify the pageToken parameter and return a next-page-token in the response if there are more results available. After the initial request, the value of next-page-token from each response must be used as the pageToken parameter value for the next request. The server must return null value for the next-page-token in the last response. -Servers that support pagination must return all results in a single response with the value of next-page-token set to null if the query parameter pageToken is not set in the request. -Servers that do not support pagination should ignore the pageToken parameter and return all results in a single response. The next-page-token must be omitted from the response. -Clients must interpret either null or missing response value of next-page-token as the end of the listing results.

-
pageSize
integer >= 1

For servers that support pagination, this signals an upper bound of the number of results that a client will receive. For servers that do not support pagination, clients may receive results larger than the indicated pageSize.

-
parent
string
Example: parent=accounting%1Ftax

An optional namespace, underneath which to list namespaces. If not provided or empty, all top-level namespaces should be listed. If parent is a multipart namespace, the parts must be separated by the unit separator (0x1F) byte.

-

Responses

Response samples

Content type
application/json
Example
{
  • "namespaces": [
    ]
}

Create a namespace

Create a namespace, with an optional set of properties. The server might also add properties, such as last_modified_time etc.

-
Authorizations:
OAuth2BearerAuth
path Parameters
prefix
required
string

An optional prefix in the path

-
Request Body schema: application/json
required
namespace
required
Array of strings (Namespace)

Reference to one or more levels of a namespace

-
object
Default: {}

Configured string to string map of properties for the namespace

-

Responses

Request samples

Content type
application/json
{
  • "namespace": [
    ],
  • "properties": {
    }
}

Response samples

Content type
application/json
{
  • "namespace": [
    ],
  • "properties": {
    }
}

Load the metadata properties for a namespace

Return all stored metadata properties for a given namespace

-
Authorizations:
OAuth2BearerAuth
path Parameters
prefix
required
string

An optional prefix in the path

-
namespace
required
string
Examples:
  • accounting -
  • accounting%1Ftax -

A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (0x1F) byte.

-

Responses

Response samples

Content type
application/json
{
  • "namespace": [
    ],
  • "properties": {
    }
}

Check if a namespace exists

Check if a namespace exists. The response does not contain a body.

-
Authorizations:
OAuth2BearerAuth
path Parameters
prefix
required
string

An optional prefix in the path

-
namespace
required
string
Examples:
  • accounting -
  • accounting%1Ftax -

A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (0x1F) byte.

-

Responses

Response samples

Content type
application/json
{
  • "error": {
    }
}

Drop a namespace from the catalog. Namespace must be empty.

Authorizations:
OAuth2BearerAuth
path Parameters
prefix
required
string

An optional prefix in the path

-
namespace
required
string
Examples:
  • accounting -
  • accounting%1Ftax -

A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (0x1F) byte.

-

Responses

Response samples

Content type
application/json
{
  • "error": {
    }
}

Set or remove properties on a namespace

Set and/or remove properties on a namespace. The request body specifies a list of properties to remove and a map of key value pairs to update. -Properties that are not in the request are not modified or removed by this call. -Server implementations are not required to support namespace properties.

-
Authorizations:
OAuth2BearerAuth
path Parameters
prefix
required
string

An optional prefix in the path

-
namespace
required
string
Examples:
  • accounting -
  • accounting%1Ftax -

A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (0x1F) byte.

-
Request Body schema: application/json
required
removals
Array of strings unique
object

Responses

Request samples

Content type
application/json
{
  • "removals": [
    ],
  • "updates": {
    }
}

Response samples

Content type
application/json
{
  • "updated": [
    ],
  • "removed": [
    ],
  • "missing": [
    ]
}

List all table identifiers underneath a given namespace

Return all table identifiers under this namespace

-
Authorizations:
OAuth2BearerAuth
path Parameters
prefix
required
string

An optional prefix in the path

-
namespace
required
string
Examples:
  • accounting -
  • accounting%1Ftax -

A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (0x1F) byte.

-
query Parameters
pageToken
string or null (PageToken)

An opaque token that allows clients to make use of pagination for list APIs (e.g. ListTables). Clients may initiate the first paginated request by sending an empty query parameter pageToken to the server. -Servers that support pagination should identify the pageToken parameter and return a next-page-token in the response if there are more results available. After the initial request, the value of next-page-token from each response must be used as the pageToken parameter value for the next request. The server must return null value for the next-page-token in the last response. -Servers that support pagination must return all results in a single response with the value of next-page-token set to null if the query parameter pageToken is not set in the request. -Servers that do not support pagination should ignore the pageToken parameter and return all results in a single response. The next-page-token must be omitted from the response. -Clients must interpret either null or missing response value of next-page-token as the end of the listing results.

-
pageSize
integer >= 1

For servers that support pagination, this signals an upper bound of the number of results that a client will receive. For servers that do not support pagination, clients may receive results larger than the indicated pageSize.

-

Responses

Response samples

Content type
application/json
Example
{
  • "identifiers": [
    ]
}

Create a table in the given namespace

Create a table or start a create transaction, like atomic CTAS.

-

If stage-create is false, the table is created immediately.

-

If stage-create is true, the table is not created, but table metadata is initialized and returned. The service should prepare as needed for a commit to the table commit endpoint to complete the create transaction. The client uses the returned metadata to begin a transaction. To commit the transaction, the client sends all create and subsequent changes to the table commit route. Changes from the table create operation include changes like AddSchemaUpdate and SetCurrentSchemaUpdate that set the initial table state.

-
Authorizations:
OAuth2BearerAuth
path Parameters
prefix
required
string

An optional prefix in the path

-
namespace
required
string
Examples:
  • accounting -
  • accounting%1Ftax -

A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (0x1F) byte.

-
header Parameters
X-Iceberg-Access-Delegation
string
Enum: "vended-credentials" "remote-signing"
Example: vended-credentials,remote-signing

Optional signal to the server that the client supports delegated access via a comma-separated list of access mechanisms. The server may choose to supply access via any or none of the requested mechanisms.

-

Specific properties and handling for vended-credentials is documented in the LoadTableResult schema section of this spec document.

-

The protocol and specification for remote-signing is documented in the s3-signer-open-api.yaml OpenApi spec in the aws module.

-
Request Body schema: application/json
required
name
required
string
location
string
required
object (Schema)
object (PartitionSpec)
object (SortOrder)
stage-create
boolean
object

Responses

Request samples

Content type
application/json
{
  • "name": "string",
  • "location": "string",
  • "schema": {
    },
  • "partition-spec": {
    },
  • "write-order": {
    },
  • "stage-create": true,
  • "properties": {
    }
}

Response samples

Content type
application/json
{
  • "metadata-location": "string",
  • "metadata": {
    },
  • "config": {
    }
}

Register a table in the given namespace using given metadata file location

Register a table using given metadata file location.

-
Authorizations:
OAuth2BearerAuth
path Parameters
prefix
required
string

An optional prefix in the path

-
namespace
required
string
Examples:
  • accounting -
  • accounting%1Ftax -

A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (0x1F) byte.

-
Request Body schema: application/json
required
name
required
string
metadata-location
required
string

Responses

Request samples

Content type
application/json
{
  • "name": "string",
  • "metadata-location": "string"
}

Response samples

Content type
application/json
{
  • "metadata-location": "string",
  • "metadata": {
    },
  • "config": {
    }
}

Load a table from the catalog

Load a table from the catalog.

-

The response contains both configuration and table metadata. The configuration, if non-empty is used as additional configuration for the table that overrides catalog configuration. For example, this configuration may change the FileIO implementation to be used for the table.

-

The response also contains the table's full metadata, matching the table metadata JSON file.

-

The catalog configuration may contain credentials that should be used for subsequent requests for the table. The configuration key "token" is used to pass an access token to be used as a bearer token for table requests. Otherwise, a token may be passed using a RFC 8693 token type as a configuration key. For example, "urn:ietf:params:oauth:token-type:jwt=".

-
Authorizations:
OAuth2BearerAuth
path Parameters
prefix
required
string

An optional prefix in the path

-
namespace
required
string
Examples:
  • accounting -
  • accounting%1Ftax -

A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (0x1F) byte.

-
table
required
string
Example: sales

A table name

-
query Parameters
snapshots
string
Enum: "all" "refs"

The snapshots to return in the body of the metadata. Setting the value to all would return the full set of snapshots currently valid for the table. Setting the value to refs would load all snapshots referenced by branches or tags. -Default if no param is provided is all.

-
header Parameters
X-Iceberg-Access-Delegation
string
Enum: "vended-credentials" "remote-signing"
Example: vended-credentials,remote-signing

Optional signal to the server that the client supports delegated access via a comma-separated list of access mechanisms. The server may choose to supply access via any or none of the requested mechanisms.

-

Specific properties and handling for vended-credentials is documented in the LoadTableResult schema section of this spec document.

-

The protocol and specification for remote-signing is documented in the s3-signer-open-api.yaml OpenApi spec in the aws module.

-

Responses

Response samples

Content type
application/json
{
  • "metadata-location": "string",
  • "metadata": {
    },
  • "config": {
    }
}

Commit updates to a table

Commit updates to a table.

-

Commits have two parts, requirements and updates. Requirements are assertions that will be validated before attempting to make and commit changes. For example, assert-ref-snapshot-id will check that a named ref's snapshot ID has a certain value.

-

Updates are changes to make to table metadata. For example, after asserting that the current main ref is at the expected snapshot, a commit may add a new child snapshot and set the ref to the new snapshot id.

-

Create table transactions that are started by createTable with stage-create set to true are committed using this route. Transactions should include all changes to the table, including table initialization, like AddSchemaUpdate and SetCurrentSchemaUpdate. The assert-create requirement is used to ensure that the table was not created concurrently.

-
Authorizations:
OAuth2BearerAuth
path Parameters
prefix
required
string

An optional prefix in the path

-
namespace
required
string
Examples:
  • accounting -
  • accounting%1Ftax -

A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (0x1F) byte.

-
table
required
string
Example: sales

A table name

-
Request Body schema: application/json
required
object (TableIdentifier)
required
Array of objects (TableRequirement)
required
Array of AssignUUIDUpdate (object) or UpgradeFormatVersionUpdate (object) or AddSchemaUpdate (object) or SetCurrentSchemaUpdate (object) or AddPartitionSpecUpdate (object) or SetDefaultSpecUpdate (object) or AddSortOrderUpdate (object) or SetDefaultSortOrderUpdate (object) or AddSnapshotUpdate (object) or SetSnapshotRefUpdate (object) or RemoveSnapshotsUpdate (object) or RemoveSnapshotRefUpdate (object) or SetLocationUpdate (object) or SetPropertiesUpdate (object) or RemovePropertiesUpdate (object) or SetStatisticsUpdate (object) or RemoveStatisticsUpdate (object) (TableUpdate)

Responses

Request samples

Content type
application/json
{
  • "identifier": {
    },
  • "requirements": [
    ],
  • "updates": [
    ]
}

Response samples

Content type
application/json
{
  • "metadata-location": "string",
  • "metadata": {
    }
}

Drop a table from the catalog

Remove a table from the catalog

-
Authorizations:
OAuth2BearerAuth
path Parameters
prefix
required
string

An optional prefix in the path

-
namespace
required
string
Examples:
  • accounting -
  • accounting%1Ftax -

A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (0x1F) byte.

-
table
required
string
Example: sales

A table name

-
query Parameters
purgeRequested
boolean
Default: false

Whether the user requested to purge the underlying table's data and metadata

-

Responses

Response samples

Content type
application/json
{
  • "error": {
    }
}

Check if a table exists

Check if a table exists within a given namespace. The response does not contain a body.

-
Authorizations:
OAuth2BearerAuth
path Parameters
prefix
required
string

An optional prefix in the path

-
namespace
required
string
Examples:
  • accounting -
  • accounting%1Ftax -

A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (0x1F) byte.

-
table
required
string
Example: sales

A table name

-

Responses

Response samples

Content type
application/json
{
  • "error": {
    }
}

Rename a table from its current name to a new name

Rename a table from one identifier to another. It's valid to move a table across namespaces, but the server implementation is not required to support it.

-
Authorizations:
OAuth2BearerAuth
path Parameters
prefix
required
string

An optional prefix in the path

-
Request Body schema: application/json
required

Current table identifier to rename and new table identifier to rename to

-
required
object (TableIdentifier)
required
object (TableIdentifier)

Responses

Request samples

Content type
application/json
{
  • "source": {
    },
  • "destination": {
    }
}

Response samples

Content type
application/json
{
  • "error": {
    }
}

Send a metrics report to this endpoint to be processed by the backend

Authorizations:
OAuth2BearerAuth
path Parameters
prefix
required
string

An optional prefix in the path

-
namespace
required
string
Examples:
  • accounting -
  • accounting%1Ftax -

A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (0x1F) byte.

-
table
required
string
Example: sales

A table name

-
Request Body schema: application/json
required

The request containing the metrics report to be sent

-
Any of
table-name
required
string
snapshot-id
required
integer <int64>
required
AndOrExpression (object) or NotExpression (object) or SetExpression (object) or LiteralExpression (object) or UnaryExpression (object) (Expression)
schema-id
required
integer
projected-field-ids
required
Array of integers
projected-field-names
required
Array of strings
required
object (Metrics)
object
report-type
required
string

Responses

Request samples

Content type
application/json
Example
{
  • "table-name": "string",
  • "snapshot-id": 0,
  • "filter": {
    },
  • "schema-id": 0,
  • "projected-field-ids": [
    ],
  • "projected-field-names": [
    ],
  • "metrics": {
    },
  • "metadata": {
    },
  • "report-type": "string"
}

Response samples

Content type
application/json
{
  • "error": {
    }
}

Commit updates to multiple tables in an atomic operation

Authorizations:
OAuth2BearerAuth
path Parameters
prefix
required
string

An optional prefix in the path

-
Request Body schema: application/json
required

Commit updates to multiple tables in an atomic operation

-

A commit for a single table consists of a table identifier with requirements and updates. Requirements are assertions that will be validated before attempting to make and commit changes. For example, assert-ref-snapshot-id will check that a named ref's snapshot ID has a certain value.

-

Updates are changes to make to table metadata. For example, after asserting that the current main ref is at the expected snapshot, a commit may add a new child snapshot and set the ref to the new snapshot id.

-
required
Array of objects (CommitTableRequest)

Responses

Request samples

Content type
application/json
{
  • "table-changes": [
    ]
}

Response samples

Content type
application/json
{
  • "error": {
    }
}

List all view identifiers underneath a given namespace

Return all view identifiers under this namespace

-
Authorizations:
OAuth2BearerAuth
path Parameters
prefix
required
string

An optional prefix in the path

-
namespace
required
string
Examples:
  • accounting -
  • accounting%1Ftax -

A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (0x1F) byte.

-
query Parameters
pageToken
string or null (PageToken)

An opaque token that allows clients to make use of pagination for list APIs (e.g. ListTables). Clients may initiate the first paginated request by sending an empty query parameter pageToken to the server. -Servers that support pagination should identify the pageToken parameter and return a next-page-token in the response if there are more results available. After the initial request, the value of next-page-token from each response must be used as the pageToken parameter value for the next request. The server must return null value for the next-page-token in the last response. -Servers that support pagination must return all results in a single response with the value of next-page-token set to null if the query parameter pageToken is not set in the request. -Servers that do not support pagination should ignore the pageToken parameter and return all results in a single response. The next-page-token must be omitted from the response. -Clients must interpret either null or missing response value of next-page-token as the end of the listing results.

-
pageSize
integer >= 1

For servers that support pagination, this signals an upper bound of the number of results that a client will receive. For servers that do not support pagination, clients may receive results larger than the indicated pageSize.

-

Responses

Response samples

Content type
application/json
Example
{
  • "identifiers": [
    ]
}

Create a view in the given namespace

Create a view in the given namespace.

-
Authorizations:
OAuth2BearerAuth
path Parameters
prefix
required
string

An optional prefix in the path

-
namespace
required
string
Examples:
  • accounting -
  • accounting%1Ftax -

A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (0x1F) byte.

-
Request Body schema: application/json
required
name
required
string
location
string
required
object (Schema)
required
object (ViewVersion)
required
object

Responses

Request samples

Content type
application/json
{
  • "name": "string",
  • "location": "string",
  • "schema": {
    },
  • "view-version": {
    },
  • "properties": {
    }
}

Response samples

Content type
application/json
{
  • "metadata-location": "string",
  • "metadata": {
    },
  • "config": {
    }
}

Load a view from the catalog

Load a view from the catalog.

-

The response contains both configuration and view metadata. The configuration, if non-empty is used as additional configuration for the view that overrides catalog configuration.

-

The response also contains the view's full metadata, matching the view metadata JSON file.

-

The catalog configuration may contain credentials that should be used for subsequent requests for the view. The configuration key "token" is used to pass an access token to be used as a bearer token for view requests. Otherwise, a token may be passed using a RFC 8693 token type as a configuration key. For example, "urn:ietf:params:oauth:token-type:jwt=".

-
Authorizations:
OAuth2BearerAuth
path Parameters
prefix
required
string

An optional prefix in the path

-
namespace
required
string
Examples:
  • accounting -
  • accounting%1Ftax -

A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (0x1F) byte.

-
view
required
string
Example: sales

A view name

-

Responses

Response samples

Content type
application/json
{
  • "metadata-location": "string",
  • "metadata": {
    },
  • "config": {
    }
}

Replace a view

Commit updates to a view.

-
Authorizations:
OAuth2BearerAuth
path Parameters
prefix
required
string

An optional prefix in the path

-
namespace
required
string
Examples:
  • accounting -
  • accounting%1Ftax -

A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (0x1F) byte.

-
view
required
string
Example: sales

A view name

-
Request Body schema: application/json
required
object (TableIdentifier)
Array of objects (ViewRequirement)
required
Array of AssignUUIDUpdate (object) or UpgradeFormatVersionUpdate (object) or AddSchemaUpdate (object) or SetLocationUpdate (object) or SetPropertiesUpdate (object) or RemovePropertiesUpdate (object) or AddViewVersionUpdate (object) or SetCurrentViewVersionUpdate (object) (ViewUpdate)

Responses

Request samples

Content type
application/json
{
  • "identifier": {
    },
  • "requirements": [
    ],
  • "updates": [
    ]
}

Response samples

Content type
application/json
{
  • "metadata-location": "string",
  • "metadata": {
    },
  • "config": {
    }
}

Drop a view from the catalog

Remove a view from the catalog

-
Authorizations:
OAuth2BearerAuth
path Parameters
prefix
required
string

An optional prefix in the path

-
namespace
required
string
Examples:
  • accounting -
  • accounting%1Ftax -

A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (0x1F) byte.

-
view
required
string
Example: sales

A view name

-

Responses

Response samples

Content type
application/json
{
  • "error": {
    }
}

Check if a view exists

Check if a view exists within a given namespace. This request does not return a response body.

-
Authorizations:
OAuth2BearerAuth
path Parameters
prefix
required
string

An optional prefix in the path

-
namespace
required
string
Examples:
  • accounting -
  • accounting%1Ftax -

A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (0x1F) byte.

-
view
required
string
Example: sales

A view name

-

Responses

Response samples

Content type
application/json
{
  • "error": {
    }
}

Rename a view from its current name to a new name

Rename a view from one identifier to another. It's valid to move a view across namespaces, but the server implementation is not required to support it.

-
Authorizations:
OAuth2BearerAuth
path Parameters
prefix
required
string

An optional prefix in the path

-
Request Body schema: application/json
required

Current view identifier to rename and new view identifier to rename to

-
required
object (TableIdentifier)
required
object (TableIdentifier)

Responses

Request samples

Content type
application/json
{
  • "source": {
    },
  • "destination": {
    }
}

Response samples

Content type
application/json
{
  • "error": {
    }
}
- - - - diff --git a/docs/img/example-workflow.svg b/docs/img/example-workflow.svg new file mode 100644 index 0000000000..7db3df677d --- /dev/null +++ b/docs/img/example-workflow.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/img/logos/Polaris-Catalog-BLOG-symmetrical-subhead.png b/docs/img/logos/Polaris-Catalog-BLOG-symmetrical-subhead.png new file mode 100644 index 0000000000000000000000000000000000000000..eb941b89e869e19540efa2fdbbfd23fe47a4d6af GIT binary patch literal 856695 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�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 z zQ-*?fSPQe6F$e{-k2{>5K(kMUSR_<+`=+`75ZQB4DF~0OREUSo&}?br*|5Gw@2UgV z{U)TJ8zI)F*K;75NAo%hn9J1h`1SY9m|iudaR015oS9Q^Crfr> zUV2^pV3GakuH0+S&x~n|)yIh`DemtiBv%J_DMTi4xdmhWbNBIy{FCSJbMvhh7cw`E z&;OhMfN$^X=C_F5GIV{Z3?>HVDH)u=R(72EWQ4#zQ)>5D?$X4L?%L!qoQX|n3N5&e zP5zL)0>uS8vGQ0-zhV&PX+M6jZ2SaB{1P7cGGnXbtRPcpoj?H<1{9wSytlrU*`1IQ zD1wt{6he5I0w(RrQv%B_1^sFIM}1k~d|a{5Q5On@fqAz@VkO;tH1Ah zJJ?;AIrMD;fA3kG?P`q#$}QawP~VPFE}O_{h_)f))FjtP|1Q_D zQywbE?wlkY>%-0^${l7PM)4yps2B0FL%qqo&hNc@r;-nwuJs2y?ly}D23I&+6JVZk zl58!F2{n1%`6I!gm4JjAOAFRk+~2f*XRM@ci5ANqG=8otY)i)#Lcmie+%Q6_p=3?+ zF6%jNHc@7qy?>70zHLp5%00H4uVhtXsFX^!Ln; z%j>A-j`1j&cRzh6OV-{(!85+WuB3Rno*0a$KgKaPf6&Zo4WktWHBzjS$XqD@i4&Ez zNCqXB2Jy3_DBlYkN!aFzop7-$;vof+fZW0i;_W-hI0;gWX44(fJ#o|aCBG7VuxUUt z57H2zvKtnb={kHR8Di5_SxJEyXPqqG`f@2!?eZv|NVBLg8iZlY?1$&MDE3TN>s#$f z;nRDqPV|wl;f0x*bL@men#Xso85U;FU-Ojj&19PTzh7VOKz^h)jd1KWWe?sjg#AiG zo%(-(?|CjxHEZPU=XXkq%+MCyE)5<;*#7+7jVuMHx!R21?#0o+xE<-`Pg0NyFNs^c zFE`dCJ%SO$;~>jItsB#SGTgF5hj*2BhZzI;ZZUan5zmDbtW4-Hs12)tdkBbm83P(z zHc(k{OuK-3Xfg5~G~&v1{+aQ>iW zN`$r2;6#>$HgFf7%{o3qbBv7?s1|(;;rX=~gv0WHxKgXIYq=X7ozsPsI?O%Y|Ae%3 zau$CJ+Di7n^`z6JI0lxz+Oy%er+%tOb59yVFjqgdZXbzv3OuwF!`kZ}nuwrw-9g!`BN>n~x*f zs5}HBxZwgvlF(8K2R2W#WFK+}zOsZmI45RUr@!UPm#6zSKBYh8Nk8$KH5-D+j?$Ip zsWT=jS{+3>teKPOD8K|Cik~L3>@ZW#WN7bATK+kzJP{Y`x=dHI-*8kIvM9n`lO~T* z3hbk+*$criGd%zhz||ja9g`Ye0BJBP4@hMH;`Yt={E-^3&aS7GW{t>>%d=ks6M1Vi z(5EBv9CaAtdI|K7zbAlDw)g$pGzP&5gl=1q6I{Hkl|*g!q|bpE;)mCvvES_TZAC(; zwFuw}Qt|7+%HVib^xm_MPv!4%m8XoH0eJnnKRg_`OI^9&X;S2q7PT?duA03hVI8eh zszoP1y%C5QO6%ICy!+kHy)I(vsT*CRs5;55!^(?WP9k4d7UaOaQb8%MHoZ~t*01TbW4E9*}*8h?s+&?pZJA0zs6incP zR*S7xB?`>`t>Q&pz7^Ko`Gc(a^u<{gI7zZ0%0Zj*i%3EKmfcq=ky$uJD1lIU%-=3x zwzaDsZy}IUcBD&omjC_0`!}2*Mv<3}A-at$zEGc6m8XxI*>alpg7s$&G zE(*x>HoJYdZ;x-O1#ayAZKQaRhaW9pDpWF|{aBwp?ewl47|70qkCFqBjXpg}MBFKb z`cAMIRvQaU3Kt9Vs6D-5pAmbH2&ML-qTqA{*u|h}gg-jWm3wPoERo?tXD?^ScInX* zNzi-j?Y7wflMA^4ZU(#b*PHnj@5?}uZxHUNT25%b#0_2&=vqc;jVdw)pr+LJmIiVZ z9wL512#u+4G6?a|&?7?0FtmM9oIiB_DDn(Qw@)UMjdY}F8f^U8wOCMu_Tx?B(01E1 zVP*ax89jJ|&%5UX#!o|Yni?$fHU2YqulunPhm+s${hC1B5u3}&>REt^NyTZRG^1ox z%t#J=zFjmA;#H`gLhFD0Fa?A0BzzAKGw{WraIFm$%3hS??ZXn#Ww^jOA5z%v0h1?~ z2YIO373DB(xhqB6pZVuWaLYD;L3HE=GsOrd%jhIPtd^D|hR(gTY{_XQmrB;YYS?L_ zE}vT!!nFZzLzQ3cKuL+I){dPIZR))KO3u>UAA;YEixxk8|ERB3*^lw((xKB!rugW< z&fqQ8EP~a0a^8yh8JD@_XjOErY8ML#{Z!*rOxw69UeI<}qU&1ID{{Ayzb@o>3x)Ka zlz=wWj#pqukk+ebtxE~Q+Vg)zt{xZ@VgSOTkA#N2PI=;vaA&_(s0#%Y+ov=h$M2AL zcVr0t;^vZxSctc#$1u9K!zKB69DD_S10Puu3YZvuw)*V{ktVjPznUg}R& z;?L(I*d%6H>VMWyv921bA+t%(<=}6!U7^vY%ONLBm^`+VNUwfgUN5gD(AEO!3#9v+5Ij1=_ z;|@@|DE@_8(oc+FRlVAAV1;j9#!ft_q{x*dNB+&57GBJ{c(4Q@>B$v3c<7(z<@?Qz zeCgMh9T}rXG6Z*1E8Tg5Hh zz`4TD^!f3~rcwWge~m5&4~d~>uE9Llm0Q71Ljn;}Ic;pE^+D7)A)1>+xi_nXP`fh) zZCEqo7eo%#c!{NGqFWhE(X<7^Lh%MHDR}R2l?d74-}8Nb`mA_q5y22oobMdSPF!qZ z`Kg@E5#dRPy}C6>3GUs$U-=TnfR56}i_)ZNkl&sXfG+OjSyU+FlFHW+X9GGPUPO~l zrLbDFd?5^U;?vcNrF*0S^C)!xX=KDBeXe-fIi)dSRkJ}7z zz=&MKz|inQ7I{i%>j+ZLGzVf z8V)qnf}Dz5mZgs?br5ik0ZQxIU|rLp$ATn?+t)vt5y4>?fcqvSO>$d>^0OObMq?C2 zAyapVkKGfgy2n3?&=}$Mpe`8{k$))?*79O^;W4GPrN`fHStnd}+&oCEF#k>|qo91ZM?r(}ejm)6 z!<bisxb^7vK#q&IM}l_#>J0;$+j$RY#g(He+dqVc zUuEvyp9vdE#IZyp2g=+Y4JmymL8@zih?mqUwB^J?{?qcwTWgsfNH{-!4G^xi0&k4c zgH^XRryzx;BOn^O$G>?adAGdlJD1ekT%2;eb1!p8cZ|9)PXVPvvOoDeDwY93DWvRv z6_zSM(QsfBLaCp2fNzm`?ckdHpi=s-m7HRv)O9ykuG&{{*d^-Fws<;#?C2c}#hDtT z2>=sUfi>aAR5rXom)u=>4=Hp;v(s!(xv5Opu#Scx`>dy}CMC0VmrgR>5wWo&_KeG_D@#MLE-jjpC>?(n zg_qt-YZ`SB3i;+QT09#t%fg(TEt74|BcXQ~ zl{Y3#X`-GGTtAo5I0R)E+4qinZd>lpGdsnK)PwL30Mna~cm%rF)*mYU@WVp;9K@{y z!Lvd{6tAXep^Zj{po^QYthbEYc$MiLt9Re7(s!J>u(BXHDYb`~|3qO^eL(*a>xo;A z#%DLyCoEapc8HWODBvt`(mXBRJ3p)Yk|m|pPG-#9_#?4A z(Jec8*UfP4SBKVO=+rti)ub~s(4VL2zH!{z*WIC46EG{%e<%l0sMZ&C5`>~MMG|iy zomtJsTLKErf##s})>x(1-Ed9XxG<6S=w-Bxwql{}Rs(lm!A)XAOgiyWCf+#8voZSy zLfkCFStU)B+Rd{TZMaep*UMz}E3eDR53yEaK#86Lp~Yb7E=nu8#%Naf4+is%tdvH| zw90!C{Qri$zciWzN{=T-U^}!1^IAVyU9vg6qH#1as17ICzNIcrK0le6?Z}CQ)8T?n z5+Pgq*e1+cNC$dShm*r^rX1fWDz4L8Ny7R*3(KesZu?W!(kGK>n4KZY;%0w4dqtEy zPbrpOJo}_t>HER=z01|?(gTTZuUKQ_IGt)aMGCOl1>3}9|3~_iCPRxU+v*98QrOuuO+e7i6G=)XnYC9m7%SwT5l zL?)o}xC_0?dU|KBI3dQJtAsC&RT`rabo||D$#3=7UH!I0?14$Jf?XYyz@X7M3G+I` zG9$**aej`ttdWwNV|hb}yWere59}h!(=mBHvalt?y?s;ZH_w}&SWm>c7CrGCp1>t= zI2iS2B^D~qqB?HM&mOR zEF!DV9~Y|kL9SGBah@I6Zwc?0GXqTiv@a)~7UvQ}UulwX&H84SeQK{n5YY=`9RmxZ z&PA2c>2jKs6llrX9JY~DYN;ICGha>*=1~n~%1Y4I%J-UMtryYpRRA2wR;Sy8pO>iQ zkMm-+hw0FS)wSX6Q`F`iTr!xCi=SGJt_h)s8bv2G67KK0ufrpPk$BmmKMrt7u^ zJTpy{xAWuMeRs@FN;AY?3!osa{#g2AEjz%EzVWGE0&lPe&*(y&;%JbJ_{D^R3k*-= z>PlP-QUW0T!Yp^=P4K<&(lWYjN$^Yha6D8_Wb*jKb>dSK@a|_5gd3nis`L$QcmreUkx0bkNukj4$j{qIXj9MJrP5Zw_=}1o5v-7g!9f z?#qH$yb+w@mB;m5tTBrlnqLZlro9o`4-`=2 zsQXca)c6wmG=oUch~v*cGUyzT3t{%QpM<@Sx^tx!ki?}|PKw1-JRD3-*Ep5A1ZtJ& zmZKR%5Wf>8yWfMNiM3LW;c<)QXX9vRgAtK{Gaxu5hJ!fc@iz`(W@#$`#Sw(r5yE~e z_dTGhT3c~@5MnhikSSC^(wx*ze47&;KZSt!gzC`}fa79Trlo{GOD>OROPNYXxe zPlA{)@a?cYFuw=2X#IK=4b>Qsa*v{HiXM-0+9G2AG?g6>2})aoL&e=#PCU1BL|{rn zv-Wb*z+KyqRx1+xpD!ln1EhkTxO4A)T#}Vz70&tAaOyDxP_&y&b%vMbyxAUn=x~88 z6f5kYoB(^Pym0s4?PNe_M+{AK;%(*egn@Ayg`Px%NehQUX005$5rgvOlFHeP#Smxxp@VZHf=pFs*UH_=E$#fj<7yrUioRn>q^=6%9#nNI3 z1IWz{r-MK`J>wED)V0~Jn$$4951la@D?gCA7lpm-W#+)L44Q-f~>a zZm0=lY<88K;l5SI8cx-*`}sY&iXWB=I1JnPS zkVT!&O$!mo^BKixupBKj?_xQFNNA_u5z;<2iZPl(^f@73>`oZJ6V|-i(s8_7A`^h_ zzb+UXd2urr-3q<1C*)d=#ENE$qw~hHHv&iyHIFvx-?_S`RumL~xyYbF!Jqj8`)CF> zqcYrTZGN!435Sa<=Hm&C{zURJA5>WdL+$!C?nZ1wmLsS!2V z&s(dCSUf=1wx@yeOyr6GzN$5V0tE@Mbp1j^!nCH11=0@0h(IT?DB@yW0p0*5eRE}> zL@vqEWo-ar1T4`g&$%nCjtLcVAz$)U@L;8k?XTKv6#jb@{2TJO=bQCqGCcuNIzliP zIKG^hSRd=dPl2FpY}snRLlp)+CQ@-4;dFd&Uv&q*8K?moBOBk_*paunDW^2HBHKQ=&Q^Q}CU4j46YLkKLdAlsT}k( z2Z|bJW#~s)oWuw|te)6bc8CNS=oNk0S3bZlC1g^oWp*wny;@_CN`U+Ey?=jxE#YBk zgiw)pfqZf2LCUL$sOGnYYY49l+0)Mir(%tn*yHyo&RZ0oz3C%UeCG3+lswxXtB6C@ zYqWLzB__=ptt$wFXC)@GqZMQxdvPmtryWVZcn`r9q-|mgk;;YfYo7WZU?o)-(TlIE z{@WPBE)Rv zcyKKh5b=e@AO0d1W+I42sl!SuVzyZ)mmMvU|3`J%WFl|4!RV zI^V!Ue5L>hETo-?N+u?HNozTZ5!xaoOc8}D`=g`ch>`-k$BN?7rOh4JIr;OOS#dQL ze`l4ne*1~p2gz5reqSMlI`c}MKTIB;fie%^jM1w*a_oNTsoL-OxMi}X(a=9XE|LtX zfAYn#`o7X*O+`-qH0p-#K8^p9^xLV%M4m2yiuA9HcVOPH&F}Dv?(4V@Ou_FhT4g7r z18=BQhqwmWm6Vy~xDa@_e}s(@Kf5E?S~7xe``^g#*DE=F?^_r~rD+0ri~@OttJ5<5 z7-N6wszi%VmcE001d2aS=+Wx-gI%bd(nMS!o(pySOn(qhFqjVAo)elSqh>V)(dn=l z`!%-&a>))wlU$Ocm(IwOJ;eov+gux^u7&JghiAX*@Q;H&xgfyyZ8<`_Q^Fr-hilQF zx(wXF42X=+DloahQ!+!JS4EO>@b^ZyX)@pFyngbDeI$=E^^%`##4YpnXWoppMOMR! z8Wy5-d?o)0nYOVxHg38|<93A!#$$+f8!d^%HY(YK#Yb`l+?G&;dQ(~XS?TkN3-xc( z9BG1k+VKwRCH6L;gR6#;2+_=mAWcH66|=ytRUW!_IP$5j!uG_=Hv`=5Lc;{?);}|L?0py~hv2@H-4I1Kspqj6-=tKiWb-#6SNgh& z8XLP4Swa!9;=P)NKN)6pSY4RKn;gBR-S&7lID-;f&3$sTQjY{{m6`Jp;65G~swJzh zbIKB(Hs#2+pGz?=%R2PHbwEH{x%Qs2-SU()gvNK%o$-l0AqGs*e{}-WI80+OZD5hO1v<2ZZYeh zY8|R8)|Caeoe(FX$20}!3@pX`i*{rQm+-YTIr_u}R`G*lo1nIWyrjiM#Z)Qmw@RgH zQqkwfK%Ob%I;)Cs9zl{oL3$1A6REgAY_+83w|x0m>yeJ@Mk-q{zw#(FMN;rkx5I?3 zqjJ(Vl|SI^1#lP&m1w_^P|+nP{sG?O6oc+DRW#Cr3fmtd#fH4BGy=C3P}^ zU!%=Ghx@nkV02}CmPO>L1qC+{$&Io74l3(L;fC+V$5sU1*0qr0=}K9PxkTgA`#tS{ zzPSR?_AFLgJD)a0CHw9)Va=kk0s!|KgI#$_bQf($>48X(#1x-M+MX$Q32+jB|2%8Z zLr|2qQ-*&E%Upmm%L5lb*vPGwP{i3c~PjKentHj zj4Hkk-p}%$5GZl{?vDm8ljz9YHXk3w`}39==5a9^$-s%!H=M}%%Nehl4M=)aMuE1k zw;(X=Mqd&@4+eK;s4tUT$<}DoNO%wyhE{W#7XiX7NG;*Rx5>>M0AXh}zs4d1P7tnN zZkjyLZAXi|KQGGbgya{j?p;-^9S$N&mqqaP14O_*UEvy|~n2WC8HNssv(OEKG_7Q6RN7Ob9j>j<>eM_(iUa{!tP zhPc31_@J?%FjPj=ybY@NoS;S%h8uN1gq23V`un8KBy@=fZ0hlD45ow+OSceIFLki{JO_t371gs-evMBZJF7PR!E8l}tQ+ewrh~B%I4)2ssh%Uz{NTbphjn&sV{T z>6Qm!7vQ>Hv_`W@uQvqK`O(e!c}`dQutSwMRRm9-CPhU1YjRW5Z)M_SAoXWRbxe9o z#^V0BIFY>}y7rKyF*n;ETI{y23EqJ;$Htc_0E+lrV+sAJ%m8HUJ&s;&PhV2%L|QCW zZe!Um{AAZjq15p8qv76h?GMA(&pF@PLkU4?(JCC2oWP1fivs&*tz;BzXBR zxy!+f&}Cl}Vq{K$mjVO5NwH~=AdNd)tzQ|hGF>&b9rVS93C9I1l5J)_xL->kh$w@I zJU9E!z*)YJ$az<0fzN3PeSa4@8-c&5=W;?PqPE-`w(!YbR+gqu6!8lF2%Qs31`P3+ z&XL$`dKWT~i%iEiK|^VR$7dyMI6XVX`NgWPFP~P9)1kcb~Tk99ro#IW~AD{lC%7{M~={Go(*|quUfQ)#P zV&+%NhQ_Z~sOK6jb64S&Cn|v58_omSq0E!q)u{Q;QNihYhvY0!a^0*V9Tm&(1tPy^ znP%`f1{$5$5sZ<3H3LBwZr9>&fb9Wv_CgA`ewCcjPOuvikTYer8g-x}wZRZ?TDxy0_7s(43nQ`sP*m?TlAy zlao&ftEO?JP4Fk&t=F(YV%vZiV(WgzY6FO(e19jqr?tFSKL8#hNs$g#pL73^%fBxY zRP`+gY~*92&&zrlOb74U7lRHh^?yIrcL+2Pc|^$|Op!_)AS>B4d%#y|_B+*xh`)^| zN#paqa`p{N`tj;Fji-DLuonbC)4J#LB)dlk54!l3C}WduVRM<=GfIqZlHl2?D(%*z zK)7ZR6K7q(ZZ@2ll)d=lU+NL(A&aU~bRKz@Fa_j{MjuXE_J)i4ts3Hq|7+6Qj3Xf; zecDD#~apK2bdNPG=1@FcH2~`{%RE0UvW8}w;wY9=xi$`pL~kJMvlHa z-U<5}7Q73(z#%lfxROXJdDsN<)OG0qooxo7FRIN@v|hsDHhU*-gq)T!Ta26 z?N}g_SAC1A=Pj9N0@**I(=4rCN!dQwSsQ0>5J3{ypYB0Uo=~dT<$oW@{DvsemqGYU zkK0bw%D>z!o6CN!ic`MmYypktM~dweGRNy4H8eQk&oE>o>x^2i(0=dSV$dcq_QlAt zQdWF`^utQIJ#K_NqajOGz>;p;*4BUGEFA?gE$TWm5^E2`!m=@O$$5F_D;HtGO*%?A zugJT1fT|oGlg30YyaJB2o_4J~QgTCRRfl&ZttgI{OZ{Nf#6xLf_jwDM@cc6<)s>Ny zu^S3PojCs5k}I={2s>^lA3rU7vs1w2FeS- zOsWn>q@x33g+G$H`zErdf$5OKYO15i3vDt z5hoDxjNEtxYlfmc`*1vi#`lqRB_QU0mo7fm7nb?cc-@)bV#wOrxGo)m7U?!~QAl`Y z4SyRv7TsF#V=w)E4;Y-&*yX9OB*915zvJ)B`xYIv^n#Rdc+5Mzx`SH(eKf+O5WA=m zg|+!1hEKR9<>DEV&f8d7uuH0KH=&4jo#ZZEy|`F{vF_ObQVVY5ad?au^XCGs{0GC>{X{?@Z{B#Uj5F`>RntB>Lwm|m(~D<|x3pxKawrsO6qxcC zrnZ83;P_mt{6l7z+dxYaz>MTh$d2V1BV91M)>8#hCo)HXC@p+BUlV*e?vndOPa4$L z|H|y(3!u_q=fU0|$o$f}?Oz|KJ`NJM5cqZU%Xu2KfXXI@n7`-s^;mqa=jW%8?u>)D z&9QsVv%XQF4f3+i(JnQzUGwLX8iM(5XFVn4MB0&pxT@^8B<5-sO@Y@M30(FL8%*Om ziTyuFy|6_K1q|57YjnJMvf&7^=^0f8PW404;kzsvAgChgLpSzwz@VcqmXUTZnB+X9 zt~X$fUk-f3Bi9=DIbgAc{KU1k9gnNC9Q2xhI)f^^R|)FEWL9g{LjM^BCYq%zjX)?F zWC6GWLagCc>hPewj1*3!EQZ2fbc1x1mv^I5071nq^F@Y-jZs3bTGswUN9KVV<^Z<& zAe19x2=4S&Ej=^)wv#QJ_|a#>Lf=W8OLUI5aiK^=WVc7e8F$1NKa+3$fik2}Zq8gI zjhub3zGNYNpoBtK39|E{5!2JCw_j)x&fsRe+L{VrtbZRIM}WBZ(<9J;AI~Uv5O{$W z5$Hoy8KX%Z8uT!ioq*k$v%Co8LG8l_7y|r4%2TR)EFn4JpKZ22V$FE$b*scxPfZ5t zeYcpOKLekw;W231D?gU{DbXdK`}c|Q1>j&u672K><$d1WW99FGqgb|j1U#%GjTqs8 zKOF9d5rB#icWmg*Yq8-sO((&uEwY55Fd9`!5qA(>opG@tr*TNa&t~fQqUvdt+(65l zoj>H@6K2CGI0YK}w(kl_ouUS{xc-~h&GR~4l38gp6V#Fbea(n(bpZt%*`n;PF`0FvVGhmsk`d9i_j&#Rc7a z0Fr;hslHtuAXcj;f0gKx4%AzWPMXMob{Y?II$5B5#4GsUJ{tUf6p+VNi16d>N4a&m z9H5ZF8oN5iMlV^4D@GHeh~FEfdeMJkZmPB=b5ctF5e&i&cTlKN;>U@38I95EbWlQE zua~hkFpBMhtl**R>jq*nwi`s0saV+MF{;Y zPlJ%sI2UqIBfw67f56KFL|Y;-+-udPxvJ=aYucj2a7K6v(FFObY)?I4oHx*fcBcD4 zdxDat`rP&mZ-%pj*2a)q>8$Alg2(AoKa`Pq>U|PqHe)Il2c^MCqCyd(NfFpYjkm;h36;BEHK9;QK%v}!>`=~}44U}rNC9UJ zKa%dfG=4cN{3d%)S`f+iXl|DNNdh@Oo&ZEc&wz|)yY2vxG1`Z-YR(=m&Au?uOy=T? z5lfZ1$#7295rEA0vpc^xZ_XlZ$Rvo7TYPiOg})z||K&9M+I>u3HZM9jz5j`T-x5=u zgF8tM>>>|@AVnFK=zc2cduZUvl*0P2_=!NZTnJd1LbDptz`oDq#eTLLu@Z>gfdEAS zcaXeZ#|WfXufvpLw4%w}V)3C?CJ^0ybr3^(z{pNV+C z?9`O8Cias6w&B5^2923bG>AuWE%5f(yWv7?)qS+dTcuQr&6W&NZ8q9XdyuMATKc@o zl!(sJXz8ZlF7AaBKl7b{W^}BieUi3A=P#em-y&buJE~}vLp6w$lY@qqFXHtcHPYFV zOkX=AO51I9tk^I^1`J^J%5RN?#ydh_wv5e2wHaVwew5BSWU;>HrX*MXKesSKf`{QJCI}e$gPDb^MV20oC7}m?~ zewGCv_et(Ak~xTyAW^MDixioK!RBw?{LZ-1Iq);>&G@Lv`R|Y5$OjnWA0OBOjV8vw zQHtVc(2U?sh|Mih6YzDL9iDE*uvasf-u0J_!D)agJE8!}RVM|&PbA}(D zxTo(d8n$cr^?D~M>z~g&r7QO^(@XTUBUJiN36x`6%==?sV;Rg$NI;GL zF>u9yPgWVK07>^mu}x({#3@c?>SFvd&DG+VWEEHxZ)s*1Z&BsW)kPbd%(VbgTeoAP z*B8OTs%zk0H$B(*n`PZe=GC*YkXKod-;PhjgR}DBnyQNJnWR39?=v131DM!tpzE{+ z*(bEU-8rXf4^;V5gF+hsN#UmHsg6*VrdVPw?|aLF*+@jc78V?mq44TJ&4bsTdQL-* zfhP&=)y{*Pwa#dEr_S8CF**@p;AUad4_@UdxSJx zj)HP=zIVcni_1IeA}<*O4sJ`Ww_Y;^%Wx5ud8+DEe~K!1mUIj+(o)+jPlm*Y4yKQr zoWq(6%=02#`G*%TNCIB;#$GGsTx%0yD-UV3Z->;Q&6Dt5lQJf1gOdK# z=54@-K3%oj3&Rgb12{ zo&D{^0XoOw7f|S%KV1aw({*>e{nERCKhzfKrPvrT#c|z!be*r{57cKLJ~9tqCz3^& z@+ko|*cqObQ4b}l4=1@{d5P44M0|<%{#!8OOa&hy%D)uP*?SLRJP$mn>{{ zR+?a~`B+wth}D%jZcDkdDUH@9Vb}f#+*|BbS|8KcFEh8#WNuLt;KA`W%>gy`?a3wI ze51^CZ%tNrrMH3Sz4GxPS=laH%Gq|fNKWHoaX(gmJ(6dT@{mik5}yI3Hhc|LtKcab zqNqI*zb^Tx>ewAxR1HRp;#kE9?v<1oNcBx!^fIK|^?>;S$%N_9g_hWA7UR_Zr1O$n zSBD33wQxO=d3p&fZkq8Knb#m*?BEdFy!W0dfg?jkmRpNgYeCdNB-v?4Iol`uFJxvG z!s2H65<$mD|1s4+FJrTNz@2AM7bfy3#(O7;AnARu#>gF$(DtW$FcBrn%nY0SL%5Ri z+k*IKenx6V9>A~!3Fydb6--lv$K`6)xLZOt9yhPgV5Ndw62@(6^g$er`n3Iwq~`-0 zmI8wZkf>#TRtzQ|O|9m8bzsQel4c*6Ck6oV^m|M9)D20~IcV0Ien2%8Ht zBKmWb{+dWn9hJ#zj%zPA{Lr1ZD($̲N%0o|K(5(|cc`xBo>K|&MAkyb)Gk3_Gt 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%0Ay}9OP5$Tt1ob%7H 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�tcuAeeLB&GIH;}bvLVuCL6_KZ)b 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 z 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

5L#5ST58Zea2*ZASrdvC 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

&*x- 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 SqE$yzkR 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 z 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 z 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�N=l2u>n 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 literal 0 HcmV?d00001 diff --git a/docs/img/logos/polaris-brandmark.png b/docs/img/logos/polaris-brandmark.png new file mode 100644 index 0000000000000000000000000000000000000000..6573f7a61bbfa4431720e9179297c3eed71dd6c9 GIT binary patch literal 283351 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

6TLb3Fh^@LT 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 z

p&!U|knV1*BRh<|qGwq+YR1_t7w*;OnOrBQ~i#Q!(}k9a zQ%_I9|2ARlF}#KUl}O+%2O%c82?_@Vt1WkX$N#&J2Nf*U9d5O%fVe`PXTO zXBADZp>(G#aIQ!_7J_=l0A`Tl4^=C&kZk2pwH6pNqfaNstK}v5^$C?Wm4Rpp4??}s zmcNXwl)O_Se~QWM=wuwMor;eT!0C(t<920Ru4uc*MIUcw|ACVUb}Ti_Oh`rtzD>)% z8Grl9OM*y-3TG7!O^zMEhFhUWK82?_wD3)j-H1Tv1TDsq93EXjOXvy5DwfY$uWGos zLF>EJZP|L0GoPX_fcLbpOa*_MxIVTBUBw=(Ey4MQv^lDq*q$DI?*KBInCDq zp&*al8r)5q$-Y1K(?5&Xbosi6$fx?#*9`a(~IVC-)WW~q$cLXXSAPx!q zA7IsH>MQ;yO$C$I!!#8GTuV-zh!Z@vVjSdF3Y<6ax2sY>P{-Q>0tV2YyHeoxHvH3P zv+oigS&up+ZRL;@zRy3V@d<$Pyn66QBDtMQv|fGsojx;ao^`#R35ZB$O-4s4yJd4@;~d<9|EgBL-1fq#KsgQk@+b~kv$g$pzmoJ>1^o?2ADDu*t$Eb0KbV=|S z$4p>NK2j!;@Xhv=YK&FTN*Kr7=)GDNAcIZHOeRo=m8;yPD(TVF-04(}R`1PTvP<|QU#G-a-9G(Nh+35p?|QtywX3anp#ZHB}&Ux-(8Rq0|OdxF!Im^y4)rDZ1e1wBPCqa-Ww#UQ^Wa1r~h5k#omWT6ruZ z9C7TttQUJBUE94J)$s}y#y$JPoZY3a>Gax`p|zQ`4UgbVXgjG$9#E`3EgcD+vij`H z@$8K=O<@^92}bR>ow3UfZ`3;uDz z|6opLFpXD!i&^-wAv7PQGDDsNtD-bl#td@Lr z|3D}A(7Mg;80ZH5dY6>(|Z9s zY2`?(7*Hj~Pv94tSQ%NiAug*5BFL`6fCO7|39vq^NBmE|!>TSuC)QUgDLJOfypQ02 z01x^9qUgo{t6&kFMRIFTwM2;^N;*UC0~uA_`2UjN<1WBrzD`?vnwpaL5Jec!C-_jN z&J07t$EoL<{6VKMB+^R>&U6?#&Wu!KM->Jcqe-zkWcB7iso7n!PTuxtP+-x6Zz|H# zuQc%hi`wLiGmo&j=-U~Y^m2?xgRl?C31wd+Vy|`d=}T3>mrwYeucslRj^i)+rcIt=u#aQ6X}BVx zUT(+^s?Rl)d;VNrnf(szeON4$6N7lr#~kW|1#=cgfNwIGZSq1 z(~C>GwUgCzu2~~{D0|E zWY`)pJ3?E(i6itf;XW%pzOOF*+QT&6vp=1Q0mJ=D#aE6a z1+G(Uv2Upo_*=nHG_XLz31n(d)b`d&DV}OY@2ohT-sw9yCOtN-KHGgyr3!6Y#4-qk z(+Luu-j2g`$--mwq#0KP$x;6k4ccZv?;%$K$1b<&oWM%J!aAnkRqdfuVilO6zNm+qjepXD&CvjGH!d8KjJ?thr$<5NGB2IDdN? z*r;Z4e{`?i`-XuUCC(~< zbq&65ILd&hC;Y0)ugOvnq3cxLu)ZA*cuE_%3u2H}m`6E!1Rk3!c9lr@SFEQ{I*$%V zd*Oe8{64O6?;8fp|MM?x0gEPA;I(wyX0Ix2?o_PVYYB7dJjNEDHRj?HtX_0px~9Y0 zB2b-NIPZVJ|D8*P>)3B{Z`ZC0^Mr603o^dH(qyCC1~)$1Wk3=syO7n)_3#LeJ{9w5 zR!f973aQA-ZbnuWPc)qAI@9+QmPi+|Vg=9?D@P@gB@QVv;^)Acx5g#S!AwP>M(&8Z zVTXhaHcOe%vW*GInq~3%%=hIyNVxOc*_+U1r-%{TaahCs9(N#=oS+2DDMm0i1-{Zd zsW`r}yQ0j}3CDdrrb9Xluc5^xp#tHKOTbyR=eo7|q(7%sFOgn=hYAtmpq6uYv5a$0 zq~l;h&>qIA^o}Z$*`$d`eM7&vz_q3^qEB-&9k=%3j8jq&InBTKcl{2$9-sc>i{1oR z@Bfx>*7_F%V&flyP5DaWAB+YF*D#bb_vf@K*zCB7~w@gb49YCU8%9ePm&@wvA~9H9^SL z&=5!#EWEI~1~T&?SK!R5V|mGpIT5@cmC{I9SzB|I=_>u@J8m8)<0?Y|AL$7jazZNKMt;oZOEw|h?&1E)v$pVN~Acd3W^QC|!Hfput(*Wbc_ z_F49Weeoje*NkGjEeqhltAhU#4itC?{vTOld|?)cPTt1GL~?^ zXFB_bKim8-);D93|1`gSiQTs-_W_nFSWgEmBGpNa#27=b$6Tje9Rs$xDe7o)?1{1s zX^v1Lu#i^v61M?t^3_onK+u4BqlmFH^7W8KfLPf`Bdw*BVRpOOma}uBIT1}!HJB2; z99>h-lg^zRdyG<~JnhLlXl&FG95B>W>WZcN@zHdeI&*KxR-7IZD54V?BB>0T`^hRX z=D>+zM#X%{U6vfVDrzws?JQVY<5icisqEdgdHzyCI|Oj^tUY>H3AwSNxYqn>?X=G5})xZ^gfViC|_02lk=Mqv|~Qjs$h=F8CjAoU^|9 zKa7^2Ye@HjTFC8+9~`PiKi0>Lzbp~`UhscNGQsecY?<-q|1$57&W=hMSdAri^nYpl z@}kiZ@OIJj35dd|j=Lv~wh3xJmC*oP`H68)nS?WPTTR#E-PmG7djK97GBV;HeL7S3X7b8=o69?=_xJz)Z+hK(^%;1)EERtAfA=TwWncQm$Jq-1L%x6$ zSM=4EHvY~18~=U5KY|yH6!dlnJY9=%55OU_b^n2X8@<>^G0nN*4*#;;;R!?_CG?sZ z>$Y*_xyLC!#(xbSg`cx}G7;G9f5G6ZfopjQ{TTn@0yL;f{3G?!rrmI6_BFkTnmI5o z0VkHf)t~ZJl{-`*wWa%Ho1GEM+)uAjzLtM?P8?iSIalm?tmr2LGxuNR)g51(;s*aO zx2zy;@&ELDl|MtOqcR0y0gYqMS-FBK676<@iyk-s~75vs2EZ#gO3IX3>M_Ql=7lLw=&ehC-+SAgsi-7IFi;rJ== zkHG(6Ji8(%e9;NnjEBv>qj4&N^b_~Dq^cE0*XJGX7CM{{pQhF{s6)~N62&+i zJkF<~M+sz2MemnLowhWo1+#(61!-W@;`AD&$ri`AWaF~lwCx$;#hA^=hWhk2M#jp^ z2sKHEETIeP%W`{Jk*N{uO}9^{lErLwW>$@Nno8KE>=i48s5Nnjn5DRXL9!jOD&mo& zyORtGk#46hk-R18Mg%(yRwn4F71-89(1XHJUC9m5TaLXJ)6LTUhqYpoKtm#K9N7eP zfO$x>3ZH`FYi+Hgqp(gjzL$Ue@^u5e!jC9YJB=Lu%np{0Yc#&?1bKr<*Q#$x>hYFM zl;@gDBX5=lzXPwwXZG;KhH7e1|lMpj$KiTsZ?8n zPKpazpm|`a?|u)~+*J=$qWu8`3755?tO5&tYOG{2D+^$wM@+5K4Mg@+4Y!HDDU9By zTfx+X_k87n45B<=gXzl2KOc5q?OE2MF`V6Bv&m-Gh@YhCaagP@sJFBhv(fkbAp_ZcCFK_CW7qhP0?HVk&tRfc3$+T=f{YB|1{6y|4MLV3CB zIlO}VfAsjUy5=A%eojsxQYv37@Haik`S1g;OM{;^$9sRz?|S*Ks_}OGk0TZS=enqF z%bOxxr&2h78p}~t5q_Km7j|RDzhDc|pu((ha&>DcChCAUEZ?2n5tl#iX+x;QH2~jen{t zZ@u;*?YBOk`9i9&^W2Eaw#EOnqtRc=g=L=h-!|^bizIh!1M&*b{9U5}DFWF1kG*H? z&bL40{|H79^VrG%NUrFJEu@{=*wb;p2kSH;%+`bsVO*TBcJ$T=Ew}kVijNZ%#|v{! zvkP?RO!fNJL!-j4!@7yaXa=lLowusL&5*C5XM>S@TPT*2Pn<`TBOoi^ zi@L-4EF-KXWV?I$->G7B=zZoU#H~`6Dz|lXJryQ(RFpy4kW&f^#k6BI?{tpFT@DGU3j)$?0-#R^oE5$-~ zT)L<00saTqjL072|15i}^hRI6PYWCW2l5IE<7v&u_^09qb12jbDGSCwrZZ2`1^;#* zx4EoH`65{96eG!e0xAYZ8C1nZ_xK02Y%HHo7wJG!+}54Zd?n{z1CTWpx9j4q<-u(V zY?9(SE>j=$hOJN(DR0PI#Hdop)WS7kF1o@f%hkM*)Ii}?W;x=4x({|K86GtMgJ!hZy3 z{QF@4{I8TU5B>`e@Gl<`r0<3S)){WmUd?Y0u)>>9Dzw3+Xo};E~hRoGqm=5Ltef0aIHS_xxXV&;O!6 zyWZf=7H42pvRLeR!2dSl?Z!8AWWJq0@44!5Hg<2s5$-9bDl$LZnU6lmhH0qE_ z_ejFdvadGjO%t}ov47qc`9+N2V(1`u&8MYnqGW(J0giL>gfigUnS{NLnjK*Kf>k1q z2)DU~jfNTcLSHffI@eK-{Z0uqH%GiGd9v}a7DhD$$#8hZr)lh$DC44I1NHvs4goH{ zQh+!LeU7L$+Z@gHa$w+fHR5vy8P<0M(YNm+i+D{=X8Vb*d+B9zvd`Hv-Eg*vhZ(x=(}!jdWTa7 zlA`fnf&aeZ|8|U#RkE~7V2<|oR{Nn-sv3<%(8e%u&_|hSv zhCh=lSop7}N_QDS#>Rg-fJG?UJklrlR0N`Mmw(?8PAaP4_dL=N+Fs__Ak(PH%pFMuyf68BZ@G zqN*5*oT#dDlXFW1^<1)Kak7J!twldtjI+Ng!g?r{=v=yGgbcXOyei; zPYl@GdO5GW2$J{*CKlmX#-i8Ar!9mgf=Z2lR91sqgwabsM}^WGNaCHtb<1`z>Fo4`{{^I%I^>y#pGEeuwZ%)uL==G~p$|%^hq{?QZuIa`?aP`xuy7o=%&< z<>1`je$G;2R)MIqYv1nN`>#a-E`7>2aVKFF->Lg7?~;379?3a4lk_o zlqbkPB;R5TYm#O;lRkN^GN8MJQr%#(zi6(hljg5bi<1v@A+l)(Xfyu^ytDWf&R$E| z#l2dq{^c|shPth!z-q$Fi7)!XFRa(4!OxB38-MF>!TaC+&A8c7IBwD875_#2Pci2Z zZY4oXEoPR*+h&AKs=}AtKL-DHVx|8j{s&HM|F%+pu(&KnreLn& z#6^EVRG{tzJAX$bLuysK>_s&xy}MLxx-b0CDOLQ?)O&~^5!27}@9==q0m%CLEo{~D zX-wnVFO!S=zOnS7BwQSO`8@ky)wDS-d$0zQQ%#g)z$*1|2KNf}38v5_p}06!+z5_| z`2^Y?e8%JirNktV=UDQL$Xp%1OyF5&f$T&CKeEsFa{%Q)8oy<46|;Au90B?Qc^dGP zj%WRl_tU}gNynmm2R)E^KnCRTfNdEceB3}1Xl)D%=Fj!|e@(fgb8>m(dIBTniX}ek zemRQT7MwuWXcT2MRPp3A$iWorkDgDjBcD6NlW8tE*p8=;AAXqyid$m~F<`FlGE zQ_?Q^AMyVV!qiUKC_D8`|&!!U2 z1CZsX)g4>aeQJ3beM`=iq4gGJRfQVRg8Fl>&L{AdBW=TjQ$vIlCZB z)H~-aoy3SYvf6P;ggBp$aUfIPBmg<_v6JFJ>oX7EojR(8Fg;5E_7VHq$1CE>r2RT#c6X5;@Pb;Vu8l7C2lVG|KBJjDOF zIcX&E4-HRq6Zb~H$iLZraJ;4<=X-*T-nXw`9N?7QO#5@{C5V8uqr~){|23|+@jtTi z)j7uUxKbSJq(2G%zXn~KJI7MSyjDqY&Wt&P?B@SU{+DdX45LFP>8`4-dbt0x=d~v+ z_G5wFpbdZ_Qc#v7Cl_8y$pS7(2a3k00aPJ31D1_&Xok^Zd!EYxg~;5!9GuMQ%$po0 zM^;rTu^;_&^he!c7Bx_xHCon*Qxdg%d6K&$>}txS)bE+GGo7m9$cPCe)m95@Wk9K6jm|UZ z7|~9rF|gLX1f=!ZyW$EssNyhcM_pw*rlBGrmg8(yGe?@S5g_m{KWmEWWGkK+pJV(-pf(ikh-aK& zCI0I%{@I$tlO$BHI*^je5r()h0$<_3gU%7b#=^hm73*Ne7H5z&_`r|%rok0>(ZhoC z*o{k%{jwQ90)*?PVeQ|g5iBkN9OU4_|Kp1v@_$FQXty`Y?)wSp&fvH7dDWdfhyVQ| zSDGP4*3H2P{+|=i&kxD*Zix_+e5^-(OA6C<1EJHtGxhV|4i5Vo*h#4<4NhV9{2p#Z zds8^xPKnC^L~;-m{EdcNQgX7ELI&iT5OP06y=a+pO<>j))s<8h);_1XWd(M=zE*&s zi=hAynf>*Q&3#g4%lF_rzxnlrqMyshZPWXoc(0HZuYB3~Cs6>w z=C}}L!wyD4{Oy;{*dCAYpNkBlpOVu66d-Ba)an1w$S3JNpc$o^=WBj9bi1QHt zJjbkRv@^b%50{)ozr3sZCgSs=81>6vR~p5id*_0=QrVmFFJ5&3SpoFqigI{(M2O!x zg&A>BrtmRKPTESXEL9=TF`MCB-S0S0ohl$2KdKy=_w*;h|CRi|mkQIqTAuryS(8)A zy-;v_<~I00?c)*uzih^@stCxy>p^CU5z4Ex-RmId*k6@%NW%S2W#iz&4~L`Ry)f-; zr)~0_<>FW_DxUX2Xj{@qd=Spt5wXOzKt&w?LBzoXgijpw@N@-e|Di;WH zB7(6=qpT9$jMg$q2?pBp=y68mfZt&16+daOP34?C>BW^t{PI?Sl57_Mhd^VIQxil* z$~nrrI6U6(%qy&_!wbu|V&+>8-*bSC)C|~qX`{s+<2|tgK-uly-mt$Pz2)3V$f^|(UiqKL-BX08 zI8`gcJ$*kzQZ=~d$z>360uUJsv_S4uYo@(KoDl0`DpewT%G6UqE+zaLJQU|ijtbo& z_unXh>qVatxGQF)HrrwC%;RKVhX}cB)tVr6JY#BQ5R;{xQlv5u^xgb;mZuoWfYCWQw=|l+J8jdn_@RA%V^^>o}8pf6_4^ zoDM7XYf*?&ySZ@yZV9O6h~)pnOVOEXLIKZ}9TT;s3YZ-l38(d>NbJWTK#rUx6~#U| znsJ#CiaoYg(!cCWzPP^go8N`k+49;q|DYm@gKdM5z`N6-#gAREf;~$l2U4$5e{}8d5MGt7Bslb0f z#(&ZKU-1vbLrWuQy5;c>a}DnBj}R`Unt8Dbi;3@Wp(oPO4rPdBcD;9M$w?l~>gOe& z1XX+)gIX6KmR1oQu(SXBHpd-b_@8PIeNU)>y3kAho#(S>3mF2pjH}}B^DEm8E6Oox z6ggLM+NK}3U! z#^^A5bL504g-erQ+)~SkvTJSDj|xw>?~F-ShXtMAGg%c5PVVCp#sJO`8u4g0I6GwW zf&eLxc;;imuNqj=qe$Cq4Xf*2HnVd8Z2ZN^vRHFLEwD(*4-TK#Q%B7L?)kUtTKM9W z+8-}WAcWZ$;?52o=(1s^j{_~u5+ocm9^9&ufFD6`EMwr9rsGl4C5Vq32rw;&*vG{f z4k5mrd;d4TzEJe@?U=9AK?0Vf%TudByuZb!*VYeQ@E@MC7aN(NZcBnz^F|VzC9|7Y?b}lMASv+ zQ4t)DMCF0lA@qnkL4&--DTCh(s{@pdq1b-sY*VG!9>I>$h6Kd;qq4Lty)SljM4MTl zR0GP6Kt3ueNB|1Ftu<^a911vFrQM_Nc%){X7FF#8^6E>5!yPCCbUf@C=_Bd3=t ziU!*pVrVD;EQnM*ffrz?y9w|-XjE!pY;p8D&b2O*RZa+B)lil4Xet`a^*E8m_xu$2 zd#dr}u0xkAluxc*F+cb-PFM>l}$PE07g$^P%~6hT_l)+dIl#JS@q9QZA{Y{45Q0o z*3uppTVJlOC@$!DEf;52w$a{G)eKYKbnrxXV$$Uz??h0>XgR3kMEUpf>f&_Ufj2tL zm}G=%*F3ex#bs^|hojN{unI#D@eL|Q8#Amq`Lrj!JyZN4V{6ymJziQ7<7EGi3D+Xq zom(fEWd#g&%AgfFe6$3WKGgqF|KSm1V?8A03~Y=U3Iv@c^|&V9Q%DyfHLQ@p1xQs#dR04SxO}w)Y`w-~5~^mF?LX*X*S#5LGp_ZFW=u zv1%+62-6n+J*r$A7dRg^9qU1p#w{?E4e5@FU0O3Q@Q?m3NO05de!(>QU0 z^W@0p9z|0f5;%J?EZB*jCS-prz`^}K_6l1FUsB;2rraO`8`6`Pscg7H#tsfYx*gVU zbsUk^#WB73K-RVi`vvZ%~P0*on;Im}$^1Hv{?8x`@ zRO{VWwb$?I=&H46#m#Cc=d<*K4LL74^;(b4Vvd!PKRby*Yh_|K?~)s)Njenz9y&;$<$sZlO6L~U$PBX-cm|LHF%+`nHY6Y|$A1+6R754F)|J_&fo^Y_6 z|5L^c${5^HCS2l-;Io(Gu7cGeznQrrq&(_uwhD4lJ5S)3*1j?H!wEf=tpW2eVk<7o z*>8zxbq`1tanW`yvr*bsgej_TPv1auR9Q$Qq1&q{xVTQjM3@w>)`ps+?N+M>&HHv?3e zLplLYz8QhnQkCNpx8vxD*EEiC&<+wKYKi?OXL_9uAu>7*&{I~D2{{W@UREArs1ztF zToJbM)S2N*BYUgW>cW4O1Ck-{!pd9eO*a04 zr8Wjyz6oNs8ohTfLdZ)GE0tDvlSji}caDuKFm>$wV{9hF^kPO(5$6wAo7XN3kXz@Dfb=pZiUn*Fdj5fkfNkB4FkXhJt z1TI`<6gWmyr;MTeoox#k(@DJcye&;_kPLudw8BYa)8~^rQrP}L_vD!gShrc;j=5`0Pyv8H^cVeyV zzoSr40{%h!oQgF)Z3T;-3G5j~PThJ*k9H z*Oogc);uAX2;@fg3;%Pz`|bQc$1^g=@@G;4Nk;;#$E3l{01FQ_x$PMFavzVPS-aab0fiFpTp4fphJ6 zC>l{`Rcnljm>)rqmW2#zGJ+vIVo6|WG<-^HpF?Shv@IsrC1cOr1cc+X2O4?f`C0ZQ z23S$A37I7zt|L4FX+E85d&hFCmpN*&Qo+lfupi+IaT#9O_u?#@gHABPqQGu2QZCo! zIN$v9f!C$M*WQ6(KJ|b9nteXe?@AE;E z3cfpKTKqo&_`eEYJaR{@4!p z*@bj%C#f2KUrg@EyCOQw-iQF-io<-Uy%%si&69nZaoA~LaB9!PKc483CfCNr7;obk zncG$r!r{np+^oH4RazxYo9(_lG5ESP_2!L7x}F zKqQNDK|`1^7>$rYROeOQ~Q{)D3F0wNT;TN3xMS`NOw}*Ds?dbO!Yls&4SsZuo!RinkQ*9aRWv~D-FwSOf3|UuH`&{>(m>kA++0jv%+h9up{!B=B=#K9dLym^dMncIYG>9(sSYtvlj&UR%d3aNf^$xIbNbbC19KCZC8Y#UZ z&5xF0w^3Ai11ZR|<1HYqszVuW>rK_FUpBVC9aV=Lz6^Tdzo)8(EB@Q~2baZ$KukcA_k}C`&_#eUlONvVcO9aDJkeKaOou*@{=R{D9lrVhL((PS`G_9iP#{l$r zewE3rZDRVm8MQ)0Xu!)ps$(IC#(&KMY=j}9?&*qd`1{7L1IX^MR!NFA&)(>aaDwN* zPGcb78_@DD@RXoZ0|f_ot#PPQ zA6o3zz$5wUO9S;pv>wxX9^YA_0gnz)bx)(}9mMpUTfft!C#vnRk}*p!!AvV%x&jJB z6L~RbBTA1ln_@CSNsFbf-~ao*sa}@`Uyo08eBE#QjreoldY&v4;Y>wCrGW#^+Z9;& z@3uq9$g0ZJLi~p-Qqs5`{m_9ezb^P^rKOE>(l4uao-@mjSf@yDX%XV8?#%Foe=%aMCk@v@AGfA~!A(?L05Uj| zdXldYRiZTx&vf#?bWP7Z3k6;wd~_5p@emYf76Ycs%^hFcWB#wY=YO|}OYdA(W*fSE zb;LRA{YnRhJ6F2mJSCO!xNLII2$-}{=D4ajQ&K<&Fj7qRUpZnd((6#fp)-=2IIcXI zD$j;B@5t543vkF~0C(>}I_DUUT=qE?^1-}QKnlmD>WMXEq3WQaGhR;>%$Pd{R0fqn zR_k$92Bs405+O0-JciH;`g(;NJi{v60IoGRBUSfZ)sa9?K+fn5w6Z(Vz1^xcTXwCXdBbj8ARDQ))wNdrVuf<+fuc5uK-Cd_x3dK~m7TDxG`z#^%kJ z*)Hf^V7GfuF0F|1<)iohuKz`S%QyU1ydJN|Cv$w;KmWULU#WxfnDylVb$_ zvGIQ#jy&A#+bYp84>oG|csot^^Rfq#KQIp_p+L2g_g zm@t5G6>-frK56`?-=3HAl=Y%uArwPaC~?Ul%5ekHr~9f)4r?8c%*!7mW+($5ONHr6 zE--$N?xDFi|Ca&~DFK`PQ#VT&5bv4)%hj<499NYhxFh^i@PCZ`A^%&hj29mB|K?*) zb!jixX(st!8t7o7`{Vx0o;6Lk75K`e$Hf%+|HDyp^UNb@Tq{~`-vAhc;H_hvY*hAZ zqj8XNQBdL1z680o)#wNYibdV%XUd!BFD?Td4R((%Loh#avSmgJtJ))Hk?HfayIb4yW~O1FrJMsu}7 zLAL@6@qY*Z?~T&kv485y|Iz>A|8b$5=io$3cIAHt8gVRQrJhD`@xOyM$tjyb#^9r` zkVo=CoZwJri=pGP2djd*&W6aUr|zNbib3cfMj+Ww$ABH+=&GP6Ob&(NTh1mWs;O!f zxeQPoF*5HBZkmM3%J#ax1tNd4c2&Ic=sgRG3G$*4OTt(J%Dp&1WFJ;L~iokMf zN1tVHCKHsDgC}PvaaD1CyjHKf$GB?(z%Ws7VwDO%;W#fk8UnW@%tyZUU%(fA;TPcbcs+ip$L;HMe$AKOejguW{Er*l_*d3pA3hH8 zRN|e{8UF!K;(sx>I5^H-@qd02H;WzD7~;BRs9EXvox6 zwsAMEu9AISyXZ+oTTjc#FCnV(&IP2gzKYVVASC=3G*21vu+9S* zG4Sec$44P>B%?H|<-2y^M|ms4=)P^dP4J&)O5o=pxrG)>SE$OR-$_jTgl4N6s3DBK z*h!JNte>OKnWR7Zzy4?Ob-&@);q`buK9%ET)B8vN^ta!hEG%(7M5L&lk1*||(-2B4 zDk*KS988#O;Gcni#!a;P9$z+%t+JB5_vDFnkN*gVI2I$=j4}QD_rt^`${43zB(Rrj z<~!#m$2a~{x;c5%32CST|B_$^-hMq7e`S@-oN&eeTtC;0zrva*jlG=!wxYPx&;R{){>S}M zmO%jlb*LnzwAE?ze;oIl+-ou3k#8ChVFsx; zt#WcI%1!b>%>3kRwz?3b3coV|7$BxD9W?utl~}RaO}bV-BO7P03Z&EuO<6N{)Q6n3 zBCn8K0+qDEe%a9ef#3J~U4yU3Kiu&(zxFF{%Z2v5rOH)z;{W;!k=8lJh;OrI4L7UFua`2iv-+1jO(&9V*hvu5UpC%^R zjLg)m zz9SaeU|}#P-Q40JRF;Af*sa8WIdlrEY~UWN3X*ATAwX*`R)LEKNh~{z!}5}|qEXgg zEUnW0l_#U_yj4u9oxyWwJDd}%PWA@Y8kLqDJx*12A|>4UC@#`YMVZ-nPa$9?v898f z>*=HBTKu&?_TJZ}!Pn!L<#_ozo$q|l@Ao!^8v3imzd2Ptx32hSi)|+h+HK&!692|M zoSVlkdT~kH`a$RQ0RM6urXBm@w;W95hr@{3m58R8ZQnJN%te-!IIp6K8mJ@y20b_N z#gB)ND=Ie&|Iyysr!}ujXo&At<%dtFFE2d>4_1!Y53iA5Ll8lGtW(wGKCBfSt!}6? zSCq(Uh_2Q?v4R7!UKh=X$VHF%e`0_;{&x_jePQr*oByYfkwM!{rp5ooDit}FW}$K4 zPMp^{4F7NK>&&X6_QpeTqI? zawu9`WmkiUb24!ypHS2_oqk~&eEH78&I(UF=#ufNlcXj9%QBn^Y;TJyU~M9uiCO_z zsWoc$)KyCOS+m!dye&7$*oMejDyIPR&ylmx!Sd zj!=)eABAamxxg+_=~Oy>`&`p<_1-K{CXsH1{Kqj?25S{I6VkA+H8xHc;C}5~m1#!; zevG0b{-d$Nucm13shg*@Q1Acdch%Q?#XpJH@&qZwHc|E%|f zFRAeL!hg7ZMmX$X0*<<`&Z_(l;!*hby*G%yymO`S4)}-r7r5-oyy@ulM<09`4SZ|; z2|0-WVI_u9GZOoFaGZmC5i2(S0lQj-KSyt)tH6Kgr4=Hosztwz@l&oytSu$yS_WLT z*b`YnT7k0E4ZT@D2g2Y){2%_W+4VJo7~${uKZ2cx1I`F?^6B`lOOviDl$5vlA3>8> z{tuTuV^pHd>#{B6IR;A}W<2PA<$r~Mi~n;VyMo24%|6M9LA`0tORRMq24jl_=_o1`IUYb|kj|jcvoWOKPy#iAvBU*_ zQWU|Hx+7rQv2rlaVrCv!1J)at&ul0{;_Mt3a52Uash1aw#s|>D@nO70fby69Bq2@8 z!nGYm1hAmDd2UB`9@3fCt zq6qvSEQ2nMIOVnPi2r^Y{-2G1O#4wXS@>tP09Ph_>ijGG2QcZi+oXSw|4F8`3PUHN zL**IJm9%KY>WsLo?U~8`?P@>ei3F~L3%IhQVLNJc3<>No@U&H)j$Xk8t61_!)B07P zHL6)kcoBxwvEobmQHva>g|2KhDhwehM z=Iu-02tAUMb$Z87mrd@~vT9z<=&ZbkZNephEC8dwj3`22|WI$ix z=*aFgniJEGv=e@uGR>2%Tpx+`J*4KoRaZ^IDvFgKERdeRL0g^ok(|vL+wf|VOToQt z)CNbNud1kWS+bzFo}WJD0GwXm(&J~9$!7GKQQApSr`UmQx3>{4$6=6lf|TvdkAD=$?dx>@{P*Bj{u5s~@7{E^dEs9PhN{^P znCOaqQ4s0;aA>6u7-AqROE>)cy+%ildW?VdL%uPmP3dIgADK)CofSB2m})p$a6fQ& z_^EL^OGDF`0+qRRcC= zhi(1`)DJ6V;Wv-0ec}J?CxSHbNh@mH{12>PYPh{uTkTQ<3#=R$b==pnlCREv=6}oV zHuBzb)i46DUcZN~FmmN_{~jzY*QgUU74;pGVq9_5N0z5rJCG5vC$u@Ra$w~OmV=68 zAT?`cF$6x2S|rc7`8Lt#KF-UE&FM85{CkvJA&9;h*%Sd?Y0Eg9(k>nZ&*`a25#46`71Kiv|`M*8>DdBiz_7cyim=hc0EP~LFrVk&L*Dpp)|D(h0X_D$^^$fbHOW% zNk~U0RD<_^5L*H6dif&fZ+!hC=hx#O>2doyo$vmW#y|L#Rl{>NAqm&b(a%C zucXGGbdmDDFU-Q#~6tVq4($5CzM{O=`9a(|@~ulyf!F8&|xU;N)L+)%fayS7`-1mZ3HACt>Ipt8l8 zr_NCAqIW#n){%=Bv`MBfvW!Bf|4m9^m|F7kxy@OozS^ z;XWi&TmP_A^SWo1U#Qn1%Q-Va2$%FUzW60Va1{NJ)z~cg*NU)jS%sY_J%;3On}qDh zdKTEI4l7go2DZJV+${|;7IR{B1=jjcc7DDr)`JI!gLRzydfq?OHaa>nUypxW z$II90eDAmZAri)ccSZiw=RH*z*Kvcg@DC{JDtRmUNaJ7K4I|IW-^#EFyeij}T(PEu zo2y|5-*3miJO>t2`r2ae2mf~Hy*8A)FDSM|;Q!^8Cl;I#oh}N4WN+dhSJFAMcQ4;X zt;!Weaf%h1#fzMQP!sn>#SBS)jz9WlprT$ZV6A@4FBSTz_4DF0#ee1&<<&Aztu;$WH%mr@S|wT=tKc_A+qll|I47>aj=+oqJx2DYy1zeh z7u^vuax%)}jjq{d7<;`BP> zs8!Co-TesvU;#>bB5{7Zl*tCPMBbgT1)V4RgsVawuD_yh-m!)P7mmj%c5wjXnp6xb zM>Os$+YJZMY_IsC7EeMWV9@$@{Dcf|>6jJ3vL+B`gnEz+&*J{=s|AUm{>CxC}qSm*~AS3H(-~5jd4l((y z@gIA<#uHW1ey2VFJmh~Kdk2<(7i zT3IF{gX(UXMH@_x`C?+=ilTLe@Cd!cv2|-A4m$Ljk=D>MqZx@+HIrLmX6ztWW4!77 zDT?w%q${#s`Y>QBu2*JO()al@Cf@kDHCRzVq!Vnd^3Az(9hZYEdZxx)unb?NAZ@U4 zwBXJuG%DuwIKmJ7-gmt&4Za@#*pHX@V|~q6edYNhDlhW;Q-W8pgp4o}|1kdP=qh-M z4mEs)_c+OyXsxXaZ^1vWXN6*X@kB8T|IzNa49Nr$Q`13wlKAg?{5uvaI3E>{Ony5` z>1@wSTd)f(x|nIVLvi{H3RE-|9dmQWTzBs|+dflwcHSf5smE4;$UsQ)E*Ti{xJ}Q$ zdAX4K+kw#ak6NRfcG>?g=}Vs%|1WyDGNI;80bvLKOY@4hRy*eZJ(e9{RcT5?C~@oo z0&rQs0*i3W)`JTdcAe9o@tnVA%oPzwhu}x9nK0;4(ApnO(L^^@SJ=7#sU$i6!pOiR zuRTihaS1>jf{kLy@@%2JIWm=)S97b zBtzQUqp3B(x1HHJ$_jJ@NY~moXq4eqCk^I9_3uuoHc*tJf?!C+hmV zATcu{Uco9VTt@FZXPozSJIfEFZTmgtvua4=slb9nxB=}=U{&w`ecy=JMwmsqz?X7oeEE~Qs+F403u@q`&-<_I@6e;m2X>*hDe}3Ko zT$0)OZ1ehY09}E9yc7PJw)VzWwo2_y7aU7ANK>i>L|o>m&0`h}N3*Z7ng(pWb71q&5%a#tb!;7xj_M6Np?w z;}FB3vD%jCtC;K9ae>*eo!RO_ZkfZ{995X_`N3?woUo>yceodARHX_uqMi@u+MC$y zyAaVdSZ#C;*tqkR5(#}Z=2%1+2hlN`lgVn?aH^n^nUA(I7Rz48;laK?mQvw448U)} z=zZ^9;4z)1%Bkd|mmhsy8hkxIjmPclblwD6U-Sk4lVkX2U*xXnoJ+?y)?i|&v3F_f zAWRDo8~=TL>@1UF@Ws)S$g zf2C=Oc6HzU=#X?)R;r@b;unzK1r=^{Rjc>`qzN#Oo`~SFaM5;0AV&Gd>&?Wj26l!j z{=dj!{vTf00Z;09J@i#c(MbN+GT=S`Pk@oveu6Rc*L(hVKjwd65$@uDB>$^_Rkd)j zgNIKMW+SY&FrYfHcpAe47^iCV4y(j)5qp?#p?h_vrh*o6(p3SxO|8IT84#VrjLU}2 z3L>=7+m6C2KE;V99naqMTO)L>jE=gzWMu@TWNFK=q8~H`?!Bg_^-hKu;rEK#-|Qa?KJN2uivlucLnWk;lJ$Lh5u;x;J?h+ zzq~B*l>15 z`sXOJ8HMI5*)2Y#nkZKq#k_`r#Og3F7?kJT$8GGh;)c3pC$%OfWe$8CKPMNU?2FDR zWGp!sq+_AZn1~)d7cxYsJ&yfP>atUAgaHYt-4U!oRWJ^w8!>GP!&Ozf?a68*ll53< zx;f@R2MxcDO^$VL;o5kZG`hlVb+OrTczW#irJ+z`{@BW?t3_kYvf2Kb0mwy1le>q7bbk1QnV*I0H@02|Jtr=Iuf$%>I|HZP{ zP7I^c1r~_I55EQfMM{GIUCy>*8KUL94@em!(v`l!KdbjGc^3YAiIHQyjiiPH;AJkb zicV!=f~*J-=$AsFUC}#S6*!$chweoP_WZo0-3jgwRl^?fzxzqzHvij1D&*if(61^G zS+TS9+A+rDN%Q}CX3D``#1nV?Un)X~S~wc>|GH=M|HvVk?s;?faZy>}U;A$B<-hyy zhGIK|28#3Mv*#rcCq#c|2o#lOYdUz>_t^N!M3a3Nq5_MomCcL#8FSW z>U3Da6BUDfz&GNQXlu!QOD3P2Lta6H!l?GNTx5>F)z6&|yp8xAI z$!o%ayMQa>anD2W%}YZ4Wd2XNZ5>-qjuj51`*(c3ZoIt7-O2@QuzmQ~#&Aw7M1|(U z9wMe-4Il?iWrcS6Zmq?valtLRJ_Ekp-Ic0juddqB(imll*&zaM*_Gg?R6vp4O>yZ{ z<_5W%nL)~)Mj^c-Zf^;}bH~7?eW@e}Je(48FH}@{ie79-(kMS5pe_rgT>_D<)aab4 z5!^1eqwzS-_y(_Viqdc#3amSQU8&l6M7+uz`pj4Lj$q|l(sDqkT|zyj@H4d|SIg9T z-wN3mHPvIyJfSpf=9q;-TDk+c>F1pI!0&y1q3AmuFH3;$`-|U)um58od=p&#EXHI3 z14RAHrI%D&4lRThIqP&BcU@n49DWhFR=dgS|hUSmHH?<@Z6f`9T>%fl`Fn*q+% zOsp6FPmAO(NvY_Pdsj2+gOhXw?O^dqM}oVT`>;j?_Fw+IaS0wYj|^CcOFLBy3lh}L zd%Ab9C;cORc?NqyRTgSSR8f`e;Q#5zJ$b6l|FLH~gY*Z*lXDr-LAm5l7b5I{Z~yNMn&cCFa;%sxGUaR! z+aR+FM@Lx7K03m>r$e?1+ne{pv|Px{@c=&UxqyqqC9d?7f&K{S5*#O*Q!NC2#p`zs zzT@#ywfaqe|5>SyqJd}_z}I-T#n z{k%cGuK2Ha!2kT&*!ZXOd^!6>cIxFE0Xd#ZCv=bh)2i{$YQ7_kULItGdmeAr++Iuk zACj2$IQ#eU%S#IRUU8j#B|AGpHaC(5{zZ?eDs6fXPNS<&?r8-XaE?bA_;*iW#Bs1c z@iO8h>_d4!5V&&CoVOs)0dL`dwoz3vHblT$406o>4wSHu1{31)!(t2noaoV0$kWnk z$R7IR7#)yyEG5pDK5P7G{}C8g&i#Yq{wYH0P7KUjLwF6+i`gR|Eu*iKgei71>!2l+ zS_dCL$tM(Q4Fv%kTHKjPU8`spRyT)n05(-DiRIS0dvguF^;bDcfdUHcJs9m5H9^*< zN}j?>=Lmgjm`&(P@MWZmaCR$I)|nk6%CSdQpeVakJ1lmSyrK;3V4_~$TyC$}e&kk~ zJV7%xl;I?p4Pybak4+rPOpndW{FsoVH5+LBw8zyPvoC2DK&P4lq#-_yNHQyKa-P_? zy;%&mCO&8p&)LT@p|wuRvTq=%X7 z3^E%R{#O`q;s1sI3;)CT?_TwpH4)~9HO>qFJK<(qs%XXB!17rScK&NTFgJ%R^p-OU#@lguBqVxLu>1KLYa)7QL_^-9ZI@@t z!>Le3*w1a8y@AHL>RM`2O)7nX_`J9;AUvaC;AAb2&F~t}( zPo2c=X>ExROWzR9L|LBGp$f|UApE!dwAu>CNLm4zNcdkdUf_Q|q^ph}od!c=Yb;Ek ztA&4idM9bU@o(qY3NHiyBJ%b_Pa{zUt+e@zem?lHDtS>y1TFwp@US1S+K1~Q(#!fx z?NSI9rN)Vz&ZPZ;|u^BPwqhoKJ zuL^>umfJh|eUk;Ec`_3+$ydq31^iNwa1AqW(s_%sNL&q`aclF^I1L6ql((r4gKniE zrQk4VCxBU8FhoAd}kJo?lpZ)&={|7lGf-OB{!Y2uZ z|G_IbFIC3>2A7F(>*zZ(*^Q%9tcEB>$k9l-&sulY6PB56e@DS&RTG}Ew| z##eoXZhN>ysDehh_vmTuD_3A@n>)sY*bs0JR!x(o7 zXmlc&Y?cS70-kd;qs_4(;IJx zyza~T6p#1+!9Ren{9{AIMCC^HR!l~MFP68%f5}Dh z#GLHM`0v60va4-iosRdVt*60%62K~ql<|)X{#$!@*vq{+_eGMb>ypzrXj4V{)mW-h zOtO8Q_^oGH(sjdrOJkcp8H45+c>Yl!0>G;Rt-{p<{@?u^{&$(D-mV^GpYO>3`@S91 zMc~IV2cMb$GY*KhbgBFHoIF2Y=PGYnL%Vy5aL~ZQL}zHuOE`JwdT+R%{Vy_rTSF?r z)BzzKYUPl(!kYkMw%{vc$F|{wzFiXiE!3uyl?4JM6#(?ga-`FVtkBOf<=#nrIU0BvABWTo;?Fu-8WRDfs z_qm*h z`7?%#FA%VrC3RZ=8mPT{MSC&%yGu#ZvV8?J$PzwMpgAm zuZqzj*;Ie7WQsf!JRi%C9rp69L+EcV}$x8;BmAd5z6#*a+8~JB`vSzb_w)k5C zjjMbc|G;DX(~hLceJDu73dU%Qi$iG^aNjafRP<1rC$GKUzsY@$!!50{XjXi9g>BXf zG~g6il}x#*Hma*7u_nD@lCRzq%w@n$bRA$aVjS&*4w|H@cyR&*_0j(Wdw&CU-FB4) zg3o+=NYey{LH-@1M#7I2!?J*TtE@(=ERwW#LotSc4Jbo<5C|n;L6Qq-69~D97JsQx zgo{d5p%6e3p%fCuAE;`Rh?GFJ5D0(ODoO0o0~6>r3`kUvclLMon$I(zx%PR#d!a1I z{n^R=-gEZZd#$m6x6{T;JkySwESg4b^hkUw0gT4Wl zR$^M!9j!gbu*2#}+*XsEc0K}mObuf|^lW6Or)qle4W!Vh(-{4T=YOW36xk|i}6 z8t-vSaA2X^+$e1WrA_xPzFmNi9&7zSC~-DW8dF(|9XA>je~?<=p)df(Fp@OM5TilF zv+gakyx`;h&v@!sRjYf6)g{5oG%Dqkq#Q?9K9bv-NXT+|1^<~7|Ad3E6vwl)9{ij? znXmf(AHq||ukBbrr}JVw{b}!(oOCo4S*Th7;g>=P|EiWNYp@&2^3nJw(H0G#@z29p zH!N~r!+%HWAY%&uvEd)lJCqaPv$pQUe_-Mtj(;eY7CS*x`m_-+5yITw8@w$wg^uC- z4Dh35Bc#+CLab$3uh3LmMV*Y45qs>je#qoK-|;R>r11Ffu?~^}H3lYuOi_kT9si2| z*^$z9P#kc^f50ja(kU>K?2i9E=93Q4ePJZJGM|m1YA(|x+a9d=)mx5r`BWainH(t{ z`kLcf?vatET*LpOJg7<4Z`a`E-0o}%vuS8KtmZI!zu?xdg#o_jIy12Cm7%>$Lt(Nn zQMiB@wY)Z*09=G(n;m>3_=jVm62sj*#{N!sa5>t#CgH-b#9@@CUEe)GxPz4o<|sc? zMG-gS5f)b2@&Rt^;%0fC52?LFBE!!a3>Hc#Ewjze8{0dx~!v9tGRn}KJ)$FC%)@XJ^vDz@MJ$ORjW^Z`Pbqt|G&54 zQSr}!jWGO|1#a|vM~(l8yYU~v0(}0%8i z_YbjQDVk-7fAU9I3Nx$F{NpyObB%vCqQ(}@1?*vy9Whojc5A3&&z!L9o{<~=p}nsz zTjAfvwk?yiX zj==#Vkqi!~d`u;>pnZ|@UV^8cBowj-!Mna=$kmSvAarRByVQvuU-RjodQ`BU%*UJF z@^g6hi~e-})xZA_H~bf6zltdve9IKH_)bUEk%m z;Hl%W9hcuMeA_R*BYxl~ep*p^a4~lL2Q!*wYR0gvENNXjR@;v&ifHbOe=%Mt##j>@ z{@Jl6sg3`5H2gCX8vaL~xZ@wfzwYjZq$1u?trCZZr79RYiMS@(cA_VdF-sk7lkr-; zne5$V{cAkEi-m2>yC3A<&(=38)=s@`*Sj6Iq(*%WIb<)~iT}77|ASE?_G^Qmqv`)j zGO6>&CzfDsON6jB;p!^w0h5Z%-L|6CV#FLC1|?Tr3p@~pw9zW;9{on=lM#l$R$g&* z^vu9~r+|aRoI&u4Rv=-gY%p`IWiCTk!MY>3*4R|{Y|>8|udM+AAhS!YqgK8fsMg%sBc5i^(enr)2zu*%C0sCl#sKu}P|2!#6@%w4PUM|PGk6*S5L z;?th>AK&}$Kf#mv_}W+hoxJR|di~G5nd&-@g@1B;s7i1-%FGH8<%_(Lf9jI^;2%bu zu##t;|K{&~%~KC0{dF9dM>jm{!4EHE>evSZpg8Q2fu{)Xx4*owRe9p8dGlrI&N1_AaW-?&X*f%6E&m zYlXML;y!(i<>XHC%Xwjmi26&Dr{&nzV1-qlVbjs$Yn$Ju5OJJr{L>Om)}_i6#+Tc* z#Bx%}N|j897)}$l8L`Kb^-Y}`5R*v{6lMiw=u6i25GtY)N%2=Uu2f zOEUFZ*v0|Q94YXI_R=NG_3VEUZm5c-2OG8a>hTd(9pz`sn-z#_<3|?nqVxHrd0Q}@ zF0Z&fex!_>L2I9$)Ne^z4M7~v!iz3A)O^{{{_v-K0-mhLQMLM_KaCfD{Wo78#jxQY z3;zZCsJCUgsP`iHKAY`-yFKrRe~5E>22s#`(?5R;p7VtdEF+{vY;h znG|zpk~@6ua-6aquj-r49Gi&aM=eN?94&Q{jX z4OMaifu<4=_adfcs>GilUP;#wQGHZ#U55sfi%AHz;+;>5@u2-y0Q4N{VXy%OnqfZ4It`dXx@__QDxBeU+ z`0OvlBj5GicSKQHm*V0{LB-l;cq3Du3 z10;i?`;lJ|XZ+XcymIlxzd3R$%)qV@6NKH6_1d09Nm0WBKD>XM;F&PdK|S?L8W>fR zFl9k;BIi0s-@avY~;rKsS)Nq!r@y`Z4 zX^Xfc{xcY4S_(3ClR5l;Wb>QuF2_u;lDg(Th?Z`+bf(QGYmqFydXk!HqFAK40 zc4Y`fipwR!#i?$j%#{Do!Iq@V>di9A0&}7rk}zfII&khy(;qZtqoyBmv(1R&1oT=^ z=ei(IE8=KA9io@4`)su6i^@3yZIlGj=IToGX(hSAqy!*6b!lDEt%)NrQn6U}Gte?I zT#neVFS%BySzthuZsZO_fHPZBr#MUGyEqC>3P@*_n!t9JIkb)Lf6i(zX9#e~usNKx zx^9ln8Ak3OOTYQtXaBx743P)=1GVsK0cY1-Pb4&vWV z6+RdK4GXH+W`UD?Tv}$g_Y1%7m3Y~=d^0B}jQgbQtMn zd=&me0`PJ_VoUHiQcsq2VZYB()40c%u6b5}w`-|^qrL4%pYw{Ge%xF;$>hp=3=1nq zfC{-&J5=4QvTV6rYDRMJ%`IpDh_OHC>O7YHLv=$rF3<|BL~8~1rMNNZx~k{IOECZH zLRvX<8K8u?Su2MD)t|e(T?RNlOL9f&UW{MDEjerXFWp&~1+V5=z3Q=pd%PS7iXhcq zolE!TV7`>CR3%qkEL<;cI|o^upjlFSX1`2D$wwAbJSWK)c|>eUAP9f}>)l`(-xKgm z<-$s+7aPr zCH;0h@L6B*)V{38cwBx?=i%r5F~fg^2vD3;fv&#C1Jd%qzv#2cDX!t4Oqfb4%&D0U0!jH?7c(I{oq!Lh~_g7r`t7Er7C&B zfW!B8jeJ`i7Peek?Su)AejO3MQ-iGBGG&ir#(!A+2S(;_jD`3Ir{(G66osJWeDwIg z?!mC~IT7Pzp9xv(&-z#Vr-kZApRH|G(7cLf@{D6X$-N66L*bfIjClh(Tow`zhYzpR z9B0V2pp%69YNhs)Pe7$*lZ(NxC#NZ=Y50}m;Od3!*BpS6^Q)dA>6)qa@YAO3g zc)$%$UD*B-=7MPj@ZI*UjAX5a#%BU$4p29>N69D16pk#7dO37Sz3db)rA#$e#ZqDF zxU`(jx=05ZI^ZP(<~U+a9aqZ9zz5D&yBL;IZ)$WUql03_?y|+m^(@fZpvoAUU0$7e zumqf4<%3mMN+ruKc}tg#m;B+6k0;a8;AMI3UeyW@{e^G2{M5}lnHEJJ4gbuif^kTy z?FwTZBx!`mAR;>=l$(=4wG7aw&=T!1U3X8Wz$s8yDckPvZsIW{AD}3Ul(_OX~~N7oS_hy-{fo@ zbU}b0rp0@Bf+XVa_#X-nITH@OPmIYp->Xj_hxh0DVK%PU>q|)^Hvq2SAF^rhj^XEP z)BnUjtfmba)Z8LmgVBuAPN9AHf$qOq9WVj{ z^er1Etie9BzHtyDi-3a&Ovs2S>YkIo#R}n3C)IYLn6tT%uA&vudb08DM<=s@eG8{r z9lW5IR4A!Ip5R3%F%)ce(hUZU1^quIf2m6(RlY)|BiKa>)Az-AURD!daSP^s z*{PKIcC~2%U4bMmZj6xHll8E+ExN8|TA;2rB$Ev4fJeE07+Sd9TOE?Z=+elpi+3=l*2-(qCFt zpbcCp88brYZ^+pPn9MNrUk~gi8(@(aQnLR?rO`LdDLlDR|0>w2foR~dT%q05dvT+&DMIAxsl31&G1H_B(}sRm21(G=4nJ$jYXX$om<>QnyVGfzUP zpI1?L4^0JL@+A4z;3I$I@8ZGFdr`jb4L`L`^h`5}q~kP*|7iTf@Q*czvp@?e9nJ-Y zR-md`{7+Nw>-Yymd=vj_-CYOc-{@~EzVutZ1E2WtSK)2H^maUT+nXGNM|mDK{;=$HNv_J4gcX$!rmK>_g(1OHIwSrAJt@7k+D)3`f6| zvBq{_-r}9+I(ZHMQ>Nw$+!6mxCm#Pj$?pF`D#aE2lLL1A=Y8k*aW!U}aO`%%r8dMP2DmZgPO%sDfrLffe3 zaoG;zmRZmlJ4CWYRPYSRwp1RFUv@@PGad=xd=@8KeD5cXA9M+uJo<;|t$Q za0DRhtEWd1mV8rVB14#X{G$n<-65a_dPC!!8OdK}5@OM?8y*`DQ~#6+5^hCk1s><> z9a!fNlK}|d0b;U%>7Vg*XnKW zct_8iK;J9;r)`+cocK3zEqcLN^J&!Cx4BTQ3t!jpALt1c&iF4;{X};cB597u{6g2h z`ul$n4}SI+Lki%^BVr=bj|in4g7mOCv1MLM>NzVmeno5XXXS@DycUgq)LJTUtAlZ z^A<09%SXr6-~>^hS>H?u7$jr@U1Ae`<9JgIcWcb}j}C!p#xKou_}t@v#}Z%8)Rv$z zal)*O1@P$cALGNvjDNI_Fk(+9sFW|@4c0L$SkMz7qR-uA`Js3+t@c3yFbgR9qxNDO z4GB2fp&A(L^FTRwl__y0TR*(-^38;_vS()$er+nym-Nd}X?E#EE78f(`qX8I4W2;e zU2%LsKLo0T2(BAvvMd$V&2MBiv}pd>LBMj zkAM-<(2o8sP*+%77Fi#z%qCmeGHFnaMv7W0Smh3tK8qd2EC}e^V0P<@p*>a`dU!B) zP}$Axd6D72;}$P&{G+YOviyoi!!;KEH@;g???_ng7U|Rl zeF?~-QW*X@kW&J}Nhe(=EKI0+RFwewody~Z_nem0_!*NKconAN3HzYB3ku5$VH>u$ zrZqw-ni<=O@h6VJ+FiEixIj`{`-qUnj@;nb9AmPN-=b;rEG-*xmeQmsnrmawIZ z84=?-7M7$iJI( zJ*39k+4~|#CWM<}LW2f%u|&@hFY8f`%g^b2^n)MHc~kasu25Pf_qL*= zO2)Q`QlfcM;cRQSD`FFS-ESvQ?0bE- zHm#rWZ?}330^`4*iuMb>yD$Ew?JM~2@rn-;ILH5pF<)%q=5eZEfxBJWD^#=y)0N;5 z@|747747Eq;nrk!0)p`jq^)Y)RVfR4D=%m(+n1Cua@Y3a#o_7kopXXpP39gZ{%Q`Z z&o!!m$}a7lx@U9Zq(W+Vw~!SrfXk1wpF4%UZFqt)OyO+Mvn0|QgIBgb6=0~!CcygT zc8It@jzq6|S|TDKQy1B4Div4tN1{gWmV_BA01wJAKF!?piB)x!9+_He+gzFjF6VCD zCz#WrzX`NVdAy)q(iH6BzY@<+d-i|wgngjstAFqx z$xC!Qs{?*B&-?EJfT~J&;@_H`__vsm9{;UU6>$gtNzBp}yozUZ^lQhzhVdx)@A0JF z1n0TC|+@C{X{#})U3ZkJ{u0aN%z}}_SBb!RqGhsL6;5<(nqQ)uVbEv1v zdlg`V!MK%_HX;F}GmmvXQfWYxj0kguV?0)u>u)Gz|HrIC=^C9|w9INj=`AG|u z*`J4W%C?~l(S3lDkU_cr%j8kZef2QGaO^GQ!a&e)Y(pg@@jGQhJUn!^Gz^5GH~C8 zfAib@8%DxI`AlP|IVvnJ@cu`>^Kar(&3bBI*7f7~Ih{ZI6?o70c-n$?lj~j6ghK5qF&bNsFBfd2L$xqb^{MWV7 z<6nu7Va#-O6{om{{~ELFOF7_<{|)EAZv5jq{8UfvutE37KXEI~6N42O9cJ97Xjs+`;k8quavPUJ>Z&8-t7$|R75r^Fbf z5Q_x)&IX9w5*r78@+s&omqDn2e~@6r++SBZh^i#E&xCDw>isk2EV=9+Vd?BD@zPdl zL?i#Kb~OkQLzy(p(bgSdxkWE|W`5&;@flCJZwLn=D+uS+sus1WlK@E z^dYKdG9URE@Gtx+R2lvqB(PknSwZ%!pl~PtiS%&%UvB1;`z@dyE<5NuoV+ty#xy3r z?8kb}7rf-DBaO_WA`}nhQ>4eLnZ2Zb{HiIN1qd{)-y?~*dyoK6M=xOd6%1Uc-c@G`eHnTr6G1r`dFCQ z1d}_@O&3l1Ualr&CW3^RB}`70=lH(_9^6p48S!738L`S@;;DGB;lIa0pb7ugmhg|6 zvtPx(eRi~cV-LV&;6k&-f z9LSXzQhXQ@ktY_h)_@B|$t<%YCI&@O+D?ee?JWw@k3xyL!k~0Cz|dDn-t%G8WPwPO z_kxf61M#fi`C)jX9G9xqLx1rv;W=OYWq9*je!lp>Nu71e0x3^tJp8;rs;4TpgB1Rm3dKg_KKbE`PWjj0AOW?X)XnQ(8h+q7+&8yZ zMMOD2#THVXcbUvpa5uIs=Ck%0W}>U}JTKnYtu`^J>FtR3DF|93HLELe`kR^A3DLNv zOt4(P+weFvWn7?EWnN@AwDDt?SeKPFum6N8>-06E*%lA?u`X;@|b1 z8~*1Q+K%?9joilizr#oWpI)kWzxr>Y_sLXY7s>`z_A6IZ7O2oC{g%!uIx{X4Qbnkt zT~12xrMTK;odzgI*65HIEsD;R8HCR?nNyWYDv5}RO>uQ+bSlh*B-!CjK-%i4pH zhrS>ivFPu^OyM}pD`Ya0|H`MtNjOr&WY@dwLS9oDx))Z**Lb;yJD&MI?}cxB=yUL{ zzvZ{!iEzC3$A1bR`_h-^-~Hhq>r^)o_LXw8*Kt8Fxd_i`2@;y6V#7apefi?0kDI7X zHfYZ+;6@oC(lH{@+v0cOzx{|Lst(E}C0MZHNnEd(EU{ytDh&VY^}qh*U%~hM&p&eX z@~n^iU3lu4$K^K*-~6+0!H@ovH(uVJ@!#nMsqDfPSaY-}Hreo>nE0<;|G?{hu~_El zzMLVnJZUQo!Ku6AgPM*o?~?zSUol|_adSt81*{amaVg6=v$-#g$twU=`&?!3;BLuJ zFNKc#o8sdNFLtnI%rW^l{O|F9-oHg3$t(`#^=a4Ves5h9HZI{`7g$`nhX3yRI{r63 zMBWkqE15gt-^5VDI>S8hAs?uhvx0S8=)c^eZxy;ilt*~ z=p%ipg~XjTDtl08>%MP4jb|oT4I1)>EzyQUyW|pe)qwg^v^k3eI%!^G$hrCcxUa@> z`dV+@>C2tR>0+cL4R-Z`nO+VC@XA3KG@)Igv{5moRIQPyfU#?ngthxj6iKb&ExBwT zV&av5;(2)byS*D8?}w^ZUvjT%^>aVpWZBN(TMdFq`r#g7_-7tc*~W2(?6~7U4gV;A zxXlKQ7YKH@oCwy{_r?El=K}HR)5G`lkUuE?hQqk(*a^UWe8)epy!XG$=J!v0$yYx0 zP|_>MD_-zf_=w-}K@}l3{FDEC66nC4_y?ES%0?Cal3R_|MM^-eDV)y8`1X^yksQiU7}<9}TjXD&A~y9@tT zjq(H*vSZ6JW1KP;WU79$RRYIxev-S|5(O!s1-wYb@fVT7jFesTKPni!Fgs{-KSX6V zd;Xh6tq=lB-?k$E{{eeK%=e_5oz+0Jm`b5yi zp-#*29gLQDd;0_F3}@6$Sxw$uH0Vi+Z#PWYD&lb1L? zopIF!@z11__k@<`lDyjSU&-Bd{KrN)HQLgu%Q*3$UEH{efBt8Aw%M-n4=`CZ{9Ek2 zf`3Gu^mE#S)t-H;V52As{XB*VQh_a7+ND(1b{LF1h}ycW_`AIN@bpMyZY(p=se@;g zHr7g={Rm{C2kd~6qsvx2&mVn^Doq3CM#^-3g_hB$LO3~J(u+H2!OO`TdIRywxaLUeMpS>ra;37=@-5oKsWt~{7+|E;PVyLFHJTir z3RQ>*nAA|i&_$1cT z33xmot7`R^SJmpRKhOCR$#!wcV5SO3=68g1n2#w>32>IQd!8beTG0vUw8!BWN8HL(wzP_%G&t^S$Ki*`NPn zy!Q1!j;D@3E}P!J>nmT3W{S{WR3)=%Jd!*9fi#8*k!c;bY4~U2Vi5@<0=nh+rXBIl z2Gb%zl@FSa>(KJD%}Ds+F@)mz4)Kg)_QSq3D(STa;MYV=!pwPh`BsIqp4k>4Z^Bc_w5dl}{3|oWD1*1&ko@ldL00!( z|H#W}%>R^ITrsL>VkPVK)4?`(MZZ znvK*oJ>`{`=Z=3Gf?j(K|9I^955?y6Zv)A}+jofe4gWp+fv6mf|7->P!Y{o8-~2tV zIWB!_UsfOQ|6ach?|HA9^*ulI!|Mbm?>qj>P!sa%>~cu&p@R?JGoki*BQ>T^8g5T|FSyW&f8w<P3I)+wmv7B zCZy?i=BFcIXV%(gXuHV9WB2ASH~s|^v}(Ew0m~8`B_ckIQlImrk2JPK2pBOz)O{!Z znb65L*YH2v*f{%2{O4s?@K2#J++z&)g#WTd@j&C>WB)=gPx-^y7kiDbx;c^YSAg?Mx_HJ9#^zmAxD??Ce z$Eb&$;0Z!VhH?f?ZU7cF(-gRQOafgtOv`&ECKWPDpZ0B!YK;b_{2{3WbGb8^(DdD@ z6_r{!sUfT}sE^F%fLaKbCJY>=rqZ#C?z-S71vITuy9FsED?}E$1cIC5goT#1>?`sc zm1dGu{E-%_u+z(jrP8*Yc{;WDmgTz+ftar!{fOV#SZhrR@l{LSwf=LyxM zqpXwtZ>pT%XZ#-rhhcfOL&CiS|FrMkYg600tR#%-y{O+s(l{3W)kNbT5GA%4hW|_p z;E3k0#W9(;Rtdy+`7A z{i(#fP@}NJ*Zi-p^x%%kitIdkUJmc*6q@|*#(!Pf7h-!UF74sMTpTk_VRcD){ zc7Y=J04+HX*Vm;M&tsHYt@C`!Ee+idI)gdAf_K{5tf;i+H;UBK^LU*)r_vK^z}x{g zEy7Eq)!-pL2-cc)FIlV9jB=P7i_*Z3V$q42Kcg%4qVta8{AJ&2b7kyUg=|A^pt-4; zNHS~R15^Ey9SoK*J)sK{vZS|V-O`uju<9p!dEM<{lG#)tdhl3&H8u;#03ksPJm6fH zB&7a~=!{qjZi|dx^U^cFxO>iGgD3G8*lt&%`1*hHMm*>9Uz|@RS$**5ba+I6Wca@vhJW<*n2P{4#h!Z_ zLh%w74EtR>H-E;bm$_iPsaQt7kc&{}MQJv!UZ%6<#<``HJ(1ijOV_@i^gblhT=pJ& ztE{_@8Qq4M^}hJ$_31&msn+7v-SPikC2qV2I=7UPvgvq_|K0u;|1Ie?byIfXKfT0Z zmqu*yzeR~|OYWm@|bOG?|G$m;T1kTPGbUtF3Bv_*ZLHQfus?mcWY)WmZX(TWjD{8Nj$ShDd93NPjvnL$OQ|sN z@F#!5n6rlXJ6(S8}is1?`?lP5gc4X4e#A?#6%Jod#7@ z;Zpct{qS-bz3~rNl6*J*t$+GJnlK8Mb!D`O<@akccg88KO|!kI z1d6fD+p3GJ6q-Yyeq7swRU52ts{6GN7CkU-!1nA9R=)^_G77H%y_hw*d44%>ky$9$ zu|x~(NLlH!TyF^M>oC@bO$lw>BD)+xqz1FBk?vi}bvzLY=qDenf5rAN=d5ZRdbC@Y z?eOh8oa(RmS9on;R4bZ&$QMk;)JS2pvKn6=vD)st+JN@p&d6PD)riIsW23zN3#rj` zUApVn0*L&mzc4Wdb<>+tgHoVppUl3Z%_@JBW>p!?-e6NlYFrr~_`dIr@A{LUi^tRP zvcLLW_|*UU&*Lp``vs7o0+z2ed*Z(W3GYAQUo1giLNkjuQ*0SS9Q2lV zUVRGAId+inmrD{OU8>KkpmwPi*Jd%71eNZsS$z(Zief5n&Rlh^U@Qg&>*8~-+b z>F4FmlQ#W?<3AFHV2o{ixj;|4YuhHo4<= zeT$Hk*NLQ!^U&&zeHm%E`I##D{6n8usFmpH;Sf|sga-M)JRE;mc zUBt0cPt-R-WxN<$UuYhdj&#wQYB6a_*@wW3zz#^N)llEDFx;wL@-2(g^b|>U$JQ^( zg~+n>ojhB;BcwCSSO=R%kk~s_XAFrPGAj{e$zzKHicoN zlNdNRt$XF8$!HecaNGy~YD~afR>)1z3SE(oe_dht4`OEGKMeo$CHk#`LQedr?yK=( z1~*pJBFu-wxvNu4IUpckLn5Q`|GL+|;rK1X%jWlYeV2E^lll0_5B=cd?`vQG2H73V zwg>(j;w&GgSy>+j@xPvp!{_1gQ8|}`%rNk8PN3@$3>3*EiKHn$RufG%OQe;k3nzubFS1|ICWX&s|S)=N8}9L{~dw zIhXcAFd=d)ltS5=u`gU3ER_y+GF73Pq1tqoaoM$ZC@i27-Ma3!8m>0r%U0{3lv|-e zoGoRWgC*av2XMOlX$p!_qh;1o?P_hEr`(VleR+V5c~v1+4OzYAP>O%$m_t#b*qEWt za_%F*?%VyDgDZxTtDU6FKMA}NhwTc6w6@|)~)Tf zUcQ-WKjJ!${xy2T`uT z*ZU&?!~EcaN?xwvzZzN}3O-#+G}2^{sTi=%Jy!hR8vfJ1azCh5vTlGyXmPTjti7zbpQCUd8w1pJ;LT*^rSVxr%@G8MfT1_@AdX zPM=wFB%NAlGd=cGI~@NJF?4?={`0u9$vrmX1vh+`el22wM6&c)k(<6D)oV6dPEOJR z&#})v`1jJH8NUgf*{BURFr99NY%R(vptl#Sr2!WQozAoc!}xM$5wLe{1p2AHX{?oZ zcykPRa0e!DBP^-#{#T-{NtLpz*e`Vigf!YD6mX^XfKrrJakkqFEu9pRWQ20R1&)%9 zXydnVg1R|c?1@BqQJb(b;u>A2y)7TAA9g~qJm?2UmnR(#T~B=2j_^wPuj8_Mo=(zt7-bsvz%%?*H-^@sufMD8~)if z%)ge4ltZrMtjOGCu~OE$eZxQQkN<;n^sU@14iGW-LxB4R?Wg2m6BGY?bYKgGKb>SS z5Bs0S67j~%LrEX{61?gk{2-p}$19%yS@Dq{`a!Tg2aSI*bOpAG7RSHQ2~5&aoPlu% z{;^Glnb2#(U4Pm5aUXII)^vr$tk9Q46-?TL=~i2E)PwOLvZe7BdK6tOAl9i)E$Xkw z&;Co!_G{G*CjQg#4~;$T;&ov-{-K~-G8)!f6;f1vHsgOLW0dif)1B@a!yD<7#eZCh z|2H?gj++~r?Dpgteoys8rH#|4P41j*HBsCKwq?girx~g|5lV;>{VGL!Sw@H^*ACvQ zZcJZfDqu5R8c?$cm!B1iaQ_dnXgPp`OBZIEZX(fv`@Y*>m*Z#y$ zjVGkUU`gRnNfaA{8zhR>=RleA*V`E)aQ6^^^0Ox<4lCt~bpKjgUp}rKS zdGSm{JN~!YX8dQo_s|@1j{m^@@n6!6Sa!jGgx{M}P0btLi6@erv}%h>S{2RohfSXA z-y%F5BaOXp>2@gtOfQZgx#<3NF4ALYHh-ZiUiN$o7BvcBZa}1^LY6XQ-#4-*h!pF7 z<9rIBY?jFKVcG7p)!}Vk0bt0O%zU!=xDrlS+|eD3HS{QTuS9z}mp-GfhR4#WqtGRe z`3Zj+wDvjF1VLsS?3A~*Bfj*Nn~IsF(K+iWzb&8zTr)@&1Qu9rrWlll9b{v#_LtRW zFZxK3Ha*)vkdWp?4)V5IgX<-q^ataed1>%cwR-lwApF8_{8qf}9mhjMRVw!YWas%z zohV*tx91cw&&0G|1mA>*r*4PwxJ83@Neu2#+gGf`_b21e$zLR_7jFRR7EJA zY4A<_PpJh?8&2U+)lu_7;y*ODszMZCg&&;pZz4g436W2dcY2CdDKB37&EJNH{@h>0 zlli#(oX#s>{Q2#eHruA2a>1Ki6~ut>U&UEEoopk=x3)SiL^S2EBtyx)!X0~zuCZ8k z=E}ikHOWR_2JB0Z-6wBj0#f%93?nOWSz(iMPGA|Ql>|uOrokcW7Az8&%VxpUrK>Fy z|55laQCJb&+Ets>u8JrIRqK;A00x3|6LDxE&g{hS2AJe zpsNX1Oi{XzpuxI_HnbHB{2W~#+%6%VtX6M2IFgJ!D6CP(5UIg5r;qFA8=ckQ<>k^H z&xD&$&l!o((nH-J_0_BHSv-$x)Q*btH6V6Y9u&6cx3cRn%`X7LoP=SE1Yzm6l)bFa zz?i*3-@rw?fX+ncsF_03!Ks`W4J-qA@2RN-QGuUmpxv3e?W!`sl>PfoSU?VJi5E_e zJ-pUZF0Y#9XclC9&Z@wc~+%RxC7i22ULdI;H@GG|IfS-WdKXHr;#q$Y1-Lc;GXi-21Yg z^}xS>JczVe==jSC7~0dKxt;#UMu_vEQBN8`T}&VDYI;h%+b4{>MzPQgPJs zj$1HHhq;~a?yLM`kw{{&zKj`bFOewu_>kZ1>T*LefOfF1+hNZ&8kv%OZ#mciH$lk0 zt>Ga)w!)xA2mj+q2dcimR<@klSzkRvSvJWqb>=K?8 zrT{%(Y!Y6&Ib+77n(y3Zco_zeW2zkn81X?(MsYv{~>m;wW z8P>voKhLf7(Z0yJr#3IRV#G^gEgt-Hc1J` z+aLWWKZEDI;ESKs4<&uUAO3{nKY3<(_`v^h#2NoxF-!hPIMa8RtJMn3iU)H0#K0(p8%Y=^OH0c$!W2X=1L z<#07%XpMMZoF1?|)ovF4J4u|Z(vPNSy5}EK(5i6F_+LveO43@=`~K+j-kCb39Og*7 zZK!7%=o$Y+8;@DiXa&lg{BBD$^)s32GX?d`(3i(u#lT7AawkAvhfm zz{nLC**o6{BXluZdXy;U?204$Rd8WLcOIj<3HgqHdydlpB_m~(Xmh2{D3ao86H1BO zyv}dS(dp3!Kl!*47Fd4hb0nKl$TSs{+L48{P}DPh8Yrjp2w>9;baPpLa^JerlQDKw zx?29wzzH>$n#Ou#=_kX+*bsAHLA%Dik48U{#Q5Is{z4r2>+L-@PE)B`OJLe zul)@?S&#K|I{&`mpA$1Dfb4JhK>kAxm1oFTz|fFQa3GhhMiXKDfm-68OLF8rUP#|l zSR@uXBQV;s&q=*w!r6SQexRhxC#HAdABF`Ul@L(�f9md(U!J6cGNUg1#ct@ZW+X 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

Polaris Catalog Documentation

Download OpenAPI specification:Download

+

Quick Start

This guide serves as a introduction to several key entities that can be managed with Polaris, describes how to build and deploy Polaris locally, and finally includes examples of how to use Polaris with Spark and Trino.

+

Prerequisites

This guide covers building Polaris, deploying it locally or via Docker, and interacting with it using the command-line interface and Apache Spark. Before proceeding with Polaris, be sure to satisfy the relevant prerequisites listed here.

+

Building and Deploying Polaris

+

To get the latest Polaris code, you'll need to clone the repository using git. You can install git using homebrew:

+
brew install git
+
+

Then, use git to clone the Polaris repo:

+
cd ~
+git clone https://github.com/polaris-catalog/polaris.git
+
+

With Docker

+

If you plan to deploy Polaris inside Docker], you'll need to install docker itself. For can be done using homebrew:

+
brew install docker
+
+

Once installed, make sure Docker is running. This can be done on macOS with:

+
open -a Docker
+
+

From Source

+

If you plan to build Polaris from source yourself, you will need to satisfy a few prerequisites first.

+

Polaris is built using gradle and is compatible with Java 21. We recommend the use of jenv to manage multiple Java versions. For example, to install Java 21 via [homebre]w(https://brew.sh/) and configure it with jenv:

+
cd ~/polaris
+jenv local 21
+brew install openjdk@21 gradle@8 jenv
+jenv add $(brew --prefix openjdk@21)
+jenv local 21
+
+

Connecting to Polaris

+

Polaris is compatible with any Apache Iceberg client that supports the REST API. Depending on the client you plan to use, refer to the prerequisites below.

+

With Spark

+

If you want to connect to Polaris with Apache Spark, you'll need to start by cloning Spark. As above, make sure git is installed first. You can install it with homebrew:

+
brew install git
+
+

Then, clone Spark and check out a versioned branch. This guide uses Spark 3.5.0.

+
cd ~
+git clone https://github.com/apache/spark.git
+cd ~/spark
+git checkout branch-3.5.0
+
+

Deploying Polaris

Polaris can be deployed via a lightweight docker image or as a standalone process. Before starting, be sure that you've satisfied the relevant prerequisites detailed above.

+

Docker Image

+

To start using Polaris in Docker, launch Polaris while Docker is running:

+
cd ~/polaris
+docker compose -f docker-compose.yml up --build
+
+

Once the polaris-polaris container is up, you can continue to Defining a Catalog.

+

Building Polaris

+

Run Polaris locally with:

+
cd ~/polaris
+./gradlew runApp
+
+

You should see output for some time as Polaris builds and starts up. Eventually, you won’t see any more logs and should see messages that resemble the following:

+
INFO  [...] [main] [] o.e.j.s.handler.ContextHandler: Started i.d.j.MutableServletContextHandler@...
+INFO  [...] [main] [] o.e.j.server.AbstractConnector: Started application@...
+INFO  [...] [main] [] o.e.j.server.AbstractConnector: Started admin@...
+INFO  [...] [main] [] o.eclipse.jetty.server.Server: Started Server@...
+
+

At this point, Polaris is running.

+

Bootstrapping Polaris

For this tutorial, we'll launch an instance of Polaris that stores entities only in-memory. This means that any entities that you define will be destroyed when Polaris is shut down. It also means that Polaris will automatically bootstrap itself with root credentials. For more information on how to configure Polaris for production usage, see the docs.

+

When Polaris is launched using in-memory mode the root CLIENT_ID and CLIENT_SECRET can be found in stdout on initial startup. For example:

+
Bootstrapped with credentials: {"client-id": "XXXX", "client-secret": "YYYY"}
+
+

Be sure to note of these credentials as we'll be using them below.

+

Defining a Catalog

In Polaris, the catalog is the top-level entity that objects like tables and views are organized under. With a Polaris service running, you can create a catalog like so:

+
cd ~/polaris
+
+./polaris \
+  --client-id ${CLIENT_ID} \
+  --client-secret ${CLIENT_SECRET} \
+  catalogs \
+  create \
+  --storage-type s3 \
+  --default-base-location ${DEFAULT_BASE_LOCATION} \
+  --role-arn ${ROLE_ARN} \
+  quickstart_catalog
+
+

This will create a new catalog called quickstart_catalog.

+

The DEFAULT_BASE_LOCATION you provide will be the default location that objects in this catalog should be stored in, and the ROLE_ARN you provide should be a Role ARN with access to read and write data in that location. These credentials will be provided to engines reading data from the catalog once they have authenticated with Polaris using credentials that have access to those resources.

+

If you’re using a storage type other than S3, such as Azure, you’ll provide a different type of credential than a Role ARN. For more details on supported storage types, see the docs.

+

Additionally, if Polaris is running somewhere other than localhost:8181, you can specify the correct hostname and port by providing --host and --port flags. For the full set of options supported by the CLI, please refer to the docs.

+

Creating a Principal and Assigning it Privileges

+

With a catalog created, we can create a principal that has access to manage that catalog. For details on how to configure the Polaris CLI, see the section above or refer to the docs.

+
./polaris \
+  --client-id ${CLIENT_ID} \
+  --client-secret ${CLIENT_SECRET} \
+  principals \
+  create \
+  quickstart_user
+
+./polaris \
+  --client-id ${CLIENT_ID} \
+  --client-secret ${CLIENT_SECRET} \
+  principal-roles \
+  create \
+  quickstart_user_role
+
+./polaris \
+  --client-id ${CLIENT_ID} \
+  --client-secret ${CLIENT_SECRET} \
+  catalog-roles \
+  create \
+  --catalog quickstart_catalog \
+  quickstart_catalog_role
+
+

Be sure to provide the necessary credentials, hostname, and port as before.

+

When the principals create command completes successfully, it will return the credentials for this new principal. Be sure to note these down for later. For example:

+
./polaris ... principals create example
+{"clientId": "XXXX", "clientSecret": "YYYY"}
+
+

Now, we grant the principal the principal role we created, and grant the catalog role the principal role we created. For more information on these entities, please refer to the linked documentation.

+
./polaris \
+  --client-id ${CLIENT_ID} \
+  --client-secret ${CLIENT_SECRET} \
+  principal-roles \
+  grant \
+  --principal quickstart_user \
+  quickstart_user_role
+
+./polaris \
+  --client-id ${CLIENT_ID} \
+  --client-secret ${CLIENT_SECRET} \
+  catalog-roles \
+  grant \
+  --catalog quickstart_catalog \
+  --principal-role quickstart_user_role \
+  quickstart_catalog_role
+
+

Now, we’ve linked our principal to the catalog via roles like so:

+

Principal to Catalog

+

In order to give this principal the ability to interact with the catalog, we must assign some privileges. For the time being, we will give this principal the ability to fully manage content in our new catalog. We can do this with the CLI like so:

+
./polaris \
+  --client-id ${CLIENT_ID} \
+  --client-secret ${CLIENT_SECRET} \
+  privileges \
+  --catalog quickstart_catalog \
+  --catalog-role quickstart_catalog_role \
+  catalog \
+  grant \
+  CATALOG_MANAGE_CONTENT
+
+

This grants the catalog privileges CATALOG_MANAGE_CONTENT to our catalog role, linking everything together like so:

+

Principal to Catalog with Catalog Role

+

CATALOG_MANAGE_CONTENT has create/list/read/write privileges on all entities within the catalog. The same privilege could be granted to a namespace, in which case the principal could create/list/read/write any entity under that namespace.

+

Using Iceberg & Polaris

At this point, we’ve created a principal and granted it the ability to manage a catalog. We can now use an external engine to assume that principal, access our catalog, and store data in that catalog using Apache Iceberg.

+

Connecting with Spark

+

To use a Polaris-managed catalog in Apache Spark, we can configure Spark to use the Iceberg catalog REST API.

+

This guide uses Apache Spark 3.5, but be sure to find the appropriate iceberg-spark package for your Spark version. With a local Spark clone, we on the branch-3.5 branch we can run the following:

+

Note: the credentials provided here are those for our principal, not the root credentials.

+
bin/spark-shell \
+--packages org.apache.iceberg:iceberg-spark-runtime-3.5_2.12:1.5.2,org.apache.hadoop:hadoop-aws:3.4.0 \
+--conf spark.sql.extensions=org.apache.iceberg.spark.extensions.IcebergSparkSessionExtensions \
+--conf spark.sql.catalog.quickstart_catalog.warehouse=quickstart_catalog \
+--conf spark.sql.catalog.quickstart_catalog.header.X-Iceberg-Access-Delegation=true \
+--conf spark.sql.catalog.quickstart_catalog=org.apache.iceberg.spark.SparkCatalog \
+--conf spark.sql.catalog.quickstart_catalog.catalog-impl=org.apache.iceberg.rest.RESTCatalog \
+--conf spark.sql.catalog.quickstart_catalog.uri=http://localhost:8181/api/catalog \
+--conf spark.sql.catalog.quickstart_catalog.credential='XXXX:YYYY' \
+--conf spark.sql.catalog.quickstart_catalog.scope='PRINCIPAL_ROLE:ALL' \
+--conf spark.sql.catalog.quickstart_catalog.token-refresh-enabled=true
+
+

Replace XXXX and YYYY with the client ID and client secret generated when you created the quickstart_user principal.

+

Similar to the CLI commands above, this configures Spark to use the Polaris running at localhost:8181 as a catalog. If your Polaris server is running elsewhere, but sure to update the configuration appropriately.

+

Finally, note that we include the hadoop-aws package here. If your table is using a different filesystem, be sure to include the appropriate dependency.

+

Once the Spark session starts, we can create a namespace and table within the catalog:

+
spark.sql("USE quickstart_catalog")
+spark.sql("CREATE NAMESPACE IF NOT EXISTS quickstart_namespace")
+spark.sql("CREATE NAMESPACE IF NOT EXISTS quickstart_namespace.schema")
+spark.sql("USE NAMESPACE quickstart_namespace.schema")
+spark.sql("""
+    CREATE TABLE IF NOT EXISTS quickstart_table (
+        id BIGINT, data STRING
+    ) 
+USING ICEBERG
+""")
+
+

We can now use this table like any other:

+
spark.sql("INSERT INTO quickstart_table VALUES (1, 'some data')")
+spark.sql("SELECT * FROM quickstart_table").show(false)
+. . .
++---+---------+
+|id |data     |
++---+---------+
+|1  |some data|
++---+---------+
+
+

If at any time access is revoked...

+
./polaris \
+  --client-id ${CLIENT_ID} \
+  --client-secret ${CLIENT_SECRET} \
+  privileges \
+  --catalog quickstart_catalog \
+  --catalog-role quickstart_catalog_role \
+  catalog \
+  revoke \
+  CATALOG_MANAGE_CONTENT
+
+

Spark will lose access to the table:

+
spark.sql("SELECT * FROM quickstart_table").show(false)
+
+org.apache.iceberg.exceptions.ForbiddenException: Forbidden: Principal 'quickstart_user' with activated PrincipalRoles '[]' and activated ids '[6, 7]' is not authorized for op LOAD_TABLE_WITH_READ_DELEGATION
+
+

Polaris Catalog Overview

- +

Polaris Catalog is a catalog implementation for Apache Iceberg built on the open source Apache Iceberg REST protocol.

+

With Polaris Catalog, you can provide centralized, secure read and write access across different REST-compatible query engines to your Iceberg tables.

+

Conceptual diagram of Polaris Catalog.

+

Key concepts

This section introduces key concepts associated with using Polaris Catalog.

+

In the following diagram, a sample Polaris Catalog structure with nested namespaces is shown for Catalog1. No tables +or namespaces have been created yet for Catalog2 or Catalog3:

+

Diagram that shows an example Polaris Catalog structure.

+

Catalog

+

In Polaris Catalog, you can create one or more catalog resources to organize Iceberg tables.

+

Configure your catalog by setting values in the storage configuration for S3, Azure, or Google Cloud Storage. An Iceberg catalog enables a +query engine to manage and organize tables. The catalog forms the first architectural layer in the Iceberg table specification and must support:

+
    +
  • Storing the current metadata pointer for one or more Iceberg tables. A metadata pointer maps a table name to the location of that table's +current metadata file.

    +
  • +
  • Performing atomic operations so that you can update the current metadata pointer for a table to the metadata pointer of a new version of +the table.

    +
  • +
+

To learn more about Iceberg catalogs, see the Apache Iceberg documentation.

+

Catalog types

+

A catalog can be one of the following two types:

+
    +
  • Internal: The catalog is managed by Polaris. Tables from this catalog can be read and written in Polaris.

    +
  • +
  • External: The catalog is externally managed by another Iceberg catalog provider (for example, Snowflake, Glue, Dremio Arctic). Tables from +this catalog are synced to Polaris. These tables are read-only in Polaris. In the current release, only Snowflake external catalog is provided.

    +
  • +
+

A catalog is configured with a storage configuration that can point to S3, Azure storage, or GCS.

+

To create a new catalog, see Create a catalog.

+

Namespace

+

You create namespaces to logically group Iceberg tables within a catalog. A catalog can have one or more namespaces. You can also create +nested namespaces. Iceberg tables belong to namespaces.

+

Iceberg tables & catalogs

+

In an internal catalog, an Iceberg table is registered in Polaris Catalog, but read and written via query engines. The table data and +metadata is stored in your external cloud storage. The table uses Polaris Catalog as the Iceberg catalog.

+

If you have tables that use Snowflake as the Iceberg catalog (Snowflake-managed tables), you can sync these tables to an external +catalog in Polaris Catalog. If you sync this catalog to Polaris Catalog, it appears as an external catalog in Polaris Catalog. The table data and +metadata is stored in your external cloud storage. The Snowflake query engine can read from or write to these tables. However, the other query +engines can only read from these tables.

+

Important

+

To ensure that the access privileges defined for a catalog are enforced +correctly, you must:

+
    +
  • Ensure a directory only contains the data files that belong to a +single table.

    +
  • +
  • Create a directory hierarchy that matches the namespace hierarchy +for the catalog.

    +
  • +
+

For example, if a catalog includes:

+
    +
  • Top-level namespace namespace1

    +
  • +
  • Nested namespace namespace1a

    +
  • +
  • A customers table, which is grouped under nested namespace +namespace1a

    +
  • +
  • An orders table, which is grouped under nested namespace namespace1a

    +
  • +
+

The directory hierarchy for the catalog must be:

+
    +
  • /namespace1/namespace1a/customers/<files for the customers table +*only*>

    +
  • +
  • /namespace1/namespace1a/orders/<files for the orders table *only*>

    +
  • +
+

Service principal

+

A service principal is an entity that you create in Polaris Catalog. Each service principal encapsulates credentials that you use to connect +to Polaris Catalog.

+

Query engines use service principals to connect to catalogs.

+

Polaris Catalog generates a Client ID and Client Secret pair for each service principal.

+

The following table displays example service principals that you might create in Polaris Catalog:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
Service connection nameDescription
Flink ingestionFor Apache Flink to ingest streaming data into Iceberg tables.
Spark ETL pipelineFor Apache Spark to run ETL pipeline jobs on Iceberg tables.
Snowflake data pipelinesFor Snowflake to run data pipelines for transforming data in Iceberg tables.
Trino BI dashboardFor Trino to run BI queries for powering a dashboard.
Snowflake AI teamFor Snowflake to run AI jobs on data in Iceberg tables.
+

Service connection

+

A service connection represents a REST-compatible engine (such as Apache Spark, Apache Flink, or Trino) that can read from and write to Polaris +Catalog. When creating a new service connection, the Polaris administrator grants the service principal that is created with the new service +connection with either a new or existing principal role. A principal role is a resource in Polaris that you can use to logically group Polaris +service principals together and grant privileges on securable objects. For more information, see Principal role. Polaris Catalog uses a role-based access control (RBAC) model to grant service principals access to resources. For more information, +see Access control. For a diagram of this model, see RBAC model.

+

If the Polaris administrator grants the service principal for the new service connection with a new principal role, the service principal +doesn't have any privileges granted to it yet. When securing the catalog that the new service connection will connect to, the Polaris +administrator grants privileges to catalog roles and then grants these catalog roles to the new principal role. As a result, the service +principal for the new service connection is bestowed with these privileges. For more information about catalog roles, see Catalog role.

+

If the Polaris administrator grants an existing principal role to the service principal for the new service connection, the service principal +is bestowed with the privileges granted to the catalog roles that are granted to the existing principal role. If needed, the Polaris +administrator can grant additional catalog roles to the existing principal role or remove catalog roles from it to adjust the privileges +bestowed to the service principal. For an example of how RBAC works in Polaris, see RBAC example.

+

Storage configuration

+

A storage configuration stores a generated identity and access management (IAM) entity for your external cloud storage and is created +when you create a catalog. The storage configuration is used to set the values to connect Polaris Catalog to your cloud storage. During the +catalog creation process, an IAM entity is generated and used to create a trust relationship between the cloud storage provider and Polaris +Catalog.

+

When you create a catalog, you supply the following information about your external cloud storage:

+ + + + + + + + + + + + + + + + + + + +
Cloud storage providerInformation
Amazon S3
  • Default base location for your Amazon S3 bucket
  • Locations for your Amazon S3 bucket
  • S3 role ARN
  • External ID (optional)
Google Cloud Storage (GCS)
  • Default base location for your GCS bucket
  • Locations for your Amazon GCS bucket
Azure
  • Default base location for your Microsoft Azure container
  • Locations for your Microsoft Azure container
  • Azure tenant ID
+

Example workflow

In the following example workflow, Bob creates an Iceberg table named Table1 and Alice reads data from Table1.

+
    +
  1. Bob uses Apache Spark to create the Table1 table under the +Namespace1 namespace in the Catalog1 catalog and insert values into +Table1.

    +

    Bob can create Table1 and insert data into it, because he is using a +service connection with a service principal that is bestowed with +the privileges to perform these actions.

    +
  2. +
  3. Alice uses Snowflake to read data from Table1.

    +

    Alice can read data from Table1, because she is using a service +connection with a service principal with a catalog integration that +is bestowed with the privileges to perform this action. Alice +creates an unmanaged table in Snowflake to read data from Table1.

    +
  4. +
+

Diagram that shows an example workflow for Polaris Catalog

+

Security and access control

This section describes security and access control.

+

Credential vending

+

To secure interactions with service connections, Polaris Catalog vends temporary storage credentials to the query engine during query +execution. These credentials allow the query engine to run the query without needing to have access to your external cloud storage for +Iceberg tables. This process is called credential vending.

+

Identity and access management (IAM)

+

Polaris Catalog uses the identity and access management (IAM) entity to securely connect to your storage for accessing table data, Iceberg +metadata, and manifest files that store the table schema, partitions, and other metadata. Polaris Catalog retains the IAM entity for your +storage location.

+

Access control

+

Polaris Catalog enforces the access control that you configure across all tables registered with the service, and governs security for all +queries from query engines in a consistent manner.

+

Polaris uses a role-based access control (RBAC) model that lets you centrally configure access for Polaris service principals to catalogs, +namespaces, and tables.

+

Polaris RBAC uses two different role types to delegate privileges:

+
    +
  • Principal roles: Granted to Polaris service principals and +analogous to roles in other access control systems that you grant to +service principals.

    +
  • +
  • Catalog roles: Configured with certain privileges on Polaris +catalog resources, and granted to principal roles.

    +
  • +
+

For more information, see Access control.

+

Polaris Catalog Entities

- Polaris Management Service - - - - - - - - + Copyright (c) 2024 Snowflake Computing 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. + +--> + +<p>This page documents various entities that can be managed in Polaris.</p> +"> + +

This page documents various entities that can be managed in Polaris.

+

Catalog

A catalog is a top-level entity in Polaris that may contain other entities like namespaces and tables. These map directly to Apache Iceberg catalogs.

+

For information on managing catalogs with the REST API or for more information on what data can be associated with a catalog, see the API docs.

+

Storage Type

+

All catalogs in Polaris are associated with a storage type. Valid Storage Types are S3, Azure, and GCS. The FILE type is also additionally available for testing. Each of these types relates to a different storage provider where data within the catalog may reside. Depending on the storage type, various other configurations may be set for a catalog including credentials to be used when accessing data inside the catalog.

+

For details on how to use Storage Types in the REST API, see the API docs.

+

Namespace

A namespace is a logical entity that resides within a catalog and can contain other entities such as tables or views. Some other systems may refer to namespaces as schemas or databases.

+

In Polaris, namespaces can be nested up to 16 levels. For example, a.b.c.d.e.f.g is a valid namespace. b is said to reside within a, and so on.

+

For information on managing namespaces with the REST API or for more information on what data can be associated with a namespace, see the API docs.

+

Table

Polaris tables are entites that map to Apache Iceberg tables.

+

For information on managing tables with the REST API or for more information on what data can be associated with a table, see the API docs.

+

View

Polaris views are entites that map to Apache Iceberg views.

+

For information on managing views with the REST API or for more information on what data can be associated with a view, see the API docs.

+

Principal

Polaris principals are unique identities that can be used to represent users or services. Each principal may have one or more principal roles assigned to it for the purpose of accessing catalogs and the entities within them.

+

For information on managing principals with the REST API or for more information on what data can be associated with a principal, see the API docs.

+

Principal Role

Polaris principal roles are labels that may be granted to principals. Each principal may have one or more principal roles, and the same principal role may be granted to multiple principals. Principal roles may be assigned based on the persona or responsibilities of a given principal, or on how that principal will need to access different entities within Polaris.

+

For information on managing principal roles with the REST API or for more information on what data can be associated with a principal role, see the API docs.

+

Catalog Role

Polaris catalog roles are labels that may be granted to catalogs. Each catalog may have one or more catalog roles, and the same catalog role may be granted to multiple catalogs. Catalog roles may be assigned based on the nature of data that will reside in a catalog, or by the groups of users and services that might need to access that data.

+

Each catalog role may have multiple privileges granted to it, and each catalog role can be granted to one or more principal roles. This is the mechanism by which principals are granted access to entities inside a catalog such as namespaces and tables.

+

Privilege

Polaris privileges are granted to catalog roles in order to grant principals with a given principal role some degree of access to catalogs with a given catalog role. When a privilege is granted to a catalog role, any principal roles granted that catalog role receive the privilege. In turn, any principals who are granted that principal role receive it.

+

A privilege can be scoped to any entity inside a catalog, including the catalog itself.

+

For a list of supported privileges for each privilege class, see the API docs:

+ +

Access Control

+ +

This section provides information about how access control works for Polaris Catalog.

+

Polaris Catalog uses a role-based access control (RBAC) model, in which the Polaris administrator assigns access privileges to catalog roles, +and then grants service principals access to resources by assigning catalog roles to principal roles.

+

The key concepts to understanding access control in Polaris are:

+
    +
  • Securable object
  • +
  • Principal role
  • +
  • Catalog role
  • +
  • Privilege
  • +
+

Securable object

A securable object is an object to which access can be granted. Polaris +has the following securable objects:

+
    +
  • Catalog
  • +
  • Namespace
  • +
  • Iceberg table
  • +
  • View
  • +
+

Principal role

A principal role is a resource in Polaris that you can use to logically group Polaris service principals together and grant privileges on +securable objects.

+

Polaris supports a many-to-one relationship between service principals and principal roles. For example, to grant the same privileges to +multiple service principals, you can grant a single principal role to those service principals. A service principal can be granted one +principal role. When registering a service connection, the Polaris administrator specifies the principal role that is granted to the +service principal.

+

You don't grant privileges directly to a principal role. Instead, you configure object permissions at the catalog role level, and then grant +catalog roles to a principal role.

+

The following table shows examples of principal roles that you might configure in Polaris:

+ + + + + + + + + + + + + + + +
Principal role nameDescription
Data_engineerA role that is granted to multiple service principals for running data engineering jobs.
Data_scientistA role that is granted to multiple service principals for running data science or AI jobs.
+

Catalog role

A catalog role belongs to a particular catalog resource in Polaris and specifies a set of permissions for actions on the catalog, or on objects +in the catalog, such as catalog namespaces or tables. You can create one or more catalog roles for a catalog.

+

You grant privileges to a catalog role, and then grant the catalog role to a principal role to bestow the privileges to one or more service +principals.

+

Note

+

If you update the privileges bestowed to a service principal, the updates won't take effect for up to one hour. This means that if you +revoke or grant some privileges for a catalog, the updated privileges won't take effect on any service principal with access to that catalog +for up to one hour.

+

Polaris also supports a many-to-many relationship between catalog roles and principal roles. You can grant the same catalog role to one or more +principal roles. Likewise, a principal role can be granted to one or more catalog roles.

+

The following table displays examples of catalog roles that you might +configure in Polaris:

+ + + + + + + + + + + + + + + + + + + +
Example Catalog roleDescription
Catalog administratorsA role that has been granted multiple privileges to emulate full access to the catalog.

Principal roles that have been granted this role are permitted to create, alter, read, write, and drop tables in the catalog.
Catalog readersA role that has been granted read-only privileges to tables in the catalog.

Principal roles that have been granted this role are allowed to read from tables in the catalog.
Catalog contributorA role that has been granted read and write access privileges to all tables that belong to the catalog.

Principal roles that have been granted this role are allowed to perform read and write operations on tables in the catalog.
+

RBAC model

The following diagram illustrates the RBAC model used by Polaris Catalog. For each catalog, the Polaris administrator assigns access +privileges to catalog roles, and then grants service principals access to resources by assigning catalog roles to principal roles. Polaris +supports a many-to-one relationship between service principals and principal roles.

+

Diagram that shows the RBAC model for Polaris Catalog.

+

Access control privileges

This section describes the privileges that are available in the Polaris access control model. Privileges are granted to catalog roles, catalog +roles are granted to principal roles, and principal roles are granted to service principals to specify the operations that service principals can +perform on objects in Polaris.

+

To grant the full set of privileges (drop, list, read, write, etc.) on an object, you can use the full privilege option.

+

Table privileges

+

Note

+

The TABLE_FULL_METADATA full privilege doesn't grant access to the TABLE_READ_DATA or TABLE_WRITE_DATA individual privileges.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Full privilegeIndividual privilegeDescription
TABLE_FULL_METADATATABLE_CREATEEnables registering a table with the catalog.
TABLE_DROPEnables dropping a table from the catalog.
TABLE_LISTEnables listing any tables in the catalog.
TABLE_READ_PROPERTIESEnables reading properties of the table.
TABLE_WRITE_PROPERTIESEnables configuring properties for the table.
N/ATABLE_READ_DATAEnables reading data from the table by receiving short-lived read-only storage credentials from the catalog.
N/ATABLE_WRITE_DATAEnables writing data to the table by receiving short-lived read+write storage credentials from the catalog.
+

View privileges

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Full privilegeIndividual privilegeDescription
VIEW_FULL_METADATAVIEW_CREATEEnables registering a view with the catalog.
VIEW_DROPEnables dropping a view from the catalog.
VIEW_LISTEnables listing any views in the catalog.
VIEW_READ_PROPERTIESEnables reading all the view properties.
VIEW_WRITE_PROPERTIESEnables configuring view properties.
+

Namespace privileges

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Full privilegeIndividual privilegeDescription
NAMESPACE_FULL_METADATANAMESPACE_CREATEEnables creating a namespace in a catalog.
NAMESPACE_DROPEnables dropping the namespace from the catalog.
NAMESPACE_LISTEnables listing any object in the namespace, including nested namespaces and tables.
NAMESPACE_READ_PROPERTIESEnables reading all the namespace properties.
NAMESPACE_WRITE_PROPERTIESEnables configuring namespace properties.
+

Catalog privileges

+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
PrivilegeDescription
CATALOG_MANAGE_ACCESSIncludes the ability to grant or revoke privileges on objects in a catalog to catalog roles, and the ability to grant or revoke catalog roles to or from principal roles.
CATALOG_MANAGE_CONTENTEnables full management of content for the catalog. This privilege encompasses the following privileges:
  • CATALOG_MANAGE_METADATA
  • TABLE_FULL_METADATA
  • NAMESPACE_FULL_METADATA
  • VIEW_FULL_METADATA
  • TABLE_WRITE_DATA
  • TABLE_READ_DATA
  • CATALOG_READ_PROPERTIES
  • CATALOG_WRITE_PROPERTIES
CATALOG_MANAGE_METADATAEnables full management of the catalog, as well as catalog roles, namespaces, and tables.
CATALOG_READ_PROPERTIESEnables listing catalogs and reading properties of the catalog.
CATALOG_WRITE_PROPERTIESEnables configuring catalog properties.
+

RBAC example

The following diagram illustrates how RBAC works in Polaris, and +includes the following users:

+
    +
  • Alice: A service admin who signs up for Polaris. Alice can +create service principals. She can also create catalogs and +namespaces, and configure access control for Polaris resources.
  • +
+
+

Note

+

The service principal for Alice is not visible in the Polaris Catalog +user interface.

+
+
    +
  • Bob: A data engineer who uses Snowpipe Streaming (in Snowflake) +and Apache Spark connections to interact with Polaris.

    +
      +
    • Alice has created a service principal for Bob. It has been +granted the Data_engineer principal role, which in turn has been +granted the following catalog roles: Catalog contributor and +Data administrator (for both the Silver and Gold zone catalogs +in the following diagram).

      +
    • +
    • The Catalog contributor role grants permission to create +namespaces and tables in the Bronze zone catalog.

      +
    • +
    • The Data administrator roles grant full administrative rights to +the Silver zone catalog and Gold zone catalog.

      +
    • +
    +
  • +
  • Mark: A data scientist who uses Snowflake AI services to +interact with Polaris.

    +
      +
    • Alice has created a service principal for Mark. It has been +granted the Data_scientist principal role, which in turn has +been granted the catalog role named Catalog reader.

      +
    • +
    • The Catalog reader role grants read-only access for a catalog +named Gold zone catalog.

      +
    • +
    +
  • +
+

Diagram that shows an example of how RBAC works in Polaris Catalog.

+

other

listCatalogs

List all catalogs in this polaris service

+
Authorizations:
Polaris_Management_Service_OAuth2

Responses

Response samples

Content type
application/json
{
  • "catalogs": [
    ]
}

createCatalog

Add a new Catalog

+
Authorizations:
Polaris_Management_Service_OAuth2
Request Body schema: application/json
required

The Catalog to create

+
required
object (Polaris_Management_Service_Catalog)

A catalog object. A catalog may be internal or external. Internal catalogs are managed entirely by an external catalog interface. Third party catalogs may be other Iceberg REST implementations or other services with their own proprietary APIs

+

Responses

Request samples

Content type
application/json
{
  • "catalog": {
    }
}

getCatalog

Get the details of a catalog

+
Authorizations:
Polaris_Management_Service_OAuth2
path Parameters
catalogName
required
string [ 1 .. 256 ] characters ^(?!\s*[s|S][y|Y][s|S][t|T][e|E][m|M]\$).*$

The name of the catalog

+

Responses

Response samples

Content type
application/json
Example
{
  • "type": "INTERNAL",
  • "name": "string",
  • "properties": {
    },
  • "createTimestamp": 0,
  • "lastUpdateTimestamp": 0,
  • "entityVersion": 0,
  • "storageConfigInfo": {
    }
}

updateCatalog

Update an existing catalog

+
Authorizations:
Polaris_Management_Service_OAuth2
path Parameters
catalogName
required
string [ 1 .. 256 ] characters ^(?!\s*[s|S][y|Y][s|S][t|T][e|E][m|M]\$).*$

The name of the catalog

+
Request Body schema: application/json
required

The catalog details to use in the update

+
currentEntityVersion
integer

The version of the object onto which this update is applied; if the object changed, the update will fail and the caller should retry after fetching the latest version.

+
object
object (Polaris_Management_Service_StorageConfigInfo)

A storage configuration used by catalogs

+

Responses

Request samples

Content type
application/json
{
  • "currentEntityVersion": 0,
  • "properties": {
    },
  • "storageConfigInfo": {
    }
}

Response samples

Content type
application/json
Example
{
  • "type": "INTERNAL",
  • "name": "string",
  • "properties": {
    },
  • "createTimestamp": 0,
  • "lastUpdateTimestamp": 0,
  • "entityVersion": 0,
  • "storageConfigInfo": {
    }
}

deleteCatalog

Delete an existing catalog. This is a cascading operation that deletes all metadata, including principals, roles and grants. If the catalog is an internal catalog, all tables and namespaces are dropped without purge.

+
Authorizations:
Polaris_Management_Service_OAuth2
path Parameters
catalogName
required
string [ 1 .. 256 ] characters ^(?!\s*[s|S][y|Y][s|S][t|T][e|E][m|M]\$).*$

The name of the catalog

+

Responses

listPrincipals

List the principals for the current catalog

+
Authorizations:
Polaris_Management_Service_OAuth2

Responses

Response samples

Content type
application/json
{
  • "principals": [
    ]
}

createPrincipal

Create a principal

+
Authorizations:
Polaris_Management_Service_OAuth2
Request Body schema: application/json
required

The principal to create

+
object (Polaris_Management_Service_Principal)

A Polaris principal.

+
credentialRotationRequired
boolean

If true, the initial credentials can only be used to call rotateCredentials

+

Responses

Request samples

Content type
application/json
{
  • "principal": {
    },
  • "credentialRotationRequired": true
}

Response samples

Content type
application/json
{
  • "principal": {
    },
  • "credentials": {
    }
}

getPrincipal

Get the principal details

+
Authorizations:
Polaris_Management_Service_OAuth2
path Parameters
principalName
required
string [ 1 .. 256 ] characters ^(?!\s*[s|S][y|Y][s|S][t|T][e|E][m|M]\$).*$

The principal name

+

Responses

Response samples

Content type
application/json
{
  • "name": "string",
  • "clientId": "string",
  • "properties": {
    },
  • "createTimestamp": 0,
  • "lastUpdateTimestamp": 0,
  • "entityVersion": 0
}

updatePrincipal

Update an existing principal

+
Authorizations:
Polaris_Management_Service_OAuth2
path Parameters
principalName
required
string [ 1 .. 256 ] characters ^(?!\s*[s|S][y|Y][s|S][t|T][e|E][m|M]\$).*$

The principal name

+
Request Body schema: application/json
required

The principal details to use in the update

+
currentEntityVersion
required
integer

The version of the object onto which this update is applied; if the object changed, the update will fail and the caller should retry after fetching the latest version.

+
required
object

Responses

Request samples

Content type
application/json
{
  • "currentEntityVersion": 0,
  • "properties": {
    }
}

Response samples

Content type
application/json
{
  • "name": "string",
  • "clientId": "string",
  • "properties": {
    },
  • "createTimestamp": 0,
  • "lastUpdateTimestamp": 0,
  • "entityVersion": 0
}

deletePrincipal

Remove a principal from polaris

+
Authorizations:
Polaris_Management_Service_OAuth2
path Parameters
principalName
required
string [ 1 .. 256 ] characters ^(?!\s*[s|S][y|Y][s|S][t|T][e|E][m|M]\$).*$

The principal name

+

Responses

rotateCredentials

Rotate a principal's credentials. The new credentials will be returned in the response. This is the only API, aside from createPrincipal, that returns the user's credentials. This API is not idempotent.

+
Authorizations:
Polaris_Management_Service_OAuth2
path Parameters
principalName
required
string [ 1 .. 256 ] characters ^(?!\s*[s|S][y|Y][s|S][t|T][e|E][m|M]\$).*$

The user name

+

Responses

Response samples

Content type
application/json
{
  • "principal": {
    },
  • "credentials": {
    }
}

listPrincipalRolesAssigned

List the roles assigned to the principal

+
Authorizations:
Polaris_Management_Service_OAuth2
path Parameters
principalName
required
string [ 1 .. 256 ] characters ^(?!\s*[s|S][y|Y][s|S][t|T][e|E][m|M]\$).*$

The name of the target principal

+

Responses

Response samples

Content type
application/json
{
  • "roles": [
    ]
}

assignPrincipalRole

Add a role to the principal

+
Authorizations:
Polaris_Management_Service_OAuth2
path Parameters
principalName
required
string [ 1 .. 256 ] characters ^(?!\s*[s|S][y|Y][s|S][t|T][e|E][m|M]\$).*$

The name of the target principal

+
Request Body schema: application/json
required

The principal role to assign

+
object (Polaris_Management_Service_PrincipalRole)

Responses

Request samples

Content type
application/json
{
  • "principalRole": {
    }
}

revokePrincipalRole

Remove a role from a catalog principal

+
Authorizations:
Polaris_Management_Service_OAuth2
path Parameters
principalName
required
string [ 1 .. 256 ] characters ^(?!\s*[s|S][y|Y][s|S][t|T][e|E][m|M]\$).*$

The name of the target principal

+
principalRoleName
required
string [ 1 .. 256 ] characters ^(?!\s*[s|S][y|Y][s|S][t|T][e|E][m|M]\$).*$

The name of the role

+

Responses

listPrincipalRoles

List the principal roles

+
Authorizations:
Polaris_Management_Service_OAuth2

Responses

Response samples

Content type
application/json
{
  • "roles": [
    ]
}

createPrincipalRole

Create a principal role

+
Authorizations:
Polaris_Management_Service_OAuth2
Request Body schema: application/json
required

The principal to create

+
object (Polaris_Management_Service_PrincipalRole)

Responses

Request samples

Content type
application/json
{
  • "principalRole": {
    }
}

getPrincipalRole

Get the principal role details

+
Authorizations:
Polaris_Management_Service_OAuth2
path Parameters
principalRoleName
required
string [ 1 .. 256 ] characters ^(?!\s*[s|S][y|Y][s|S][t|T][e|E][m|M]\$).*$

The principal role name

+

Responses

Response samples

Content type
application/json
{
  • "name": "string",
  • "properties": {
    },
  • "createTimestamp": 0,
  • "lastUpdateTimestamp": 0,
  • "entityVersion": 0
}

updatePrincipalRole

Update an existing principalRole

+
Authorizations:
Polaris_Management_Service_OAuth2
path Parameters
principalRoleName
required
string [ 1 .. 256 ] characters ^(?!\s*[s|S][y|Y][s|S][t|T][e|E][m|M]\$).*$

The principal role name

+
Request Body schema: application/json
required

The principalRole details to use in the update

+
currentEntityVersion
required
integer

The version of the object onto which this update is applied; if the object changed, the update will fail and the caller should retry after fetching the latest version.

+
required
object

Responses

Request samples

Content type
application/json
{
  • "currentEntityVersion": 0,
  • "properties": {
    }
}

Response samples

Content type
application/json
{
  • "name": "string",
  • "properties": {
    },
  • "createTimestamp": 0,
  • "lastUpdateTimestamp": 0,
  • "entityVersion": 0
}

deletePrincipalRole

Remove a principal role from polaris

+
Authorizations:
Polaris_Management_Service_OAuth2
path Parameters
principalRoleName
required
string [ 1 .. 256 ] characters ^(?!\s*[s|S][y|Y][s|S][t|T][e|E][m|M]\$).*$

The principal role name

+

Responses

listAssigneePrincipalsForPrincipalRole

List the Principals to whom the target principal role has been assigned

+
Authorizations:
Polaris_Management_Service_OAuth2
path Parameters
principalRoleName
required
string [ 1 .. 256 ] characters ^(?!\s*[s|S][y|Y][s|S][t|T][e|E][m|M]\$).*$

The principal role name

+

Responses

Response samples

Content type
application/json
{
  • "principals": [
    ]
}

listCatalogRolesForPrincipalRole

Get the catalog roles mapped to the principal role

+
Authorizations:
Polaris_Management_Service_OAuth2
path Parameters
principalRoleName
required
string [ 1 .. 256 ] characters ^(?!\s*[s|S][y|Y][s|S][t|T][e|E][m|M]\$).*$

The principal role name

+
catalogName
required
string [ 1 .. 256 ] characters ^(?!\s*[s|S][y|Y][s|S][t|T][e|E][m|M]\$).*$

The name of the catalog where the catalogRoles reside

+

Responses

Response samples

Content type
application/json
{
  • "roles": [
    ]
}

assignCatalogRoleToPrincipalRole

Assign a catalog role to a principal role

+
Authorizations:
Polaris_Management_Service_OAuth2
path Parameters
principalRoleName
required
string [ 1 .. 256 ] characters ^(?!\s*[s|S][y|Y][s|S][t|T][e|E][m|M]\$).*$

The principal role name

+
catalogName
required
string [ 1 .. 256 ] characters ^(?!\s*[s|S][y|Y][s|S][t|T][e|E][m|M]\$).*$

The name of the catalog where the catalogRoles reside

+
Request Body schema: application/json
required

The principal to create

+
object (Polaris_Management_Service_CatalogRole)

Responses

Request samples

Content type
application/json
{
  • "catalogRole": {
    }
}

revokeCatalogRoleFromPrincipalRole

Remove a catalog role from a principal role

+
Authorizations:
Polaris_Management_Service_OAuth2
path Parameters
principalRoleName
required
string [ 1 .. 256 ] characters ^(?!\s*[s|S][y|Y][s|S][t|T][e|E][m|M]\$).*$

The principal role name

+
catalogName
required
string [ 1 .. 256 ] characters ^(?!\s*[s|S][y|Y][s|S][t|T][e|E][m|M]\$).*$

The name of the catalog that contains the role to revoke

+
catalogRoleName
required
string [ 1 .. 256 ] characters ^(?!\s*[s|S][y|Y][s|S][t|T][e|E][m|M]\$).*$

The name of the catalog role that should be revoked

+

Responses

listCatalogRoles

List existing roles in the catalog

+
Authorizations:
Polaris_Management_Service_OAuth2
path Parameters
catalogName
required
string [ 1 .. 256 ] characters ^(?!\s*[s|S][y|Y][s|S][t|T][e|E][m|M]\$).*$

The catalog for which we are reading/updating roles

+

Responses

Response samples

Content type
application/json
{
  • "roles": [
    ]
}

createCatalogRole

Create a new role in the catalog

+
Authorizations:
Polaris_Management_Service_OAuth2
path Parameters
catalogName
required
string [ 1 .. 256 ] characters ^(?!\s*[s|S][y|Y][s|S][t|T][e|E][m|M]\$).*$

The catalog for which we are reading/updating roles

+
Request Body schema: application/json
object (Polaris_Management_Service_CatalogRole)

Responses

Request samples

Content type
application/json
{
  • "catalogRole": {
    }
}

getCatalogRole

Get the details of an existing role

+
Authorizations:
Polaris_Management_Service_OAuth2
path Parameters
catalogName
required
string [ 1 .. 256 ] characters ^(?!\s*[s|S][y|Y][s|S][t|T][e|E][m|M]\$).*$

The catalog for which we are retrieving roles

+
catalogRoleName
required
string [ 1 .. 256 ] characters ^(?!\s*[s|S][y|Y][s|S][t|T][e|E][m|M]\$).*$

The name of the role

+

Responses

Response samples

Content type
application/json
{
  • "name": "string",
  • "properties": {
    },
  • "createTimestamp": 0,
  • "lastUpdateTimestamp": 0,
  • "entityVersion": 0
}

updateCatalogRole

Update an existing role in the catalog

+
Authorizations:
Polaris_Management_Service_OAuth2
path Parameters
catalogName
required
string [ 1 .. 256 ] characters ^(?!\s*[s|S][y|Y][s|S][t|T][e|E][m|M]\$).*$

The catalog for which we are retrieving roles

+
catalogRoleName
required
string [ 1 .. 256 ] characters ^(?!\s*[s|S][y|Y][s|S][t|T][e|E][m|M]\$).*$

The name of the role

+
Request Body schema: application/json
currentEntityVersion
required
integer

The version of the object onto which this update is applied; if the object changed, the update will fail and the caller should retry after fetching the latest version.

+
required
object

Responses

Request samples

Content type
application/json
{
  • "currentEntityVersion": 0,
  • "properties": {
    }
}

Response samples

Content type
application/json
{
  • "name": "string",
  • "properties": {
    },
  • "createTimestamp": 0,
  • "lastUpdateTimestamp": 0,
  • "entityVersion": 0
}

deleteCatalogRole

Delete an existing role from the catalog. All associated grants will also be deleted

+
Authorizations:
Polaris_Management_Service_OAuth2
path Parameters
catalogName
required
string [ 1 .. 256 ] characters ^(?!\s*[s|S][y|Y][s|S][t|T][e|E][m|M]\$).*$

The catalog for which we are retrieving roles

+
catalogRoleName
required
string [ 1 .. 256 ] characters ^(?!\s*[s|S][y|Y][s|S][t|T][e|E][m|M]\$).*$

The name of the role

+

Responses

listAssigneePrincipalRolesForCatalogRole

List the PrincipalRoles to which the target catalog role has been assigned

+
Authorizations:
Polaris_Management_Service_OAuth2
path Parameters
catalogName
required
string [ 1 .. 256 ] characters ^(?!\s*[s|S][y|Y][s|S][t|T][e|E][m|M]\$).*$

The name of the catalog where the catalog role resides

+
catalogRoleName
required
string [ 1 .. 256 ] characters ^(?!\s*[s|S][y|Y][s|S][t|T][e|E][m|M]\$).*$

The name of the catalog role

+

Responses

Response samples

Content type
application/json
{
  • "roles": [
    ]
}

listGrantsForCatalogRole

List the grants the catalog role holds

+
Authorizations:
Polaris_Management_Service_OAuth2
path Parameters
catalogName
required
string [ 1 .. 256 ] characters ^(?!\s*[s|S][y|Y][s|S][t|T][e|E][m|M]\$).*$

The name of the catalog where the role will receive the grant

+
catalogRoleName
required
string [ 1 .. 256 ] characters ^(?!\s*[s|S][y|Y][s|S][t|T][e|E][m|M]\$).*$

The name of the role receiving the grant (must exist)

+

Responses

Response samples

Content type
application/json
{
  • "grants": [
    ]
}

addGrantToCatalogRole

Add a new grant to the catalog role

+
Authorizations:
Polaris_Management_Service_OAuth2
path Parameters
catalogName
required
string [ 1 .. 256 ] characters ^(?!\s*[s|S][y|Y][s|S][t|T][e|E][m|M]\$).*$

The name of the catalog where the role will receive the grant

+
catalogRoleName
required
string [ 1 .. 256 ] characters ^(?!\s*[s|S][y|Y][s|S][t|T][e|E][m|M]\$).*$

The name of the role receiving the grant (must exist)

+
Request Body schema: application/json
object (Polaris_Management_Service_GrantResource)

Responses

Request samples

Content type
application/json
{
  • "grant": {
    }
}

revokeGrantFromCatalogRole

Delete a specific grant from the role. This may be a subset or a superset of the grants the role has. In case of a subset, the role will retain the grants not specified. If the cascade parameter is true, grant revocation will have a cascading effect - that is, if a principal has specific grants on a subresource, and grants are revoked on a parent resource, the grants present on the subresource will be revoked as well. By default, this behavior is disabled and grant revocation only affects the specified resource.

+
Authorizations:
Polaris_Management_Service_OAuth2
path Parameters
catalogName
required
string [ 1 .. 256 ] characters ^(?!\s*[s|S][y|Y][s|S][t|T][e|E][m|M]\$).*$

The name of the catalog where the role will receive the grant

+
catalogRoleName
required
string [ 1 .. 256 ] characters ^(?!\s*[s|S][y|Y][s|S][t|T][e|E][m|M]\$).*$

The name of the role receiving the grant (must exist)

+
query Parameters
cascade
boolean
Default: false

If true, the grant revocation cascades to all subresources.

+
Request Body schema: application/json
object (Polaris_Management_Service_GrantResource)

Responses

Request samples

Content type
application/json
{
  • "grant": {
    }
}

Configuration API

List all catalog configuration settings

All REST clients should first call this route to get catalog configuration properties from the server to configure the catalog and its HTTP client. Configuration from the server consists of two sets of key/value pairs.

+
    +
  • defaults - properties that should be used as default configuration; applied before client configuration
  • +
  • overrides - properties that should be used to override client configuration; applied after defaults and client configuration
  • +
+

Catalog configuration is constructed by setting the defaults, then client- provided configuration, and finally overrides. The final property set is then used to configure the catalog.

+

For example, a default configuration property might set the size of the client pool, which can be replaced with a client-specific setting. An override might be used to set the warehouse location, which is stored on the server rather than in client configuration.

+

Common catalog configuration settings are documented at https://iceberg.apache.org/docs/latest/configuration/#catalog-properties

+
Authorizations:
Apache_Iceberg_REST_Catalog_API_OAuth2Apache_Iceberg_REST_Catalog_API_BearerAuth
query Parameters
warehouse
string

Warehouse location or identifier to request from the service

+

Responses

Response samples

Content type
application/json
{
  • "overrides": {
    },
  • "defaults": {
    }
}

OAuth2 API

Get a token using an OAuth2 flow

Exchange credentials for a token using the OAuth2 client credentials flow or token exchange.

+

This endpoint is used for three purposes -

+
    +
  1. To exchange client credentials (client ID and secret) for an access token This uses the client credentials flow.
  2. +
  3. To exchange a client token and an identity token for a more specific access token This uses the token exchange flow.
  4. +
  5. To exchange an access token for one with the same claims and a refreshed expiration period This uses the token exchange flow.
  6. +
+

For example, a catalog client may be configured with client credentials from the OAuth2 Authorization flow. This client would exchange its client ID and secret for an access token using the client credentials request with this endpoint (1). Subsequent requests would then use that access token.

+

Some clients may also handle sessions that have additional user context. These clients would use the token exchange flow to exchange a user token (the "subject" token) from the session for a more specific access token for that user, using the catalog's access token as the "actor" token (2). The user ID token is the "subject" token and can be any token type allowed by the OAuth2 token exchange flow, including a unsecured JWT token with a sub claim. This request should use the catalog's bearer token in the "Authorization" header.

+

Clients may also use the token exchange flow to refresh a token that is about to expire by sending a token exchange request (3). The request's "subject" token should be the expiring token. This request should use the subject token in the "Authorization" header.

+
Authorizations:
Apache_Iceberg_REST_Catalog_API_BearerAuth
Request Body schema: application/x-www-form-urlencoded
required
Any of
grant_type
required
string
Value: "client_credentials"
scope
string
client_id
required
string

Client ID

+

This can be sent in the request body, but OAuth2 recommends sending it in a Basic Authorization header.

+
client_secret
required
string

Client secret

+

This can be sent in the request body, but OAuth2 recommends sending it in a Basic Authorization header.

+

Responses

Response samples

Content type
application/json
{
  • "access_token": "string",
  • "token_type": "bearer",
  • "expires_in": 0,
  • "issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
  • "refresh_token": "string",
  • "scope": "string"
}

Catalog API

List namespaces, optionally providing a parent namespace to list underneath

List all namespaces at a certain level, optionally starting from a given parent namespace. If table accounting.tax.paid.info exists, using 'SELECT NAMESPACE IN accounting' would translate into GET /namespaces?parent=accounting and must return a namespace, ["accounting", "tax"] only. Using 'SELECT NAMESPACE IN accounting.tax' would translate into GET /namespaces?parent=accounting%1Ftax and must return a namespace, ["accounting", "tax", "paid"]. If parent is not provided, all top-level namespaces should be listed.

+
Authorizations:
Apache_Iceberg_REST_Catalog_API_OAuth2Apache_Iceberg_REST_Catalog_API_BearerAuth
path Parameters
prefix
required
string

An optional prefix in the path

+
query Parameters
pageToken
string or null (Apache_Iceberg_REST_Catalog_API_PageToken)

An opaque token that allows clients to make use of pagination for list APIs (e.g. ListTables). Clients may initiate the first paginated request by sending an empty query parameter pageToken to the server. +Servers that support pagination should identify the pageToken parameter and return a next-page-token in the response if there are more results available. After the initial request, the value of next-page-token from each response must be used as the pageToken parameter value for the next request. The server must return null value for the next-page-token in the last response. +Servers that support pagination must return all results in a single response with the value of next-page-token set to null if the query parameter pageToken is not set in the request. +Servers that do not support pagination should ignore the pageToken parameter and return all results in a single response. The next-page-token must be omitted from the response. +Clients must interpret either null or missing response value of next-page-token as the end of the listing results.

+
pageSize
integer >= 1

For servers that support pagination, this signals an upper bound of the number of results that a client will receive. For servers that do not support pagination, clients may receive results larger than the indicated pageSize.

+
parent
string
Example: parent=accounting%1Ftax

An optional namespace, underneath which to list namespaces. If not provided or empty, all top-level namespaces should be listed. If parent is a multipart namespace, the parts must be separated by the unit separator (0x1F) byte.

+

Responses

Response samples

Content type
application/json
Example
{
  • "namespaces": [
    ]
}

Create a namespace

Create a namespace, with an optional set of properties. The server might also add properties, such as last_modified_time etc.

+
Authorizations:
Apache_Iceberg_REST_Catalog_API_OAuth2Apache_Iceberg_REST_Catalog_API_BearerAuth
path Parameters
prefix
required
string

An optional prefix in the path

+
Request Body schema: application/json
required
namespace
required
Array of strings (Apache_Iceberg_REST_Catalog_API_Namespace)

Reference to one or more levels of a namespace

+
object
Default: {}

Configured string to string map of properties for the namespace

+

Responses

Request samples

Content type
application/json
{
  • "namespace": [
    ],
  • "properties": {
    }
}

Response samples

Content type
application/json
{
  • "namespace": [
    ],
  • "properties": {
    }
}

Load the metadata properties for a namespace

Return all stored metadata properties for a given namespace

+
Authorizations:
Apache_Iceberg_REST_Catalog_API_OAuth2Apache_Iceberg_REST_Catalog_API_BearerAuth
path Parameters
prefix
required
string

An optional prefix in the path

+
namespace
required
string
Examples:
  • accounting -
  • accounting%1Ftax -

A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (0x1F) byte.

+

Responses

Response samples

Content type
application/json
{
  • "namespace": [
    ],
  • "properties": {
    }
}

Check if a namespace exists

Check if a namespace exists. The response does not contain a body.

+
Authorizations:
Apache_Iceberg_REST_Catalog_API_OAuth2Apache_Iceberg_REST_Catalog_API_BearerAuth
path Parameters
prefix
required
string

An optional prefix in the path

+
namespace
required
string
Examples:
  • accounting -
  • accounting%1Ftax -

A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (0x1F) byte.

+

Responses

Response samples

Content type
application/json
{
  • "error": {
    }
}

Drop a namespace from the catalog. Namespace must be empty.

Authorizations:
Apache_Iceberg_REST_Catalog_API_OAuth2Apache_Iceberg_REST_Catalog_API_BearerAuth
path Parameters
prefix
required
string

An optional prefix in the path

+
namespace
required
string
Examples:
  • accounting -
  • accounting%1Ftax -

A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (0x1F) byte.

+

Responses

Response samples

Content type
application/json
{
  • "error": {
    }
}

Set or remove properties on a namespace

Set and/or remove properties on a namespace. The request body specifies a list of properties to remove and a map of key value pairs to update. +Properties that are not in the request are not modified or removed by this call. +Server implementations are not required to support namespace properties.

+
Authorizations:
Apache_Iceberg_REST_Catalog_API_OAuth2Apache_Iceberg_REST_Catalog_API_BearerAuth
path Parameters
prefix
required
string

An optional prefix in the path

+
namespace
required
string
Examples:
  • accounting -
  • accounting%1Ftax -

A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (0x1F) byte.

+
Request Body schema: application/json
required
removals
Array of strings unique
object

Responses

Request samples

Content type
application/json
{
  • "removals": [
    ],
  • "updates": {
    }
}

Response samples

Content type
application/json
{
  • "updated": [
    ],
  • "removed": [
    ],
  • "missing": [
    ]
}

List all table identifiers underneath a given namespace

Return all table identifiers under this namespace

+
Authorizations:
Apache_Iceberg_REST_Catalog_API_OAuth2Apache_Iceberg_REST_Catalog_API_BearerAuth
path Parameters
prefix
required
string

An optional prefix in the path

+
namespace
required
string
Examples:
  • accounting -
  • accounting%1Ftax -

A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (0x1F) byte.

+
query Parameters
pageToken
string or null (Apache_Iceberg_REST_Catalog_API_PageToken)

An opaque token that allows clients to make use of pagination for list APIs (e.g. ListTables). Clients may initiate the first paginated request by sending an empty query parameter pageToken to the server. +Servers that support pagination should identify the pageToken parameter and return a next-page-token in the response if there are more results available. After the initial request, the value of next-page-token from each response must be used as the pageToken parameter value for the next request. The server must return null value for the next-page-token in the last response. +Servers that support pagination must return all results in a single response with the value of next-page-token set to null if the query parameter pageToken is not set in the request. +Servers that do not support pagination should ignore the pageToken parameter and return all results in a single response. The next-page-token must be omitted from the response. +Clients must interpret either null or missing response value of next-page-token as the end of the listing results.

+
pageSize
integer >= 1

For servers that support pagination, this signals an upper bound of the number of results that a client will receive. For servers that do not support pagination, clients may receive results larger than the indicated pageSize.

+

Responses

Response samples

Content type
application/json
Example
{
  • "identifiers": [
    ]
}

Create a table in the given namespace

Create a table or start a create transaction, like atomic CTAS.

+

If stage-create is false, the table is created immediately.

+

If stage-create is true, the table is not created, but table metadata is initialized and returned. The service should prepare as needed for a commit to the table commit endpoint to complete the create transaction. The client uses the returned metadata to begin a transaction. To commit the transaction, the client sends all create and subsequent changes to the table commit route. Changes from the table create operation include changes like AddSchemaUpdate and SetCurrentSchemaUpdate that set the initial table state.

+
Authorizations:
Apache_Iceberg_REST_Catalog_API_OAuth2Apache_Iceberg_REST_Catalog_API_BearerAuth
path Parameters
prefix
required
string

An optional prefix in the path

+
namespace
required
string
Examples:
  • accounting -
  • accounting%1Ftax -

A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (0x1F) byte.

+
header Parameters
X-Iceberg-Access-Delegation
string
Enum: "vended-credentials" "remote-signing"
Example: vended-credentials,remote-signing

Optional signal to the server that the client supports delegated access via a comma-separated list of access mechanisms. The server may choose to supply access via any or none of the requested mechanisms.

+

Specific properties and handling for vended-credentials is documented in the LoadTableResult schema section of this spec document.

+

The protocol and specification for remote-signing is documented in the s3-signer-open-api.yaml OpenApi spec in the aws module.

+
Request Body schema: application/json
required
name
required
string
location
string
required
object (Apache_Iceberg_REST_Catalog_API_Schema)
object (Apache_Iceberg_REST_Catalog_API_PartitionSpec)
object (Apache_Iceberg_REST_Catalog_API_SortOrder)
stage-create
boolean
object

Responses

Request samples

Content type
application/json
{
  • "name": "string",
  • "location": "string",
  • "schema": {
    },
  • "partition-spec": {
    },
  • "write-order": {
    },
  • "stage-create": true,
  • "properties": {
    }
}

Response samples

Content type
application/json
{
  • "metadata-location": "string",
  • "metadata": {
    },
  • "config": {
    }
}

Register a table in the given namespace using given metadata file location

Register a table using given metadata file location.

+
Authorizations:
Apache_Iceberg_REST_Catalog_API_OAuth2Apache_Iceberg_REST_Catalog_API_BearerAuth
path Parameters
prefix
required
string

An optional prefix in the path

+
namespace
required
string
Examples:
  • accounting -
  • accounting%1Ftax -

A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (0x1F) byte.

+
Request Body schema: application/json
required
name
required
string
metadata-location
required
string

Responses

Request samples

Content type
application/json
{
  • "name": "string",
  • "metadata-location": "string"
}

Response samples

Content type
application/json
{
  • "metadata-location": "string",
  • "metadata": {
    },
  • "config": {
    }
}

Load a table from the catalog

Load a table from the catalog.

+

The response contains both configuration and table metadata. The configuration, if non-empty is used as additional configuration for the table that overrides catalog configuration. For example, this configuration may change the FileIO implementation to be used for the table.

+

The response also contains the table's full metadata, matching the table metadata JSON file.

+

The catalog configuration may contain credentials that should be used for subsequent requests for the table. The configuration key "token" is used to pass an access token to be used as a bearer token for table requests. Otherwise, a token may be passed using a RFC 8693 token type as a configuration key. For example, "urn:ietf:params:oauth:token-type:jwt=".

+
Authorizations:
Apache_Iceberg_REST_Catalog_API_OAuth2Apache_Iceberg_REST_Catalog_API_BearerAuth
path Parameters
prefix
required
string

An optional prefix in the path

+
namespace
required
string
Examples:
  • accounting -
  • accounting%1Ftax -

A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (0x1F) byte.

+
table
required
string
Example: sales

A table name

+
query Parameters
snapshots
string
Enum: "all" "refs"

The snapshots to return in the body of the metadata. Setting the value to all would return the full set of snapshots currently valid for the table. Setting the value to refs would load all snapshots referenced by branches or tags. +Default if no param is provided is all.

+
header Parameters
X-Iceberg-Access-Delegation
string
Enum: "vended-credentials" "remote-signing"
Example: vended-credentials,remote-signing

Optional signal to the server that the client supports delegated access via a comma-separated list of access mechanisms. The server may choose to supply access via any or none of the requested mechanisms.

+

Specific properties and handling for vended-credentials is documented in the LoadTableResult schema section of this spec document.

+

The protocol and specification for remote-signing is documented in the s3-signer-open-api.yaml OpenApi spec in the aws module.

+

Responses

Response samples

Content type
application/json
{
  • "metadata-location": "string",
  • "metadata": {
    },
  • "config": {
    }
}

Commit updates to a table

Commit updates to a table.

+

Commits have two parts, requirements and updates. Requirements are assertions that will be validated before attempting to make and commit changes. For example, assert-ref-snapshot-id will check that a named ref's snapshot ID has a certain value.

+

Updates are changes to make to table metadata. For example, after asserting that the current main ref is at the expected snapshot, a commit may add a new child snapshot and set the ref to the new snapshot id.

+

Create table transactions that are started by createTable with stage-create set to true are committed using this route. Transactions should include all changes to the table, including table initialization, like AddSchemaUpdate and SetCurrentSchemaUpdate. The assert-create requirement is used to ensure that the table was not created concurrently.

+
Authorizations:
Apache_Iceberg_REST_Catalog_API_OAuth2Apache_Iceberg_REST_Catalog_API_BearerAuth
path Parameters
prefix
required
string

An optional prefix in the path

+
namespace
required
string
Examples:
  • accounting -
  • accounting%1Ftax -

A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (0x1F) byte.

+
table
required
string
Example: sales

A table name

+
Request Body schema: application/json
required
object (Apache_Iceberg_REST_Catalog_API_TableIdentifier)
required
Array of objects (Apache_Iceberg_REST_Catalog_API_TableRequirement)
required
Array of Apache_Iceberg_REST_Catalog_API_AssignUUIDUpdate (object) or Apache_Iceberg_REST_Catalog_API_UpgradeFormatVersionUpdate (object) or Apache_Iceberg_REST_Catalog_API_AddSchemaUpdate (object) or Apache_Iceberg_REST_Catalog_API_SetCurrentSchemaUpdate (object) or Apache_Iceberg_REST_Catalog_API_AddPartitionSpecUpdate (object) or Apache_Iceberg_REST_Catalog_API_SetDefaultSpecUpdate (object) or Apache_Iceberg_REST_Catalog_API_AddSortOrderUpdate (object) or Apache_Iceberg_REST_Catalog_API_SetDefaultSortOrderUpdate (object) or Apache_Iceberg_REST_Catalog_API_AddSnapshotUpdate (object) or Apache_Iceberg_REST_Catalog_API_SetSnapshotRefUpdate (object) or Apache_Iceberg_REST_Catalog_API_RemoveSnapshotsUpdate (object) or Apache_Iceberg_REST_Catalog_API_RemoveSnapshotRefUpdate (object) or Apache_Iceberg_REST_Catalog_API_SetLocationUpdate (object) or Apache_Iceberg_REST_Catalog_API_SetPropertiesUpdate (object) or Apache_Iceberg_REST_Catalog_API_RemovePropertiesUpdate (object) or Apache_Iceberg_REST_Catalog_API_SetStatisticsUpdate (object) or Apache_Iceberg_REST_Catalog_API_RemoveStatisticsUpdate (object) (Apache_Iceberg_REST_Catalog_API_TableUpdate)

Responses

Request samples

Content type
application/json
{
  • "identifier": {
    },
  • "requirements": [
    ],
  • "updates": [
    ]
}

Response samples

Content type
application/json
{
  • "metadata-location": "string",
  • "metadata": {
    }
}

Drop a table from the catalog

Remove a table from the catalog

+
Authorizations:
Apache_Iceberg_REST_Catalog_API_OAuth2Apache_Iceberg_REST_Catalog_API_BearerAuth
path Parameters
prefix
required
string

An optional prefix in the path

+
namespace
required
string
Examples:
  • accounting -
  • accounting%1Ftax -

A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (0x1F) byte.

+
table
required
string
Example: sales

A table name

+
query Parameters
purgeRequested
boolean
Default: false

Whether the user requested to purge the underlying table's data and metadata

+

Responses

Response samples

Content type
application/json
{
  • "error": {
    }
}

Check if a table exists

Check if a table exists within a given namespace. The response does not contain a body.

+
Authorizations:
Apache_Iceberg_REST_Catalog_API_OAuth2Apache_Iceberg_REST_Catalog_API_BearerAuth
path Parameters
prefix
required
string

An optional prefix in the path

+
namespace
required
string
Examples:
  • accounting -
  • accounting%1Ftax -

A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (0x1F) byte.

+
table
required
string
Example: sales

A table name

+

Responses

Response samples

Content type
application/json
{
  • "error": {
    }
}

Rename a table from its current name to a new name

Rename a table from one identifier to another. It's valid to move a table across namespaces, but the server implementation is not required to support it.

+
Authorizations:
Apache_Iceberg_REST_Catalog_API_OAuth2Apache_Iceberg_REST_Catalog_API_BearerAuth
path Parameters
prefix
required
string

An optional prefix in the path

+
Request Body schema: application/json
required

Current table identifier to rename and new table identifier to rename to

+
required
object (Apache_Iceberg_REST_Catalog_API_TableIdentifier)
required
object (Apache_Iceberg_REST_Catalog_API_TableIdentifier)

Responses

Request samples

Content type
application/json
{
  • "source": {
    },
  • "destination": {
    }
}

Response samples

Content type
application/json
{
  • "error": {
    }
}

Send a metrics report to this endpoint to be processed by the backend

Authorizations:
Apache_Iceberg_REST_Catalog_API_OAuth2Apache_Iceberg_REST_Catalog_API_BearerAuth
path Parameters
prefix
required
string

An optional prefix in the path

+
namespace
required
string
Examples:
  • accounting -
  • accounting%1Ftax -

A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (0x1F) byte.

+
table
required
string
Example: sales

A table name

+
Request Body schema: application/json
required

The request containing the metrics report to be sent

+
Any of
table-name
required
string
snapshot-id
required
integer <int64>
required
Apache_Iceberg_REST_Catalog_API_AndOrExpression (object) or Apache_Iceberg_REST_Catalog_API_NotExpression (object) or Apache_Iceberg_REST_Catalog_API_SetExpression (object) or Apache_Iceberg_REST_Catalog_API_LiteralExpression (object) or Apache_Iceberg_REST_Catalog_API_UnaryExpression (object) (Apache_Iceberg_REST_Catalog_API_Expression)
schema-id
required
integer
projected-field-ids
required
Array of integers
projected-field-names
required
Array of strings
required
object (Apache_Iceberg_REST_Catalog_API_Metrics)
object
report-type
required
string

Responses

Request samples

Content type
application/json
Example
{
  • "table-name": "string",
  • "snapshot-id": 0,
  • "filter": {
    },
  • "schema-id": 0,
  • "projected-field-ids": [
    ],
  • "projected-field-names": [
    ],
  • "metrics": {
    },
  • "metadata": {
    },
  • "report-type": "string"
}

Response samples

Content type
application/json
{
  • "error": {
    }
}

Sends a notification to the table

Authorizations:
Apache_Iceberg_REST_Catalog_API_OAuth2Apache_Iceberg_REST_Catalog_API_BearerAuth
path Parameters
prefix
required
string

An optional prefix in the path

+
namespace
required
string
Examples:
  • accounting -
  • accounting%1Ftax -

A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (0x1F) byte.

+
table
required
string
Example: sales

A table name

+
Request Body schema: application/json
required

The request containing the notification to be sent

+
notification-type
required
string (Apache_Iceberg_REST_Catalog_API_NotificationType)
Enum: "UNKNOWN" "CREATE" "UPDATE" "DROP"
object (Apache_Iceberg_REST_Catalog_API_TableUpdateNotification)

Responses

Request samples

Content type
application/json
{
  • "notification-type": "UNKNOWN",
  • "payload": {
    }
}

Response samples

Content type
application/json
{
  • "error": {
    }
}

Commit updates to multiple tables in an atomic operation

Authorizations:
Apache_Iceberg_REST_Catalog_API_OAuth2Apache_Iceberg_REST_Catalog_API_BearerAuth
path Parameters
prefix
required
string

An optional prefix in the path

+
Request Body schema: application/json
required

Commit updates to multiple tables in an atomic operation

+

A commit for a single table consists of a table identifier with requirements and updates. Requirements are assertions that will be validated before attempting to make and commit changes. For example, assert-ref-snapshot-id will check that a named ref's snapshot ID has a certain value.

+

Updates are changes to make to table metadata. For example, after asserting that the current main ref is at the expected snapshot, a commit may add a new child snapshot and set the ref to the new snapshot id.

+
required
Array of objects (Apache_Iceberg_REST_Catalog_API_CommitTableRequest)

Responses

Request samples

Content type
application/json
{
  • "table-changes": [
    ]
}

Response samples

Content type
application/json
{
  • "error": {
    }
}

List all view identifiers underneath a given namespace

Return all view identifiers under this namespace

+
Authorizations:
Apache_Iceberg_REST_Catalog_API_OAuth2Apache_Iceberg_REST_Catalog_API_BearerAuth
path Parameters
prefix
required
string

An optional prefix in the path

+
namespace
required
string
Examples:
  • accounting -
  • accounting%1Ftax -

A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (0x1F) byte.

+
query Parameters
pageToken
string or null (Apache_Iceberg_REST_Catalog_API_PageToken)

An opaque token that allows clients to make use of pagination for list APIs (e.g. ListTables). Clients may initiate the first paginated request by sending an empty query parameter pageToken to the server. +Servers that support pagination should identify the pageToken parameter and return a next-page-token in the response if there are more results available. After the initial request, the value of next-page-token from each response must be used as the pageToken parameter value for the next request. The server must return null value for the next-page-token in the last response. +Servers that support pagination must return all results in a single response with the value of next-page-token set to null if the query parameter pageToken is not set in the request. +Servers that do not support pagination should ignore the pageToken parameter and return all results in a single response. The next-page-token must be omitted from the response. +Clients must interpret either null or missing response value of next-page-token as the end of the listing results.

+
pageSize
integer >= 1

For servers that support pagination, this signals an upper bound of the number of results that a client will receive. For servers that do not support pagination, clients may receive results larger than the indicated pageSize.

+

Responses

Response samples

Content type
application/json
Example
{
  • "identifiers": [
    ]
}

Create a view in the given namespace

Create a view in the given namespace.

+
Authorizations:
Apache_Iceberg_REST_Catalog_API_OAuth2Apache_Iceberg_REST_Catalog_API_BearerAuth
path Parameters
prefix
required
string

An optional prefix in the path

+
namespace
required
string
Examples:
  • accounting -
  • accounting%1Ftax -

A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (0x1F) byte.

+
Request Body schema: application/json
required
name
required
string
location
string
required
object (Apache_Iceberg_REST_Catalog_API_Schema)
required
object (Apache_Iceberg_REST_Catalog_API_ViewVersion)
required
object

Responses

Request samples

Content type
application/json
{
  • "name": "string",
  • "location": "string",
  • "schema": {
    },
  • "view-version": {
    },
  • "properties": {
    }
}

Response samples

Content type
application/json
{
  • "metadata-location": "string",
  • "metadata": {
    },
  • "config": {
    }
}

Load a view from the catalog

Load a view from the catalog.

+

The response contains both configuration and view metadata. The configuration, if non-empty is used as additional configuration for the view that overrides catalog configuration.

+

The response also contains the view's full metadata, matching the view metadata JSON file.

+

The catalog configuration may contain credentials that should be used for subsequent requests for the view. The configuration key "token" is used to pass an access token to be used as a bearer token for view requests. Otherwise, a token may be passed using a RFC 8693 token type as a configuration key. For example, "urn:ietf:params:oauth:token-type:jwt=".

+
Authorizations:
Apache_Iceberg_REST_Catalog_API_OAuth2Apache_Iceberg_REST_Catalog_API_BearerAuth
path Parameters
prefix
required
string

An optional prefix in the path

+
namespace
required
string
Examples:
  • accounting -
  • accounting%1Ftax -

A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (0x1F) byte.

+
view
required
string
Example: sales

A view name

+

Responses

Response samples

Content type
application/json
{
  • "metadata-location": "string",
  • "metadata": {
    },
  • "config": {
    }
}

Replace a view

Commit updates to a view.

+
Authorizations:
Apache_Iceberg_REST_Catalog_API_OAuth2Apache_Iceberg_REST_Catalog_API_BearerAuth
path Parameters
prefix
required
string

An optional prefix in the path

+
namespace
required
string
Examples:
  • accounting -
  • accounting%1Ftax -

A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (0x1F) byte.

+
view
required
string
Example: sales

A view name

+
Request Body schema: application/json
required
object (Apache_Iceberg_REST_Catalog_API_TableIdentifier)
Array of objects (Apache_Iceberg_REST_Catalog_API_ViewRequirement)
required
Array of Apache_Iceberg_REST_Catalog_API_AssignUUIDUpdate (object) or Apache_Iceberg_REST_Catalog_API_UpgradeFormatVersionUpdate (object) or Apache_Iceberg_REST_Catalog_API_AddSchemaUpdate (object) or Apache_Iceberg_REST_Catalog_API_SetLocationUpdate (object) or Apache_Iceberg_REST_Catalog_API_SetPropertiesUpdate (object) or Apache_Iceberg_REST_Catalog_API_RemovePropertiesUpdate (object) or Apache_Iceberg_REST_Catalog_API_AddViewVersionUpdate (object) or Apache_Iceberg_REST_Catalog_API_SetCurrentViewVersionUpdate (object) (Apache_Iceberg_REST_Catalog_API_ViewUpdate)

Responses

Request samples

Content type
application/json
{
  • "identifier": {
    },
  • "requirements": [
    ],
  • "updates": [
    ]
}

Response samples

Content type
application/json
{
  • "metadata-location": "string",
  • "metadata": {
    },
  • "config": {
    }
}

Drop a view from the catalog

Remove a view from the catalog

+
Authorizations:
Apache_Iceberg_REST_Catalog_API_OAuth2Apache_Iceberg_REST_Catalog_API_BearerAuth
path Parameters
prefix
required
string

An optional prefix in the path

+
namespace
required
string
Examples:
  • accounting -
  • accounting%1Ftax -

A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (0x1F) byte.

+
view
required
string
Example: sales

A view name

+

Responses

Response samples

Content type
application/json
{
  • "error": {
    }
}

Check if a view exists

Check if a view exists within a given namespace. This request does not return a response body.

+
Authorizations:
Apache_Iceberg_REST_Catalog_API_OAuth2Apache_Iceberg_REST_Catalog_API_BearerAuth
path Parameters
prefix
required
string

An optional prefix in the path

+
namespace
required
string
Examples:
  • accounting -
  • accounting%1Ftax -

A namespace identifier as a single string. Multipart namespace parts should be separated by the unit separator (0x1F) byte.

+
view
required
string
Example: sales

A view name

+

Responses

Response samples

Content type
application/json
{
  • "error": {
    }
}

Rename a view from its current name to a new name

Rename a view from one identifier to another. It's valid to move a view across namespaces, but the server implementation is not required to support it.

+
Authorizations:
Apache_Iceberg_REST_Catalog_API_OAuth2Apache_Iceberg_REST_Catalog_API_BearerAuth
path Parameters
prefix
required
string

An optional prefix in the path

+
Request Body schema: application/json
required

Current view identifier to rename and new view identifier to rename to

+
required
object (Apache_Iceberg_REST_Catalog_API_TableIdentifier)
required
object (Apache_Iceberg_REST_Catalog_API_TableIdentifier)

Responses

Request samples

Content type
application/json
{
  • "source": {
    },
  • "destination": {
    }
}

Response samples

Content type
application/json
{
  • "error": {
    }
}
+ - \ No newline at end of file + + diff --git a/docs/overview.md b/docs/overview.md new file mode 100644 index 0000000000..39935a94c5 --- /dev/null +++ b/docs/overview.md @@ -0,0 +1,213 @@ + + +Polaris Catalog is a catalog implementation for Apache Iceberg built on the open source Apache Iceberg REST protocol. + +With Polaris Catalog, you can provide centralized, secure read and write access across different REST-compatible query engines to your Iceberg tables. + +![Conceptual diagram of Polaris Catalog.](./img/overview.svg "Polaris Catalog overview") + +## Key concepts + +This section introduces key concepts associated with using Polaris Catalog. + +In the following diagram, a sample [Polaris Catalog structure](./overview.md#catalog) with nested [namespaces](./overview.md#namespace) is shown for Catalog1. No tables +or namespaces have been created yet for Catalog2 or Catalog3: + +![Diagram that shows an example Polaris Catalog structure.](./img/sample-catalog-structure.svg "Sample Polaris Catalog structure") + +### Catalog + +In Polaris Catalog, you can create one or more catalog resources to organize Iceberg tables. + +Configure your catalog by setting values in the storage configuration for S3, Azure, or Google Cloud Storage. An Iceberg catalog enables a +query engine to manage and organize tables. The catalog forms the first architectural layer in the [Iceberg table specification](https://iceberg.apache.org/spec/#overview) and must support: + +- Storing the current metadata pointer for one or more Iceberg tables. A metadata pointer maps a table name to the location of that table's + current metadata file. + +- Performing atomic operations so that you can update the current metadata pointer for a table to the metadata pointer of a new version of + the table. + +To learn more about Iceberg catalogs, see the [Apache Iceberg documentation](https://iceberg.apache.org/concepts/catalog/). + +#### Catalog types + +A catalog can be one of the following two types: + +- Internal: The catalog is managed by Polaris. Tables from this catalog can be read and written in Polaris. + +- External: The catalog is externally managed by another Iceberg catalog provider (for example, Snowflake, Glue, Dremio Arctic). Tables from + this catalog are synced to Polaris. These tables are read-only in Polaris. In the current release, only Snowflake external catalog is provided. + +A catalog is configured with a storage configuration that can point to S3, Azure storage, or GCS. + +To create a new catalog, see [Create a catalog](./create-a-catalog.md "Sample Polaris Catalog structure"). + +### Namespace + +You create *namespaces* to logically group Iceberg tables within a catalog. A catalog can have one or more namespaces. You can also create +nested namespaces. Iceberg tables belong to namespaces. + +### Iceberg tables & catalogs + +In an internal catalog, an Iceberg table is registered in Polaris Catalog, but read and written via query engines. The table data and +metadata is stored in your external cloud storage. The table uses Polaris Catalog as the Iceberg catalog. + +If you have tables that use Snowflake as the Iceberg catalog (Snowflake-managed tables), you can sync these tables to an external +catalog in Polaris Catalog. If you sync this catalog to Polaris Catalog, it appears as an external catalog in Polaris Catalog. The table data and +metadata is stored in your external cloud storage. The Snowflake query engine can read from or write to these tables. However, the other query +engines can only read from these tables. + +**Important** + +To ensure that the access privileges defined for a catalog are enforced +correctly, you must: + +- Ensure a directory only contains the data files that belong to a + single table. + +- Create a directory hierarchy that matches the namespace hierarchy + for the catalog. + +For example, if a catalog includes: + +- Top-level namespace namespace1 + +- Nested namespace namespace1a + +- A customers table, which is grouped under nested namespace + namespace1a + +- An orders table, which is grouped under nested namespace namespace1a + +The directory hierarchy for the catalog must be: + +- /namespace1/namespace1a/customers/\ + +- /namespace1/namespace1a/orders/\ + +### Service principal + +A service principal is an entity that you create in Polaris Catalog. Each service principal encapsulates credentials that you use to connect +to Polaris Catalog. + +Query engines use service principals to connect to catalogs. + +Polaris Catalog generates a Client ID and Client Secret pair for each service principal. + +The following table displays example service principals that you might create in Polaris Catalog: + + | Service connection name | Description | + | --------------------------- | ----------- | + | Flink ingestion | For Apache Flink to ingest streaming data into Iceberg tables. | + | Spark ETL pipeline | For Apache Spark to run ETL pipeline jobs on Iceberg tables. | + | Snowflake data pipelines | For Snowflake to run data pipelines for transforming data in Iceberg tables. | + | Trino BI dashboard | For Trino to run BI queries for powering a dashboard. | + | Snowflake AI team | For Snowflake to run AI jobs on data in Iceberg tables. | + +### Service connection + +A service connection represents a REST-compatible engine (such as Apache Spark, Apache Flink, or Trino) that can read from and write to Polaris +Catalog. When creating a new service connection, the Polaris administrator grants the service principal that is created with the new service +connection with either a new or existing principal role. A principal role is a resource in Polaris that you can use to logically group Polaris +service principals together and grant privileges on securable objects. For more information, see [Principal role](./access-control.md#principal-role "Principal role"). Polaris Catalog uses a role-based access control (RBAC) model to grant service principals access to resources. For more information, +see [Access control](./access-control.md "Access control"). For a diagram of this model, see [RBAC model](./access-control.md#rbac-model "RBAC model"). + +If the Polaris administrator grants the service principal for the new service connection with a new principal role, the service principal +doesn\'t have any privileges granted to it yet. When securing the catalog that the new service connection will connect to, the Polaris +administrator grants privileges to catalog roles and then grants these catalog roles to the new principal role. As a result, the service +principal for the new service connection is bestowed with these privileges. For more information about catalog roles, see [Catalog role](./access-control.md#catalog-role "Catalog role"). + +If the Polaris administrator grants an existing principal role to the service principal for the new service connection, the service principal +is bestowed with the privileges granted to the catalog roles that are granted to the existing principal role. If needed, the Polaris +administrator can grant additional catalog roles to the existing principal role or remove catalog roles from it to adjust the privileges +bestowed to the service principal. For an example of how RBAC works in Polaris, see [RBAC example](./access-control.md#rbac-example "RBAC example"). + +### Storage configuration + +A storage configuration stores a generated identity and access management (IAM) entity for your external cloud storage and is created +when you create a catalog. The storage configuration is used to set the values to connect Polaris Catalog to your cloud storage. During the +catalog creation process, an IAM entity is generated and used to create a trust relationship between the cloud storage provider and Polaris +Catalog. + +When you create a catalog, you supply the following information about your external cloud storage: + +| Cloud storage provider | Information | +| -----------------------| ----------- | +| Amazon S3 |
  • Default base location for your Amazon S3 bucket
  • Locations for your Amazon S3 bucket
  • S3 role ARN
  • External ID (optional)
| +| Google Cloud Storage (GCS) |
  • Default base location for your GCS bucket
  • Locations for your Amazon GCS bucket
| +| Azure |
  • Default base location for your Microsoft Azure container
  • Locations for your Microsoft Azure container
  • Azure tenant ID
| + +## Example workflow + +In the following example workflow, Bob creates an Iceberg table named Table1 and Alice reads data from Table1. + +1. Bob uses Apache Spark to create the Table1 table under the + Namespace1 namespace in the Catalog1 catalog and insert values into + Table1. + + Bob can create Table1 and insert data into it, because he is using a + service connection with a service principal that is bestowed with + the privileges to perform these actions. + +2. Alice uses Snowflake to read data from Table1. + + Alice can read data from Table1, because she is using a service + connection with a service principal with a catalog integration that + is bestowed with the privileges to perform this action. Alice + creates an unmanaged table in Snowflake to read data from Table1. + +![Diagram that shows an example workflow for Polaris Catalog](./img/example-workflow.svg "Example workflow for Polaris Catalog") + +## Security and access control + +This section describes security and access control. + +### Credential vending + +To secure interactions with service connections, Polaris Catalog vends temporary storage credentials to the query engine during query +execution. These credentials allow the query engine to run the query without needing to have access to your external cloud storage for +Iceberg tables. This process is called credential vending. + +### Identity and access management (IAM) + +Polaris Catalog uses the identity and access management (IAM) entity to securely connect to your storage for accessing table data, Iceberg +metadata, and manifest files that store the table schema, partitions, and other metadata. Polaris Catalog retains the IAM entity for your +storage location. + +### Access control + +Polaris Catalog enforces the access control that you configure across all tables registered with the service, and governs security for all +queries from query engines in a consistent manner. + +Polaris uses a role-based access control (RBAC) model that lets you centrally configure access for Polaris service principals to catalogs, +namespaces, and tables. + +Polaris RBAC uses two different role types to delegate privileges: + +- **Principal roles:** Granted to Polaris service principals and + analogous to roles in other access control systems that you grant to + service principals. + +- **Catalog roles:** Configured with certain privileges on Polaris + catalog resources, and granted to principal roles. + +For more information, see [Access control](./access-control.md "Access control"). + diff --git a/docs/polaris-management/index.html b/docs/polaris-management/index.html deleted file mode 100644 index a1da3d8455..0000000000 --- a/docs/polaris-management/index.html +++ /dev/null @@ -1,877 +0,0 @@ - - - - - - - - Polaris Management Service - - - - - - - - - -

Polaris Management Service (0.0.1)

Download OpenAPI specification:Download

Defines the management APIs for using Polaris to create and manage Iceberg catalogs and their principals

-

listCatalogs

List all catalogs in this polaris service

-
Authorizations:
OAuth2

Responses

Response samples

Content type
application/json
{
  • "catalogs": [
    ]
}

createCatalog

Add a new Catalog

-
Authorizations:
OAuth2
Request Body schema: application/json
required

The Catalog to create

-
type
required
string
Default: "INTERNAL"

the type of catalog - internal or external

-
name
required
string

The name of the catalog

-
readOnly
boolean
Default: false

True if writes should be disabled from query engines

-
object
createTimestamp
integer <int64>

The creation time represented as unix epoch timestamp in milliseconds

-
lastUpdateTimestamp
integer <int64>

The last update time represented as unix epoch timestamp in milliseconds

-
entityVersion
integer

The version of the catalog object used to determine if the catalog metadata has changed

-
object (StorageConfigInfo)

A storage configuration used by catalogs

-

Responses

Request samples

Content type
application/json
Example
{
  • "type": "INTERNAL",
  • "name": "string",
  • "readOnly": false,
  • "properties": {
    },
  • "createTimestamp": 0,
  • "lastUpdateTimestamp": 0,
  • "entityVersion": 0,
  • "storageConfigInfo": {
    }
}

getCatalog

Get the details of a catalog

-
Authorizations:
OAuth2
path Parameters
catalogName
required
string

The name of the catalog

-

Responses

Response samples

Content type
application/json
Example
{
  • "type": "INTERNAL",
  • "name": "string",
  • "readOnly": false,
  • "properties": {
    },
  • "createTimestamp": 0,
  • "lastUpdateTimestamp": 0,
  • "entityVersion": 0,
  • "storageConfigInfo": {
    }
}

updateCatalog

Update an existing catalog

-
Authorizations:
OAuth2
path Parameters
catalogName
required
string

The name of the catalog

-
Request Body schema: application/json
required

The catalog details to use in the update

-
currentEntityVersion
integer

The version of the object onto which this update is applied; if the object changed, the update will fail and the caller should retry after fetching the latest version.

-
object
object (StorageConfigInfo)

A storage configuration used by catalogs

-

Responses

Request samples

Content type
application/json
{
  • "currentEntityVersion": 0,
  • "properties": {
    },
  • "storageConfigInfo": {
    }
}

Response samples

Content type
application/json
Example
{
  • "type": "INTERNAL",
  • "name": "string",
  • "readOnly": false,
  • "properties": {
    },
  • "createTimestamp": 0,
  • "lastUpdateTimestamp": 0,
  • "entityVersion": 0,
  • "storageConfigInfo": {
    }
}

deleteCatalog

Delete an existing catalog. This is a cascading operation that deletes all metadata, including principals, roles and grants. If the catalog is an internal catalog, all tables and namespaces are dropped without purge.

-
Authorizations:
OAuth2
path Parameters
catalogName
required
string

The name of the catalog

-

Responses

listPrincipals

List the principals for the current catalog

-
Authorizations:
OAuth2

Responses

Response samples

Content type
application/json
{
  • "principals": [
    ]
}

createPrincipal

Create a principal

-
Authorizations:
OAuth2
Request Body schema: application/json
required

The principal to create

-
type
required
string
Value: "SERVICE"
name
required
string
clientId
string

The output-only OAuth clientId associated with this principal if applicable

-
object
createTimestamp
integer <int64>
lastUpdateTimestamp
integer <int64>
entityVersion
integer

The version of the principal object used to determine if the principal metadata has changed

-

Responses

Request samples

Content type
application/json
{
  • "type": "SERVICE",
  • "name": "string",
  • "clientId": "string",
  • "properties": {
    },
  • "createTimestamp": 0,
  • "lastUpdateTimestamp": 0,
  • "entityVersion": 0
}

Response samples

Content type
application/json
{
  • "principal": {
    },
  • "credentials": {
    }
}

getPrincipal

Get the principal details

-
Authorizations:
OAuth2
path Parameters
principalName
required
string

The principal name

-

Responses

Response samples

Content type
application/json
{
  • "type": "SERVICE",
  • "name": "string",
  • "clientId": "string",
  • "properties": {
    },
  • "createTimestamp": 0,
  • "lastUpdateTimestamp": 0,
  • "entityVersion": 0
}

updatePrincipal

Update an existing principal

-
Authorizations:
OAuth2
path Parameters
principalName
required
string

The principal name

-
Request Body schema: application/json
required

The principal details to use in the update

-
currentEntityVersion
integer

The version of the object onto which this update is applied; if the object changed, the update will fail and the caller should retry after fetching the latest version.

-
object

Responses

Request samples

Content type
application/json
{
  • "currentEntityVersion": 0,
  • "properties": {
    }
}

Response samples

Content type
application/json
{
  • "type": "SERVICE",
  • "name": "string",
  • "clientId": "string",
  • "properties": {
    },
  • "createTimestamp": 0,
  • "lastUpdateTimestamp": 0,
  • "entityVersion": 0
}

deletePrincipal

Remove a principal from polaris

-
Authorizations:
OAuth2
path Parameters
principalName
required
string

The principal name

-

Responses

rotateCredentials

Rotate a principal's credentials. The new credentials will be returned in the response. This is the only API, aside from createPrincipal, that returns the user's credentials. This API is not idempotent.

-
Authorizations:
OAuth2
path Parameters
principalName
required
string

The user name

-

Responses

Response samples

Content type
application/json
{
  • "principal": {
    },
  • "credentials": {
    }
}

listPrincipalRolesAssigned

List the roles assigned to the principal

-
Authorizations:
OAuth2
path Parameters
principalName
required
string

The name of the target principal

-

Responses

Response samples

Content type
application/json
{
  • "roles": [
    ]
}

assignPrincipalRole

Add a role to the principal

-
Authorizations:
OAuth2
path Parameters
principalName
required
string

The name of the target principal

-
Request Body schema: application/json
required

The principal role to assign

-
name
required
string

The name of the role

-
object
createTimestamp
integer <int64>
lastUpdateTimestamp
integer <int64>
entityVersion
integer

The version of the principal role object used to determine if the principal role metadata has changed

-

Responses

Request samples

Content type
application/json
{
  • "name": "string",
  • "properties": {
    },
  • "createTimestamp": 0,
  • "lastUpdateTimestamp": 0,
  • "entityVersion": 0
}

revokePrincipalRole

Remove a role from a catalog principal

-
Authorizations:
OAuth2
path Parameters
principalName
required
string

The name of the target principal

-
principalRoleName
required
string

The name of the role

-

Responses

listPrincipalRoles

List the principal roles

-
Authorizations:
OAuth2

Responses

Response samples

Content type
application/json
{
  • "roles": [
    ]
}

createPrincipalRole

Create a principal role

-
Authorizations:
OAuth2
Request Body schema: application/json
required

The principal to create

-
name
required
string

The name of the role

-
object
createTimestamp
integer <int64>
lastUpdateTimestamp
integer <int64>
entityVersion
integer

The version of the principal role object used to determine if the principal role metadata has changed

-

Responses

Request samples

Content type
application/json
{
  • "name": "string",
  • "properties": {
    },
  • "createTimestamp": 0,
  • "lastUpdateTimestamp": 0,
  • "entityVersion": 0
}

getPrincipalRole

Get the principal role details

-
Authorizations:
OAuth2
path Parameters
principalRoleName
required
string

The principal role name

-

Responses

Response samples

Content type
application/json
{
  • "name": "string",
  • "properties": {
    },
  • "createTimestamp": 0,
  • "lastUpdateTimestamp": 0,
  • "entityVersion": 0
}

updatePrincipalRole

Update an existing principalRole

-
Authorizations:
OAuth2
path Parameters
principalRoleName
required
string

The principal role name

-
Request Body schema: application/json
required

The principalRole details to use in the update

-
currentEntityVersion
integer

The version of the object onto which this update is applied; if the object changed, the update will fail and the caller should retry after fetching the latest version.

-
object

Responses

Request samples

Content type
application/json
{
  • "currentEntityVersion": 0,
  • "properties": {
    }
}

Response samples

Content type
application/json
{
  • "name": "string",
  • "properties": {
    },
  • "createTimestamp": 0,
  • "lastUpdateTimestamp": 0,
  • "entityVersion": 0
}

deletePrincipalRole

Remove a principal role from polaris

-
Authorizations:
OAuth2
path Parameters
principalRoleName
required
string

The principal role name

-

Responses

listAssigneePrincipalsForPrincipalRole

List the Principals to whom the target principal role has been assigned

-
Authorizations:
OAuth2
path Parameters
principalRoleName
required
string

The principal role name

-

Responses

Response samples

Content type
application/json
{
  • "principals": [
    ]
}

listCatalogRolesForPrincipalRole

Get the catalog roles mapped to the principal role

-
Authorizations:
OAuth2
path Parameters
principalRoleName
required
string

The principal role name

-
catalogName
required
string

The name of the catalog where the catalogRoles reside

-

Responses

Response samples

Content type
application/json
{
  • "roles": [
    ]
}

assignCatalogRoleToPrincipalRole

Assign a catalog role to a principal role

-
Authorizations:
OAuth2
path Parameters
principalRoleName
required
string

The principal role name

-
catalogName
required
string

The name of the catalog where the catalogRoles reside

-
Request Body schema: application/json
required

The principal to create

-
name
required
string

The name of the role

-
object
createTimestamp
integer <int64>
lastUpdateTimestamp
integer <int64>
entityVersion
integer

The version of the catalog role object used to determine if the catalog role metadata has changed

-

Responses

Request samples

Content type
application/json
{
  • "name": "string",
  • "properties": {
    },
  • "createTimestamp": 0,
  • "lastUpdateTimestamp": 0,
  • "entityVersion": 0
}

revokeCatalogRoleFromPrincipalRole

Remove a catalog role from a principal role

-
Authorizations:
OAuth2
path Parameters
principalRoleName
required
string

The principal role name

-
catalogName
required
string

The name of the catalog that contains the role to revoke

-
catalogRoleName
required
string

The name of the catalog role that should be revoked

-

Responses

listCatalogRoles

List existing roles in the catalog

-
Authorizations:
OAuth2
path Parameters
catalogName
required
string

The catalog for which we are reading/updating roles

-

Responses

Response samples

Content type
application/json
{
  • "roles": [
    ]
}

createCatalogRole

Create a new role in the catalog

-
Authorizations:
OAuth2
path Parameters
catalogName
required
string

The catalog for which we are reading/updating roles

-
Request Body schema: application/json
name
required
string

The name of the role

-
object
createTimestamp
integer <int64>
lastUpdateTimestamp
integer <int64>
entityVersion
integer

The version of the catalog role object used to determine if the catalog role metadata has changed

-

Responses

Request samples

Content type
application/json
{
  • "name": "string",
  • "properties": {
    },
  • "createTimestamp": 0,
  • "lastUpdateTimestamp": 0,
  • "entityVersion": 0
}

getCatalogRole

Get the details of an existing role

-
Authorizations:
OAuth2
path Parameters
catalogName
required
string

The catalog for which we are retrieving roles

-
catalogRoleName
required
string

The name of the role

-

Responses

Response samples

Content type
application/json
{
  • "name": "string",
  • "properties": {
    },
  • "createTimestamp": 0,
  • "lastUpdateTimestamp": 0,
  • "entityVersion": 0
}

updateCatalogRole

Update an existing role in the catalog

-
Authorizations:
OAuth2
path Parameters
catalogName
required
string

The catalog for which we are retrieving roles

-
catalogRoleName
required
string

The name of the role

-
Request Body schema: application/json
currentEntityVersion
integer

The version of the object onto which this update is applied; if the object changed, the update will fail and the caller should retry after fetching the latest version.

-
object

Responses

Request samples

Content type
application/json
{
  • "currentEntityVersion": 0,
  • "properties": {
    }
}

Response samples

Content type
application/json
{
  • "name": "string",
  • "properties": {
    },
  • "createTimestamp": 0,
  • "lastUpdateTimestamp": 0,
  • "entityVersion": 0
}

deleteCatalogRole

Delete an existing role from the catalog. All associated grants will also be deleted

-
Authorizations:
OAuth2
path Parameters
catalogName
required
string

The catalog for which we are retrieving roles

-
catalogRoleName
required
string

The name of the role

-

Responses

listAssigneePrincipalRolesForCatalogRole

List the PrincipalRoles to whome the tagetcatalog role has been assigned

-
Authorizations:
OAuth2
path Parameters
catalogName
required
string

The name of the catalog where the catalog role resides

-
catalogRoleName
required
string

The name of the catalog role

-

Responses

Response samples

Content type
application/json
{
  • "roles": [
    ]
}

listGrantsForCatalogRole

List the grants the catalog role holds

-
Authorizations:
OAuth2
path Parameters
catalogName
required
string

The name of the catalog where the role will receive the grant

-
catalogRoleName
required
string

The name of the role receiving the grant (must exist)

-

Responses

Response samples

Content type
application/json
{
  • "grants": [
    ]
}

addGrantToCatalogRole

Add a new grant to the catalog role

-
Authorizations:
OAuth2
path Parameters
catalogName
required
string

The name of the catalog where the role will receive the grant

-
catalogRoleName
required
string

The name of the role receiving the grant (must exist)

-
Request Body schema: application/json
type
required
string
privilege
required
string (CatalogPrivilege)
Enum: "MANAGE_CATALOG" "NAMESPACE_CREATE" "TABLE_CREATE" "VIEW_CREATE" "NAMESPACE_DROP" "TABLE_DROP" "VIEW_DROP" "NAMESPACE_LIST" "TABLE_LIST" "VIEW_LIST" "NAMESPACE_READ_PROPERTIES" "TABLE_READ_PROPERTIES" "VIEW_READ_PROPERTIES" "NAMESPACE_WRITE_PROPERTIES" "TABLE_WRITE_PROPERTIES" "VIEW_WRITE_PROPERTIES" "TABLE_READ_DATA" "TABLE_WRITE_DATA" "NAMESPACE_FULL" "TABLE_FULL" "VIEW_FULL"

Responses

Request samples

Content type
application/json
Example
{
  • "type": "catalog",
  • "privilege": "MANAGE_CATALOG"
}

revokeGrantFromCatalogRole

Delete a specific grant from the role. This may be a subset or a superset of the grants the role has. In case of a subset, the role will retain the grants not specified. If the cascade parameter is true, grant revocation will have a cascading effect - that is, if a principal has specific grants on a subresource, and grants are revoked on a parent resource, the grants present on the subresource will be revoked as well. By default, this behavior is disabled and grant revocation only affects the specified resource.

-
Authorizations:
OAuth2
path Parameters
catalogName
required
string

The name of the catalog where the role will receive the grant

-
catalogRoleName
required
string

The name of the role receiving the grant (must exist)

-
query Parameters
cascade
boolean
Default: false

If true, the grant revocation cascades to all subresources.

-
Request Body schema: application/json
type
required
string
privilege
required
string (CatalogPrivilege)
Enum: "MANAGE_CATALOG" "NAMESPACE_CREATE" "TABLE_CREATE" "VIEW_CREATE" "NAMESPACE_DROP" "TABLE_DROP" "VIEW_DROP" "NAMESPACE_LIST" "TABLE_LIST" "VIEW_LIST" "NAMESPACE_READ_PROPERTIES" "TABLE_READ_PROPERTIES" "VIEW_READ_PROPERTIES" "NAMESPACE_WRITE_PROPERTIES" "TABLE_WRITE_PROPERTIES" "VIEW_WRITE_PROPERTIES" "TABLE_READ_DATA" "TABLE_WRITE_DATA" "NAMESPACE_FULL" "TABLE_FULL" "VIEW_FULL"

Responses

Request samples

Content type
application/json
Example
{
  • "type": "catalog",
  • "privilege": "MANAGE_CATALOG"
}
- - - - diff --git a/spec/docs.yaml b/spec/docs.yaml new file mode 100644 index 0000000000..3099721e16 --- /dev/null +++ b/spec/docs.yaml @@ -0,0 +1,27 @@ +openapi: 3.0.0 + +info: + title: Polaris Catalog Documentation + x-logo: + url: ./img/logos/polaris-catalog-stacked-logo.svg + altText: Polaris Catalog Logo + description: + $ref: ../docs/quickstart.md + contact: + email: community [at] polaris.io + url: https://github.com/polaris-catalog/polaris + license: + name: Apache v2.0 + url: https://github.com/polaris-catalog/polaris/blob/main/LICENSE + +tags: + - name: Polaris Catalog Overview + description: + $ref: ../docs/overview.md + - name: Polaris Catalog Entities + description: + $ref: ../docs/entities.md + - name: Access Control + description: + $ref: ../docs/access-control.md + \ No newline at end of file diff --git a/spec/index.yaml b/spec/index.yaml new file mode 100644 index 0000000000..22a516b13e --- /dev/null +++ b/spec/index.yaml @@ -0,0 +1,6860 @@ +openapi: 3.0.0 +info: + title: Polaris Catalog Documentation + x-logo: + url: ./img/logos/polaris-catalog-stacked-logo.svg + altText: Polaris Catalog Logo + description: "\n\n# Quick Start\n\nThis guide serves as a introduction to several key entities that can be managed with Polaris, describes how to build and deploy Polaris locally, and finally includes examples of how to use Polaris with Spark and Trino.\n\n## Prerequisites\n\nThis guide covers building Polaris, deploying it locally or via [Docker](https://www.docker.com/), and interacting with it using the command-line interface and [Apache Spark](https://spark.apache.org/). Before proceeding with Polaris, be sure to satisfy the relevant prerequisites listed here. \n\n### Building and Deploying Polaris\n\nTo get the latest Polaris code, you'll need to clone the repository using [git](https://git-scm.com/). You can install git using [homebrew](https://brew.sh/):\n\n```\nbrew install git\n```\n\nThen, use git to clone the Polaris repo:\n\n```\ncd ~\ngit clone https://github.com/polaris-catalog/polaris.git\n```\n\n#### With Docker\n\nIf you plan to deploy Polaris inside [Docker](https://www.docker.com/)], you'll need to install docker itself. For can be done using [homebrew](https://brew.sh/):\n\n```\nbrew install docker\n```\n\nOnce installed, make sure Docker is running. This can be done on macOS with:\n\n```\nopen -a Docker\n```\n\n#### From Source\n\nIf you plan to build Polaris from source yourself, you will need to satisfy a few prerequisites first.\n\nPolaris is built using [gradle](https://gradle.org/) and is compatible with Java 21. We recommend the use of [jenv](https://www.jenv.be/) to manage multiple Java versions. For example, to install Java 21 via [homebre]w(https://brew.sh/) and configure it with jenv: \n\n```\ncd ~/polaris\njenv local 21\nbrew install openjdk@21 gradle@8 jenv\njenv add $(brew --prefix openjdk@21)\njenv local 21\n```\n\n### Connecting to Polaris\n\nPolaris is compatible with any [Apache Iceberg](https://iceberg.apache.org/) client that supports the REST API. Depending on the client you plan to use, refer to the prerequisites below.\n\n#### With Spark\n\nIf you want to connect to Polaris with [Apache Spark](https://spark.apache.org/), you'll need to start by cloning Spark. As [above](#building-and-deploying-polaris), make sure [git](https://git-scm.com/) is installed first. You can install it with [homebrew](https://brew.sh/):\n\n```\nbrew install git\n```\n\nThen, clone Spark and check out a versioned branch. This guide uses [Spark 3.5.0](https://spark.apache.org/releases/spark-release-3-5-0.html).\n\n```\ncd ~\ngit clone https://github.com/apache/spark.git\ncd ~/spark\ngit checkout branch-3.5.0\n```\n\n## Deploying Polaris \n\nPolaris can be deployed via a lightweight docker image or as a standalone process. Before starting, be sure that you've satisfied the relevant [prerequisites](#building-and-deploying-polaris) detailed above.\n\n### Docker Image\n\nTo start using Polaris in Docker, launch Polaris while Docker is running:\n\n```\ncd ~/polaris\ndocker compose -f docker-compose.yml up --build\n```\n\nOnce the `polaris-polaris` container is up, you can continue to [Defining a Catalog](#defining-a-catalog).\n\n### Building Polaris\n\nRun Polaris locally with:\n\n```\ncd ~/polaris\n./gradlew runApp\n```\n\nYou should see output for some time as Polaris builds and starts up. Eventually, you won’t see any more logs and should see messages that resemble the following:\n\n```\nINFO [...] [main] [] o.e.j.s.handler.ContextHandler: Started i.d.j.MutableServletContextHandler@...\nINFO [...] [main] [] o.e.j.server.AbstractConnector: Started application@...\nINFO [...] [main] [] o.e.j.server.AbstractConnector: Started admin@...\nINFO [...] [main] [] o.eclipse.jetty.server.Server: Started Server@...\n```\n\nAt this point, Polaris is running.\n\n## Bootstrapping Polaris\n\nFor this tutorial, we'll launch an instance of Polaris that stores entities only in-memory. This means that any entities that you define will be destroyed when Polaris is shut down. It also means that Polaris will automatically bootstrap itself with root credentials. For more information on how to configure Polaris for production usage, see the [docs](./configuring-polaris-for-production.md).\n\nWhen Polaris is launched using in-memory mode the root `CLIENT_ID` and `CLIENT_SECRET` can be found in stdout on initial startup. For example:\n\n```\nBootstrapped with credentials: {\"client-id\": \"XXXX\", \"client-secret\": \"YYYY\"}\n```\n\nBe sure to note of these credentials as we'll be using them below.\n\n## Defining a Catalog\n\nIn Polaris, the [catalog](./entities/catalog.md) is the top-level entity that objects like [tables](./entities.md#table) and [views](./entities.md#view) are organized under. With a Polaris service running, you can create a catalog like so:\n\n```\ncd ~/polaris\n\n./polaris \\\n --client-id ${CLIENT_ID} \\\n --client-secret ${CLIENT_SECRET} \\\n catalogs \\\n create \\\n --storage-type s3 \\\n --default-base-location ${DEFAULT_BASE_LOCATION} \\\n --role-arn ${ROLE_ARN} \\\n quickstart_catalog\n```\n\nThis will create a new catalog called **quickstart_catalog**. \n\nThe `DEFAULT_BASE_LOCATION` you provide will be the default location that objects in this catalog should be stored in, and the `ROLE_ARN` you provide should be a [Role ARN](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference-arns.html) with access to read and write data in that location. These credentials will be provided to engines reading data from the catalog once they have authenticated with Polaris using credentials that have access to those resources.\n\nIf you’re using a storage type other than S3, such as Azure, you’ll provide a different type of credential than a Role ARN. For more details on supported storage types, see the [docs](./entities.md#storage-type). \n\nAdditionally, if Polaris is running somewhere other than `localhost:8181`, you can specify the correct hostname and port by providing `--host` and `--port` flags. For the full set of options supported by the CLI, please refer to the [docs](./command-line-interface.md).\n\n\n### Creating a Principal and Assigning it Privileges\n\nWith a catalog created, we can create a [principal](./entities.md#principal) that has access to manage that catalog. For details on how to configure the Polaris CLI, see [the section above](#defining-a-catalog) or refer to the [docs](./command-line-interface.md).\n\n```\n./polaris \\\n --client-id ${CLIENT_ID} \\\n --client-secret ${CLIENT_SECRET} \\\n principals \\\n create \\\n quickstart_user\n\n./polaris \\\n --client-id ${CLIENT_ID} \\\n --client-secret ${CLIENT_SECRET} \\\n principal-roles \\\n create \\\n quickstart_user_role\n\n./polaris \\\n --client-id ${CLIENT_ID} \\\n --client-secret ${CLIENT_SECRET} \\\n catalog-roles \\\n create \\\n --catalog quickstart_catalog \\\n quickstart_catalog_role\n```\n\n\nBe sure to provide the necessary credentials, hostname, and port as before.\n\nWhen the `principals create` command completes successfully, it will return the credentials for this new principal. Be sure to note these down for later. For example:\n\n```\n./polaris ... principals create example\n{\"clientId\": \"XXXX\", \"clientSecret\": \"YYYY\"}\n```\n\nNow, we grant the principal the [principal role](./entities.md#principal-role) we created, and grant the [catalog role](./entities.md#catalog-role) the principal role we created. For more information on these entities, please refer to the linked documentation.\n\n```\n./polaris \\\n --client-id ${CLIENT_ID} \\\n --client-secret ${CLIENT_SECRET} \\\n principal-roles \\\n grant \\\n --principal quickstart_user \\\n quickstart_user_role\n\n./polaris \\\n --client-id ${CLIENT_ID} \\\n --client-secret ${CLIENT_SECRET} \\\n catalog-roles \\\n grant \\\n --catalog quickstart_catalog \\\n --principal-role quickstart_user_role \\\n quickstart_catalog_role\n```\n\nNow, we’ve linked our principal to the catalog via roles like so:\n\n![Principal to Catalog](./img/quickstart/privilege-illustration-1.png \"Principal to Catalog\")\n\nIn order to give this principal the ability to interact with the catalog, we must assign some [privileges](./entities.md#privileges). For the time being, we will give this principal the ability to fully manage content in our new catalog. We can do this with the CLI like so:\n\n```\n./polaris \\\n --client-id ${CLIENT_ID} \\\n --client-secret ${CLIENT_SECRET} \\\n privileges \\\n --catalog quickstart_catalog \\\n --catalog-role quickstart_catalog_role \\\n catalog \\\n grant \\\n CATALOG_MANAGE_CONTENT\n```\n\nThis grants the [catalog privileges](./entities.md#privilege) `CATALOG_MANAGE_CONTENT` to our catalog role, linking everything together like so:\n\n![Principal to Catalog with Catalog Role](./img/quickstart/privilege-illustration-2.png \"Principal to Catalog with Catalog Role\")\n\n`CATALOG_MANAGE_CONTENT` has create/list/read/write privileges on all entities within the catalog. The same privilege could be granted to a namespace, in which case the principal could create/list/read/write any entity under that namespace.\n\n## Using Iceberg & Polaris\n\nAt this point, we’ve created a principal and granted it the ability to manage a catalog. We can now use an external engine to assume that principal, access our catalog, and store data in that catalog using [Apache Iceberg](https://iceberg.apache.org/).\n\n### Connecting with Spark\n\nTo use a Polaris-managed catalog in [Apache Spark](https://spark.apache.org/), we can configure Spark to use the Iceberg catalog REST API. \n\nThis guide uses [Apache Spark 3.5](https://spark.apache.org/releases/spark-release-3-5-0.html), but be sure to find [the appropriate iceberg-spark package for your Spark version](https://mvnrepository.com/artifact/org.apache.iceberg/iceberg-spark). With a local Spark clone, we on the `branch-3.5` branch we can run the following:\n\n_Note: the credentials provided here are those for our principal, not the root credentials._\n\n```\nbin/spark-shell \\\n--packages org.apache.iceberg:iceberg-spark-runtime-3.5_2.12:1.5.2,org.apache.hadoop:hadoop-aws:3.4.0 \\\n--conf spark.sql.extensions=org.apache.iceberg.spark.extensions.IcebergSparkSessionExtensions \\\n--conf spark.sql.catalog.quickstart_catalog.warehouse=quickstart_catalog \\\n--conf spark.sql.catalog.quickstart_catalog.header.X-Iceberg-Access-Delegation=true \\\n--conf spark.sql.catalog.quickstart_catalog=org.apache.iceberg.spark.SparkCatalog \\\n--conf spark.sql.catalog.quickstart_catalog.catalog-impl=org.apache.iceberg.rest.RESTCatalog \\\n--conf spark.sql.catalog.quickstart_catalog.uri=http://localhost:8181/api/catalog \\\n--conf spark.sql.catalog.quickstart_catalog.credential='XXXX:YYYY' \\\n--conf spark.sql.catalog.quickstart_catalog.scope='PRINCIPAL_ROLE:ALL' \\\n--conf spark.sql.catalog.quickstart_catalog.token-refresh-enabled=true\n```\n\n\nReplace `XXXX` and `YYYY` with the client ID and client secret generated when you created the `quickstart_user` principal.\n\nSimilar to the CLI commands above, this configures Spark to use the Polaris running at `localhost:8181` as a catalog. If your Polaris server is running elsewhere, but sure to update the configuration appropriately.\n\nFinally, note that we include the `hadoop-aws` package here. If your table is using a different filesystem, be sure to include the appropriate dependency.\n\nOnce the Spark session starts, we can create a namespace and table within the catalog:\n\n```\nspark.sql(\"USE quickstart_catalog\")\nspark.sql(\"CREATE NAMESPACE IF NOT EXISTS quickstart_namespace\")\nspark.sql(\"CREATE NAMESPACE IF NOT EXISTS quickstart_namespace.schema\")\nspark.sql(\"USE NAMESPACE quickstart_namespace.schema\")\nspark.sql(\"\"\"\n\tCREATE TABLE IF NOT EXISTS quickstart_table (\n\t\tid BIGINT, data STRING\n\t) \nUSING ICEBERG\n\"\"\")\n```\n\nWe can now use this table like any other:\n\n```\nspark.sql(\"INSERT INTO quickstart_table VALUES (1, 'some data')\")\nspark.sql(\"SELECT * FROM quickstart_table\").show(false)\n. . .\n+---+---------+\n|id |data |\n+---+---------+\n|1 |some data|\n+---+---------+\n```\n\nIf at any time access is revoked...\n\n```\n./polaris \\\n --client-id ${CLIENT_ID} \\\n --client-secret ${CLIENT_SECRET} \\\n privileges \\\n --catalog quickstart_catalog \\\n --catalog-role quickstart_catalog_role \\\n catalog \\\n revoke \\\n CATALOG_MANAGE_CONTENT\n```\n\nSpark will lose access to the table:\n\n```\nspark.sql(\"SELECT * FROM quickstart_table\").show(false)\n\norg.apache.iceberg.exceptions.ForbiddenException: Forbidden: Principal 'quickstart_user' with activated PrincipalRoles '[]' and activated ids '[6, 7]' is not authorized for op LOAD_TABLE_WITH_READ_DELEGATION\n```\n" + contact: + email: community [at] polaris.io + url: https://github.com/polaris-catalog/polaris + license: + name: Apache v2.0 + url: https://github.com/polaris-catalog/polaris/blob/main/LICENSE +servers: + - url: '{scheme}://{host}/api/management/v1' + description: Server URL when the port can be inferred from the scheme + variables: + scheme: + description: The scheme of the URI, either http or https. + default: https + host: + description: The host address for the specified server + default: localhost + - url: '{scheme}://{host}/{basePath}' + description: Server URL when the port can be inferred from the scheme + variables: + scheme: + description: The scheme of the URI, either http or https. + default: https + host: + description: The host address for the specified server + default: localhost + basePath: + description: Optional prefix to be appended to all routes + default: '' + - url: '{scheme}://{host}:{port}/{basePath}' + description: Generic base server URL, with all parts configurable + variables: + scheme: + description: The scheme of the URI, either http or https. + default: https + host: + description: The host address for the specified server + default: localhost + port: + description: The port used when addressing the host + default: '443' + basePath: + description: Optional prefix to be appended to all routes + default: '' +tags: + - name: Polaris Catalog Overview + description: >+ + + + + Polaris Catalog is a catalog implementation for Apache Iceberg built on + the open source Apache Iceberg REST protocol. + + + With Polaris Catalog, you can provide centralized, secure read and write + access across different REST-compatible query engines to your Iceberg + tables. + + + ![Conceptual diagram of Polaris Catalog.](./img/overview.svg "Polaris + Catalog overview") + + + ## Key concepts + + + This section introduces key concepts associated with using Polaris + Catalog. + + + In the following diagram, a sample [Polaris Catalog + structure](./overview.md#catalog) with nested + [namespaces](./overview.md#namespace) is shown for Catalog1. No tables + + or namespaces have been created yet for Catalog2 or Catalog3: + + + ![Diagram that shows an example Polaris Catalog + structure.](./img/sample-catalog-structure.svg "Sample Polaris Catalog + structure") + + + ### Catalog + + + In Polaris Catalog, you can create one or more catalog resources to + organize Iceberg tables. + + + Configure your catalog by setting values in the storage configuration for + S3, Azure, or Google Cloud Storage. An Iceberg catalog enables a + + query engine to manage and organize tables. The catalog forms the first + architectural layer in the [Iceberg table + specification](https://iceberg.apache.org/spec/#overview) and must + support: + + + - Storing the current metadata pointer for one or more Iceberg tables. A + metadata pointer maps a table name to the location of that table's + current metadata file. + + - Performing atomic operations so that you can update the current + metadata pointer for a table to the metadata pointer of a new version of + the table. + + To learn more about Iceberg catalogs, see the [Apache Iceberg + documentation](https://iceberg.apache.org/concepts/catalog/). + + + #### Catalog types + + + A catalog can be one of the following two types: + + + - Internal: The catalog is managed by Polaris. Tables from this catalog + can be read and written in Polaris. + + + - External: The catalog is externally managed by another Iceberg catalog + provider (for example, Snowflake, Glue, Dremio Arctic). Tables from + this catalog are synced to Polaris. These tables are read-only in Polaris. In the current release, only Snowflake external catalog is provided. + + A catalog is configured with a storage configuration that can point to S3, + Azure storage, or GCS. + + + To create a new catalog, see [Create a catalog](./create-a-catalog.md + "Sample Polaris Catalog structure"). + + + ### Namespace + + + You create *namespaces* to logically group Iceberg tables within a + catalog. A catalog can have one or more namespaces. You can also create + + nested namespaces. Iceberg tables belong to namespaces. + + + ### Iceberg tables & catalogs + + + In an internal catalog, an Iceberg table is registered in Polaris Catalog, + but read and written via query engines. The table data and + + metadata is stored in your external cloud storage. The table uses Polaris + Catalog as the Iceberg catalog. + + + If you have tables that use Snowflake as the Iceberg catalog + (Snowflake-managed tables), you can sync these tables to an external + + catalog in Polaris Catalog. If you sync this catalog to Polaris Catalog, + it appears as an external catalog in Polaris Catalog. The table data and + + metadata is stored in your external cloud storage. The Snowflake query + engine can read from or write to these tables. However, the other query + + engines can only read from these tables. + + + **Important** + + + To ensure that the access privileges defined for a catalog are enforced + + correctly, you must: + + + - Ensure a directory only contains the data files that belong to a + single table. + + - Create a directory hierarchy that matches the namespace hierarchy + for the catalog. + + For example, if a catalog includes: + + + - Top-level namespace namespace1 + + + - Nested namespace namespace1a + + + - A customers table, which is grouped under nested namespace + namespace1a + + - An orders table, which is grouped under nested namespace namespace1a + + + The directory hierarchy for the catalog must be: + + + - /namespace1/namespace1a/customers/\ + + - /namespace1/namespace1a/orders/\ + + + ### Service principal + + + A service principal is an entity that you create in Polaris Catalog. Each + service principal encapsulates credentials that you use to connect + + to Polaris Catalog. + + + Query engines use service principals to connect to catalogs. + + + Polaris Catalog generates a Client ID and Client Secret pair for each + service principal. + + + The following table displays example service principals that you might + create in Polaris Catalog: + + | Service connection name | Description | + | --------------------------- | ----------- | + | Flink ingestion | For Apache Flink to ingest streaming data into Iceberg tables. | + | Spark ETL pipeline | For Apache Spark to run ETL pipeline jobs on Iceberg tables. | + | Snowflake data pipelines | For Snowflake to run data pipelines for transforming data in Iceberg tables. | + | Trino BI dashboard | For Trino to run BI queries for powering a dashboard. | + | Snowflake AI team | For Snowflake to run AI jobs on data in Iceberg tables. | + + ### Service connection + + + A service connection represents a REST-compatible engine (such as Apache + Spark, Apache Flink, or Trino) that can read from and write to Polaris + + Catalog. When creating a new service connection, the Polaris administrator + grants the service principal that is created with the new service + + connection with either a new or existing principal role. A principal role + is a resource in Polaris that you can use to logically group Polaris + + service principals together and grant privileges on securable objects. For + more information, see [Principal role](./access-control.md#principal-role + "Principal role"). Polaris Catalog uses a role-based access control (RBAC) + model to grant service principals access to resources. For more + information, + + see [Access control](./access-control.md "Access control"). For a diagram + of this model, see [RBAC model](./access-control.md#rbac-model "RBAC + model"). + + + If the Polaris administrator grants the service principal for the new + service connection with a new principal role, the service principal + + doesn\'t have any privileges granted to it yet. When securing the catalog + that the new service connection will connect to, the Polaris + + administrator grants privileges to catalog roles and then grants these + catalog roles to the new principal role. As a result, the service + + principal for the new service connection is bestowed with these + privileges. For more information about catalog roles, see [Catalog + role](./access-control.md#catalog-role "Catalog role"). + + + If the Polaris administrator grants an existing principal role to the + service principal for the new service connection, the service principal + + is bestowed with the privileges granted to the catalog roles that are + granted to the existing principal role. If needed, the Polaris + + administrator can grant additional catalog roles to the existing principal + role or remove catalog roles from it to adjust the privileges + + bestowed to the service principal. For an example of how RBAC works in + Polaris, see [RBAC example](./access-control.md#rbac-example "RBAC + example"). + + + ### Storage configuration + + + A storage configuration stores a generated identity and access management + (IAM) entity for your external cloud storage and is created + + when you create a catalog. The storage configuration is used to set the + values to connect Polaris Catalog to your cloud storage. During the + + catalog creation process, an IAM entity is generated and used to create a + trust relationship between the cloud storage provider and Polaris + + Catalog. + + + When you create a catalog, you supply the following information about your + external cloud storage: + + + | Cloud storage provider | Information | + + | -----------------------| ----------- | + + | Amazon S3 |
  • Default base location for your Amazon S3 + bucket
  • Locations for your Amazon S3 bucket
  • S3 role + ARN
  • External ID (optional)
| + + | Google Cloud Storage (GCS) |
  • Default base location for your GCS + bucket
  • Locations for your Amazon GCS bucket
| + + | Azure |
  • Default base location for your Microsoft Azure + container
  • Locations for your Microsoft Azure + container
  • Azure tenant ID
| + + + ## Example workflow + + + In the following example workflow, Bob creates an Iceberg table named + Table1 and Alice reads data from Table1. + + + 1. Bob uses Apache Spark to create the Table1 table under the + Namespace1 namespace in the Catalog1 catalog and insert values into + Table1. + + Bob can create Table1 and insert data into it, because he is using a + service connection with a service principal that is bestowed with + the privileges to perform these actions. + + 2. Alice uses Snowflake to read data from Table1. + + Alice can read data from Table1, because she is using a service + connection with a service principal with a catalog integration that + is bestowed with the privileges to perform this action. Alice + creates an unmanaged table in Snowflake to read data from Table1. + + ![Diagram that shows an example workflow for Polaris + Catalog](./img/example-workflow.svg "Example workflow for Polaris + Catalog") + + + ## Security and access control + + + This section describes security and access control. + + + ### Credential vending + + + To secure interactions with service connections, Polaris Catalog vends + temporary storage credentials to the query engine during query + + execution. These credentials allow the query engine to run the query + without needing to have access to your external cloud storage for + + Iceberg tables. This process is called credential vending. + + + ### Identity and access management (IAM) + + + Polaris Catalog uses the identity and access management (IAM) entity to + securely connect to your storage for accessing table data, Iceberg + + metadata, and manifest files that store the table schema, partitions, and + other metadata. Polaris Catalog retains the IAM entity for your + + storage location. + + + ### Access control + + + Polaris Catalog enforces the access control that you configure across all + tables registered with the service, and governs security for all + + queries from query engines in a consistent manner. + + + Polaris uses a role-based access control (RBAC) model that lets you + centrally configure access for Polaris service principals to catalogs, + + namespaces, and tables. + + + Polaris RBAC uses two different role types to delegate privileges: + + + - **Principal roles:** Granted to Polaris service principals and + analogous to roles in other access control systems that you grant to + service principals. + + - **Catalog roles:** Configured with certain privileges on Polaris + catalog resources, and granted to principal roles. + + For more information, see [Access control](./access-control.md "Access + control"). + + x-displayName: Polaris Catalog Overview + - name: Polaris Catalog Entities + description: > + + + + This page documents various entities that can be managed in Polaris. + + + ## Catalog + + + A catalog is a top-level entity in Polaris that may contain other entities + like [namespaces](#namespace) and [tables](#table). These map directly to + [Apache Iceberg catalogs](https://iceberg.apache.org/concepts/catalog/). + + + For information on managing catalogs with the REST API or for more + information on what data can be associated with a catalog, see [the API + docs](../regtests/client/python/docs/CreateCatalogRequest.md). + + + ### Storage Type + + + All catalogs in Polaris are associated with a _storage type_. Valid + Storage Types are `S3`, `Azure`, and `GCS`. The `FILE` type is also + additionally available for testing. Each of these types relates to a + different storage provider where data within the catalog may reside. + Depending on the storage type, various other configurations may be set for + a catalog including credentials to be used when accessing data inside the + catalog. + + + For details on how to use Storage Types in the REST API, see [the API + docs](../regtests/client/python/docs/StorageConfigInfo.md). + + + ## Namespace + + + A namespace is a logical entity that resides within a [catalog](#catalog) + and can contain other entities such as [tables](#table) or [views](#view). + Some other systems may refer to namespaces as _schemas_ or _databases_. + + + In Polaris, namespaces can be nested up to 16 levels. For example, + `a.b.c.d.e.f.g` is a valid namespace. `b` is said to reside within `a`, + and so on. + + + For information on managing namespaces with the REST API or for more + information on what data can be associated with a namespace, see [the API + docs](../regtests/client/python/docs/CreateNamespaceRequest.md). + + + + ## Table + + + Polaris tables are entites that map to [Apache Iceberg + tables](https://iceberg.apache.org/docs/nightly/configuration/). + + + For information on managing tables with the REST API or for more + information on what data can be associated with a table, see [the API + docs](../regtests/client/python/docs/CreateTableRequest.md). + + + ## View + + + Polaris views are entites that map to [Apache Iceberg + views](https://iceberg.apache.org/view-spec/). + + + For information on managing views with the REST API or for more + information on what data can be associated with a view, see [the API + docs](../regtests/client/python/docs/CreateViewRequest.md). + + + ## Principal + + + Polaris principals are unique identities that can be used to represent + users or services. Each principal may have one or more [principal + roles](#principal-role) assigned to it for the purpose of accessing + catalogs and the entities within them. + + + For information on managing principals with the REST API or for more + information on what data can be associated with a principal, see [the API + docs](../regtests/client/python/docs/CreatePrincipalRequest.md). + + + ## Principal Role + + + Polaris principal roles are labels that may be granted to + [principals](#principal). Each principal may have one or more principal + roles, and the same principal role may be granted to multiple principals. + Principal roles may be assigned based on the persona or responsibilities + of a given principal, or on how that principal will need to access + different entities within Polaris. + + + For information on managing principal roles with the REST API or for more + information on what data can be associated with a principal role, see [the + API docs](../regtests/client/python/docs/CreatePrincipalRoleRequest.md). + + + + ## Catalog Role + + + Polaris catalog roles are labels that may be granted to + [catalogs](#catalog). Each catalog may have one or more catalog roles, and + the same catalog role may be granted to multiple catalogs. Catalog roles + may be assigned based on the nature of data that will reside in a catalog, + or by the groups of users and services that might need to access that + data. + + + Each catalog role may have multiple [privileges](#privilege) granted to + it, and each catalog role can be granted to one or more [principal + roles](#principal-role). This is the mechanism by which principals are + granted access to entities inside a catalog such as namespaces and tables. + + + ## Privilege + + + Polaris privileges are granted to [catalog roles](#catalog-role) in order + to grant principals with a given principal role some degree of access to + catalogs with a given catalog role. When a privilege is granted to a + catalog role, any principal roles granted that catalog role receive the + privilege. In turn, any principals who are granted that principal role + receive it. + + + A privilege can be scoped to any entity inside a catalog, including the + catalog itself. + + + For a list of supported privileges for each privilege class, see the API + docs: + + * [Table Privileges](../regtests/client/python/docs/TablePrivilege.md) + + * [View Privileges](../regtests/client/python/docs/ViewPrivilege.md) + + * [Namespace + Privileges](../regtests/client/python/docs/NamespacePrivilege.md) + + * [Catalog Privileges](../regtests/client/python/docs/CatalogPrivilege.md) + x-displayName: Polaris Catalog Entities + - name: Access Control + description: > + + + + This section provides information about how access control works for + Polaris Catalog. + + + Polaris Catalog uses a role-based access control (RBAC) model, in which + the Polaris administrator assigns access privileges to catalog roles, + + and then grants service principals access to resources by assigning + catalog roles to principal roles. + + + The key concepts to understanding access control in Polaris are: + + + - **Securable object** + + - **Principal role** + + - **Catalog role** + + - **Privilege** + + + ## Securable object + + + A securable object is an object to which access can be granted. Polaris + + has the following securable objects: + + + - Catalog + + - Namespace + + - Iceberg table + + - View + + + ## Principal role + + + A principal role is a resource in Polaris that you can use to logically + group Polaris service principals together and grant privileges on + + securable objects. + + + Polaris supports a many-to-one relationship between service principals and + principal roles. For example, to grant the same privileges to + + multiple service principals, you can grant a single principal role to + those service principals. A service principal can be granted one + + principal role. When registering a service connection, the Polaris + administrator specifies the principal role that is granted to the + + service principal. + + + You don't grant privileges directly to a principal role. Instead, you + configure object permissions at the catalog role level, and then grant + + catalog roles to a principal role. + + + The following table shows examples of principal roles that you might + configure in Polaris: + + + | Principal role name | Description | + + | -----------------------| ----------- | + + | Data_engineer | A role that is granted to multiple service principals + for running data engineering jobs. | + + | Data_scientist | A role that is granted to multiple service principals + for running data science or AI jobs. | + + + ## Catalog role + + + A catalog role belongs to a particular catalog resource in Polaris and + specifies a set of permissions for actions on the catalog, or on objects + + in the catalog, such as catalog namespaces or tables. You can create one + or more catalog roles for a catalog. + + + You grant privileges to a catalog role, and then grant the catalog role to + a principal role to bestow the privileges to one or more service + + principals. + + + **Note** + + + If you update the privileges bestowed to a service principal, the updates + won\'t take effect for up to one hour. This means that if you + + revoke or grant some privileges for a catalog, the updated privileges + won\'t take effect on any service principal with access to that catalog + + for up to one hour. + + + Polaris also supports a many-to-many relationship between catalog roles + and principal roles. You can grant the same catalog role to one or more + + principal roles. Likewise, a principal role can be granted to one or more + catalog roles. + + + The following table displays examples of catalog roles that you might + + configure in Polaris: + + + | Example Catalog role | Description | + + | -----------------------| ----------- | + + | Catalog administrators | A role that has been granted multiple + privileges to emulate full access to the catalog.

Principal + roles that have been granted this role are permitted to create, alter, + read, write, and drop tables in the catalog. | + + | Catalog readers | A role that has been granted read-only privileges + to tables in the catalog.

Principal roles that have been + granted this role are allowed to read from tables in the catalog. | + + | Catalog contributor | A role that has been granted read and write + access privileges to all tables that belong to the catalog.

Principal roles that have been granted this role are allowed to perform + read and write operations on tables in the catalog. | + + + ## RBAC model + + + The following diagram illustrates the RBAC model used by Polaris Catalog. + For each catalog, the Polaris administrator assigns access + + privileges to catalog roles, and then grants service principals access to + resources by assigning catalog roles to principal roles. Polaris + + supports a many-to-one relationship between service principals and + principal roles. + + + ![Diagram that shows the RBAC model for Polaris + Catalog.](./img/rbac-model.svg "Polaris Catalog RBAC model") + + + ## Access control privileges + + + This section describes the privileges that are available in the Polaris + access control model. Privileges are granted to catalog roles, catalog + + roles are granted to principal roles, and principal roles are granted to + service principals to specify the operations that service principals can + + perform on objects in Polaris. + + + To grant the full set of privileges (drop, list, read, write, etc.) on an + object, you can use the *full privilege* option. + + + ### Table privileges + + + **Note** + + + The TABLE_FULL_METADATA full privilege doesn't grant access to the + TABLE_READ_DATA or TABLE_WRITE_DATA individual privileges. + + + | Full privilege | Individual privilege | Description | + + | -----------------------| ----------- | ---- | + + | TABLE_FULL_METADATA | TABLE_CREATE | Enables registering a table with + the catalog. | + + | | TABLE_DROP | Enables dropping a table from the catalog. | + + | | TABLE_LIST | Enables listing any tables in the catalog. | + + | | TABLE_READ_PROPERTIES | Enables reading + [properties](https://iceberg.apache.org/docs/nightly/configuration/#table-properties) + of the table. | + + | | TABLE_WRITE_PROPERTIES | Enables configuring + [properties](https://iceberg.apache.org/docs/nightly/configuration/#table-properties) + for the table. | + + | N/A | TABLE_READ_DATA | Enables reading data from the table by receiving + short-lived read-only storage credentials from the catalog. | + + | N/A | TABLE_WRITE_DATA | Enables writing data to the table by receiving + short-lived read+write storage credentials from the catalog. | + + + ### View privileges + + + | Full privilege | Individual privilege | Description | + + | -----------------------| ----------- | ---- | + + | VIEW_FULL_METADATA | VIEW_CREATE | Enables registering a view with the + catalog. | + + | | VIEW_DROP | Enables dropping a view from the catalog. | + + | | VIEW_LIST | Enables listing any views in the catalog. | + + | | VIEW_READ_PROPERTIES | Enables reading all the view properties. | + + | | VIEW_WRITE_PROPERTIES | Enables configuring view properties. | + + + ### Namespace privileges + + + | Full privilege | Individual privilege | Description | + + | -----------------------| ----------- | ---- | + + | NAMESPACE_FULL_METADATA | NAMESPACE_CREATE | Enables creating a + namespace in a catalog. | + + | | NAMESPACE_DROP | Enables dropping the namespace from the catalog. | + + | | NAMESPACE_LIST | Enables listing any object in the namespace, + including nested namespaces and tables. | + + | | NAMESPACE_READ_PROPERTIES | Enables reading all the namespace + properties. | + + | | NAMESPACE_WRITE_PROPERTIES | Enables configuring namespace + properties. | + + + ### Catalog privileges + + + | Privilege | Description | + + | -----------------------| ----------- | + + | CATALOG_MANAGE_ACCESS | Includes the ability to grant or revoke + privileges on objects in a catalog to catalog roles, and the ability to + grant or revoke catalog roles to or from principal roles. | + + | CATALOG_MANAGE_CONTENT | Enables full management of content for the + catalog. This privilege encompasses the following + privileges:
  • CATALOG_MANAGE_METADATA
  • TABLE_FULL_METADATA
  • NAMESPACE_FULL_METADATA
  • VIEW_FULL_METADATA
  • TABLE_WRITE_DATA
  • TABLE_READ_DATA
  • CATALOG_READ_PROPERTIES
  • CATALOG_WRITE_PROPERTIES
+ | + + | CATALOG_MANAGE_METADATA | Enables full management of the catalog, as + well as catalog roles, namespaces, and tables. | + + | CATALOG_READ_PROPERTIES | Enables listing catalogs and reading + properties of the catalog. | + + | CATALOG_WRITE_PROPERTIES | Enables configuring catalog properties. | + + + ## RBAC example + + + The following diagram illustrates how RBAC works in Polaris, and + + includes the following users: + + + - **Alice**: A service admin who signs up for Polaris. Alice can + create service principals. She can also create catalogs and + namespaces, and configure access control for Polaris resources. + + > **Note** + + > + + > The service principal for Alice is not visible in the Polaris Catalog + + > user interface. + + + - **Bob**: A data engineer who uses Snowpipe Streaming (in Snowflake) + and Apache Spark connections to interact with Polaris. + + - Alice has created a service principal for Bob. It has been + granted the Data_engineer principal role, which in turn has been + granted the following catalog roles: Catalog contributor and + Data administrator (for both the Silver and Gold zone catalogs + in the following diagram). + + - The Catalog contributor role grants permission to create + namespaces and tables in the Bronze zone catalog. + + - The Data administrator roles grant full administrative rights to + the Silver zone catalog and Gold zone catalog. + + - **Mark**: A data scientist who uses Snowflake AI services to + interact with Polaris. + + - Alice has created a service principal for Mark. It has been + granted the Data_scientist principal role, which in turn has + been granted the catalog role named Catalog reader. + + - The Catalog reader role grants read-only access for a catalog + named Gold zone catalog. + + ![Diagram that shows an example of how RBAC works in Polaris + Catalog.](./img/rbac-example.svg "Polaris Catalog RBAC example") + x-displayName: Access Control + - name: polaris-management-service_other + x-displayName: other + - name: Configuration API + x-displayName: Configuration API + - name: OAuth2 API + x-displayName: OAuth2 API + - name: Catalog API + x-displayName: Catalog API +paths: + /catalogs: + get: + operationId: listCatalogs + description: List all catalogs in this polaris service + responses: + '200': + description: List of catalogs in the polaris service + content: + application/json: + schema: + $ref: '#/components/schemas/Polaris_Management_Service_Catalogs' + '403': + description: The caller does not have permission to list catalog details + tags: + - polaris-management-service_other + security: + - Polaris_Management_Service_OAuth2: [] + post: + operationId: createCatalog + description: Add a new Catalog + requestBody: + description: The Catalog to create + required: true + content: + application/json: + schema: + $ref: >- + #/components/schemas/Polaris_Management_Service_CreateCatalogRequest + responses: + '201': + description: Successful response + '403': + description: The caller does not have permission to create a catalog + '404': + description: The catalog does not exist + '409': + description: A catalog with the specified name already exists + tags: + - polaris-management-service_other + security: + - Polaris_Management_Service_OAuth2: [] + /catalogs/{catalogName}: + parameters: + - name: catalogName + in: path + description: The name of the catalog + required: true + schema: + type: string + minLength: 1 + maxLength: 256 + pattern: ^(?!\s*[s|S][y|Y][s|S][t|T][e|E][m|M]\$).*$ + get: + operationId: getCatalog + description: Get the details of a catalog + responses: + '200': + description: The catalog details + content: + application/json: + schema: + $ref: '#/components/schemas/Polaris_Management_Service_Catalog' + '403': + description: The caller does not have permission to read catalog details + '404': + description: The catalog does not exist + tags: + - polaris-management-service_other + security: + - Polaris_Management_Service_OAuth2: [] + put: + operationId: updateCatalog + description: Update an existing catalog + requestBody: + description: The catalog details to use in the update + required: true + content: + application/json: + schema: + $ref: >- + #/components/schemas/Polaris_Management_Service_UpdateCatalogRequest + responses: + '200': + description: The catalog details + content: + application/json: + schema: + $ref: '#/components/schemas/Polaris_Management_Service_Catalog' + '403': + description: The caller does not have permission to update catalog details + '404': + description: The catalog does not exist + '409': + description: >- + The entity version doesn't match the currentEntityVersion; retry + after fetching latest version + tags: + - polaris-management-service_other + security: + - Polaris_Management_Service_OAuth2: [] + delete: + operationId: deleteCatalog + description: >- + Delete an existing catalog. This is a cascading operation that deletes + all metadata, including principals, roles and grants. If the catalog is + an internal catalog, all tables and namespaces are dropped without + purge. + responses: + '204': + description: Success, no content + '403': + description: The caller does not have permission to delete a catalog + '404': + description: The catalog does not exist + tags: + - polaris-management-service_other + security: + - Polaris_Management_Service_OAuth2: [] + /principals: + get: + operationId: listPrincipals + description: List the principals for the current catalog + responses: + '200': + description: List of principals for this catalog + content: + application/json: + schema: + $ref: '#/components/schemas/Polaris_Management_Service_Principals' + '403': + description: The caller does not have permission to list catalog admins + '404': + description: The catalog does not exist + tags: + - polaris-management-service_other + security: + - Polaris_Management_Service_OAuth2: [] + post: + operationId: createPrincipal + description: Create a principal + requestBody: + description: The principal to create + required: true + content: + application/json: + schema: + $ref: >- + #/components/schemas/Polaris_Management_Service_CreatePrincipalRequest + responses: + '201': + description: Successful response + content: + application/json: + schema: + $ref: >- + #/components/schemas/Polaris_Management_Service_PrincipalWithCredentials + '403': + description: The caller does not have permission to add a principal + tags: + - polaris-management-service_other + security: + - Polaris_Management_Service_OAuth2: [] + /principals/{principalName}: + parameters: + - name: principalName + in: path + description: The principal name + required: true + schema: + type: string + minLength: 1 + maxLength: 256 + pattern: ^(?!\s*[s|S][y|Y][s|S][t|T][e|E][m|M]\$).*$ + get: + operationId: getPrincipal + description: Get the principal details + responses: + '200': + description: The requested principal + content: + application/json: + schema: + $ref: '#/components/schemas/Polaris_Management_Service_Principal' + '403': + description: The caller does not have permission to get principal details + '404': + description: The catalog or principal does not exist + tags: + - polaris-management-service_other + security: + - Polaris_Management_Service_OAuth2: [] + put: + operationId: updatePrincipal + description: Update an existing principal + requestBody: + description: The principal details to use in the update + required: true + content: + application/json: + schema: + $ref: >- + #/components/schemas/Polaris_Management_Service_UpdatePrincipalRequest + responses: + '200': + description: The updated principal + content: + application/json: + schema: + $ref: '#/components/schemas/Polaris_Management_Service_Principal' + '403': + description: The caller does not have permission to update principal details + '404': + description: The principal does not exist + '409': + description: >- + The entity version doesn't match the currentEntityVersion; retry + after fetching latest version + tags: + - polaris-management-service_other + security: + - Polaris_Management_Service_OAuth2: [] + delete: + operationId: deletePrincipal + description: Remove a principal from polaris + responses: + '204': + description: Success, no content + '403': + description: The caller does not have permission to delete a principal + '404': + description: The principal does not exist + tags: + - polaris-management-service_other + security: + - Polaris_Management_Service_OAuth2: [] + /principals/{principalName}/rotate: + parameters: + - name: principalName + in: path + description: The user name + required: true + schema: + type: string + minLength: 1 + maxLength: 256 + pattern: ^(?!\s*[s|S][y|Y][s|S][t|T][e|E][m|M]\$).*$ + post: + operationId: rotateCredentials + description: >- + Rotate a principal's credentials. The new credentials will be returned + in the response. This is the only API, aside from createPrincipal, that + returns the user's credentials. This API is *not* idempotent. + responses: + '200': + description: The principal details along with the newly rotated credentials + content: + application/json: + schema: + $ref: >- + #/components/schemas/Polaris_Management_Service_PrincipalWithCredentials + '403': + description: The caller does not have permission to rotate credentials + '404': + description: The principal does not exist + tags: + - polaris-management-service_other + security: + - Polaris_Management_Service_OAuth2: [] + /principals/{principalName}/principal-roles: + parameters: + - name: principalName + in: path + description: The name of the target principal + required: true + schema: + type: string + minLength: 1 + maxLength: 256 + pattern: ^(?!\s*[s|S][y|Y][s|S][t|T][e|E][m|M]\$).*$ + get: + operationId: listPrincipalRolesAssigned + description: List the roles assigned to the principal + responses: + '200': + description: List of roles assigned to this principal + content: + application/json: + schema: + $ref: '#/components/schemas/Polaris_Management_Service_PrincipalRoles' + '403': + description: The caller does not have permission to list roles + '404': + description: The principal or catalog does not exist + tags: + - polaris-management-service_other + security: + - Polaris_Management_Service_OAuth2: [] + put: + operationId: assignPrincipalRole + description: Add a role to the principal + requestBody: + description: The principal role to assign + required: true + content: + application/json: + schema: + $ref: >- + #/components/schemas/Polaris_Management_Service_GrantPrincipalRoleRequest + responses: + '201': + description: Successful response + '403': + description: >- + The caller does not have permission to add assign a role to the + principal + '404': + description: The catalog, the principal, or the role does not exist + tags: + - polaris-management-service_other + security: + - Polaris_Management_Service_OAuth2: [] + /principals/{principalName}/principal-roles/{principalRoleName}: + parameters: + - name: principalName + in: path + description: The name of the target principal + required: true + schema: + type: string + minLength: 1 + maxLength: 256 + pattern: ^(?!\s*[s|S][y|Y][s|S][t|T][e|E][m|M]\$).*$ + - name: principalRoleName + in: path + description: The name of the role + required: true + schema: + type: string + minLength: 1 + maxLength: 256 + pattern: ^(?!\s*[s|S][y|Y][s|S][t|T][e|E][m|M]\$).*$ + delete: + operationId: revokePrincipalRole + description: Remove a role from a catalog principal + responses: + '204': + description: Success, no content + '403': + description: >- + The caller does not have permission to remove a role from the + principal + '404': + description: The catalog or principal does not exist + tags: + - polaris-management-service_other + security: + - Polaris_Management_Service_OAuth2: [] + /principal-roles: + get: + operationId: listPrincipalRoles + description: List the principal roles + responses: + '200': + description: List of principal roles + content: + application/json: + schema: + $ref: '#/components/schemas/Polaris_Management_Service_PrincipalRoles' + '403': + description: The caller does not have permission to list principal roles + '404': + description: The catalog does not exist + tags: + - polaris-management-service_other + security: + - Polaris_Management_Service_OAuth2: [] + post: + operationId: createPrincipalRole + description: Create a principal role + requestBody: + description: The principal to create + required: true + content: + application/json: + schema: + $ref: >- + #/components/schemas/Polaris_Management_Service_CreatePrincipalRoleRequest + responses: + '201': + description: Successful response + '403': + description: The caller does not have permission to add a principal role + tags: + - polaris-management-service_other + security: + - Polaris_Management_Service_OAuth2: [] + /principal-roles/{principalRoleName}: + parameters: + - name: principalRoleName + in: path + description: The principal role name + required: true + schema: + type: string + minLength: 1 + maxLength: 256 + pattern: ^(?!\s*[s|S][y|Y][s|S][t|T][e|E][m|M]\$).*$ + get: + operationId: getPrincipalRole + description: Get the principal role details + responses: + '200': + description: The requested principal role + content: + application/json: + schema: + $ref: '#/components/schemas/Polaris_Management_Service_PrincipalRole' + '403': + description: The caller does not have permission to get principal role details + '404': + description: The principal role does not exist + tags: + - polaris-management-service_other + security: + - Polaris_Management_Service_OAuth2: [] + put: + operationId: updatePrincipalRole + description: Update an existing principalRole + requestBody: + description: The principalRole details to use in the update + required: true + content: + application/json: + schema: + $ref: >- + #/components/schemas/Polaris_Management_Service_UpdatePrincipalRoleRequest + responses: + '200': + description: The updated principal role + content: + application/json: + schema: + $ref: '#/components/schemas/Polaris_Management_Service_PrincipalRole' + '403': + description: The caller does not have permission to update principal role details + '404': + description: The principal role does not exist + '409': + description: >- + The entity version doesn't match the currentEntityVersion; retry + after fetching latest version + tags: + - polaris-management-service_other + security: + - Polaris_Management_Service_OAuth2: [] + delete: + operationId: deletePrincipalRole + description: Remove a principal role from polaris + responses: + '204': + description: Success, no content + '403': + description: The caller does not have permission to delete a principal role + '404': + description: The principal role does not exist + tags: + - polaris-management-service_other + security: + - Polaris_Management_Service_OAuth2: [] + /principal-roles/{principalRoleName}/principals: + parameters: + - name: principalRoleName + in: path + description: The principal role name + required: true + schema: + type: string + minLength: 1 + maxLength: 256 + pattern: ^(?!\s*[s|S][y|Y][s|S][t|T][e|E][m|M]\$).*$ + get: + operationId: listAssigneePrincipalsForPrincipalRole + description: List the Principals to whom the target principal role has been assigned + responses: + '200': + description: >- + List the Principals to whom the target principal role has been + assigned + content: + application/json: + schema: + $ref: '#/components/schemas/Polaris_Management_Service_Principals' + '403': + description: The caller does not have permission to list principals + '404': + description: The principal role does not exist + tags: + - polaris-management-service_other + security: + - Polaris_Management_Service_OAuth2: [] + /principal-roles/{principalRoleName}/catalog-roles/{catalogName}: + parameters: + - name: principalRoleName + in: path + description: The principal role name + required: true + schema: + type: string + minLength: 1 + maxLength: 256 + pattern: ^(?!\s*[s|S][y|Y][s|S][t|T][e|E][m|M]\$).*$ + - name: catalogName + in: path + required: true + description: The name of the catalog where the catalogRoles reside + schema: + type: string + minLength: 1 + maxLength: 256 + pattern: ^(?!\s*[s|S][y|Y][s|S][t|T][e|E][m|M]\$).*$ + get: + operationId: listCatalogRolesForPrincipalRole + description: Get the catalog roles mapped to the principal role + responses: + '200': + description: The list of catalog roles mapped to the principal role + content: + application/json: + schema: + $ref: '#/components/schemas/Polaris_Management_Service_CatalogRoles' + '403': + description: The caller does not have permission to list catalog roles + '404': + description: The principal role does not exist + tags: + - polaris-management-service_other + security: + - Polaris_Management_Service_OAuth2: [] + put: + operationId: assignCatalogRoleToPrincipalRole + description: Assign a catalog role to a principal role + requestBody: + description: The principal to create + required: true + content: + application/json: + schema: + $ref: >- + #/components/schemas/Polaris_Management_Service_GrantCatalogRoleRequest + responses: + '201': + description: Successful response + '403': + description: The caller does not have permission to assign a catalog role + tags: + - polaris-management-service_other + security: + - Polaris_Management_Service_OAuth2: [] + /principal-roles/{principalRoleName}/catalog-roles/{catalogName}/{catalogRoleName}: + parameters: + - name: principalRoleName + in: path + description: The principal role name + required: true + schema: + type: string + minLength: 1 + maxLength: 256 + pattern: ^(?!\s*[s|S][y|Y][s|S][t|T][e|E][m|M]\$).*$ + - name: catalogName + in: path + description: The name of the catalog that contains the role to revoke + required: true + schema: + type: string + minLength: 1 + maxLength: 256 + pattern: ^(?!\s*[s|S][y|Y][s|S][t|T][e|E][m|M]\$).*$ + - name: catalogRoleName + in: path + description: The name of the catalog role that should be revoked + required: true + schema: + type: string + minLength: 1 + maxLength: 256 + pattern: ^(?!\s*[s|S][y|Y][s|S][t|T][e|E][m|M]\$).*$ + delete: + operationId: revokeCatalogRoleFromPrincipalRole + description: Remove a catalog role from a principal role + responses: + '204': + description: Success, no content + '403': + description: The caller does not have permission to revoke a catalog role + '404': + description: The principal role does not exist + tags: + - polaris-management-service_other + security: + - Polaris_Management_Service_OAuth2: [] + /catalogs/{catalogName}/catalog-roles: + parameters: + - name: catalogName + in: path + description: The catalog for which we are reading/updating roles + required: true + schema: + type: string + minLength: 1 + maxLength: 256 + pattern: ^(?!\s*[s|S][y|Y][s|S][t|T][e|E][m|M]\$).*$ + get: + operationId: listCatalogRoles + description: List existing roles in the catalog + responses: + '200': + description: The list of roles that exist in this catalog + content: + application/json: + schema: + $ref: '#/components/schemas/Polaris_Management_Service_CatalogRoles' + tags: + - polaris-management-service_other + security: + - Polaris_Management_Service_OAuth2: [] + post: + operationId: createCatalogRole + description: Create a new role in the catalog + requestBody: + content: + application/json: + schema: + $ref: >- + #/components/schemas/Polaris_Management_Service_CreateCatalogRoleRequest + responses: + '201': + description: Successful response + '403': + description: The principal is not authorized to create roles + '404': + description: The catalog does not exist + tags: + - polaris-management-service_other + security: + - Polaris_Management_Service_OAuth2: [] + /catalogs/{catalogName}/catalog-roles/{catalogRoleName}: + parameters: + - name: catalogName + in: path + description: The catalog for which we are retrieving roles + required: true + schema: + type: string + minLength: 1 + maxLength: 256 + pattern: ^(?!\s*[s|S][y|Y][s|S][t|T][e|E][m|M]\$).*$ + - name: catalogRoleName + in: path + description: The name of the role + required: true + schema: + type: string + minLength: 1 + maxLength: 256 + pattern: ^(?!\s*[s|S][y|Y][s|S][t|T][e|E][m|M]\$).*$ + get: + operationId: getCatalogRole + description: Get the details of an existing role + responses: + '200': + description: The specified role details + content: + application/json: + schema: + $ref: '#/components/schemas/Polaris_Management_Service_CatalogRole' + '403': + description: The principal is not authorized to read role data + '404': + description: The catalog or the role does not exist + tags: + - polaris-management-service_other + security: + - Polaris_Management_Service_OAuth2: [] + put: + operationId: updateCatalogRole + description: Update an existing role in the catalog + requestBody: + content: + application/json: + schema: + $ref: >- + #/components/schemas/Polaris_Management_Service_UpdateCatalogRoleRequest + responses: + '200': + description: The specified role details + content: + application/json: + schema: + $ref: '#/components/schemas/Polaris_Management_Service_CatalogRole' + '403': + description: The principal is not authorized to update roles + '404': + description: The catalog or the role does not exist + '409': + description: >- + The entity version doesn't match the currentEntityVersion; retry + after fetching latest version + tags: + - polaris-management-service_other + security: + - Polaris_Management_Service_OAuth2: [] + delete: + operationId: deleteCatalogRole + description: >- + Delete an existing role from the catalog. All associated grants will + also be deleted + responses: + '204': + description: Success, no content + '403': + description: The principal is not authorized to delete roles + '404': + description: The catalog or the role does not exist + tags: + - polaris-management-service_other + security: + - Polaris_Management_Service_OAuth2: [] + /catalogs/{catalogName}/catalog-roles/{catalogRoleName}/principal-roles: + parameters: + - name: catalogName + in: path + required: true + description: The name of the catalog where the catalog role resides + schema: + type: string + minLength: 1 + maxLength: 256 + pattern: ^(?!\s*[s|S][y|Y][s|S][t|T][e|E][m|M]\$).*$ + - name: catalogRoleName + in: path + required: true + description: The name of the catalog role + schema: + type: string + minLength: 1 + maxLength: 256 + pattern: ^(?!\s*[s|S][y|Y][s|S][t|T][e|E][m|M]\$).*$ + get: + operationId: listAssigneePrincipalRolesForCatalogRole + description: >- + List the PrincipalRoles to which the target catalog role has been + assigned + responses: + '200': + description: >- + List the PrincipalRoles to which the target catalog role has been + assigned + content: + application/json: + schema: + $ref: '#/components/schemas/Polaris_Management_Service_PrincipalRoles' + '403': + description: The caller does not have permission to list principal roles + '404': + description: The catalog or catalog role does not exist + tags: + - polaris-management-service_other + security: + - Polaris_Management_Service_OAuth2: [] + /catalogs/{catalogName}/catalog-roles/{catalogRoleName}/grants: + parameters: + - name: catalogName + in: path + required: true + description: The name of the catalog where the role will receive the grant + schema: + type: string + minLength: 1 + maxLength: 256 + pattern: ^(?!\s*[s|S][y|Y][s|S][t|T][e|E][m|M]\$).*$ + - name: catalogRoleName + in: path + required: true + description: The name of the role receiving the grant (must exist) + schema: + type: string + minLength: 1 + maxLength: 256 + pattern: ^(?!\s*[s|S][y|Y][s|S][t|T][e|E][m|M]\$).*$ + get: + operationId: listGrantsForCatalogRole + description: List the grants the catalog role holds + responses: + '200': + description: List of all grants given to the role in this catalog + content: + application/json: + schema: + $ref: '#/components/schemas/Polaris_Management_Service_GrantResources' + tags: + - polaris-management-service_other + security: + - Polaris_Management_Service_OAuth2: [] + put: + operationId: addGrantToCatalogRole + description: Add a new grant to the catalog role + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Polaris_Management_Service_AddGrantRequest' + responses: + '201': + description: Successful response + '403': + description: The principal is not authorized to create grants + '404': + description: The catalog or the role does not exist + tags: + - polaris-management-service_other + security: + - Polaris_Management_Service_OAuth2: [] + post: + operationId: revokeGrantFromCatalogRole + description: >- + Delete a specific grant from the role. This may be a subset or a + superset of the grants the role has. In case of a subset, the role will + retain the grants not specified. If the `cascade` parameter is true, + grant revocation will have a cascading effect - that is, if a principal + has specific grants on a subresource, and grants are revoked on a parent + resource, the grants present on the subresource will be revoked as well. + By default, this behavior is disabled and grant revocation only affects + the specified resource. + parameters: + - name: cascade + in: query + schema: + type: boolean + default: false + description: If true, the grant revocation cascades to all subresources. + requestBody: + content: + application/json: + schema: + $ref: >- + #/components/schemas/Polaris_Management_Service_RevokeGrantRequest + responses: + '201': + description: Successful response + '403': + description: The principal is not authorized to create grants + '404': + description: The catalog or the role does not exist + tags: + - polaris-management-service_other + security: + - Polaris_Management_Service_OAuth2: [] + /v1/config: + get: + tags: + - Configuration API + summary: List all catalog configuration settings + operationId: getConfig + parameters: + - name: warehouse + in: query + required: false + schema: + type: string + description: Warehouse location or identifier to request from the service + description: >2- + All REST clients should first call this route to get catalog configuration properties from the server to configure the catalog and its HTTP client. Configuration from the server consists of two sets of key/value pairs. + - defaults - properties that should be used as default configuration; + applied before client configuration + + - overrides - properties that should be used to override client + configuration; applied after defaults and client configuration + + + Catalog configuration is constructed by setting the defaults, then + client- provided configuration, and finally overrides. The final + property set is then used to configure the catalog. + + + For example, a default configuration property might set the size of the + client pool, which can be replaced with a client-specific setting. An + override might be used to set the warehouse location, which is stored on + the server rather than in client configuration. + + + Common catalog configuration settings are documented at + https://iceberg.apache.org/docs/latest/configuration/#catalog-properties + responses: + '200': + description: Server specified configuration values. + content: + application/json: + schema: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_CatalogConfig + example: + overrides: + warehouse: s3://bucket/warehouse/ + defaults: + clients: '4' + '400': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_BadRequestErrorResponse + '401': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_UnauthorizedResponse + '403': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_ForbiddenResponse + '419': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_AuthenticationTimeoutResponse + '503': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_ServiceUnavailableResponse + 5XX: + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_ServerErrorResponse + security: + - Apache_Iceberg_REST_Catalog_API_OAuth2: + - catalog + - Apache_Iceberg_REST_Catalog_API_BearerAuth: [] + /v1/oauth/tokens: + post: + tags: + - OAuth2 API + summary: Get a token using an OAuth2 flow + operationId: getToken + description: >- + Exchange credentials for a token using the OAuth2 client credentials + flow or token exchange. + + + This endpoint is used for three purposes - + + 1. To exchange client credentials (client ID and secret) for an access + token This uses the client credentials flow. + + 2. To exchange a client token and an identity token for a more specific + access token This uses the token exchange flow. + + 3. To exchange an access token for one with the same claims and a + refreshed expiration period This uses the token exchange flow. + + + For example, a catalog client may be configured with client credentials + from the OAuth2 Authorization flow. This client would exchange its + client ID and secret for an access token using the client credentials + request with this endpoint (1). Subsequent requests would then use that + access token. + + + Some clients may also handle sessions that have additional user context. + These clients would use the token exchange flow to exchange a user token + (the "subject" token) from the session for a more specific access token + for that user, using the catalog's access token as the "actor" token + (2). The user ID token is the "subject" token and can be any token type + allowed by the OAuth2 token exchange flow, including a unsecured JWT + token with a sub claim. This request should use the catalog's bearer + token in the "Authorization" header. + + + Clients may also use the token exchange flow to refresh a token that is + about to expire by sending a token exchange request (3). The request's + "subject" token should be the expiring token. This request should use + the subject token in the "Authorization" header. + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_OAuthTokenRequest + responses: + '200': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_OAuthTokenResponse + '400': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_OAuthErrorResponse + '401': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_OAuthErrorResponse + 5XX: + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_OAuthErrorResponse + security: + - Apache_Iceberg_REST_Catalog_API_BearerAuth: [] + /v1/{prefix}/namespaces: + parameters: + - $ref: '#/components/parameters/Apache_Iceberg_REST_Catalog_API_prefix' + get: + tags: + - Catalog API + summary: >- + List namespaces, optionally providing a parent namespace to list + underneath + description: >- + List all namespaces at a certain level, optionally starting from a given + parent namespace. If table accounting.tax.paid.info exists, using + 'SELECT NAMESPACE IN accounting' would translate into `GET + /namespaces?parent=accounting` and must return a namespace, + ["accounting", "tax"] only. Using 'SELECT NAMESPACE IN accounting.tax' + would translate into `GET /namespaces?parent=accounting%1Ftax` and must + return a namespace, ["accounting", "tax", "paid"]. If `parent` is not + provided, all top-level namespaces should be listed. + operationId: listNamespaces + parameters: + - $ref: '#/components/parameters/Apache_Iceberg_REST_Catalog_API_page-token' + - $ref: '#/components/parameters/Apache_Iceberg_REST_Catalog_API_page-size' + - name: parent + in: query + description: >- + An optional namespace, underneath which to list namespaces. If not + provided or empty, all top-level namespaces should be listed. If + parent is a multipart namespace, the parts must be separated by the + unit separator (`0x1F`) byte. + required: false + allowEmptyValue: true + schema: + type: string + example: accounting%1Ftax + responses: + '200': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_ListNamespacesResponse + '400': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_BadRequestErrorResponse + '401': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_UnauthorizedResponse + '403': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_ForbiddenResponse + '404': + description: >- + Not Found - Namespace provided in the `parent` query parameter is + not found. + content: + application/json: + schema: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_IcebergErrorResponse + examples: + NoSuchNamespaceExample: + $ref: >- + #/components/examples/Apache_Iceberg_REST_Catalog_API_NoSuchNamespaceError + '419': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_AuthenticationTimeoutResponse + '503': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_ServiceUnavailableResponse + 5XX: + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_ServerErrorResponse + security: + - Apache_Iceberg_REST_Catalog_API_OAuth2: + - catalog + - Apache_Iceberg_REST_Catalog_API_BearerAuth: [] + post: + tags: + - Catalog API + summary: Create a namespace + description: >- + Create a namespace, with an optional set of properties. The server might + also add properties, such as `last_modified_time` etc. + operationId: createNamespace + requestBody: + required: true + content: + application/json: + schema: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_CreateNamespaceRequest + responses: + '200': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_CreateNamespaceResponse + '400': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_BadRequestErrorResponse + '401': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_UnauthorizedResponse + '403': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_ForbiddenResponse + '406': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_UnsupportedOperationResponse + '409': + description: Conflict - The namespace already exists + content: + application/json: + schema: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_IcebergErrorResponse + examples: + NamespaceAlreadyExists: + $ref: >- + #/components/examples/Apache_Iceberg_REST_Catalog_API_NamespaceAlreadyExistsError + '419': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_AuthenticationTimeoutResponse + '503': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_ServiceUnavailableResponse + 5XX: + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_ServerErrorResponse + security: + - Apache_Iceberg_REST_Catalog_API_OAuth2: + - catalog + - Apache_Iceberg_REST_Catalog_API_BearerAuth: [] + /v1/{prefix}/namespaces/{namespace}: + parameters: + - $ref: '#/components/parameters/Apache_Iceberg_REST_Catalog_API_prefix' + - $ref: '#/components/parameters/Apache_Iceberg_REST_Catalog_API_namespace' + get: + tags: + - Catalog API + summary: Load the metadata properties for a namespace + operationId: loadNamespaceMetadata + description: Return all stored metadata properties for a given namespace + responses: + '200': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_GetNamespaceResponse + '400': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_BadRequestErrorResponse + '401': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_UnauthorizedResponse + '403': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_ForbiddenResponse + '404': + description: Not Found - Namespace not found + content: + application/json: + schema: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_IcebergErrorResponse + examples: + NoSuchNamespaceExample: + $ref: >- + #/components/examples/Apache_Iceberg_REST_Catalog_API_NoSuchNamespaceError + '419': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_AuthenticationTimeoutResponse + '503': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_ServiceUnavailableResponse + 5XX: + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_ServerErrorResponse + security: + - Apache_Iceberg_REST_Catalog_API_OAuth2: + - catalog + - Apache_Iceberg_REST_Catalog_API_BearerAuth: [] + head: + tags: + - Catalog API + summary: Check if a namespace exists + operationId: namespaceExists + description: Check if a namespace exists. The response does not contain a body. + responses: + '204': + description: Success, no content + '400': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_BadRequestErrorResponse + '401': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_UnauthorizedResponse + '403': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_ForbiddenResponse + '404': + description: Not Found - Namespace not found + content: + application/json: + schema: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_IcebergErrorResponse + examples: + NoSuchNamespaceExample: + $ref: >- + #/components/examples/Apache_Iceberg_REST_Catalog_API_NoSuchNamespaceError + '419': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_AuthenticationTimeoutResponse + '503': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_ServiceUnavailableResponse + 5XX: + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_ServerErrorResponse + security: + - Apache_Iceberg_REST_Catalog_API_OAuth2: + - catalog + - Apache_Iceberg_REST_Catalog_API_BearerAuth: [] + delete: + tags: + - Catalog API + summary: Drop a namespace from the catalog. Namespace must be empty. + operationId: dropNamespace + responses: + '204': + description: Success, no content + '400': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_BadRequestErrorResponse + '401': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_UnauthorizedResponse + '403': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_ForbiddenResponse + '404': + description: Not Found - Namespace to delete does not exist. + content: + application/json: + schema: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_IcebergErrorResponse + examples: + NoSuchNamespaceExample: + $ref: >- + #/components/examples/Apache_Iceberg_REST_Catalog_API_NoSuchNamespaceError + '419': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_AuthenticationTimeoutResponse + '503': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_ServiceUnavailableResponse + 5XX: + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_ServerErrorResponse + security: + - Apache_Iceberg_REST_Catalog_API_OAuth2: + - catalog + - Apache_Iceberg_REST_Catalog_API_BearerAuth: [] + /v1/{prefix}/namespaces/{namespace}/properties: + parameters: + - $ref: '#/components/parameters/Apache_Iceberg_REST_Catalog_API_prefix' + - $ref: '#/components/parameters/Apache_Iceberg_REST_Catalog_API_namespace' + post: + tags: + - Catalog API + summary: Set or remove properties on a namespace + operationId: updateProperties + description: >- + Set and/or remove properties on a namespace. The request body specifies + a list of properties to remove and a map of key value pairs to update. + + Properties that are not in the request are not modified or removed by + this call. + + Server implementations are not required to support namespace properties. + requestBody: + required: true + content: + application/json: + schema: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_UpdateNamespacePropertiesRequest + examples: + UpdateAndRemoveProperties: + $ref: >- + #/components/examples/Apache_Iceberg_REST_Catalog_API_UpdateAndRemoveNamespacePropertiesRequest + responses: + '200': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_UpdateNamespacePropertiesResponse + '400': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_BadRequestErrorResponse + '401': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_UnauthorizedResponse + '403': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_ForbiddenResponse + '404': + description: Not Found - Namespace not found + content: + application/json: + schema: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_IcebergErrorResponse + examples: + NamespaceNotFound: + $ref: >- + #/components/examples/Apache_Iceberg_REST_Catalog_API_NoSuchNamespaceError + '406': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_UnsupportedOperationResponse + '419': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_AuthenticationTimeoutResponse + '422': + description: >- + Unprocessable Entity - A property key was included in both + `removals` and `updates` + content: + application/json: + schema: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_IcebergErrorResponse + examples: + UnprocessableEntityDuplicateKey: + $ref: >- + #/components/examples/Apache_Iceberg_REST_Catalog_API_UnprocessableEntityDuplicateKey + '503': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_ServiceUnavailableResponse + 5XX: + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_ServerErrorResponse + security: + - Apache_Iceberg_REST_Catalog_API_OAuth2: + - catalog + - Apache_Iceberg_REST_Catalog_API_BearerAuth: [] + /v1/{prefix}/namespaces/{namespace}/tables: + parameters: + - $ref: '#/components/parameters/Apache_Iceberg_REST_Catalog_API_prefix' + - $ref: '#/components/parameters/Apache_Iceberg_REST_Catalog_API_namespace' + get: + tags: + - Catalog API + summary: List all table identifiers underneath a given namespace + description: Return all table identifiers under this namespace + operationId: listTables + parameters: + - $ref: '#/components/parameters/Apache_Iceberg_REST_Catalog_API_page-token' + - $ref: '#/components/parameters/Apache_Iceberg_REST_Catalog_API_page-size' + responses: + '200': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_ListTablesResponse + '400': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_BadRequestErrorResponse + '401': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_UnauthorizedResponse + '403': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_ForbiddenResponse + '404': + description: Not Found - The namespace specified does not exist + content: + application/json: + schema: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_IcebergErrorResponse + examples: + NamespaceNotFound: + $ref: >- + #/components/examples/Apache_Iceberg_REST_Catalog_API_NoSuchNamespaceError + '419': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_AuthenticationTimeoutResponse + '503': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_ServiceUnavailableResponse + 5XX: + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_ServerErrorResponse + security: + - Apache_Iceberg_REST_Catalog_API_OAuth2: + - catalog + - Apache_Iceberg_REST_Catalog_API_BearerAuth: [] + post: + tags: + - Catalog API + summary: Create a table in the given namespace + description: >- + Create a table or start a create transaction, like atomic CTAS. + + + If `stage-create` is false, the table is created immediately. + + + If `stage-create` is true, the table is not created, but table metadata + is initialized and returned. The service should prepare as needed for a + commit to the table commit endpoint to complete the create transaction. + The client uses the returned metadata to begin a transaction. To commit + the transaction, the client sends all create and subsequent changes to + the table commit route. Changes from the table create operation include + changes like AddSchemaUpdate and SetCurrentSchemaUpdate that set the + initial table state. + operationId: createTable + parameters: + - $ref: '#/components/parameters/Apache_Iceberg_REST_Catalog_API_data-access' + requestBody: + required: true + content: + application/json: + schema: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_CreateTableRequest + responses: + '200': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_CreateTableResponse + '400': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_BadRequestErrorResponse + '401': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_UnauthorizedResponse + '403': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_ForbiddenResponse + '404': + description: Not Found - The namespace specified does not exist + content: + application/json: + schema: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_IcebergErrorResponse + examples: + NamespaceNotFound: + $ref: >- + #/components/examples/Apache_Iceberg_REST_Catalog_API_NoSuchNamespaceError + '409': + description: Conflict - The table already exists + content: + application/json: + schema: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_IcebergErrorResponse + examples: + NamespaceAlreadyExists: + $ref: >- + #/components/examples/Apache_Iceberg_REST_Catalog_API_TableAlreadyExistsError + '419': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_AuthenticationTimeoutResponse + '503': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_ServiceUnavailableResponse + 5XX: + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_ServerErrorResponse + security: + - Apache_Iceberg_REST_Catalog_API_OAuth2: + - catalog + - Apache_Iceberg_REST_Catalog_API_BearerAuth: [] + /v1/{prefix}/namespaces/{namespace}/register: + parameters: + - $ref: '#/components/parameters/Apache_Iceberg_REST_Catalog_API_prefix' + - $ref: '#/components/parameters/Apache_Iceberg_REST_Catalog_API_namespace' + post: + tags: + - Catalog API + summary: >- + Register a table in the given namespace using given metadata file + location + description: Register a table using given metadata file location. + operationId: registerTable + requestBody: + required: true + content: + application/json: + schema: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_RegisterTableRequest + responses: + '200': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_LoadTableResponse + '400': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_BadRequestErrorResponse + '401': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_UnauthorizedResponse + '403': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_ForbiddenResponse + '404': + description: Not Found - The namespace specified does not exist + content: + application/json: + schema: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_IcebergErrorResponse + examples: + NamespaceNotFound: + $ref: >- + #/components/examples/Apache_Iceberg_REST_Catalog_API_NoSuchNamespaceError + '409': + description: Conflict - The table already exists + content: + application/json: + schema: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_IcebergErrorResponse + examples: + NamespaceAlreadyExists: + $ref: >- + #/components/examples/Apache_Iceberg_REST_Catalog_API_TableAlreadyExistsError + '419': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_AuthenticationTimeoutResponse + '503': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_ServiceUnavailableResponse + 5XX: + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_ServerErrorResponse + security: + - Apache_Iceberg_REST_Catalog_API_OAuth2: + - catalog + - Apache_Iceberg_REST_Catalog_API_BearerAuth: [] + /v1/{prefix}/namespaces/{namespace}/tables/{table}: + parameters: + - $ref: '#/components/parameters/Apache_Iceberg_REST_Catalog_API_prefix' + - $ref: '#/components/parameters/Apache_Iceberg_REST_Catalog_API_namespace' + - $ref: '#/components/parameters/Apache_Iceberg_REST_Catalog_API_table' + get: + tags: + - Catalog API + summary: Load a table from the catalog + operationId: loadTable + description: >- + Load a table from the catalog. + + + The response contains both configuration and table metadata. The + configuration, if non-empty is used as additional configuration for the + table that overrides catalog configuration. For example, this + configuration may change the FileIO implementation to be used for the + table. + + + The response also contains the table's full metadata, matching the table + metadata JSON file. + + + The catalog configuration may contain credentials that should be used + for subsequent requests for the table. The configuration key "token" is + used to pass an access token to be used as a bearer token for table + requests. Otherwise, a token may be passed using a RFC 8693 token type + as a configuration key. For example, + "urn:ietf:params:oauth:token-type:jwt=". + parameters: + - $ref: '#/components/parameters/Apache_Iceberg_REST_Catalog_API_data-access' + - in: query + name: snapshots + description: >- + The snapshots to return in the body of the metadata. Setting the + value to `all` would return the full set of snapshots currently + valid for the table. Setting the value to `refs` would load all + snapshots referenced by branches or tags. + + Default if no param is provided is `all`. + required: false + schema: + type: string + enum: + - all + - refs + responses: + '200': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_LoadTableResponse + '400': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_BadRequestErrorResponse + '401': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_UnauthorizedResponse + '403': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_ForbiddenResponse + '404': + description: Not Found - NoSuchTableException, table to load does not exist + content: + application/json: + schema: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_IcebergErrorResponse + examples: + TableToLoadDoesNotExist: + $ref: >- + #/components/examples/Apache_Iceberg_REST_Catalog_API_NoSuchTableError + '419': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_AuthenticationTimeoutResponse + '503': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_ServiceUnavailableResponse + 5XX: + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_ServerErrorResponse + security: + - Apache_Iceberg_REST_Catalog_API_OAuth2: + - catalog + - Apache_Iceberg_REST_Catalog_API_BearerAuth: [] + post: + tags: + - Catalog API + summary: Commit updates to a table + operationId: updateTable + description: >- + Commit updates to a table. + + + Commits have two parts, requirements and updates. Requirements are + assertions that will be validated before attempting to make and commit + changes. For example, `assert-ref-snapshot-id` will check that a named + ref's snapshot ID has a certain value. + + + Updates are changes to make to table metadata. For example, after + asserting that the current main ref is at the expected snapshot, a + commit may add a new child snapshot and set the ref to the new snapshot + id. + + + Create table transactions that are started by createTable with + `stage-create` set to true are committed using this route. Transactions + should include all changes to the table, including table initialization, + like AddSchemaUpdate and SetCurrentSchemaUpdate. The `assert-create` + requirement is used to ensure that the table was not created + concurrently. + requestBody: + required: true + content: + application/json: + schema: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_CommitTableRequest + responses: + '200': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_CommitTableResponse + '400': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_BadRequestErrorResponse + '401': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_UnauthorizedResponse + '403': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_ForbiddenResponse + '404': + description: Not Found - NoSuchTableException, table to load does not exist + content: + application/json: + schema: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_IcebergErrorResponse + examples: + TableToUpdateDoesNotExist: + $ref: >- + #/components/examples/Apache_Iceberg_REST_Catalog_API_NoSuchTableError + '409': + description: >- + Conflict - CommitFailedException, one or more requirements failed. + The client may retry. + content: + application/json: + schema: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_IcebergErrorResponse + '419': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_AuthenticationTimeoutResponse + '500': + description: >- + An unknown server-side problem occurred; the commit state is + unknown. + content: + application/json: + schema: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_IcebergErrorResponse + example: + error: + message: Internal Server Error + type: CommitStateUnknownException + code: 500 + '502': + description: >- + A gateway or proxy received an invalid response from the upstream + server; the commit state is unknown. + content: + application/json: + schema: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_IcebergErrorResponse + example: + error: + message: Invalid response from the upstream server + type: CommitStateUnknownException + code: 502 + '503': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_ServiceUnavailableResponse + '504': + description: A server-side gateway timeout occurred; the commit state is unknown. + content: + application/json: + schema: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_IcebergErrorResponse + example: + error: + message: Gateway timed out during commit + type: CommitStateUnknownException + code: 504 + 5XX: + description: A server-side problem that might not be addressable on the client. + content: + application/json: + schema: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_IcebergErrorResponse + example: + error: + message: Bad Gateway + type: InternalServerError + code: 502 + security: + - Apache_Iceberg_REST_Catalog_API_OAuth2: + - catalog + - Apache_Iceberg_REST_Catalog_API_BearerAuth: [] + delete: + tags: + - Catalog API + summary: Drop a table from the catalog + operationId: dropTable + description: Remove a table from the catalog + parameters: + - name: purgeRequested + in: query + required: false + description: >- + Whether the user requested to purge the underlying table's data and + metadata + schema: + type: boolean + default: false + responses: + '204': + description: Success, no content + '400': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_BadRequestErrorResponse + '401': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_UnauthorizedResponse + '403': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_ForbiddenResponse + '404': + description: Not Found - NoSuchTableException, Table to drop does not exist + content: + application/json: + schema: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_IcebergErrorResponse + examples: + TableToDeleteDoesNotExist: + $ref: >- + #/components/examples/Apache_Iceberg_REST_Catalog_API_NoSuchTableError + '419': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_AuthenticationTimeoutResponse + '503': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_ServiceUnavailableResponse + 5XX: + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_ServerErrorResponse + security: + - Apache_Iceberg_REST_Catalog_API_OAuth2: + - catalog + - Apache_Iceberg_REST_Catalog_API_BearerAuth: [] + head: + tags: + - Catalog API + summary: Check if a table exists + operationId: tableExists + description: >- + Check if a table exists within a given namespace. The response does not + contain a body. + responses: + '204': + description: Success, no content + '400': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_BadRequestErrorResponse + '401': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_UnauthorizedResponse + '403': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_ForbiddenResponse + '404': + description: Not Found - NoSuchTableException, Table not found + content: + application/json: + schema: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_IcebergErrorResponse + examples: + TableToLoadDoesNotExist: + $ref: >- + #/components/examples/Apache_Iceberg_REST_Catalog_API_NoSuchTableError + '419': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_AuthenticationTimeoutResponse + '503': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_ServiceUnavailableResponse + 5XX: + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_ServerErrorResponse + security: + - Apache_Iceberg_REST_Catalog_API_OAuth2: + - catalog + - Apache_Iceberg_REST_Catalog_API_BearerAuth: [] + /v1/{prefix}/tables/rename: + parameters: + - $ref: '#/components/parameters/Apache_Iceberg_REST_Catalog_API_prefix' + post: + tags: + - Catalog API + summary: Rename a table from its current name to a new name + description: >- + Rename a table from one identifier to another. It's valid to move a + table across namespaces, but the server implementation is not required + to support it. + operationId: renameTable + requestBody: + description: >- + Current table identifier to rename and new table identifier to rename + to + content: + application/json: + schema: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_RenameTableRequest + examples: + RenameTableSameNamespace: + $ref: >- + #/components/examples/Apache_Iceberg_REST_Catalog_API_RenameTableSameNamespace + required: true + responses: + '204': + description: Success, no content + '400': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_BadRequestErrorResponse + '401': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_UnauthorizedResponse + '403': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_ForbiddenResponse + '404': + description: >- + Not Found - NoSuchTableException, Table to rename does not exist - + NoSuchNamespaceException, The target namespace of the new table + identifier does not exist + content: + application/json: + schema: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_IcebergErrorResponse + examples: + TableToRenameDoesNotExist: + $ref: >- + #/components/examples/Apache_Iceberg_REST_Catalog_API_NoSuchTableError + NamespaceToRenameToDoesNotExist: + $ref: >- + #/components/examples/Apache_Iceberg_REST_Catalog_API_NoSuchNamespaceError + '406': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_UnsupportedOperationResponse + '409': + description: >- + Conflict - The target identifier to rename to already exists as a + table or view + content: + application/json: + schema: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_IcebergErrorResponse + example: + summary: The requested table identifier already exists + value: + error: + message: The given table already exists + type: AlreadyExistsException + code: 409 + '419': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_AuthenticationTimeoutResponse + '503': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_ServiceUnavailableResponse + 5XX: + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_ServerErrorResponse + security: + - Apache_Iceberg_REST_Catalog_API_OAuth2: + - catalog + - Apache_Iceberg_REST_Catalog_API_BearerAuth: [] + /v1/{prefix}/namespaces/{namespace}/tables/{table}/metrics: + parameters: + - $ref: '#/components/parameters/Apache_Iceberg_REST_Catalog_API_prefix' + - $ref: '#/components/parameters/Apache_Iceberg_REST_Catalog_API_namespace' + - $ref: '#/components/parameters/Apache_Iceberg_REST_Catalog_API_table' + post: + tags: + - Catalog API + summary: Send a metrics report to this endpoint to be processed by the backend + operationId: reportMetrics + requestBody: + description: The request containing the metrics report to be sent + content: + application/json: + schema: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_ReportMetricsRequest + required: true + responses: + '204': + description: Success, no content + '400': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_BadRequestErrorResponse + '401': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_UnauthorizedResponse + '403': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_ForbiddenResponse + '404': + description: Not Found - NoSuchTableException, table to load does not exist + content: + application/json: + schema: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_IcebergErrorResponse + examples: + TableToLoadDoesNotExist: + $ref: >- + #/components/examples/Apache_Iceberg_REST_Catalog_API_NoSuchTableError + '419': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_AuthenticationTimeoutResponse + '503': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_ServiceUnavailableResponse + 5XX: + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_ServerErrorResponse + security: + - Apache_Iceberg_REST_Catalog_API_OAuth2: + - catalog + - Apache_Iceberg_REST_Catalog_API_BearerAuth: [] + /v1/{prefix}/namespaces/{namespace}/tables/{table}/notifications: + parameters: + - $ref: '#/components/parameters/Apache_Iceberg_REST_Catalog_API_prefix' + - $ref: '#/components/parameters/Apache_Iceberg_REST_Catalog_API_namespace' + - $ref: '#/components/parameters/Apache_Iceberg_REST_Catalog_API_table' + post: + tags: + - Catalog API + summary: Sends a notification to the table + operationId: sendNotification + requestBody: + description: The request containing the notification to be sent + content: + application/json: + schema: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_NotificationRequest + required: true + responses: + '204': + description: Success, no content + '400': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_BadRequestErrorResponse + '401': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_UnauthorizedResponse + '403': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_ForbiddenResponse + '404': + description: Not Found - NoSuchTableException, table to load does not exist + content: + application/json: + schema: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_IcebergErrorResponse + examples: + TableToLoadDoesNotExist: + $ref: >- + #/components/examples/Apache_Iceberg_REST_Catalog_API_NoSuchTableError + '419': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_AuthenticationTimeoutResponse + '503': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_ServiceUnavailableResponse + 5XX: + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_ServerErrorResponse + security: + - Apache_Iceberg_REST_Catalog_API_OAuth2: + - catalog + - Apache_Iceberg_REST_Catalog_API_BearerAuth: [] + /v1/{prefix}/transactions/commit: + parameters: + - $ref: '#/components/parameters/Apache_Iceberg_REST_Catalog_API_prefix' + post: + tags: + - Catalog API + summary: Commit updates to multiple tables in an atomic operation + operationId: commitTransaction + requestBody: + description: >- + Commit updates to multiple tables in an atomic operation + + + A commit for a single table consists of a table identifier with + requirements and updates. Requirements are assertions that will be + validated before attempting to make and commit changes. For example, + `assert-ref-snapshot-id` will check that a named ref's snapshot ID has + a certain value. + + + Updates are changes to make to table metadata. For example, after + asserting that the current main ref is at the expected snapshot, a + commit may add a new child snapshot and set the ref to the new + snapshot id. + content: + application/json: + schema: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_CommitTransactionRequest + required: true + responses: + '204': + description: Success, no content + '400': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_BadRequestErrorResponse + '401': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_UnauthorizedResponse + '403': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_ForbiddenResponse + '404': + description: Not Found - NoSuchTableException, table to load does not exist + content: + application/json: + schema: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_IcebergErrorResponse + examples: + TableToUpdateDoesNotExist: + $ref: >- + #/components/examples/Apache_Iceberg_REST_Catalog_API_NoSuchTableError + '409': + description: >- + Conflict - CommitFailedException, one or more requirements failed. + The client may retry. + content: + application/json: + schema: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_IcebergErrorResponse + '419': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_AuthenticationTimeoutResponse + '500': + description: >- + An unknown server-side problem occurred; the commit state is + unknown. + content: + application/json: + schema: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_IcebergErrorResponse + example: + error: + message: Internal Server Error + type: CommitStateUnknownException + code: 500 + '502': + description: >- + A gateway or proxy received an invalid response from the upstream + server; the commit state is unknown. + content: + application/json: + schema: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_IcebergErrorResponse + example: + error: + message: Invalid response from the upstream server + type: CommitStateUnknownException + code: 502 + '503': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_ServiceUnavailableResponse + '504': + description: A server-side gateway timeout occurred; the commit state is unknown. + content: + application/json: + schema: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_IcebergErrorResponse + example: + error: + message: Gateway timed out during commit + type: CommitStateUnknownException + code: 504 + 5XX: + description: A server-side problem that might not be addressable on the client. + content: + application/json: + schema: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_IcebergErrorResponse + example: + error: + message: Bad Gateway + type: InternalServerError + code: 502 + security: + - Apache_Iceberg_REST_Catalog_API_OAuth2: + - catalog + - Apache_Iceberg_REST_Catalog_API_BearerAuth: [] + /v1/{prefix}/namespaces/{namespace}/views: + parameters: + - $ref: '#/components/parameters/Apache_Iceberg_REST_Catalog_API_prefix' + - $ref: '#/components/parameters/Apache_Iceberg_REST_Catalog_API_namespace' + get: + tags: + - Catalog API + summary: List all view identifiers underneath a given namespace + description: Return all view identifiers under this namespace + operationId: listViews + parameters: + - $ref: '#/components/parameters/Apache_Iceberg_REST_Catalog_API_page-token' + - $ref: '#/components/parameters/Apache_Iceberg_REST_Catalog_API_page-size' + responses: + '200': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_ListTablesResponse + '400': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_BadRequestErrorResponse + '401': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_UnauthorizedResponse + '403': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_ForbiddenResponse + '404': + description: Not Found - The namespace specified does not exist + content: + application/json: + schema: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_ErrorModel + examples: + NamespaceNotFound: + $ref: >- + #/components/examples/Apache_Iceberg_REST_Catalog_API_NoSuchNamespaceError + '419': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_AuthenticationTimeoutResponse + '503': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_ServiceUnavailableResponse + 5XX: + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_ServerErrorResponse + security: + - Apache_Iceberg_REST_Catalog_API_OAuth2: + - catalog + - Apache_Iceberg_REST_Catalog_API_BearerAuth: [] + post: + tags: + - Catalog API + summary: Create a view in the given namespace + description: Create a view in the given namespace. + operationId: createView + requestBody: + required: true + content: + application/json: + schema: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_CreateViewRequest + responses: + '200': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_LoadViewResponse + '400': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_BadRequestErrorResponse + '401': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_UnauthorizedResponse + '403': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_ForbiddenResponse + '404': + description: Not Found - The namespace specified does not exist + content: + application/json: + schema: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_ErrorModel + examples: + NamespaceNotFound: + $ref: >- + #/components/examples/Apache_Iceberg_REST_Catalog_API_NoSuchNamespaceError + '409': + description: Conflict - The view already exists + content: + application/json: + schema: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_ErrorModel + examples: + NamespaceAlreadyExists: + $ref: >- + #/components/examples/Apache_Iceberg_REST_Catalog_API_ViewAlreadyExistsError + '419': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_AuthenticationTimeoutResponse + '503': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_ServiceUnavailableResponse + 5XX: + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_ServerErrorResponse + security: + - Apache_Iceberg_REST_Catalog_API_OAuth2: + - catalog + - Apache_Iceberg_REST_Catalog_API_BearerAuth: [] + /v1/{prefix}/namespaces/{namespace}/views/{view}: + parameters: + - $ref: '#/components/parameters/Apache_Iceberg_REST_Catalog_API_prefix' + - $ref: '#/components/parameters/Apache_Iceberg_REST_Catalog_API_namespace' + - $ref: '#/components/parameters/Apache_Iceberg_REST_Catalog_API_view' + get: + tags: + - Catalog API + summary: Load a view from the catalog + operationId: loadView + description: >- + Load a view from the catalog. + + + The response contains both configuration and view metadata. The + configuration, if non-empty is used as additional configuration for the + view that overrides catalog configuration. + + + The response also contains the view's full metadata, matching the view + metadata JSON file. + + + The catalog configuration may contain credentials that should be used + for subsequent requests for the view. The configuration key "token" is + used to pass an access token to be used as a bearer token for view + requests. Otherwise, a token may be passed using a RFC 8693 token type + as a configuration key. For example, + "urn:ietf:params:oauth:token-type:jwt=". + responses: + '200': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_LoadViewResponse + '400': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_BadRequestErrorResponse + '401': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_UnauthorizedResponse + '403': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_ForbiddenResponse + '404': + description: Not Found - NoSuchViewException, view to load does not exist + content: + application/json: + schema: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_ErrorModel + examples: + ViewToLoadDoesNotExist: + $ref: >- + #/components/examples/Apache_Iceberg_REST_Catalog_API_NoSuchViewError + '419': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_AuthenticationTimeoutResponse + '503': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_ServiceUnavailableResponse + 5XX: + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_ServerErrorResponse + security: + - Apache_Iceberg_REST_Catalog_API_OAuth2: + - catalog + - Apache_Iceberg_REST_Catalog_API_BearerAuth: [] + post: + tags: + - Catalog API + summary: Replace a view + operationId: replaceView + description: Commit updates to a view. + requestBody: + required: true + content: + application/json: + schema: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_CommitViewRequest + responses: + '200': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_LoadViewResponse + '400': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_BadRequestErrorResponse + '401': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_UnauthorizedResponse + '403': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_ForbiddenResponse + '404': + description: Not Found - NoSuchViewException, view to load does not exist + content: + application/json: + schema: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_ErrorModel + examples: + ViewToUpdateDoesNotExist: + $ref: >- + #/components/examples/Apache_Iceberg_REST_Catalog_API_NoSuchViewError + '409': + description: Conflict - CommitFailedException. The client may retry. + content: + application/json: + schema: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_ErrorModel + '419': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_AuthenticationTimeoutResponse + '500': + description: >- + An unknown server-side problem occurred; the commit state is + unknown. + content: + application/json: + schema: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_ErrorModel + example: + error: + message: Internal Server Error + type: CommitStateUnknownException + code: 500 + '502': + description: >- + A gateway or proxy received an invalid response from the upstream + server; the commit state is unknown. + content: + application/json: + schema: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_ErrorModel + example: + error: + message: Invalid response from the upstream server + type: CommitStateUnknownException + code: 502 + '503': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_ServiceUnavailableResponse + '504': + description: A server-side gateway timeout occurred; the commit state is unknown. + content: + application/json: + schema: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_ErrorModel + example: + error: + message: Gateway timed out during commit + type: CommitStateUnknownException + code: 504 + 5XX: + description: A server-side problem that might not be addressable on the client. + content: + application/json: + schema: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_ErrorModel + example: + error: + message: Bad Gateway + type: InternalServerError + code: 502 + security: + - Apache_Iceberg_REST_Catalog_API_OAuth2: + - catalog + - Apache_Iceberg_REST_Catalog_API_BearerAuth: [] + delete: + tags: + - Catalog API + summary: Drop a view from the catalog + operationId: dropView + description: Remove a view from the catalog + responses: + '204': + description: Success, no content + '400': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_BadRequestErrorResponse + '401': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_UnauthorizedResponse + '403': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_ForbiddenResponse + '404': + description: Not Found - NoSuchViewException, view to drop does not exist + content: + application/json: + schema: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_ErrorModel + examples: + ViewToDeleteDoesNotExist: + $ref: >- + #/components/examples/Apache_Iceberg_REST_Catalog_API_NoSuchViewError + '419': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_AuthenticationTimeoutResponse + '503': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_ServiceUnavailableResponse + 5XX: + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_ServerErrorResponse + security: + - Apache_Iceberg_REST_Catalog_API_OAuth2: + - catalog + - Apache_Iceberg_REST_Catalog_API_BearerAuth: [] + head: + tags: + - Catalog API + summary: Check if a view exists + operationId: viewExists + description: >- + Check if a view exists within a given namespace. This request does not + return a response body. + responses: + '204': + description: Success, no content + '400': + description: Bad Request + '401': + description: Unauthorized + '404': + description: Not Found + '419': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_AuthenticationTimeoutResponse + '503': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_ServiceUnavailableResponse + 5XX: + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_ServerErrorResponse + security: + - Apache_Iceberg_REST_Catalog_API_OAuth2: + - catalog + - Apache_Iceberg_REST_Catalog_API_BearerAuth: [] + /v1/{prefix}/views/rename: + parameters: + - $ref: '#/components/parameters/Apache_Iceberg_REST_Catalog_API_prefix' + post: + tags: + - Catalog API + summary: Rename a view from its current name to a new name + description: >- + Rename a view from one identifier to another. It's valid to move a view + across namespaces, but the server implementation is not required to + support it. + operationId: renameView + requestBody: + description: Current view identifier to rename and new view identifier to rename to + content: + application/json: + schema: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_RenameTableRequest + examples: + RenameViewSameNamespace: + $ref: >- + #/components/examples/Apache_Iceberg_REST_Catalog_API_RenameViewSameNamespace + required: true + responses: + '204': + description: Success, no content + '400': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_BadRequestErrorResponse + '401': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_UnauthorizedResponse + '403': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_ForbiddenResponse + '404': + description: >- + Not Found - NoSuchViewException, view to rename does not exist - + NoSuchNamespaceException, The target namespace of the new identifier + does not exist + content: + application/json: + schema: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_ErrorModel + examples: + ViewToRenameDoesNotExist: + $ref: >- + #/components/examples/Apache_Iceberg_REST_Catalog_API_NoSuchViewError + NamespaceToRenameToDoesNotExist: + $ref: >- + #/components/examples/Apache_Iceberg_REST_Catalog_API_NoSuchNamespaceError + '406': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_UnsupportedOperationResponse + '409': + description: >- + Conflict - The target identifier to rename to already exists as a + table or view + content: + application/json: + schema: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_ErrorModel + example: + summary: The requested view identifier already exists + value: + error: + message: The given view already exists + type: AlreadyExistsException + code: 409 + '419': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_AuthenticationTimeoutResponse + '503': + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_ServiceUnavailableResponse + 5XX: + $ref: >- + #/components/responses/Apache_Iceberg_REST_Catalog_API_ServerErrorResponse + security: + - Apache_Iceberg_REST_Catalog_API_OAuth2: + - catalog + - Apache_Iceberg_REST_Catalog_API_BearerAuth: [] +components: + securitySchemes: + Polaris_Management_Service_OAuth2: + type: oauth2 + description: Uses OAuth 2 with client credentials flow + flows: + implicit: + authorizationUrl: '{scheme}://{host}/api/v1/oauth/tokens' + scopes: {} + Apache_Iceberg_REST_Catalog_API_OAuth2: + type: oauth2 + description: >- + This scheme is used for OAuth2 authorization. + + + For unauthorized requests, services should return an appropriate 401 or + 403 response. Implementations must not return altered success (200) + responses when a request is unauthenticated or unauthorized. + + If a separate authorization server is used, substitute the tokenUrl with + the full token path of the external authorization server, and use the + resulting token to access the resources defined in the spec. + flows: + clientCredentials: + tokenUrl: /v1/oauth/tokens + scopes: + catalog: Allows interacting with the Config and Catalog APIs + Apache_Iceberg_REST_Catalog_API_BearerAuth: + type: http + scheme: bearer + schemas: + Polaris_Management_Service_Catalogs: + type: object + description: A list of Catalog objects + properties: + catalogs: + type: array + items: + $ref: '#/components/schemas/Polaris_Management_Service_Catalog' + required: + - catalogs + Polaris_Management_Service_CreateCatalogRequest: + type: object + description: Request to create a new catalog + properties: + catalog: + $ref: '#/components/schemas/Polaris_Management_Service_Catalog' + required: + - catalog + Polaris_Management_Service_Catalog: + type: object + description: >- + A catalog object. A catalog may be internal or external. Internal + catalogs are managed entirely by an external catalog interface. Third + party catalogs may be other Iceberg REST implementations or other + services with their own proprietary APIs + properties: + type: + type: string + enum: + - INTERNAL + - EXTERNAL + description: the type of catalog - internal or external + default: INTERNAL + name: + type: string + minLength: 1 + maxLength: 256 + pattern: ^(?!\s*[s|S][y|Y][s|S][t|T][e|E][m|M]\$).*$ + description: The name of the catalog + properties: + type: object + properties: + default-base-location: + type: string + additionalProperties: + type: string + required: + - default-base-location + createTimestamp: + type: integer + format: int64 + description: >- + The creation time represented as unix epoch timestamp in + milliseconds + lastUpdateTimestamp: + type: integer + format: int64 + description: >- + The last update time represented as unix epoch timestamp in + milliseconds + entityVersion: + type: integer + description: >- + The version of the catalog object used to determine if the catalog + metadata has changed + storageConfigInfo: + $ref: '#/components/schemas/Polaris_Management_Service_StorageConfigInfo' + required: + - name + - type + - storageConfigInfo + - properties + discriminator: + propertyName: type + mapping: + INTERNAL: '#/components/schemas/Polaris_Management_Service_PolarisCatalog' + EXTERNAL: '#/components/schemas/Polaris_Management_Service_ExternalCatalog' + Polaris_Management_Service_PolarisCatalog: + type: object + allOf: + - $ref: '#/components/schemas/Polaris_Management_Service_Catalog' + description: >- + The base catalog type - this contains all the fields necessary to + construct an INTERNAL catalog + Polaris_Management_Service_ExternalCatalog: + description: An externally managed catalog + type: object + allOf: + - $ref: '#/components/schemas/Polaris_Management_Service_Catalog' + - type: object + properties: + remoteUrl: + type: string + description: URL to the remote catalog API + Polaris_Management_Service_StorageConfigInfo: + type: object + description: A storage configuration used by catalogs + properties: + storageType: + type: string + enum: + - S3 + - GCS + - AZURE + - FILE + description: >- + The cloud provider type this storage is built on. FILE is supported + for testing purposes only + allowedLocations: + type: array + items: + type: string + example: >- + For AWS [s3://bucketname/prefix/], for AZURE + [abfss://container@storageaccount.blob.core.windows.net/prefix/], + for GCP [gs://bucketname/prefix/] + required: + - storageType + discriminator: + propertyName: storageType + mapping: + S3: '#/components/schemas/Polaris_Management_Service_AwsStorageConfigInfo' + AZURE: >- + #/components/schemas/Polaris_Management_Service_AzureStorageConfigInfo + GCS: '#/components/schemas/Polaris_Management_Service_GcpStorageConfigInfo' + FILE: >- + #/components/schemas/Polaris_Management_Service_FileStorageConfigInfo + Polaris_Management_Service_AwsStorageConfigInfo: + type: object + description: aws storage configuration info + allOf: + - $ref: '#/components/schemas/Polaris_Management_Service_StorageConfigInfo' + properties: + roleArn: + type: string + description: the aws role arn that grants privileges on the S3 buckets + example: arn:aws:iam::123456789001:principal/abc1-b-self1234 + externalId: + type: string + description: >- + an optional external id used to establish a trust relationship with + AWS in the trust policy + userArn: + type: string + description: the aws user arn used to assume the aws role + example: arn:aws:iam::123456789001:user/abc1-b-self1234 + required: + - roleArn + Polaris_Management_Service_AzureStorageConfigInfo: + type: object + description: azure storage configuration info + allOf: + - $ref: '#/components/schemas/Polaris_Management_Service_StorageConfigInfo' + properties: + tenantId: + type: string + description: the tenant id that the storage accounts belong to + multiTenantAppName: + type: string + description: the name of the azure client application + consentUrl: + type: string + description: URL to the Azure permissions request page + required: + - tenantId + Polaris_Management_Service_GcpStorageConfigInfo: + type: object + description: gcp storage configuration info + allOf: + - $ref: '#/components/schemas/Polaris_Management_Service_StorageConfigInfo' + properties: + gcsServiceAccount: + type: string + description: a Google cloud storage service account + Polaris_Management_Service_FileStorageConfigInfo: + type: object + description: gcp storage configuration info + allOf: + - $ref: '#/components/schemas/Polaris_Management_Service_StorageConfigInfo' + Polaris_Management_Service_UpdateCatalogRequest: + description: Updates to apply to a Catalog + type: object + properties: + currentEntityVersion: + type: integer + description: >- + The version of the object onto which this update is applied; if the + object changed, the update will fail and the caller should retry + after fetching the latest version. + properties: + type: object + additionalProperties: + type: string + storageConfigInfo: + $ref: '#/components/schemas/Polaris_Management_Service_StorageConfigInfo' + Polaris_Management_Service_Principals: + description: A list of Principals + type: object + properties: + principals: + type: array + items: + $ref: '#/components/schemas/Polaris_Management_Service_Principal' + required: + - principals + Polaris_Management_Service_PrincipalWithCredentials: + description: >- + A user with its client id and secret. This type is returned when a new + principal is created or when its credentials are rotated + type: object + properties: + principal: + $ref: '#/components/schemas/Polaris_Management_Service_Principal' + credentials: + type: object + properties: + clientId: + type: string + clientSecret: + type: string + required: + - principal + - credentials + Polaris_Management_Service_CreatePrincipalRequest: + type: object + properties: + principal: + $ref: '#/components/schemas/Polaris_Management_Service_Principal' + credentialRotationRequired: + type: boolean + description: >- + If true, the initial credentials can only be used to call + rotateCredentials + Polaris_Management_Service_Principal: + description: A Polaris principal. + type: object + properties: + name: + type: string + minLength: 1 + maxLength: 256 + pattern: ^(?!\s*[s|S][y|Y][s|S][t|T][e|E][m|M]\$).*$ + clientId: + type: string + description: >- + The output-only OAuth clientId associated with this principal if + applicable + properties: + type: object + additionalProperties: + type: string + createTimestamp: + type: integer + format: int64 + lastUpdateTimestamp: + type: integer + format: int64 + entityVersion: + type: integer + description: >- + The version of the principal object used to determine if the + principal metadata has changed + required: + - name + Polaris_Management_Service_UpdatePrincipalRequest: + description: Updates to apply to a Principal + type: object + properties: + currentEntityVersion: + type: integer + description: >- + The version of the object onto which this update is applied; if the + object changed, the update will fail and the caller should retry + after fetching the latest version. + properties: + type: object + additionalProperties: + type: string + required: + - currentEntityVersion + - properties + Polaris_Management_Service_PrincipalRoles: + type: object + properties: + roles: + type: array + items: + $ref: '#/components/schemas/Polaris_Management_Service_PrincipalRole' + required: + - roles + Polaris_Management_Service_GrantPrincipalRoleRequest: + type: object + properties: + principalRole: + $ref: '#/components/schemas/Polaris_Management_Service_PrincipalRole' + Polaris_Management_Service_CreatePrincipalRoleRequest: + type: object + properties: + principalRole: + $ref: '#/components/schemas/Polaris_Management_Service_PrincipalRole' + Polaris_Management_Service_PrincipalRole: + type: object + properties: + name: + type: string + minLength: 1 + maxLength: 256 + pattern: ^(?!\s*[s|S][y|Y][s|S][t|T][e|E][m|M]\$).*$ + description: The name of the role + properties: + type: object + additionalProperties: + type: string + createTimestamp: + type: integer + format: int64 + lastUpdateTimestamp: + type: integer + format: int64 + entityVersion: + type: integer + description: >- + The version of the principal role object used to determine if the + principal role metadata has changed + required: + - name + Polaris_Management_Service_UpdatePrincipalRoleRequest: + description: Updates to apply to a Principal Role + type: object + properties: + currentEntityVersion: + type: integer + description: >- + The version of the object onto which this update is applied; if the + object changed, the update will fail and the caller should retry + after fetching the latest version. + properties: + type: object + additionalProperties: + type: string + required: + - currentEntityVersion + - properties + Polaris_Management_Service_CatalogRoles: + type: object + properties: + roles: + type: array + items: + $ref: '#/components/schemas/Polaris_Management_Service_CatalogRole' + description: The list of catalog roles + required: + - roles + Polaris_Management_Service_GrantCatalogRoleRequest: + type: object + properties: + catalogRole: + $ref: '#/components/schemas/Polaris_Management_Service_CatalogRole' + Polaris_Management_Service_CreateCatalogRoleRequest: + type: object + properties: + catalogRole: + $ref: '#/components/schemas/Polaris_Management_Service_CatalogRole' + Polaris_Management_Service_CatalogRole: + type: object + properties: + name: + type: string + minLength: 1 + maxLength: 256 + pattern: ^(?!\s*[s|S][y|Y][s|S][t|T][e|E][m|M]\$).*$ + description: The name of the role + properties: + type: object + additionalProperties: + type: string + createTimestamp: + type: integer + format: int64 + lastUpdateTimestamp: + type: integer + format: int64 + entityVersion: + type: integer + description: >- + The version of the catalog role object used to determine if the + catalog role metadata has changed + required: + - name + Polaris_Management_Service_UpdateCatalogRoleRequest: + description: Updates to apply to a Catalog Role + type: object + properties: + currentEntityVersion: + type: integer + description: >- + The version of the object onto which this update is applied; if the + object changed, the update will fail and the caller should retry + after fetching the latest version. + properties: + type: object + additionalProperties: + type: string + required: + - currentEntityVersion + - properties + Polaris_Management_Service_ViewPrivilege: + type: string + enum: + - CATALOG_MANAGE_ACCESS + - VIEW_CREATE + - VIEW_DROP + - VIEW_LIST + - VIEW_READ_PROPERTIES + - VIEW_WRITE_PROPERTIES + - VIEW_FULL_METADATA + Polaris_Management_Service_TablePrivilege: + type: string + enum: + - CATALOG_MANAGE_ACCESS + - TABLE_DROP + - TABLE_LIST + - TABLE_READ_PROPERTIES + - VIEW_READ_PROPERTIES + - TABLE_WRITE_PROPERTIES + - TABLE_READ_DATA + - TABLE_WRITE_DATA + - TABLE_FULL_METADATA + Polaris_Management_Service_NamespacePrivilege: + type: string + enum: + - CATALOG_MANAGE_ACCESS + - CATALOG_MANAGE_CONTENT + - CATALOG_MANAGE_METADATA + - NAMESPACE_CREATE + - TABLE_CREATE + - VIEW_CREATE + - NAMESPACE_DROP + - TABLE_DROP + - VIEW_DROP + - NAMESPACE_LIST + - TABLE_LIST + - VIEW_LIST + - NAMESPACE_READ_PROPERTIES + - TABLE_READ_PROPERTIES + - VIEW_READ_PROPERTIES + - NAMESPACE_WRITE_PROPERTIES + - TABLE_WRITE_PROPERTIES + - VIEW_WRITE_PROPERTIES + - TABLE_READ_DATA + - TABLE_WRITE_DATA + - NAMESPACE_FULL_METADATA + - TABLE_FULL_METADATA + - VIEW_FULL_METADATA + Polaris_Management_Service_CatalogPrivilege: + type: string + enum: + - CATALOG_MANAGE_ACCESS + - CATALOG_MANAGE_CONTENT + - CATALOG_MANAGE_METADATA + - CATALOG_READ_PROPERTIES + - CATALOG_WRITE_PROPERTIES + - NAMESPACE_CREATE + - TABLE_CREATE + - VIEW_CREATE + - NAMESPACE_DROP + - TABLE_DROP + - VIEW_DROP + - NAMESPACE_LIST + - TABLE_LIST + - VIEW_LIST + - NAMESPACE_READ_PROPERTIES + - TABLE_READ_PROPERTIES + - VIEW_READ_PROPERTIES + - NAMESPACE_WRITE_PROPERTIES + - TABLE_WRITE_PROPERTIES + - VIEW_WRITE_PROPERTIES + - TABLE_READ_DATA + - TABLE_WRITE_DATA + - NAMESPACE_FULL_METADATA + - TABLE_FULL_METADATA + - VIEW_FULL_METADATA + Polaris_Management_Service_AddGrantRequest: + type: object + properties: + grant: + $ref: '#/components/schemas/Polaris_Management_Service_GrantResource' + Polaris_Management_Service_RevokeGrantRequest: + type: object + properties: + grant: + $ref: '#/components/schemas/Polaris_Management_Service_GrantResource' + Polaris_Management_Service_ViewGrant: + allOf: + - $ref: '#/components/schemas/Polaris_Management_Service_GrantResource' + - type: object + properties: + namespace: + type: array + items: + type: string + viewName: + type: string + minLength: 1 + maxLength: 256 + privilege: + $ref: '#/components/schemas/Polaris_Management_Service_ViewPrivilege' + required: + - namespace + - viewName + - privilege + Polaris_Management_Service_TableGrant: + allOf: + - $ref: '#/components/schemas/Polaris_Management_Service_GrantResource' + - type: object + properties: + namespace: + type: array + items: + type: string + tableName: + type: string + minLength: 1 + maxLength: 256 + privilege: + $ref: '#/components/schemas/Polaris_Management_Service_TablePrivilege' + required: + - namespace + - tableName + - privilege + Polaris_Management_Service_NamespaceGrant: + allOf: + - $ref: '#/components/schemas/Polaris_Management_Service_GrantResource' + - type: object + properties: + namespace: + type: array + items: + type: string + privilege: + $ref: >- + #/components/schemas/Polaris_Management_Service_NamespacePrivilege + required: + - namespace + - privilege + Polaris_Management_Service_CatalogGrant: + allOf: + - $ref: '#/components/schemas/Polaris_Management_Service_GrantResource' + - type: object + properties: + privilege: + $ref: '#/components/schemas/Polaris_Management_Service_CatalogPrivilege' + required: + - privilege + Polaris_Management_Service_GrantResource: + type: object + discriminator: + propertyName: type + mapping: + catalog: '#/components/schemas/Polaris_Management_Service_CatalogGrant' + namespace: '#/components/schemas/Polaris_Management_Service_NamespaceGrant' + table: '#/components/schemas/Polaris_Management_Service_TableGrant' + view: '#/components/schemas/Polaris_Management_Service_ViewGrant' + properties: + type: + type: string + enum: + - catalog + - namespace + - table + - view + required: + - type + Polaris_Management_Service_GrantResources: + type: object + properties: + grants: + type: array + items: + $ref: '#/components/schemas/Polaris_Management_Service_GrantResource' + required: + - grants + Apache_Iceberg_REST_Catalog_API_ErrorModel: + type: object + description: >- + JSON error payload returned in a response with further details on the + error + required: + - message + - type + - code + properties: + message: + type: string + description: Human-readable error message + type: + type: string + description: Internal type definition of the error + example: NoSuchNamespaceException + code: + type: integer + minimum: 400 + maximum: 600 + description: HTTP response code + example: 404 + stack: + type: array + items: + type: string + Apache_Iceberg_REST_Catalog_API_CatalogConfig: + type: object + description: Server-provided configuration for the catalog. + required: + - defaults + - overrides + properties: + overrides: + type: object + additionalProperties: + type: string + description: >- + Properties that should be used to override client configuration; + applied after defaults and client configuration. + defaults: + type: object + additionalProperties: + type: string + description: >- + Properties that should be used as default configuration; applied + before client configuration. + Apache_Iceberg_REST_Catalog_API_CreateNamespaceRequest: + type: object + required: + - namespace + properties: + namespace: + $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_Namespace' + properties: + type: object + description: Configured string to string map of properties for the namespace + example: + owner: Hank Bendickson + default: {} + additionalProperties: + type: string + Apache_Iceberg_REST_Catalog_API_UpdateNamespacePropertiesRequest: + type: object + properties: + removals: + type: array + uniqueItems: true + items: + type: string + example: + - department + - access_group + updates: + type: object + example: + owner: Hank Bendickson + additionalProperties: + type: string + Apache_Iceberg_REST_Catalog_API_RenameTableRequest: + type: object + required: + - source + - destination + properties: + source: + $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_TableIdentifier' + destination: + $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_TableIdentifier' + Apache_Iceberg_REST_Catalog_API_Namespace: + description: Reference to one or more levels of a namespace + type: array + items: + type: string + example: + - accounting + - tax + Apache_Iceberg_REST_Catalog_API_PageToken: + description: >- + An opaque token that allows clients to make use of pagination for list + APIs (e.g. ListTables). Clients may initiate the first paginated request + by sending an empty query parameter `pageToken` to the server. + + Servers that support pagination should identify the `pageToken` + parameter and return a `next-page-token` in the response if there are + more results available. After the initial request, the value of + `next-page-token` from each response must be used as the `pageToken` + parameter value for the next request. The server must return `null` + value for the `next-page-token` in the last response. + + Servers that support pagination must return all results in a single + response with the value of `next-page-token` set to `null` if the query + parameter `pageToken` is not set in the request. + + Servers that do not support pagination should ignore the `pageToken` + parameter and return all results in a single response. The + `next-page-token` must be omitted from the response. + + Clients must interpret either `null` or missing response value of + `next-page-token` as the end of the listing results. + type: string + nullable: true + Apache_Iceberg_REST_Catalog_API_TableIdentifier: + type: object + required: + - namespace + - name + properties: + namespace: + $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_Namespace' + name: + type: string + nullable: false + Apache_Iceberg_REST_Catalog_API_PrimitiveType: + type: string + example: + - long + - string + - fixed[16] + - decimal(10,2) + Apache_Iceberg_REST_Catalog_API_StructField: + type: object + required: + - id + - name + - type + - required + properties: + id: + type: integer + name: + type: string + type: + $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_Type' + required: + type: boolean + doc: + type: string + Apache_Iceberg_REST_Catalog_API_StructType: + type: object + required: + - type + - fields + properties: + type: + type: string + enum: + - struct + fields: + type: array + items: + $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_StructField' + Apache_Iceberg_REST_Catalog_API_ListType: + type: object + required: + - type + - element-id + - element + - element-required + properties: + type: + type: string + enum: + - list + element-id: + type: integer + element: + $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_Type' + element-required: + type: boolean + Apache_Iceberg_REST_Catalog_API_MapType: + type: object + required: + - type + - key-id + - key + - value-id + - value + - value-required + properties: + type: + type: string + enum: + - map + key-id: + type: integer + key: + $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_Type' + value-id: + type: integer + value: + $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_Type' + value-required: + type: boolean + Apache_Iceberg_REST_Catalog_API_Type: + oneOf: + - $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_PrimitiveType' + - $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_StructType' + - $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_ListType' + - $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_MapType' + Apache_Iceberg_REST_Catalog_API_Schema: + allOf: + - $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_StructType' + - type: object + properties: + schema-id: + type: integer + readOnly: true + identifier-field-ids: + type: array + items: + type: integer + Apache_Iceberg_REST_Catalog_API_Expression: + oneOf: + - $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_AndOrExpression' + - $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_NotExpression' + - $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_SetExpression' + - $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_LiteralExpression + - $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_UnaryExpression' + Apache_Iceberg_REST_Catalog_API_ExpressionType: + type: string + example: + - eq + - and + - or + - not + - in + - not-in + - lt + - lt-eq + - gt + - gt-eq + - not-eq + - starts-with + - not-starts-with + - is-null + - not-null + - is-nan + - not-nan + Apache_Iceberg_REST_Catalog_API_AndOrExpression: + type: object + required: + - type + - left + - right + properties: + type: + $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_ExpressionType' + enum: + - and + - or + left: + $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_Expression' + right: + $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_Expression' + Apache_Iceberg_REST_Catalog_API_NotExpression: + type: object + required: + - type + - child + properties: + type: + $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_ExpressionType' + enum: + - not + child: + $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_Expression' + Apache_Iceberg_REST_Catalog_API_UnaryExpression: + type: object + required: + - type + - term + - value + properties: + type: + $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_ExpressionType' + enum: + - is-null + - not-null + - is-nan + - not-nan + term: + $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_Term' + value: + type: object + Apache_Iceberg_REST_Catalog_API_LiteralExpression: + type: object + required: + - type + - term + - value + properties: + type: + $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_ExpressionType' + enum: + - lt + - lt-eq + - gt + - gt-eq + - eq + - not-eq + - starts-with + - not-starts-with + term: + $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_Term' + value: + type: object + Apache_Iceberg_REST_Catalog_API_SetExpression: + type: object + required: + - type + - term + - values + properties: + type: + $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_ExpressionType' + enum: + - in + - not-in + term: + $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_Term' + values: + type: array + items: + type: object + Apache_Iceberg_REST_Catalog_API_Term: + oneOf: + - $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_Reference' + - $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_TransformTerm' + Apache_Iceberg_REST_Catalog_API_Reference: + type: string + example: + - column-name + Apache_Iceberg_REST_Catalog_API_TransformTerm: + type: object + required: + - type + - transform + - term + properties: + type: + type: string + enum: + - transform + transform: + $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_Transform' + term: + $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_Reference' + Apache_Iceberg_REST_Catalog_API_Transform: + type: string + example: + - identity + - year + - month + - day + - hour + - bucket[256] + - truncate[16] + Apache_Iceberg_REST_Catalog_API_PartitionField: + type: object + required: + - source-id + - transform + - name + properties: + field-id: + type: integer + source-id: + type: integer + name: + type: string + transform: + $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_Transform' + Apache_Iceberg_REST_Catalog_API_PartitionSpec: + type: object + required: + - fields + properties: + spec-id: + type: integer + readOnly: true + fields: + type: array + items: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_PartitionField + Apache_Iceberg_REST_Catalog_API_SortDirection: + type: string + enum: + - asc + - desc + Apache_Iceberg_REST_Catalog_API_NullOrder: + type: string + enum: + - nulls-first + - nulls-last + Apache_Iceberg_REST_Catalog_API_SortField: + type: object + required: + - source-id + - transform + - direction + - null-order + properties: + source-id: + type: integer + transform: + $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_Transform' + direction: + $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_SortDirection' + null-order: + $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_NullOrder' + Apache_Iceberg_REST_Catalog_API_SortOrder: + type: object + required: + - order-id + - fields + properties: + order-id: + type: integer + readOnly: true + fields: + type: array + items: + $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_SortField' + Apache_Iceberg_REST_Catalog_API_Snapshot: + type: object + required: + - snapshot-id + - timestamp-ms + - manifest-list + - summary + properties: + snapshot-id: + type: integer + format: int64 + parent-snapshot-id: + type: integer + format: int64 + sequence-number: + type: integer + format: int64 + timestamp-ms: + type: integer + format: int64 + manifest-list: + type: string + description: Location of the snapshot's manifest list file + summary: + type: object + required: + - operation + properties: + operation: + type: string + enum: + - append + - replace + - overwrite + - delete + additionalProperties: + type: string + schema-id: + type: integer + Apache_Iceberg_REST_Catalog_API_SnapshotReference: + type: object + required: + - type + - snapshot-id + properties: + type: + type: string + enum: + - tag + - branch + snapshot-id: + type: integer + format: int64 + max-ref-age-ms: + type: integer + format: int64 + max-snapshot-age-ms: + type: integer + format: int64 + min-snapshots-to-keep: + type: integer + Apache_Iceberg_REST_Catalog_API_SnapshotReferences: + type: object + additionalProperties: + $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_SnapshotReference' + Apache_Iceberg_REST_Catalog_API_SnapshotLog: + type: array + items: + type: object + required: + - snapshot-id + - timestamp-ms + properties: + snapshot-id: + type: integer + format: int64 + timestamp-ms: + type: integer + format: int64 + Apache_Iceberg_REST_Catalog_API_MetadataLog: + type: array + items: + type: object + required: + - metadata-file + - timestamp-ms + properties: + metadata-file: + type: string + timestamp-ms: + type: integer + format: int64 + Apache_Iceberg_REST_Catalog_API_TableMetadata: + type: object + required: + - format-version + - table-uuid + properties: + format-version: + type: integer + minimum: 1 + maximum: 2 + table-uuid: + type: string + location: + type: string + last-updated-ms: + type: integer + format: int64 + properties: + type: object + additionalProperties: + type: string + schemas: + type: array + items: + $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_Schema' + current-schema-id: + type: integer + last-column-id: + type: integer + partition-specs: + type: array + items: + $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_PartitionSpec' + default-spec-id: + type: integer + last-partition-id: + type: integer + sort-orders: + type: array + items: + $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_SortOrder' + default-sort-order-id: + type: integer + snapshots: + type: array + items: + $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_Snapshot' + refs: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_SnapshotReferences + current-snapshot-id: + type: integer + format: int64 + last-sequence-number: + type: integer + format: int64 + snapshot-log: + $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_SnapshotLog' + metadata-log: + $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_MetadataLog' + statistics-files: + type: array + items: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_StatisticsFile + partition-statistics-files: + type: array + items: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_PartitionStatisticsFile + Apache_Iceberg_REST_Catalog_API_SQLViewRepresentation: + type: object + required: + - type + - sql + - dialect + properties: + type: + type: string + sql: + type: string + dialect: + type: string + Apache_Iceberg_REST_Catalog_API_ViewRepresentation: + oneOf: + - $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_SQLViewRepresentation + Apache_Iceberg_REST_Catalog_API_ViewHistoryEntry: + type: object + required: + - version-id + - timestamp-ms + properties: + version-id: + type: integer + timestamp-ms: + type: integer + format: int64 + Apache_Iceberg_REST_Catalog_API_ViewVersion: + type: object + required: + - version-id + - timestamp-ms + - schema-id + - summary + - representations + - default-namespace + properties: + version-id: + type: integer + timestamp-ms: + type: integer + format: int64 + schema-id: + type: integer + description: Schema ID to set as current, or -1 to set last added schema + summary: + type: object + additionalProperties: + type: string + representations: + type: array + items: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_ViewRepresentation + default-catalog: + type: string + default-namespace: + $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_Namespace' + Apache_Iceberg_REST_Catalog_API_ViewMetadata: + type: object + required: + - view-uuid + - format-version + - location + - current-version-id + - versions + - version-log + - schemas + properties: + view-uuid: + type: string + format-version: + type: integer + minimum: 1 + maximum: 1 + location: + type: string + current-version-id: + type: integer + versions: + type: array + items: + $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_ViewVersion' + version-log: + type: array + items: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_ViewHistoryEntry + schemas: + type: array + items: + $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_Schema' + properties: + type: object + additionalProperties: + type: string + Apache_Iceberg_REST_Catalog_API_BaseUpdate: + discriminator: + propertyName: action + mapping: + assign-uuid: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_AssignUUIDUpdate + upgrade-format-version: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_UpgradeFormatVersionUpdate + add-schema: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_AddSchemaUpdate' + set-current-schema: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_SetCurrentSchemaUpdate + add-spec: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_AddPartitionSpecUpdate + set-default-spec: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_SetDefaultSpecUpdate + add-sort-order: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_AddSortOrderUpdate + set-default-sort-order: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_SetDefaultSortOrderUpdate + add-snapshot: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_AddSnapshotUpdate + set-snapshot-ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_SetSnapshotRefUpdate + remove-snapshots: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_RemoveSnapshotsUpdate + remove-snapshot-ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_RemoveSnapshotRefUpdate + set-location: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_SetLocationUpdate + set-properties: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_SetPropertiesUpdate + remove-properties: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_RemovePropertiesUpdate + add-view-version: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_AddViewVersionUpdate + set-current-view-version: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_SetCurrentViewVersionUpdate + set-statistics: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_SetStatisticsUpdate + remove-statistics: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_RemoveStatisticsUpdate + set-partition-statistics: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_SetPartitionStatisticsUpdate + remove-partition-statistics: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_RemovePartitionStatisticsUpdate + type: object + required: + - action + properties: + action: + type: string + Apache_Iceberg_REST_Catalog_API_AssignUUIDUpdate: + description: >- + Assigning a UUID to a table/view should only be done when creating the + table/view. It is not safe to re-assign the UUID if a table/view already + has a UUID assigned + allOf: + - $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_BaseUpdate' + required: + - action + - uuid + properties: + action: + type: string + enum: + - assign-uuid + uuid: + type: string + Apache_Iceberg_REST_Catalog_API_UpgradeFormatVersionUpdate: + allOf: + - $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_BaseUpdate' + required: + - action + - format-version + properties: + action: + type: string + enum: + - upgrade-format-version + format-version: + type: integer + Apache_Iceberg_REST_Catalog_API_AddSchemaUpdate: + allOf: + - $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_BaseUpdate' + required: + - action + - schema + properties: + action: + type: string + enum: + - add-schema + schema: + $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_Schema' + last-column-id: + type: integer + description: >- + The highest assigned column ID for the table. This is used to ensure + columns are always assigned an unused ID when evolving schemas. When + omitted, it will be computed on the server side. + Apache_Iceberg_REST_Catalog_API_SetCurrentSchemaUpdate: + allOf: + - $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_BaseUpdate' + required: + - action + - schema-id + properties: + action: + type: string + enum: + - set-current-schema + schema-id: + type: integer + description: Schema ID to set as current, or -1 to set last added schema + Apache_Iceberg_REST_Catalog_API_AddPartitionSpecUpdate: + allOf: + - $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_BaseUpdate' + required: + - action + - spec + properties: + action: + type: string + enum: + - add-spec + spec: + $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_PartitionSpec' + Apache_Iceberg_REST_Catalog_API_SetDefaultSpecUpdate: + allOf: + - $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_BaseUpdate' + required: + - action + - spec-id + properties: + action: + type: string + enum: + - set-default-spec + spec-id: + type: integer + description: >- + Partition spec ID to set as the default, or -1 to set last added + spec + Apache_Iceberg_REST_Catalog_API_AddSortOrderUpdate: + allOf: + - $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_BaseUpdate' + required: + - action + - sort-order + properties: + action: + type: string + enum: + - add-sort-order + sort-order: + $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_SortOrder' + Apache_Iceberg_REST_Catalog_API_SetDefaultSortOrderUpdate: + allOf: + - $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_BaseUpdate' + required: + - action + - sort-order-id + properties: + action: + type: string + enum: + - set-default-sort-order + sort-order-id: + type: integer + description: >- + Sort order ID to set as the default, or -1 to set last added sort + order + Apache_Iceberg_REST_Catalog_API_AddSnapshotUpdate: + allOf: + - $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_BaseUpdate' + required: + - action + - snapshot + properties: + action: + type: string + enum: + - add-snapshot + snapshot: + $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_Snapshot' + Apache_Iceberg_REST_Catalog_API_SetSnapshotRefUpdate: + allOf: + - $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_BaseUpdate' + - $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_SnapshotReference + required: + - action + - ref-name + properties: + action: + type: string + enum: + - set-snapshot-ref + ref-name: + type: string + Apache_Iceberg_REST_Catalog_API_RemoveSnapshotsUpdate: + allOf: + - $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_BaseUpdate' + required: + - action + - snapshot-ids + properties: + action: + type: string + enum: + - remove-snapshots + snapshot-ids: + type: array + items: + type: integer + format: int64 + Apache_Iceberg_REST_Catalog_API_RemoveSnapshotRefUpdate: + allOf: + - $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_BaseUpdate' + required: + - action + - ref-name + properties: + action: + type: string + enum: + - remove-snapshot-ref + ref-name: + type: string + Apache_Iceberg_REST_Catalog_API_SetLocationUpdate: + allOf: + - $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_BaseUpdate' + required: + - action + - location + properties: + action: + type: string + enum: + - set-location + location: + type: string + Apache_Iceberg_REST_Catalog_API_SetPropertiesUpdate: + allOf: + - $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_BaseUpdate' + required: + - action + - updates + properties: + action: + type: string + enum: + - set-properties + updates: + type: object + additionalProperties: + type: string + Apache_Iceberg_REST_Catalog_API_RemovePropertiesUpdate: + allOf: + - $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_BaseUpdate' + required: + - action + - removals + properties: + action: + type: string + enum: + - remove-properties + removals: + type: array + items: + type: string + Apache_Iceberg_REST_Catalog_API_AddViewVersionUpdate: + allOf: + - $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_BaseUpdate' + required: + - action + - view-version + properties: + action: + type: string + enum: + - add-view-version + view-version: + $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_ViewVersion' + Apache_Iceberg_REST_Catalog_API_SetCurrentViewVersionUpdate: + allOf: + - $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_BaseUpdate' + required: + - action + - view-version-id + properties: + action: + type: string + enum: + - set-current-view-version + view-version-id: + type: integer + description: >- + The view version id to set as current, or -1 to set last added view + version id + Apache_Iceberg_REST_Catalog_API_SetStatisticsUpdate: + allOf: + - $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_BaseUpdate' + required: + - action + - snapshot-id + - statistics + properties: + action: + type: string + enum: + - set-statistics + snapshot-id: + type: integer + format: int64 + statistics: + $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_StatisticsFile' + Apache_Iceberg_REST_Catalog_API_RemoveStatisticsUpdate: + allOf: + - $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_BaseUpdate' + required: + - action + - snapshot-id + properties: + action: + type: string + enum: + - remove-statistics + snapshot-id: + type: integer + format: int64 + Apache_Iceberg_REST_Catalog_API_SetPartitionStatisticsUpdate: + allOf: + - $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_BaseUpdate' + required: + - action + - partition-statistics + properties: + action: + type: string + enum: + - set-partition-statistics + partition-statistics: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_PartitionStatisticsFile + Apache_Iceberg_REST_Catalog_API_RemovePartitionStatisticsUpdate: + allOf: + - $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_BaseUpdate' + required: + - action + - snapshot-id + properties: + action: + type: string + enum: + - remove-partition-statistics + snapshot-id: + type: integer + format: int64 + Apache_Iceberg_REST_Catalog_API_TableUpdate: + anyOf: + - $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_AssignUUIDUpdate + - $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_UpgradeFormatVersionUpdate + - $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_AddSchemaUpdate' + - $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_SetCurrentSchemaUpdate + - $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_AddPartitionSpecUpdate + - $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_SetDefaultSpecUpdate + - $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_AddSortOrderUpdate + - $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_SetDefaultSortOrderUpdate + - $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_AddSnapshotUpdate + - $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_SetSnapshotRefUpdate + - $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_RemoveSnapshotsUpdate + - $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_RemoveSnapshotRefUpdate + - $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_SetLocationUpdate + - $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_SetPropertiesUpdate + - $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_RemovePropertiesUpdate + - $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_SetStatisticsUpdate + - $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_RemoveStatisticsUpdate + Apache_Iceberg_REST_Catalog_API_ViewUpdate: + anyOf: + - $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_AssignUUIDUpdate + - $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_UpgradeFormatVersionUpdate + - $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_AddSchemaUpdate' + - $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_SetLocationUpdate + - $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_SetPropertiesUpdate + - $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_RemovePropertiesUpdate + - $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_AddViewVersionUpdate + - $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_SetCurrentViewVersionUpdate + Apache_Iceberg_REST_Catalog_API_TableRequirement: + discriminator: + propertyName: type + mapping: + assert-create: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_AssertCreate' + assert-table-uuid: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_AssertTableUUID' + assert-ref-snapshot-id: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_AssertRefSnapshotId + assert-last-assigned-field-id: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_AssertLastAssignedFieldId + assert-current-schema-id: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_AssertCurrentSchemaId + assert-last-assigned-partition-id: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_AssertLastAssignedPartitionId + assert-default-spec-id: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_AssertDefaultSpecId + assert-default-sort-order-id: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_AssertDefaultSortOrderId + type: object + required: + - type + properties: + type: + type: string + Apache_Iceberg_REST_Catalog_API_AssertCreate: + allOf: + - $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_TableRequirement + type: object + description: The table must not already exist; used for create transactions + required: + - type + properties: + type: + type: string + enum: + - assert-create + Apache_Iceberg_REST_Catalog_API_AssertTableUUID: + allOf: + - $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_TableRequirement + description: The table UUID must match the requirement's `uuid` + required: + - type + - uuid + properties: + type: + type: string + enum: + - assert-table-uuid + uuid: + type: string + Apache_Iceberg_REST_Catalog_API_AssertRefSnapshotId: + allOf: + - $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_TableRequirement + description: >- + The table branch or tag identified by the requirement's `ref` must + reference the requirement's `snapshot-id`; if `snapshot-id` is `null` or + missing, the ref must not already exist + required: + - type + - ref + - snapshot-id + properties: + type: + type: string + enum: + - assert-ref-snapshot-id + ref: + type: string + snapshot-id: + type: integer + format: int64 + Apache_Iceberg_REST_Catalog_API_AssertLastAssignedFieldId: + allOf: + - $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_TableRequirement + description: >- + The table's last assigned column id must match the requirement's + `last-assigned-field-id` + required: + - type + - last-assigned-field-id + properties: + type: + type: string + enum: + - assert-last-assigned-field-id + last-assigned-field-id: + type: integer + Apache_Iceberg_REST_Catalog_API_AssertCurrentSchemaId: + allOf: + - $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_TableRequirement + description: >- + The table's current schema id must match the requirement's + `current-schema-id` + required: + - type + - current-schema-id + properties: + type: + type: string + enum: + - assert-current-schema-id + current-schema-id: + type: integer + Apache_Iceberg_REST_Catalog_API_AssertLastAssignedPartitionId: + allOf: + - $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_TableRequirement + description: >- + The table's last assigned partition id must match the requirement's + `last-assigned-partition-id` + required: + - type + - last-assigned-partition-id + properties: + type: + type: string + enum: + - assert-last-assigned-partition-id + last-assigned-partition-id: + type: integer + Apache_Iceberg_REST_Catalog_API_AssertDefaultSpecId: + allOf: + - $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_TableRequirement + description: >- + The table's default spec id must match the requirement's + `default-spec-id` + required: + - type + - default-spec-id + properties: + type: + type: string + enum: + - assert-default-spec-id + default-spec-id: + type: integer + Apache_Iceberg_REST_Catalog_API_AssertDefaultSortOrderId: + allOf: + - $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_TableRequirement + description: >- + The table's default sort order id must match the requirement's + `default-sort-order-id` + required: + - type + - default-sort-order-id + properties: + type: + type: string + enum: + - assert-default-sort-order-id + default-sort-order-id: + type: integer + Apache_Iceberg_REST_Catalog_API_ViewRequirement: + discriminator: + propertyName: type + mapping: + assert-view-uuid: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_AssertViewUUID' + type: object + required: + - type + properties: + type: + type: string + Apache_Iceberg_REST_Catalog_API_AssertViewUUID: + allOf: + - $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_ViewRequirement' + description: The view UUID must match the requirement's `uuid` + required: + - type + - uuid + properties: + type: + type: string + enum: + - assert-view-uuid + uuid: + type: string + Apache_Iceberg_REST_Catalog_API_LoadTableResult: + description: > + Result used when a table is successfully loaded. + + + + The table metadata JSON is returned in the `metadata` field. The + corresponding file location of table metadata should be returned in the + `metadata-location` field, unless the metadata is not yet committed. For + example, a create transaction may return metadata that is staged but not + committed. + + Clients can check whether metadata has changed by comparing metadata + locations after the table has been created. + + + + The `config` map returns table-specific configuration for the table's + resources, including its HTTP client and FileIO. For example, config may + contain a specific FileIO implementation class for the table depending + on its underlying storage. + + + + The following configurations should be respected by clients: + + + ## General Configurations + + + - `token`: Authorization bearer token to use for table requests if + OAuth2 security is enabled + + + ## AWS Configurations + + + The following configurations should be respected when working with + tables stored in AWS S3 + - `client.region`: region to configure client for making requests to AWS + - `s3.access-key-id`: id for for credentials that provide access to the data in S3 + - `s3.secret-access-key`: secret for credentials that provide access to data in S3 + - `s3.session-token`: if present, this value should be used for as the session token + - `s3.remote-signing-enabled`: if `true` remote signing should be performed as described in the `s3-signer-open-api.yaml` specification + type: object + required: + - metadata + properties: + metadata-location: + type: string + description: May be null if the table is staged as part of a transaction + metadata: + $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_TableMetadata' + config: + type: object + additionalProperties: + type: string + Apache_Iceberg_REST_Catalog_API_CommitTableRequest: + type: object + required: + - requirements + - updates + properties: + identifier: + description: >- + Table identifier to update; must be present for + CommitTransactionRequest + $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_TableIdentifier' + requirements: + type: array + items: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_TableRequirement + updates: + type: array + items: + $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_TableUpdate' + Apache_Iceberg_REST_Catalog_API_CommitViewRequest: + type: object + required: + - updates + properties: + identifier: + description: View identifier to update + $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_TableIdentifier' + requirements: + type: array + items: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_ViewRequirement + updates: + type: array + items: + $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_ViewUpdate' + Apache_Iceberg_REST_Catalog_API_CommitTransactionRequest: + type: object + required: + - table-changes + properties: + table-changes: + type: array + items: + description: Table commit request; must provide an `identifier` + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_CommitTableRequest + Apache_Iceberg_REST_Catalog_API_CreateTableRequest: + type: object + required: + - name + - schema + properties: + name: + type: string + location: + type: string + schema: + $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_Schema' + partition-spec: + $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_PartitionSpec' + write-order: + $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_SortOrder' + stage-create: + type: boolean + properties: + type: object + additionalProperties: + type: string + Apache_Iceberg_REST_Catalog_API_RegisterTableRequest: + type: object + required: + - name + - metadata-location + properties: + name: + type: string + metadata-location: + type: string + Apache_Iceberg_REST_Catalog_API_CreateViewRequest: + type: object + required: + - name + - schema + - view-version + - properties + properties: + name: + type: string + location: + type: string + schema: + $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_Schema' + view-version: + $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_ViewVersion' + description: >- + The view version to create, will replace the schema-id sent within + the view-version with the id assigned to the provided schema + properties: + type: object + additionalProperties: + type: string + Apache_Iceberg_REST_Catalog_API_LoadViewResult: + description: > + Result used when a view is successfully loaded. + + + + The view metadata JSON is returned in the `metadata` field. The + corresponding file location of view metadata is returned in the + `metadata-location` field. + + Clients can check whether metadata has changed by comparing metadata + locations after the view has been created. + + + The `config` map returns view-specific configuration for the view's + resources. + + + The following configurations should be respected by clients: + + + ## General Configurations + + + - `token`: Authorization bearer token to use for view requests if OAuth2 + security is enabled + type: object + required: + - metadata-location + - metadata + properties: + metadata-location: + type: string + metadata: + $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_ViewMetadata' + config: + type: object + additionalProperties: + type: string + Apache_Iceberg_REST_Catalog_API_TokenType: + type: string + enum: + - urn:ietf:params:oauth:token-type:access_token + - urn:ietf:params:oauth:token-type:refresh_token + - urn:ietf:params:oauth:token-type:id_token + - urn:ietf:params:oauth:token-type:saml1 + - urn:ietf:params:oauth:token-type:saml2 + - urn:ietf:params:oauth:token-type:jwt + description: |- + Token type identifier, from RFC 8693 Section 3 + + See https://datatracker.ietf.org/doc/html/rfc8693#section-3 + Apache_Iceberg_REST_Catalog_API_OAuthClientCredentialsRequest: + description: |- + OAuth2 client credentials request + + See https://datatracker.ietf.org/doc/html/rfc6749#section-4.4 + type: object + required: + - grant_type + - client_id + - client_secret + properties: + grant_type: + type: string + enum: + - client_credentials + scope: + type: string + client_id: + type: string + description: >- + Client ID + + + This can be sent in the request body, but OAuth2 recommends sending + it in a Basic Authorization header. + client_secret: + type: string + description: >- + Client secret + + + This can be sent in the request body, but OAuth2 recommends sending + it in a Basic Authorization header. + Apache_Iceberg_REST_Catalog_API_OAuthTokenExchangeRequest: + description: |- + OAuth2 token exchange request + + See https://datatracker.ietf.org/doc/html/rfc8693 + type: object + required: + - grant_type + - subject_token + - subject_token_type + properties: + grant_type: + type: string + enum: + - urn:ietf:params:oauth:grant-type:token-exchange + scope: + type: string + requested_token_type: + $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_TokenType' + subject_token: + type: string + description: Subject token for token exchange request + subject_token_type: + $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_TokenType' + actor_token: + type: string + description: Actor token for token exchange request + actor_token_type: + $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_TokenType' + Apache_Iceberg_REST_Catalog_API_OAuthTokenRequest: + anyOf: + - $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_OAuthClientCredentialsRequest + - $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_OAuthTokenExchangeRequest + Apache_Iceberg_REST_Catalog_API_CounterResult: + type: object + required: + - unit + - value + properties: + unit: + type: string + value: + type: integer + format: int64 + Apache_Iceberg_REST_Catalog_API_TimerResult: + type: object + required: + - time-unit + - count + - total-duration + properties: + time-unit: + type: string + count: + type: integer + format: int64 + total-duration: + type: integer + format: int64 + Apache_Iceberg_REST_Catalog_API_MetricResult: + anyOf: + - $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_CounterResult' + - $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_TimerResult' + Apache_Iceberg_REST_Catalog_API_Metrics: + type: object + additionalProperties: + $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_MetricResult' + example: + metrics: + total-planning-duration: + count: 1 + time-unit: nanoseconds + total-duration: 2644235116 + result-data-files: + unit: count + value: 1 + result-delete-files: + unit: count + value: 0 + total-data-manifests: + unit: count + value: 1 + total-delete-manifests: + unit: count + value: 0 + scanned-data-manifests: + unit: count + value: 1 + skipped-data-manifests: + unit: count + value: 0 + total-file-size-bytes: + unit: bytes + value: 10 + total-delete-file-size-bytes: + unit: bytes + value: 0 + Apache_Iceberg_REST_Catalog_API_ReportMetricsRequest: + anyOf: + - $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_ScanReport' + - $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_CommitReport' + required: + - report-type + properties: + report-type: + type: string + Apache_Iceberg_REST_Catalog_API_ScanReport: + type: object + required: + - table-name + - snapshot-id + - filter + - schema-id + - projected-field-ids + - projected-field-names + - metrics + properties: + table-name: + type: string + snapshot-id: + type: integer + format: int64 + filter: + $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_Expression' + schema-id: + type: integer + projected-field-ids: + type: array + items: + type: integer + projected-field-names: + type: array + items: + type: string + metrics: + $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_Metrics' + metadata: + type: object + additionalProperties: + type: string + Apache_Iceberg_REST_Catalog_API_CommitReport: + type: object + required: + - table-name + - snapshot-id + - sequence-number + - operation + - metrics + properties: + table-name: + type: string + snapshot-id: + type: integer + format: int64 + sequence-number: + type: integer + format: int64 + operation: + type: string + metrics: + $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_Metrics' + metadata: + type: object + additionalProperties: + type: string + Apache_Iceberg_REST_Catalog_API_NotificationRequest: + required: + - notification-type + properties: + notification-type: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_NotificationType + payload: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_TableUpdateNotification + Apache_Iceberg_REST_Catalog_API_NotificationType: + type: string + enum: + - UNKNOWN + - CREATE + - UPDATE + - DROP + Apache_Iceberg_REST_Catalog_API_TableUpdateNotification: + type: object + required: + - table-name + - timestamp + - table-uuid + - metadata-location + properties: + table-name: + type: string + timestamp: + type: integer + format: int64 + table-uuid: + type: string + metadata-location: + type: string + metadata: + $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_TableMetadata' + Apache_Iceberg_REST_Catalog_API_OAuthError: + type: object + required: + - error + properties: + error: + type: string + enum: + - invalid_request + - invalid_client + - invalid_grant + - unauthorized_client + - unsupported_grant_type + - invalid_scope + error_description: + type: string + error_uri: + type: string + Apache_Iceberg_REST_Catalog_API_OAuthTokenResponse: + type: object + required: + - access_token + - token_type + properties: + access_token: + type: string + description: The access token, for client credentials or token exchange + token_type: + type: string + enum: + - bearer + - mac + - N_A + description: |- + Access token type for client credentials or token exchange + + See https://datatracker.ietf.org/doc/html/rfc6749#section-7.1 + expires_in: + type: integer + description: >- + Lifetime of the access token in seconds for client credentials or + token exchange + issued_token_type: + $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_TokenType' + refresh_token: + type: string + description: Refresh token for client credentials or token exchange + scope: + type: string + description: Authorization scope for client credentials or token exchange + Apache_Iceberg_REST_Catalog_API_IcebergErrorResponse: + description: JSON wrapper for all error responses (non-2xx) + type: object + required: + - error + properties: + error: + $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_ErrorModel' + additionalProperties: false + example: + error: + message: The server does not support this operation + type: UnsupportedOperationException + code: 406 + Apache_Iceberg_REST_Catalog_API_CreateNamespaceResponse: + type: object + required: + - namespace + properties: + namespace: + $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_Namespace' + properties: + type: object + additionalProperties: + type: string + description: Properties stored on the namespace, if supported by the server. + example: + owner: Ralph + created_at: '1452120468' + default: {} + Apache_Iceberg_REST_Catalog_API_GetNamespaceResponse: + type: object + required: + - namespace + properties: + namespace: + $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_Namespace' + properties: + type: object + description: >- + Properties stored on the namespace, if supported by the server. If + the server does not support namespace properties, it should return + null for this field. If namespace properties are supported, but none + are set, it should return an empty object. + additionalProperties: + type: string + example: + owner: Ralph + transient_lastDdlTime: '1452120468' + default: {} + nullable: true + Apache_Iceberg_REST_Catalog_API_ListTablesResponse: + type: object + properties: + next-page-token: + $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_PageToken' + identifiers: + type: array + uniqueItems: true + items: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_TableIdentifier + Apache_Iceberg_REST_Catalog_API_ListNamespacesResponse: + type: object + properties: + next-page-token: + $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_PageToken' + namespaces: + type: array + uniqueItems: true + items: + $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_Namespace' + Apache_Iceberg_REST_Catalog_API_UpdateNamespacePropertiesResponse: + type: object + required: + - updated + - removed + properties: + updated: + description: List of property keys that were added or updated + type: array + uniqueItems: true + items: + type: string + removed: + description: List of properties that were removed + type: array + items: + type: string + missing: + type: array + items: + type: string + description: >- + List of properties requested for removal that were not found in the + namespace's properties. Represents a partial success response. + Server's do not need to implement this. + nullable: true + Apache_Iceberg_REST_Catalog_API_CommitTableResponse: + type: object + required: + - metadata-location + - metadata + properties: + metadata-location: + type: string + metadata: + $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_TableMetadata' + Apache_Iceberg_REST_Catalog_API_StatisticsFile: + type: object + required: + - snapshot-id + - statistics-path + - file-size-in-bytes + - file-footer-size-in-bytes + - blob-metadata + properties: + snapshot-id: + type: integer + format: int64 + statistics-path: + type: string + file-size-in-bytes: + type: integer + format: int64 + file-footer-size-in-bytes: + type: integer + format: int64 + blob-metadata: + type: array + items: + $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_BlobMetadata' + Apache_Iceberg_REST_Catalog_API_BlobMetadata: + type: object + required: + - type + - snapshot-id + - sequence-number + - fields + properties: + type: + type: string + snapshot-id: + type: integer + format: int64 + sequence-number: + type: integer + format: int64 + fields: + type: array + items: + type: integer + properties: + type: object + Apache_Iceberg_REST_Catalog_API_PartitionStatisticsFile: + type: object + required: + - snapshot-id + - statistics-path + - file-size-in-bytes + properties: + snapshot-id: + type: integer + format: int64 + statistics-path: + type: string + file-size-in-bytes: + type: integer + format: int64 + Apache_Iceberg_REST_Catalog_API_BooleanTypeValue: + type: boolean + example: true + Apache_Iceberg_REST_Catalog_API_IntegerTypeValue: + type: integer + example: 42 + Apache_Iceberg_REST_Catalog_API_LongTypeValue: + type: integer + format: int64 + example: 9223372036854776000 + Apache_Iceberg_REST_Catalog_API_FloatTypeValue: + type: number + format: float + example: 3.14 + Apache_Iceberg_REST_Catalog_API_DoubleTypeValue: + type: number + format: double + example: 123.456 + Apache_Iceberg_REST_Catalog_API_DecimalTypeValue: + type: string + description: >- + Decimal type values are serialized as strings. Decimals with a positive + scale serialize as numeric plain text, while decimals with a negative + scale use scientific notation and the exponent will be equal to the + negated scale. For instance, a decimal with a positive scale is + '123.4500', with zero scale is '2', and with a negative scale is + '2E+20' + example: '123.4500' + Apache_Iceberg_REST_Catalog_API_StringTypeValue: + type: string + example: hello + Apache_Iceberg_REST_Catalog_API_UUIDTypeValue: + type: string + format: uuid + pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$ + maxLength: 36 + minLength: 36 + description: >- + UUID type values are serialized as a 36-character lowercase string in + standard UUID format as specified by RFC-4122 + example: eb26bdb1-a1d8-4aa6-990e-da940875492c + Apache_Iceberg_REST_Catalog_API_DateTypeValue: + type: string + format: date + description: Date type values follow the 'YYYY-MM-DD' ISO-8601 standard date format + example: '2007-12-03' + Apache_Iceberg_REST_Catalog_API_TimeTypeValue: + type: string + description: >- + Time type values follow the 'HH:MM:SS.ssssss' ISO-8601 format with + microsecond precision + example: '22:31:08.123456' + Apache_Iceberg_REST_Catalog_API_TimestampTypeValue: + type: string + description: >- + Timestamp type values follow the 'YYYY-MM-DDTHH:MM:SS.ssssss' ISO-8601 + format with microsecond precision + example: '2007-12-03T10:15:30.123456' + Apache_Iceberg_REST_Catalog_API_TimestampTzTypeValue: + type: string + description: >- + TimestampTz type values follow the 'YYYY-MM-DDTHH:MM:SS.ssssss+00:00' + ISO-8601 format with microsecond precision, and a timezone offset + (+00:00 for UTC) + example: '2007-12-03T10:15:30.123456+00:00' + Apache_Iceberg_REST_Catalog_API_TimestampNanoTypeValue: + type: string + description: >- + Timestamp_ns type values follow the 'YYYY-MM-DDTHH:MM:SS.sssssssss' + ISO-8601 format with nanosecond precision + example: '2007-12-03T10:15:30.123456789' + Apache_Iceberg_REST_Catalog_API_TimestampTzNanoTypeValue: + type: string + description: >- + Timestamp_ns type values follow the + 'YYYY-MM-DDTHH:MM:SS.sssssssss+00:00' ISO-8601 format with nanosecond + precision, and a timezone offset (+00:00 for UTC) + example: '2007-12-03T10:15:30.123456789+00:00' + Apache_Iceberg_REST_Catalog_API_FixedTypeValue: + type: string + description: >- + Fixed length type values are stored and serialized as an uppercase + hexadecimal string preserving the fixed length + example: 78797A + Apache_Iceberg_REST_Catalog_API_BinaryTypeValue: + type: string + description: >- + Binary type values are stored and serialized as an uppercase hexadecimal + string + example: 78797A + Apache_Iceberg_REST_Catalog_API_CountMap: + type: object + properties: + keys: + type: array + items: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_IntegerTypeValue + description: List of integer column ids for each corresponding value + values: + type: array + items: + $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_LongTypeValue' + description: List of Long values, matched to 'keys' by index + example: + keys: + - 1 + - 2 + values: + - 100 + - 200 + Apache_Iceberg_REST_Catalog_API_ValueMap: + type: object + properties: + keys: + type: array + items: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_IntegerTypeValue + description: List of integer column ids for each corresponding value + values: + type: array + items: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_PrimitiveTypeValue + description: List of primitive type values, matched to 'keys' by index + example: + keys: + - 1 + - 2 + values: + - 100 + - test + Apache_Iceberg_REST_Catalog_API_PrimitiveTypeValue: + oneOf: + - $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_BooleanTypeValue + - $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_IntegerTypeValue + - $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_LongTypeValue' + - $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_FloatTypeValue' + - $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_DoubleTypeValue' + - $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_DecimalTypeValue + - $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_StringTypeValue' + - $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_UUIDTypeValue' + - $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_DateTypeValue' + - $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_TimeTypeValue' + - $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_TimestampTypeValue + - $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_TimestampTzTypeValue + - $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_TimestampNanoTypeValue + - $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_TimestampTzNanoTypeValue + - $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_FixedTypeValue' + - $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_BinaryTypeValue' + Apache_Iceberg_REST_Catalog_API_FileFormat: + type: string + enum: + - avro + - orc + - parquet + Apache_Iceberg_REST_Catalog_API_ContentFile: + discriminator: + propertyName: content + mapping: + data: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_DataFile' + position-deletes: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_PositionDeleteFile + equality-deletes: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_EqualityDeleteFile + type: object + required: + - spec-id + - content + - file-path + - file-format + - file-size-in-bytes + - record-count + properties: + content: + type: string + file-path: + type: string + file-format: + $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_FileFormat' + spec-id: + type: integer + partition: + type: array + items: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_PrimitiveTypeValue + description: >- + A list of partition field values ordered based on the fields of the + partition spec specified by the `spec-id` + example: + - 1 + - bar + file-size-in-bytes: + type: integer + format: int64 + description: Total file size in bytes + record-count: + type: integer + format: int64 + description: Number of records in the file + key-metadata: + allOf: + - $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_BinaryTypeValue + description: Encryption key metadata blob + split-offsets: + type: array + items: + type: integer + format: int64 + description: List of splittable offsets + sort-order-id: + type: integer + Apache_Iceberg_REST_Catalog_API_DataFile: + allOf: + - $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_ContentFile' + type: object + required: + - content + properties: + content: + type: string + enum: + - data + column-sizes: + allOf: + - $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_CountMap' + description: Map of column id to total count, including null and NaN + value-counts: + allOf: + - $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_CountMap' + description: Map of column id to null value count + null-value-counts: + allOf: + - $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_CountMap' + description: Map of column id to null value count + nan-value-counts: + allOf: + - $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_CountMap' + description: Map of column id to number of NaN values in the column + lower-bounds: + allOf: + - $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_ValueMap' + description: Map of column id to lower bound primitive type values + upper-bounds: + allOf: + - $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_ValueMap' + description: Map of column id to upper bound primitive type values + Apache_Iceberg_REST_Catalog_API_PositionDeleteFile: + allOf: + - $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_ContentFile' + required: + - content + properties: + content: + type: string + enum: + - position-deletes + Apache_Iceberg_REST_Catalog_API_EqualityDeleteFile: + allOf: + - $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_ContentFile' + required: + - content + properties: + content: + type: string + enum: + - equality-deletes + equality-ids: + type: array + items: + type: integer + description: List of equality field IDs + parameters: + Apache_Iceberg_REST_Catalog_API_namespace: + name: namespace + in: path + required: true + description: >- + A namespace identifier as a single string. Multipart namespace parts + should be separated by the unit separator (`0x1F`) byte. + schema: + type: string + examples: + singlepart_namespace: + value: accounting + multipart_namespace: + value: accounting%1Ftax + Apache_Iceberg_REST_Catalog_API_prefix: + name: prefix + in: path + schema: + type: string + required: true + description: An optional prefix in the path + Apache_Iceberg_REST_Catalog_API_table: + name: table + in: path + description: A table name + required: true + schema: + type: string + example: sales + Apache_Iceberg_REST_Catalog_API_view: + name: view + in: path + description: A view name + required: true + schema: + type: string + example: sales + Apache_Iceberg_REST_Catalog_API_data-access: + name: X-Iceberg-Access-Delegation + in: header + description: > + Optional signal to the server that the client supports delegated access + via a comma-separated list of access mechanisms. The server may choose + to supply access via any or none of the requested mechanisms. + + + Specific properties and handling for `vended-credentials` is documented + in the `LoadTableResult` schema section of this spec document. + + + The protocol and specification for `remote-signing` is documented in + the `s3-signer-open-api.yaml` OpenApi spec in the `aws` module. + required: false + schema: + type: string + enum: + - vended-credentials + - remote-signing + style: simple + explode: false + example: vended-credentials,remote-signing + Apache_Iceberg_REST_Catalog_API_page-token: + name: pageToken + in: query + required: false + allowEmptyValue: true + schema: + $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_PageToken' + Apache_Iceberg_REST_Catalog_API_page-size: + name: pageSize + in: query + description: >- + For servers that support pagination, this signals an upper bound of the + number of results that a client will receive. For servers that do not + support pagination, clients may receive results larger than the + indicated `pageSize`. + required: false + schema: + type: integer + minimum: 1 + responses: + Apache_Iceberg_REST_Catalog_API_OAuthTokenResponse: + description: OAuth2 token response for client credentials or token exchange + content: + application/json: + schema: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_OAuthTokenResponse + Apache_Iceberg_REST_Catalog_API_OAuthErrorResponse: + description: OAuth2 error response + content: + application/json: + schema: + $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_OAuthError' + Apache_Iceberg_REST_Catalog_API_BadRequestErrorResponse: + description: >- + Indicates a bad request error. It could be caused by an unexpected + request body format or other forms of request validation failure, such + as invalid json. Usually serves application/json content, although in + some cases simple text/plain content might be returned by the server's + middleware. + content: + application/json: + schema: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_IcebergErrorResponse + example: + error: + message: Malformed request + type: BadRequestException + code: 400 + Apache_Iceberg_REST_Catalog_API_UnauthorizedResponse: + description: >- + Unauthorized. Authentication is required and has failed or has not yet + been provided. + content: + application/json: + schema: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_IcebergErrorResponse + example: + error: + message: Not authorized to make this request + type: NotAuthorizedException + code: 401 + Apache_Iceberg_REST_Catalog_API_ForbiddenResponse: + description: Forbidden. Authenticated user does not have the necessary permissions. + content: + application/json: + schema: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_IcebergErrorResponse + example: + error: + message: Not authorized to make this request + type: NotAuthorizedException + code: 403 + Apache_Iceberg_REST_Catalog_API_UnsupportedOperationResponse: + description: >- + Not Acceptable / Unsupported Operation. The server does not support this + operation. + content: + application/json: + schema: + $ref: '#/components/schemas/Apache_Iceberg_REST_Catalog_API_ErrorModel' + example: + error: + message: The server does not support this operation + type: UnsupportedOperationException + code: 406 + Apache_Iceberg_REST_Catalog_API_IcebergErrorResponse: + description: JSON wrapper for all error responses (non-2xx) + content: + application/json: + schema: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_IcebergErrorResponse + example: + error: + message: The server does not support this operation + type: UnsupportedOperationException + code: 406 + Apache_Iceberg_REST_Catalog_API_CreateNamespaceResponse: + description: >- + Represents a successful call to create a namespace. Returns the + namespace created, as well as any properties that were stored for the + namespace, including those the server might have added. Implementations + are not required to support namespace properties. + content: + application/json: + schema: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_CreateNamespaceResponse + example: + namespace: + - accounting + - tax + properties: + owner: Ralph + created_at: '1452120468' + Apache_Iceberg_REST_Catalog_API_GetNamespaceResponse: + description: >- + Returns a namespace, as well as any properties stored on the namespace + if namespace properties are supported by the server. + content: + application/json: + schema: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_GetNamespaceResponse + Apache_Iceberg_REST_Catalog_API_ListTablesResponse: + description: A list of table identifiers + content: + application/json: + schema: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_ListTablesResponse + examples: + ListTablesResponseNonEmpty: + $ref: >- + #/components/examples/Apache_Iceberg_REST_Catalog_API_ListTablesNonEmptyExample + ListTablesResponseEmpty: + $ref: >- + #/components/examples/Apache_Iceberg_REST_Catalog_API_ListTablesEmptyExample + Apache_Iceberg_REST_Catalog_API_ListNamespacesResponse: + description: A list of namespaces + content: + application/json: + schema: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_ListNamespacesResponse + examples: + NonEmptyResponse: + $ref: >- + #/components/examples/Apache_Iceberg_REST_Catalog_API_ListNamespacesNonEmptyExample + EmptyResponse: + $ref: >- + #/components/examples/Apache_Iceberg_REST_Catalog_API_ListNamespacesEmptyExample + Apache_Iceberg_REST_Catalog_API_AuthenticationTimeoutResponse: + description: >- + Credentials have timed out. If possible, the client should refresh + credentials and retry. + content: + application/json: + schema: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_IcebergErrorResponse + example: + error: + message: Credentials have timed out + type: AuthenticationTimeoutException + code: 419 + Apache_Iceberg_REST_Catalog_API_ServiceUnavailableResponse: + description: >- + The service is not ready to handle the request. The client should wait + and retry. + + + The service may additionally send a Retry-After header to indicate when + to retry. + content: + application/json: + schema: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_IcebergErrorResponse + example: + error: + message: Slow down + type: SlowDownException + code: 503 + Apache_Iceberg_REST_Catalog_API_ServerErrorResponse: + description: >- + A server-side problem that might not be addressable from the client + side. Used for server 5xx errors without more specific documentation in + individual routes. + content: + application/json: + schema: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_IcebergErrorResponse + example: + error: + message: Internal Server Error + type: InternalServerError + code: 500 + Apache_Iceberg_REST_Catalog_API_UpdateNamespacePropertiesResponse: + description: JSON data response for a synchronous update properties request. + content: + application/json: + schema: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_UpdateNamespacePropertiesResponse + example: + updated: + - owner + removed: + - foo + missing: + - bar + Apache_Iceberg_REST_Catalog_API_CreateTableResponse: + description: Table metadata result after creating a table + content: + application/json: + schema: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_LoadTableResult + Apache_Iceberg_REST_Catalog_API_LoadTableResponse: + description: Table metadata result when loading a table + content: + application/json: + schema: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_LoadTableResult + Apache_Iceberg_REST_Catalog_API_LoadViewResponse: + description: View metadata result when loading a view + content: + application/json: + schema: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_LoadViewResult + Apache_Iceberg_REST_Catalog_API_CommitTableResponse: + description: >- + Response used when a table is successfully updated. + + The table metadata JSON is returned in the metadata field. The + corresponding file location of table metadata must be returned in the + metadata-location field. Clients can check whether metadata has changed + by comparing metadata locations. + content: + application/json: + schema: + $ref: >- + #/components/schemas/Apache_Iceberg_REST_Catalog_API_CommitTableResponse + examples: + Apache_Iceberg_REST_Catalog_API_ListTablesEmptyExample: + summary: An empty list for a namespace with no tables + value: + identifiers: [] + Apache_Iceberg_REST_Catalog_API_ListNamespacesEmptyExample: + summary: An empty list of namespaces + value: + namespaces: [] + Apache_Iceberg_REST_Catalog_API_ListNamespacesNonEmptyExample: + summary: A non-empty list of namespaces + value: + namespaces: + - - accounting + - tax + - - accounting + - credits + Apache_Iceberg_REST_Catalog_API_ListTablesNonEmptyExample: + summary: A non-empty list of table identifiers + value: + identifiers: + - namespace: + - accounting + - tax + name: paid + - namespace: + - accounting + - tax + name: owed + Apache_Iceberg_REST_Catalog_API_MultipartNamespaceAsPathVariable: + summary: A multi-part namespace, as represented in a path parameter + value: accounting%1Ftax + Apache_Iceberg_REST_Catalog_API_NamespaceAsPathVariable: + summary: A single part namespace, as represented in a path paremeter + value: accounting + Apache_Iceberg_REST_Catalog_API_NamespaceAlreadyExistsError: + summary: The requested namespace already exists + value: + error: + message: The given namespace already exists + type: AlreadyExistsException + code: 409 + Apache_Iceberg_REST_Catalog_API_NoSuchTableError: + summary: The requested table does not exist + value: + error: + message: The given table does not exist + type: NoSuchTableException + code: 404 + Apache_Iceberg_REST_Catalog_API_NoSuchViewError: + summary: The requested view does not exist + value: + error: + message: The given view does not exist + type: NoSuchViewException + code: 404 + Apache_Iceberg_REST_Catalog_API_NoSuchNamespaceError: + summary: The requested namespace does not exist + value: + error: + message: The given namespace does not exist + type: NoSuchNamespaceException + code: 404 + Apache_Iceberg_REST_Catalog_API_RenameTableSameNamespace: + summary: Rename a table in the same namespace + value: + source: + namespace: + - accounting + - tax + name: paid + destination: + namespace: + - accounting + - tax + name: owed + Apache_Iceberg_REST_Catalog_API_RenameViewSameNamespace: + summary: Rename a view in the same namespace + value: + source: + namespace: + - accounting + - tax + name: paid-view + destination: + namespace: + - accounting + - tax + name: owed-view + Apache_Iceberg_REST_Catalog_API_TableAlreadyExistsError: + summary: The requested table identifier already exists + value: + error: + message: The given table already exists + type: AlreadyExistsException + code: 409 + Apache_Iceberg_REST_Catalog_API_ViewAlreadyExistsError: + summary: The requested view identifier already exists + value: + error: + message: The given view already exists + type: AlreadyExistsException + code: 409 + Apache_Iceberg_REST_Catalog_API_UnprocessableEntityDuplicateKey: + summary: >- + The request body either has the same key multiple times in what should + be a map with unique keys or the request body has keys in two or more + fields which should be disjoint sets. + value: + error: + message: >- + The request cannot be processed as there is a key present multiple + times + type: UnprocessableEntityException + code: 422 + Apache_Iceberg_REST_Catalog_API_UpdateAndRemoveNamespacePropertiesRequest: + summary: >- + An update namespace properties request with both properties to remove + and properties to upsert. + value: + removals: + - foo + - bar + updates: + owner: Raoul +x-tagGroups: + - name: Polaris Catalog Documentation + tags: + - Polaris Catalog Overview + - Polaris Catalog Entities + - Access Control + - name: Polaris Management Service + tags: + - polaris-management-service_other + - name: Apache Iceberg REST Catalog API + tags: + - Configuration API + - OAuth2 API + - Catalog API diff --git a/spec/redocly.yaml b/spec/redocly.yaml new file mode 100644 index 0000000000..d410af6aaa --- /dev/null +++ b/spec/redocly.yaml @@ -0,0 +1,11 @@ +theme: + openapi: + theme: + typography: + fontFamily: 'Roboto, sans-serif' + logo: + gutter: '15px' + +seo: + title: Polaris Catalog Documentation + description: Learn how to work with the Open Source Polaris Catalog for Apache Iceberg \ No newline at end of file From a955dd4db4481a53976d94ef54091d5985b7adfd Mon Sep 17 00:00:00 2001 From: Robert Stupp Date: Tue, 30 Jul 2024 02:55:30 +0200 Subject: [PATCH 19/27] Load projects from properties file (#42) Fixes #41 Co-authored-by: Michael Collado <40346148+collado-mike@users.noreply.github.com> --- gradle/projects.main.properties | 20 ++++++++++++++++++++ settings.gradle | 19 +++++++++++-------- 2 files changed, 31 insertions(+), 8 deletions(-) create mode 100644 gradle/projects.main.properties diff --git a/gradle/projects.main.properties b/gradle/projects.main.properties new file mode 100644 index 0000000000..d0ecccac37 --- /dev/null +++ b/gradle/projects.main.properties @@ -0,0 +1,20 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# +# + +polaris-core=polaris-core +polaris-service=polaris-service +polaris-eclipselink=extension/persistence/eclipselink diff --git a/settings.gradle b/settings.gradle index 45a92275ec..5f58877c66 100644 --- a/settings.gradle +++ b/settings.gradle @@ -25,11 +25,14 @@ if (!JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_21)) { """) } -rootProject.name = 'polaris' - -include 'polaris-core' -include 'polaris-service' -include 'extension:persistence:hibernate' -include 'extension:persistence:eclipselink' - -project(":extension:persistence:eclipselink").name = "polaris-eclipselink" +rootProject.name = "polaris" + +Properties projects = new Properties() +file("gradle/projects.main.properties").withInputStream { projects.load(it) } +projects.entrySet().forEach { + final def name = it.key as String + include(name) + final def prj = project(":${name}") + prj.name = name + prj.projectDir = file(it.value) +} From f54de9bb21e484f035d265eff517caf8e5540343 Mon Sep 17 00:00:00 2001 From: Michael Collado <40346148+collado-mike@users.noreply.github.com> Date: Mon, 29 Jul 2024 20:09:58 -0700 Subject: [PATCH 20/27] Merge OAuth token refresh and directory overlap prevention commits (#46) * Squashed commit of the following: Co-authored-by: Evgeny Zubatov Co-authored-by: Michael Collado Co-authored-by: Shannon Chen Co-authored-by: Eric Maynard Co-authored-by: Alvin Chen commit de0b4ee768a62221a480dce7da935a27a206d076 Merge: 1c19fc8 85e69a3 Author: Michael Collado Date: Mon Jul 29 16:36:25 2024 -0700 Merge commit '3e6e01aae203356ed972502dfa596d04ec5a8ca5' into mcollado-merge-oss commit 1c19fc877231e34d5e8baa4a05902d13f6120050 Author: Michael Collado Date: Mon Jul 29 16:25:05 2024 -0700 Merge polaris-dev OSS contributions commit a3fbf4ce4bc6c629bef308349b7c7a64c8335ac9 Author: Michael Collado Date: Mon Jul 29 15:43:23 2024 -0700 Fix token refresh in oauth service to work with client credentials (#37) The Iceberg REST client _does_ retry refreshing the auth token with client credentials, but it submits them in Basic Auth form rather than as form parameters. We need to base64/url-decode them in order to validate the credentials are correct. We also need to return an accepted tokenType during refresh. Tested with ```java RESTSessionCatalog sessionCatalog = new RESTSessionCatalog(config -> HTTPClient.builder(config).uri(config.get(CatalogProperties.URI)).build(), null); sessionCatalog.initialize("demo", Map.of( "uri", "http://localhost:8181/api/catalog", "prefix", "catalog", "credential", "$URLENCODED_CLIENTID:$URLENCODED_CLIENTSECRET", "scope", "PRINCIPAL_ROLE:ALL", "token-refresh-enabled", "true", "warehouse", "martins_demo1" )); Field catalogAuth = RESTSessionCatalog.class.getDeclaredField("catalogAuth"); catalogAuth.setAccessible(true); OAuth2Util.AuthSession authSession = (OAuth2Util.AuthSession) catalogAuth.get(sessionCatalog); Field client = RESTSessionCatalog.class.getDeclaredField("client");; client.setAccessible(true); RESTClient restClient = (RESTClient) client.get(sessionCatalog); for (int i = 0; i < 10; i++) { System.out.println(authSession.refresh(restClient)); Thread.sleep(10000); } ``` commit 517cb6231d424fac59ceecb1845bdb0a3e065265 Author: Michael Collado Date: Mon Jul 29 10:47:32 2024 -0700 Changed reg test docker image to stop exposing aws credentials as env variables (#36) In the reg tests, when S3 credentials aren't present in the FileIO, the S3 client is falling back to the credentials in the environment variables, which have access to everything. This caused a previous bug to go uncaught. I verified that if I don't update the FileIO for a table, these tests fail now. commit e418aefe8964c7c67b509f8eec43055f1c17a742 Author: Michael Collado Date: Mon Jul 29 08:40:58 2024 -0700 Fix error with catalog FileIO credentials and path construction (#35) Namespace directories were being constructed backwards. Unsure why the tests didn't catch this FileIO for table creation and update was also not updating credentials correctly due to several `fileIO` variables in scope and updating the wrong one. I've renamed the variables to be more clear what each fileIO is scoped to. Future change upcoming to improve reg tests to catch issues - fileIO is falling back to environment variables in the docker image commit 7f37bb252cec7df07db3994bf981efe76a52639c Author: Michael Collado Date: Mon Jul 29 02:05:14 2024 -0700 Fix validation to read table metadata file after fileio initialization with credentials (#34) TableMetadataParser was reading metadata file before FileIO was initialized with credentials. This was uncaught in the tests because the FileIO reads the test image's environment variables. commit 0866f3ad54295a6d7822b9645f59996986d23acd Author: Michael Collado Date: Sun Jul 28 22:10:22 2024 -0700 Fixed issue when creating table under namespace with custom location (#33) Tables were still being created with the default directory structure when their parent namespace had a custom location. This fixes the issue and adds a test proving the table is successfully created and that its location is under the custom namespace directory commit ee701ff99120b948b5ed3120461a9aaf0842f944 Author: Michael Collado Date: Sun Jul 28 20:53:52 2024 -0700 Disallow overlapping base locations for table and namespaces and prevent table location from escaping via metadata file (#32) Two major changes included here: * Disables table locations and namespace locations from overlapping. Since namespaces can't overlap and tables can't overlap sibling namespaces or sibling tables, this prevents all table locations from overlapping within a catalog * Prevents metadata files from pointing at table locations outside of the table's base location Both of these features are controllable by feature flags. Because of existing unit and integration tests (notably the ones we import from Iceberg), I also made the TableMetadata location check and the namespace directory checking configurable at the catalog level. However, the overlap checking within a namespace is not configurable at the catalog level (this means there's a gap so that if a catalog allows metadata files to point to locations outside of the namespace directory, a table's location could overlap with a table in another directory). It is possible for a table or a namespace to _set_ its base-directory to something other than the default. However, tables and namespaces with overridden base locations still cannot overlap with sibling tables or namespaces. commit 51ac320f60c103c2b10cd8d2910217010f38afdd Author: Shannon Chen Date: Sun Jul 28 23:13:59 2024 +0000 The loadTable is current scoped to table location, this PR makes 1. loadTable only scoped to table location + `metadata/` and `data/`. 2. when refreshTable keep it only scoped to table location + `metadata` 3. Throw user error when the user specify `write.metadata.path` or `write.data.path` commit 838ba65849e7f4f0dd5dfcac0093262a165e52e4 Author: Eric Maynard Date: Fri Jul 26 22:04:41 2024 -0700 CREATE_TABLE shouldn't return credentials to read the table if the user doesn't have that privilege (#29) Before CREATE_TABLE returns credentials that can be used to read a table, Polaris should validate that the user has TABLE_READ_DATA on that table. commit 96257f4fa54372fb565954024cbfe256de5d6f20 Author: Alvin Chen Date: Fri Jul 26 15:43:35 2024 -0700 Call metric init on IcebergRestConfigurationApi and IcebergRestOAuth2Api class (#30) commit d6e057778811f20a462ad2746722e1a1427197cf Author: Alvin Chen Date: Fri Jul 26 10:22:34 2024 -0700 Switch Metrics Emission from Ad-Hoc to Initialization-Based (#28) Metrics are currently emitted in an ad-hoc fashion, meaning they will only be queriable in Prometheus if the corresponding API endpoint is invoked. This makes plotting difficult in Grafana, especially in the case where we need to aggregate across multiple metrics to reflect, for instance, the overall error rate across all endpoints in the application. Say we have metrics a, b, c. If b is null since the corresponding API has not yet been invoked, a+b+c would result in the value null, instead of the sum of a and c. This can be fixed in Grafana by "filling in" the metrics with a metric that is guaranteed to be present, say `up`. The promql query will then become: `(a or (up * 0)) + (b or (up * 0)) + (c or (up * 0))` However, the query becomes extremely large and slow. Thus to avoid this, we'll make sure the metrics are always emitted regardless of the corresponding API being called. We also add a counter metric to each endpoint to track the total number of invokes. Previously we had timers who have an attribute `count`. However, they are only incremented on successes (since we only record timer on success), therefore, they did not incorporate failures in the counts. commit 20bf59b9b5e8dba9a35b932f036114b85375829b Author: Eric Maynard Date: Fri Jul 26 10:09:50 2024 -0700 When a catalog is created or updated, we should check that the catalog does not have a location which overlaps with the location of an existing catalog. * Squashed commit of the following: commit b65dbc68c43e7c3ff0e1901e516c9749fda58ced Author: Michael Collado Date: Mon Jul 29 17:24:10 2024 -0700 Fix Gradle formatting error in merge * Update aws config to check for blank instead of null to address reg tests when aws keys are not available --- .gitignore | 7 +- docker-compose.yml | 10 +- gradle/wrapper/gradle-wrapper.properties | 2 +- .../io/polaris/core/PolarisConfiguration.java | 20 + .../polaris/core/entity/NamespaceEntity.java | 15 +- .../io/polaris/core/entity/PolarisEntity.java | 5 + .../core/entity/PolarisEntityConstants.java | 1 + .../polaris/core/entity/TableLikeEntity.java | 13 + .../core/monitor/PolarisMetricRegistry.java | 78 ++- .../LocalPolarisMetaStoreManagerFactory.java | 4 +- .../persistence/MetaStoreManagerFactory.java | 4 +- .../io/polaris/core/resource/TimedApi.java | 26 + .../storage/FileStorageConfigurationInfo.java | 20 +- .../storage/PolarisCredentialProperty.java | 3 +- .../PolarisStorageConfigurationInfo.java | 100 ++- .../storage/StorageConfigurationOverride.java | 39 ++ .../io/polaris/core/storage/StorageUtil.java | 25 + .../aws/AwsCredentialsStorageIntegration.java | 9 +- .../aws/AwsStorageConfigurationInfo.java | 6 +- .../azure/AzureStorageConfigurationInfo.java | 11 +- .../gcp/GcpStorageConfigurationInfo.java | 4 +- .../polaris/service/PolarisApplication.java | 32 +- .../TimedApplicationEventListener.java | 16 +- .../service/admin/PolarisAdminService.java | 89 ++- .../service/auth/DefaultOAuth2ApiService.java | 40 +- .../service/auth/TestOAuth2ApiService.java | 1 + .../service/catalog/BasePolarisCatalog.java | 610 ++++++++++++++---- .../catalog/PolarisCatalogHandlerWrapper.java | 80 ++- .../config/PolarisApplicationConfig.java | 25 + .../context/CallContextCatalogFactory.java | 6 +- .../PolarisCallContextCatalogFactory.java | 8 +- .../SqlliteCallContextCatalogFactory.java | 5 +- ...PolarisStorageIntegrationProviderImpl.java | 11 +- .../PolarisApplicationIntegrationTest.java | 58 +- .../service/admin/PolarisAuthzTestBase.java | 23 +- .../admin/PolarisOverlappingCatalogTest.java | 192 ++++++ .../PolarisServiceImplIntegrationTest.java | 2 +- .../catalog/BasePolarisCatalogTest.java | 230 ++++++- .../catalog/BasePolarisCatalogViewTest.java | 10 +- ...PolarisCatalogHandlerWrapperAuthzTest.java | 47 +- .../PolarisRestCatalogIntegrationTest.java | 206 +++++- ...PolarisRestCatalogViewIntegrationTest.java | 15 +- .../polaris-server-integrationtest.yml | 1 + regtests/setup.sh | 2 +- regtests/t_pyspark/src/conftest.py | 4 +- .../src/test_spark_sql_s3_with_privileges.py | 296 ++++++++- server-templates/api.mustache | 2 +- spec/rest-catalog-open-api.yaml | 6 + 48 files changed, 2123 insertions(+), 296 deletions(-) create mode 100644 polaris-core/src/main/java/io/polaris/core/resource/TimedApi.java create mode 100644 polaris-core/src/main/java/io/polaris/core/storage/StorageConfigurationOverride.java create mode 100644 polaris-core/src/main/java/io/polaris/core/storage/StorageUtil.java create mode 100644 polaris-service/src/test/java/io/polaris/service/admin/PolarisOverlappingCatalogTest.java diff --git a/.gitignore b/.gitignore index 04af1c3247..0e6e0be8c9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ -polaris-service/logs/ +regtests/derby.log +regtests/metastore_db regtests/output/ # Notebooks @@ -7,6 +8,10 @@ notebooks/.ipynb_checkpoints/ # Metastore metastore_db/ +.gradle +**/build/ +!src/**/build/ + # Ignore Gradle GUI config gradle-app.setting diff --git a/docker-compose.yml b/docker-compose.yml index 1c48429629..267d8a2d74 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -24,12 +24,18 @@ services: - "8182" environment: AWS_REGION: us-west-2 - AWS_ACCESS_KEY_ID: $AWS_ACCESS_KEY_ID - AWS_SECRET_ACCESS_KEY: $AWS_SECRET_ACCESS_KEY GOOGLE_APPLICATION_CREDENTIALS: $GOOGLE_APPLICATION_CREDENTIALS AZURE_TENANT_ID: $AZURE_TENANT_ID AZURE_CLIENT_ID: $AZURE_CLIENT_ID AZURE_CLIENT_SECRET: $AZURE_CLIENT_SECRET + command: # override the command to specify aws keys as dropwizard config + - java + - -Ddw.awsAccessKey=$AWS_ACCESS_KEY_ID + - -Ddw.awsSecretKey=$AWS_SECRET_ACCESS_KEY + - -jar + - /app/polaris-service-1.0.0-all.jar + - server + - polaris-server.yml volumes: - credentials:/tmp/credentials/ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index f24d7559ef..abc941ef79 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -17,7 +17,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists # See https://gradle.org/release-checksums/ for valid checksums -distributionSha256Sum=d725d707bfabd4dfdc958c624003b3c80accc03f7037b5122c4b1d0ef15cecab +distributionSha256Sum=d725d707bfabd4dfdc958c624003b3c80accc03f7037b5122c4b1d0ef15cecab # pragma: allowlist secret distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip networkTimeout=10000 validateDistributionUrl=true diff --git a/polaris-core/src/main/java/io/polaris/core/PolarisConfiguration.java b/polaris-core/src/main/java/io/polaris/core/PolarisConfiguration.java index a297f05181..177ab5afdd 100644 --- a/polaris-core/src/main/java/io/polaris/core/PolarisConfiguration.java +++ b/polaris-core/src/main/java/io/polaris/core/PolarisConfiguration.java @@ -19,6 +19,26 @@ public class PolarisConfiguration { public static final String ENFORCE_PRINCIPAL_CREDENTIAL_ROTATION_REQUIRED_CHECKING = "ENFORCE_PRINCIPAL_CREDENTIAL_ROTATION_REQUIRED_CHECKING"; + public static final String ALLOW_TABLE_LOCATION_OVERLAP = "ALLOW_TABLE_LOCATION_OVERLAP"; + public static final String ALLOW_NAMESPACE_LOCATION_OVERLAP = "ALLOW_NAMESPACE_LOCATION_OVERLAP"; + public static final String ALLOW_EXTERNAL_METADATA_FILE_LOCATION = + "ALLOW_EXTERNAL_METADATA_FILE_LOCATION"; + + public static final String ALLOW_OVERLAPPING_CATALOG_URLS = "ALLOW_OVERLAPPING_CATALOG_URLS"; + + public static final String CATALOG_ALLOW_UNSTRUCTURED_TABLE_LOCATION = + "allow.unstructured.table.location"; + public static final String CATALOG_ALLOW_EXTERNAL_TABLE_LOCATION = + "allow.external.table.location"; + + /* + * Default values for the configuration properties + */ + + public static final boolean DEFAULT_ALLOW_OVERLAPPING_CATALOG_URLS = false; + public static final boolean DEFAULT_ALLOW_TABLE_LOCATION_OVERLAP = false; + public static final boolean DEFAULT_ALLOW_EXTERNAL_METADATA_FILE_LOCATION = false; + public static final boolean DEFAULT_ALLOW_NAMESPACE_LOCATION_OVERLAP = false; private PolarisConfiguration() {} } diff --git a/polaris-core/src/main/java/io/polaris/core/entity/NamespaceEntity.java b/polaris-core/src/main/java/io/polaris/core/entity/NamespaceEntity.java index 8f5998b651..523b47cb42 100644 --- a/polaris-core/src/main/java/io/polaris/core/entity/NamespaceEntity.java +++ b/polaris-core/src/main/java/io/polaris/core/entity/NamespaceEntity.java @@ -15,6 +15,7 @@ */ package io.polaris.core.entity; +import com.fasterxml.jackson.annotation.JsonIgnore; import io.polaris.core.catalog.PolarisCatalogHelpers; import org.apache.iceberg.catalog.Namespace; import org.apache.iceberg.rest.RESTUtil; @@ -56,6 +57,11 @@ public Namespace asNamespace() { return Namespace.of(levels); } + @JsonIgnore + public String getBaseLocation() { + return getPropertiesAsMap().get(PolarisEntityConstants.ENTITY_BASE_LOCATION); + } + public static class Builder extends PolarisEntity.BaseBuilder { public Builder(Namespace namespace) { super(); @@ -64,8 +70,9 @@ public Builder(Namespace namespace) { setName(namespace.level(namespace.length() - 1)); } - public NamespaceEntity build() { - return new NamespaceEntity(buildBase()); + public Builder setBaseLocation(String baseLocation) { + properties.put(PolarisEntityConstants.ENTITY_BASE_LOCATION, baseLocation); + return this; } public Builder setParentNamespace(Namespace namespace) { @@ -74,5 +81,9 @@ public Builder setParentNamespace(Namespace namespace) { } return this; } + + public NamespaceEntity build() { + return new NamespaceEntity(buildBase()); + } } } diff --git a/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntity.java b/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntity.java index a421fc09cf..9dbf8b97d9 100644 --- a/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntity.java +++ b/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntity.java @@ -400,6 +400,11 @@ public B setProperties(Map properties) { return (B) this; } + public B addProperty(String key, String value) { + this.properties.put(key, value); + return (B) this; + } + public B setInternalProperties(Map internalProperties) { this.internalProperties = new HashMap<>(internalProperties); return (B) this; diff --git a/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntityConstants.java b/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntityConstants.java index b7031b3fb3..d3f562ddf4 100644 --- a/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntityConstants.java +++ b/polaris-core/src/main/java/io/polaris/core/entity/PolarisEntityConstants.java @@ -17,6 +17,7 @@ public class PolarisEntityConstants { + public static final String ENTITY_BASE_LOCATION = "location"; // the key for the client_id property associated with a principal private static final String CLIENT_ID_PROPERTY_NAME = "client_id"; diff --git a/polaris-core/src/main/java/io/polaris/core/entity/TableLikeEntity.java b/polaris-core/src/main/java/io/polaris/core/entity/TableLikeEntity.java index 3cf80a4739..6aab5d2c6f 100644 --- a/polaris-core/src/main/java/io/polaris/core/entity/TableLikeEntity.java +++ b/polaris-core/src/main/java/io/polaris/core/entity/TableLikeEntity.java @@ -25,6 +25,9 @@ public class TableLikeEntity extends PolarisEntity { // of the internalProperties JSON file. public static final String METADATA_LOCATION_KEY = "metadata-location"; + public static final String USER_SPECIFIED_WRITE_DATA_LOCATION_KEY = "write.data.path"; + public static final String USER_SPECIFIED_WRITE_METADATA_LOCATION_KEY = "write.metadata.path"; + public TableLikeEntity(PolarisBaseEntity sourceEntity) { super(sourceEntity); } @@ -57,6 +60,11 @@ public String getMetadataLocation() { return getInternalPropertiesAsMap().get(METADATA_LOCATION_KEY); } + @JsonIgnore + public String getBaseLocation() { + return getPropertiesAsMap().get(PolarisEntityConstants.ENTITY_BASE_LOCATION); + } + public static class Builder extends PolarisEntity.BaseBuilder { public Builder(TableIdentifier identifier, String metadataLocation) { super(); @@ -88,6 +96,11 @@ public Builder setParentNamespace(Namespace namespace) { return this; } + public Builder setBaseLocation(String location) { + properties.put(PolarisEntityConstants.ENTITY_BASE_LOCATION, location); + return this; + } + public Builder setMetadataLocation(String location) { internalProperties.put(METADATA_LOCATION_KEY, location); return this; diff --git a/polaris-core/src/main/java/io/polaris/core/monitor/PolarisMetricRegistry.java b/polaris-core/src/main/java/io/polaris/core/monitor/PolarisMetricRegistry.java index 1bef08a247..b3b8779f1d 100644 --- a/polaris-core/src/main/java/io/polaris/core/monitor/PolarisMetricRegistry.java +++ b/polaris-core/src/main/java/io/polaris/core/monitor/PolarisMetricRegistry.java @@ -18,21 +18,25 @@ import io.micrometer.core.instrument.Counter; import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.Timer; +import io.polaris.core.resource.TimedApi; +import java.lang.reflect.Method; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.TimeUnit; /** - * Manages metrics for Polaris applications, providing functionality to record timers and increment - * error counters. Also records the same for a realm-specific metric by appending a suffix and - * tagging with the realm ID. Utilizes Micrometer for metrics collection. + * Wrapper around the Micrometer {@link MeterRegistry} providing additional metric management + * functions for the Polaris application. Implements in-memory caching of timers and counters. + * Records two metrics for each instrument with one tagged by the realm ID (realm-specific metric) + * and one without. The realm-specific metric is suffixed with ".realm". */ public class PolarisMetricRegistry { private final MeterRegistry meterRegistry; private final ConcurrentMap timers = new ConcurrentHashMap<>(); - private final ConcurrentMap errorCounters = new ConcurrentHashMap<>(); + private final ConcurrentMap counters = new ConcurrentHashMap<>(); private static final String TAG_REALM = "REALM_ID"; private static final String TAG_RESP_CODE = "HTTP_RESPONSE_CODE"; + private static final String SUFFIX_COUNTER = ".count"; private static final String SUFFIX_ERROR = ".error"; private static final String SUFFIX_REALM = ".realm"; @@ -40,6 +44,31 @@ public PolarisMetricRegistry(MeterRegistry meterRegistry) { this.meterRegistry = meterRegistry; } + public MeterRegistry getMeterRegistry() { + return meterRegistry; + } + + public void init(Class... classes) { + for (Class clazz : classes) { + Method[] methods = clazz.getDeclaredMethods(); + for (Method method : methods) { + if (method.isAnnotationPresent(TimedApi.class)) { + TimedApi timedApi = method.getAnnotation(TimedApi.class); + String metric = timedApi.value(); + timers.put(metric, Timer.builder(metric).register(meterRegistry)); + counters.put( + metric + SUFFIX_COUNTER, + Counter.builder(metric + SUFFIX_COUNTER).register(meterRegistry)); + + // Error counters contain the HTTP response code in a tag, thus caching them would not be + // meaningful. + Counter.builder(metric + SUFFIX_ERROR).tags(TAG_RESP_CODE, "400").register(meterRegistry); + Counter.builder(metric + SUFFIX_ERROR).tags(TAG_RESP_CODE, "500").register(meterRegistry); + } + } + } + } + public void recordTimer(String metric, long elapsedTimeMs, String realmId) { Timer timer = timers.computeIfAbsent(metric, m -> Timer.builder(metric).register(meterRegistry)); @@ -55,25 +84,34 @@ public void recordTimer(String metric, long elapsedTimeMs, String realmId) { timerRealm.record(elapsedTimeMs, TimeUnit.MILLISECONDS); } - public void incrementErrorCounter(String metric, int statusCode, String realmId) { - String errorMetric = metric + SUFFIX_ERROR; - Counter errorCounter = - errorCounters.computeIfAbsent( - errorMetric, - m -> - Counter.builder(errorMetric) - .tag(TAG_RESP_CODE, String.valueOf(statusCode)) - .register(meterRegistry)); - errorCounter.increment(); + public void incrementCounter(String metric, String realmId) { + String counterMetric = metric + SUFFIX_COUNTER; + Counter counter = + counters.computeIfAbsent( + counterMetric, m -> Counter.builder(counterMetric).register(meterRegistry)); + counter.increment(); - Counter errorCounterRealm = - errorCounters.computeIfAbsent( - errorMetric + SUFFIX_REALM, + Counter counterRealm = + counters.computeIfAbsent( + counterMetric + SUFFIX_REALM, m -> - Counter.builder(errorMetric + SUFFIX_REALM) - .tag(TAG_RESP_CODE, String.valueOf(statusCode)) + Counter.builder(counterMetric + SUFFIX_REALM) .tag(TAG_REALM, realmId) .register(meterRegistry)); - errorCounterRealm.increment(); + counterRealm.increment(); + } + + public void incrementErrorCounter(String metric, int statusCode, String realmId) { + String errorMetric = metric + SUFFIX_ERROR; + Counter.builder(errorMetric) + .tag(TAG_RESP_CODE, String.valueOf(statusCode)) + .register(meterRegistry) + .increment(); + + Counter.builder(errorMetric + SUFFIX_REALM) + .tag(TAG_RESP_CODE, String.valueOf(statusCode)) + .tag(TAG_REALM, realmId) + .register(meterRegistry) + .increment(); } } diff --git a/polaris-core/src/main/java/io/polaris/core/persistence/LocalPolarisMetaStoreManagerFactory.java b/polaris-core/src/main/java/io/polaris/core/persistence/LocalPolarisMetaStoreManagerFactory.java index 3ff5d54cbf..70a92331a8 100644 --- a/polaris-core/src/main/java/io/polaris/core/persistence/LocalPolarisMetaStoreManagerFactory.java +++ b/polaris-core/src/main/java/io/polaris/core/persistence/LocalPolarisMetaStoreManagerFactory.java @@ -15,7 +15,6 @@ */ package io.polaris.core.persistence; -import io.micrometer.core.instrument.MeterRegistry; import io.polaris.core.PolarisCallContext; import io.polaris.core.PolarisDefaultDiagServiceImpl; import io.polaris.core.PolarisDiagnostics; @@ -26,6 +25,7 @@ import io.polaris.core.entity.PolarisEntitySubType; import io.polaris.core.entity.PolarisEntityType; import io.polaris.core.entity.PolarisPrincipalSecrets; +import io.polaris.core.monitor.PolarisMetricRegistry; import io.polaris.core.storage.PolarisStorageIntegrationProvider; import io.polaris.core.storage.cache.StorageCredentialCache; import java.util.HashMap; @@ -124,7 +124,7 @@ public synchronized StorageCredentialCache getOrCreateStorageCredentialCache( } @Override - public void setMetricRegistry(MeterRegistry metricRegistry) { + public void setMetricRegistry(PolarisMetricRegistry metricRegistry) { // no-op } diff --git a/polaris-core/src/main/java/io/polaris/core/persistence/MetaStoreManagerFactory.java b/polaris-core/src/main/java/io/polaris/core/persistence/MetaStoreManagerFactory.java index 25a215c655..199778dedb 100644 --- a/polaris-core/src/main/java/io/polaris/core/persistence/MetaStoreManagerFactory.java +++ b/polaris-core/src/main/java/io/polaris/core/persistence/MetaStoreManagerFactory.java @@ -17,8 +17,8 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo; import io.dropwizard.jackson.Discoverable; -import io.micrometer.core.instrument.MeterRegistry; import io.polaris.core.context.RealmContext; +import io.polaris.core.monitor.PolarisMetricRegistry; import io.polaris.core.storage.PolarisStorageIntegrationProvider; import io.polaris.core.storage.cache.StorageCredentialCache; import java.util.List; @@ -40,7 +40,7 @@ public interface MetaStoreManagerFactory extends Discoverable { void setStorageIntegrationProvider(PolarisStorageIntegrationProvider storageIntegrationProvider); - void setMetricRegistry(MeterRegistry metricRegistry); + void setMetricRegistry(PolarisMetricRegistry metricRegistry); Map bootstrapRealms(List realms); } diff --git a/polaris-core/src/main/java/io/polaris/core/resource/TimedApi.java b/polaris-core/src/main/java/io/polaris/core/resource/TimedApi.java new file mode 100644 index 0000000000..b4bea7bc17 --- /dev/null +++ b/polaris-core/src/main/java/io/polaris/core/resource/TimedApi.java @@ -0,0 +1,26 @@ +package io.polaris.core.resource; + +import io.polaris.core.monitor.PolarisMetricRegistry; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation to specify metrics to be registered on initialization. Users need to explicitly call + * {@link PolarisMetricRegistry#init} to register the metrics. + * + *

If used on a Jersey resource method, this annotation also serves as a marker for the {@link + * io.polaris.service.TimedApplicationEventListener} to time the underlying method and count errors + * on failures. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface TimedApi { + /** + * The name of the metric to be recorded. + * + * @return the metric name + */ + String value(); +} diff --git a/polaris-core/src/main/java/io/polaris/core/storage/FileStorageConfigurationInfo.java b/polaris-core/src/main/java/io/polaris/core/storage/FileStorageConfigurationInfo.java index 1d0d6ea39e..b602d0e847 100644 --- a/polaris-core/src/main/java/io/polaris/core/storage/FileStorageConfigurationInfo.java +++ b/polaris-core/src/main/java/io/polaris/core/storage/FileStorageConfigurationInfo.java @@ -38,16 +38,14 @@ public String getFileIoImplClassName() { } @Override - public void validatePrefixForStorageType() { - this.allowedLocations.forEach( - loc -> { - if (!loc.startsWith(storageType.getPrefix()) - && !loc.startsWith("/") - && !loc.equals("*")) { - throw new IllegalArgumentException( - String.format( - "Location prefix not allowed: '%s', expected prefix: file:// or / or *", loc)); - } - }); + public void validatePrefixForStorageType(String loc) { + if (!loc.startsWith(getStorageType().getPrefix()) + && !loc.startsWith("file:/") + && !loc.startsWith("/") + && !loc.equals("*")) { + throw new IllegalArgumentException( + String.format( + "Location prefix not allowed: '%s', expected prefix: file:// or / or *", loc)); + } } } diff --git a/polaris-core/src/main/java/io/polaris/core/storage/PolarisCredentialProperty.java b/polaris-core/src/main/java/io/polaris/core/storage/PolarisCredentialProperty.java index 9fb9aee03b..ee3e3fd60d 100644 --- a/polaris-core/src/main/java/io/polaris/core/storage/PolarisCredentialProperty.java +++ b/polaris-core/src/main/java/io/polaris/core/storage/PolarisCredentialProperty.java @@ -35,7 +35,8 @@ public enum PolarisCredentialProperty { String.class, "the azure storage account host", "the azure account name + endpoint that will append to the ADLS_SAS_TOKEN_PREFIX"), - EXPIRATION_TIME(Long.class, "", "the expiration time for the access token, in milliseconds"); + EXPIRATION_TIME( + Long.class, "expiration-time", "the expiration time for the access token, in milliseconds"); private final Class valueType; private final String propertyName; diff --git a/polaris-core/src/main/java/io/polaris/core/storage/PolarisStorageConfigurationInfo.java b/polaris-core/src/main/java/io/polaris/core/storage/PolarisStorageConfigurationInfo.java index b705a61a33..ab53b80ae0 100644 --- a/polaris-core/src/main/java/io/polaris/core/storage/PolarisStorageConfigurationInfo.java +++ b/polaris-core/src/main/java/io/polaris/core/storage/PolarisStorageConfigurationInfo.java @@ -22,12 +22,20 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; +import io.polaris.core.PolarisConfiguration; import io.polaris.core.PolarisDiagnostics; +import io.polaris.core.admin.model.Catalog; +import io.polaris.core.entity.CatalogEntity; +import io.polaris.core.entity.PolarisEntity; +import io.polaris.core.entity.PolarisEntityConstants; import io.polaris.core.storage.aws.AwsStorageConfigurationInfo; import io.polaris.core.storage.azure.AzureStorageConfigurationInfo; import io.polaris.core.storage.gcp.GcpStorageConfigurationInfo; import java.util.List; +import java.util.Optional; import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * The polaris storage configuration information, is part of a polaris entity's internal property, @@ -47,19 +55,29 @@ }) public abstract class PolarisStorageConfigurationInfo { + private static final Logger LOGGER = + LoggerFactory.getLogger(PolarisStorageConfigurationInfo.class); + // a list of allowed locations - public List allowedLocations; + private final List allowedLocations; // storage type - public StorageType storageType; + private final StorageType storageType; public PolarisStorageConfigurationInfo( @JsonProperty(value = "storageType", required = true) @NotNull StorageType storageType, @JsonProperty(value = "allowedLocations", required = true) @NotNull List allowedLocations) { + this(storageType, allowedLocations, true); + } + + protected PolarisStorageConfigurationInfo( + StorageType storageType, List allowedLocations, boolean validatePrefix) { this.allowedLocations = allowedLocations; this.storageType = storageType; - this.validatePrefixForStorageType(); + if (validatePrefix) { + allowedLocations.forEach(this::validatePrefixForStorageType); + } } public List getAllowedLocations() { @@ -104,20 +122,76 @@ public static PolarisStorageConfigurationInfo deserialize( return null; } + public static Optional forEntityPath( + PolarisDiagnostics diagnostics, List entityPath) { + return findStorageInfoFromHierarchy(entityPath) + .map( + storageInfo -> + deserialize( + diagnostics, + storageInfo + .getInternalPropertiesAsMap() + .get(PolarisEntityConstants.getStorageConfigInfoPropertyName()))) + .map( + configInfo -> { + String baseLocation = + entityPath.reversed().stream() + .flatMap( + e -> + Optional.ofNullable( + e.getPropertiesAsMap() + .get(PolarisEntityConstants.ENTITY_BASE_LOCATION)) + .stream()) + .findFirst() + .orElse(null); + CatalogEntity catalog = CatalogEntity.of(entityPath.get(0)); + boolean allowEscape = + Optional.ofNullable( + catalog + .getPropertiesAsMap() + .get(PolarisConfiguration.CATALOG_ALLOW_UNSTRUCTURED_TABLE_LOCATION)) + .map( + val -> { + LOGGER.debug( + "Found catalog level property to allow unstructured table location: {}", + val); + return Boolean.parseBoolean(val); + }) + .orElseGet(() -> Catalog.TypeEnum.EXTERNAL.equals(catalog.getCatalogType())); + if (!allowEscape && baseLocation != null) { + LOGGER.debug( + "Not allowing unstructured table location for entity: {}", + entityPath.getLast().getName()); + return new StorageConfigurationOverride(configInfo, List.of(baseLocation)); + } else { + LOGGER.debug( + "Allowing unstructured table location for entity: {}", + entityPath.getLast().getName()); + return configInfo; + } + }); + } + + private static @NotNull Optional findStorageInfoFromHierarchy( + List entityPath) { + return entityPath.reversed().stream() + .filter( + e -> + e.getInternalPropertiesAsMap() + .containsKey(PolarisEntityConstants.getStorageConfigInfoPropertyName())) + .findFirst(); + } + /** Subclasses must provide the Iceberg FileIO impl associated with their type in this method. */ public abstract String getFileIoImplClassName(); /** Validate if the provided allowed locations are valid for the storage type */ - public void validatePrefixForStorageType() { - this.allowedLocations.forEach( - loc -> { - if (!loc.toLowerCase().startsWith(storageType.prefix)) { - throw new IllegalArgumentException( - String.format( - "Location prefix not allowed: '%s', expected prefix: '%s'", - loc, storageType.prefix)); - } - }); + protected void validatePrefixForStorageType(String loc) { + if (!loc.toLowerCase().startsWith(storageType.prefix)) { + throw new IllegalArgumentException( + String.format( + "Location prefix not allowed: '%s', expected prefix: '%s'", loc, storageType.prefix)); + } } /** Validate the number of allowed locations not exceeding the max value. */ diff --git a/polaris-core/src/main/java/io/polaris/core/storage/StorageConfigurationOverride.java b/polaris-core/src/main/java/io/polaris/core/storage/StorageConfigurationOverride.java new file mode 100644 index 0000000000..00cb88a55e --- /dev/null +++ b/polaris-core/src/main/java/io/polaris/core/storage/StorageConfigurationOverride.java @@ -0,0 +1,39 @@ +package io.polaris.core.storage; + +import java.util.List; +import org.jetbrains.annotations.NotNull; + +/** + * Allows overriding the allowed locations for specific entities. Only the allowedLocations + * specified in the constructor are allowed. allowedLocations are not inherited from the parent + * storage configuration. All other storage configuration is inherited from the parent configuration + * and cannot be overridden. + */ +public class StorageConfigurationOverride extends PolarisStorageConfigurationInfo { + + private final PolarisStorageConfigurationInfo parentStorageConfiguration; + + public StorageConfigurationOverride( + @NotNull PolarisStorageConfigurationInfo parentStorageConfiguration, + List allowedLocations) { + super(parentStorageConfiguration.getStorageType(), allowedLocations, false); + this.parentStorageConfiguration = parentStorageConfiguration; + allowedLocations.forEach(this::validatePrefixForStorageType); + } + + @Override + public String getFileIoImplClassName() { + return parentStorageConfiguration.getFileIoImplClassName(); + } + + // delegate to the wrapped class in case they override the parent behavior + @Override + protected void validatePrefixForStorageType(String loc) { + parentStorageConfiguration.validatePrefixForStorageType(loc); + } + + @Override + public void validateMaxAllowedLocations(int maxAllowedLocations) { + parentStorageConfiguration.validateMaxAllowedLocations(maxAllowedLocations); + } +} diff --git a/polaris-core/src/main/java/io/polaris/core/storage/StorageUtil.java b/polaris-core/src/main/java/io/polaris/core/storage/StorageUtil.java new file mode 100644 index 0000000000..8278c943f4 --- /dev/null +++ b/polaris-core/src/main/java/io/polaris/core/storage/StorageUtil.java @@ -0,0 +1,25 @@ +package io.polaris.core.storage; + +import org.jetbrains.annotations.NotNull; + +public class StorageUtil { + /** + * Concatenating two file paths by making sure one and only one path separator is placed between + * the two paths. + * + * @param leftPath left path + * @param rightPath right path + * @param fileSep File separator to use. + * @return Well formatted file path. + */ + public static @NotNull String concatFilePrefixes( + @NotNull String leftPath, String rightPath, String fileSep) { + if (leftPath.endsWith(fileSep) && rightPath.startsWith(fileSep)) { + return leftPath + rightPath.substring(1); + } else if (!leftPath.endsWith(fileSep) && !rightPath.startsWith(fileSep)) { + return leftPath + fileSep + rightPath; + } else { + return leftPath + rightPath; + } + } +} diff --git a/polaris-core/src/main/java/io/polaris/core/storage/aws/AwsCredentialsStorageIntegration.java b/polaris-core/src/main/java/io/polaris/core/storage/aws/AwsCredentialsStorageIntegration.java index be3a3eab85..c64e6a2352 100644 --- a/polaris-core/src/main/java/io/polaris/core/storage/aws/AwsCredentialsStorageIntegration.java +++ b/polaris-core/src/main/java/io/polaris/core/storage/aws/AwsCredentialsStorageIntegration.java @@ -19,6 +19,7 @@ import io.polaris.core.storage.InMemoryStorageIntegration; import io.polaris.core.storage.PolarisCredentialProperty; import io.polaris.core.storage.PolarisStorageConfigurationInfo; +import io.polaris.core.storage.StorageUtil; import java.net.URI; import java.util.EnumMap; import java.util.HashMap; @@ -109,7 +110,8 @@ private IamPolicy policyString( URI uri = URI.create(location); allowGetObjectStatementBuilder.addResource( // TODO add support for CN and GOV - IamResource.create(arnPrefix + parseS3Path(uri) + "/*")); + IamResource.create( + arnPrefix + StorageUtil.concatFilePrefixes(parseS3Path(uri), "*", "/"))); if (allowList) { bucketListStatmentBuilder .computeIfAbsent( @@ -122,7 +124,7 @@ private IamPolicy policyString( .addCondition( IamConditionOperator.STRING_LIKE, "s3:prefix", - trimLeadingSlash(uri.getPath()) + "/*"); + StorageUtil.concatFilePrefixes(trimLeadingSlash(uri.getPath()), "*", "/")); } }); @@ -137,7 +139,8 @@ private IamPolicy policyString( URI uri = URI.create(location); // TODO add support for CN and GOV allowPutObjectStatementBuilder.addResource( - IamResource.create(arnPrefix + parseS3Path(uri) + "/*")); + IamResource.create( + arnPrefix + StorageUtil.concatFilePrefixes(parseS3Path(uri), "*", "/"))); }); policyBuilder.addStatement(allowPutObjectStatementBuilder.build()); } diff --git a/polaris-core/src/main/java/io/polaris/core/storage/aws/AwsStorageConfigurationInfo.java b/polaris-core/src/main/java/io/polaris/core/storage/aws/AwsStorageConfigurationInfo.java index 8e0aef0d13..48ed960524 100644 --- a/polaris-core/src/main/java/io/polaris/core/storage/aws/AwsStorageConfigurationInfo.java +++ b/polaris-core/src/main/java/io/polaris/core/storage/aws/AwsStorageConfigurationInfo.java @@ -107,12 +107,12 @@ public void setUserARN(@Nullable String userARN) { @Override public String toString() { return MoreObjects.toStringHelper(this) - .add("storageType", storageType) - .add("storageType", storageType.name()) + .add("storageType", getStorageType()) + .add("storageType", getStorageType().name()) .add("roleARN", roleARN) .add("userARN", userARN) .add("externalId", externalId) - .add("allowedLocation", allowedLocations) + .add("allowedLocation", getAllowedLocations()) .toString(); } } diff --git a/polaris-core/src/main/java/io/polaris/core/storage/azure/AzureStorageConfigurationInfo.java b/polaris-core/src/main/java/io/polaris/core/storage/azure/AzureStorageConfigurationInfo.java index 47bf4d53b0..dbdf843cce 100644 --- a/polaris-core/src/main/java/io/polaris/core/storage/azure/AzureStorageConfigurationInfo.java +++ b/polaris-core/src/main/java/io/polaris/core/storage/azure/AzureStorageConfigurationInfo.java @@ -21,6 +21,7 @@ import com.google.common.base.MoreObjects; import io.polaris.core.storage.PolarisStorageConfigurationInfo; import java.util.List; +import java.util.Objects; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -79,16 +80,18 @@ public void setConsentUrl(String consentUrl) { @Override public String toString() { return MoreObjects.toStringHelper(this) - .add("storageType", storageType) + .add("storageType", getStorageType()) .add("tenantId", tenantId) - .add("allowedLocation", allowedLocations) + .add("allowedLocation", getAllowedLocations()) .add("multiTenantAppName", multiTenantAppName) .add("consentUrl", consentUrl) .toString(); } @Override - public void validatePrefixForStorageType() { - this.allowedLocations.forEach(AzureLocation::new); + public void validatePrefixForStorageType(String loc) { + AzureLocation location = new AzureLocation(loc); + Objects.requireNonNull( + location); // do something with the variable so the JVM doesn't optimize out the check } } diff --git a/polaris-core/src/main/java/io/polaris/core/storage/gcp/GcpStorageConfigurationInfo.java b/polaris-core/src/main/java/io/polaris/core/storage/gcp/GcpStorageConfigurationInfo.java index 15daf1eac5..2275ce7a66 100644 --- a/polaris-core/src/main/java/io/polaris/core/storage/gcp/GcpStorageConfigurationInfo.java +++ b/polaris-core/src/main/java/io/polaris/core/storage/gcp/GcpStorageConfigurationInfo.java @@ -60,8 +60,8 @@ public String getGcpServiceAccount() { @Override public String toString() { return MoreObjects.toStringHelper(this) - .add("storageType", storageType) - .add("allowedLocation", allowedLocations) + .add("storageType", getStorageType()) + .add("allowedLocation", getAllowedLocations()) .add("gcpServiceAccount", gcpServiceAccount) .toString(); } diff --git a/polaris-service/src/main/java/io/polaris/service/PolarisApplication.java b/polaris-service/src/main/java/io/polaris/service/PolarisApplication.java index b903fc8b4a..74bff74ca2 100644 --- a/polaris-service/src/main/java/io/polaris/service/PolarisApplication.java +++ b/polaris-service/src/main/java/io/polaris/service/PolarisApplication.java @@ -46,6 +46,7 @@ import io.polaris.core.auth.PolarisAuthorizer; import io.polaris.core.context.CallContext; import io.polaris.core.context.RealmContext; +import io.polaris.core.monitor.PolarisMetricRegistry; import io.polaris.core.persistence.MetaStoreManagerFactory; import io.polaris.service.admin.PolarisServiceImpl; import io.polaris.service.admin.api.PolarisCatalogsApi; @@ -100,6 +101,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.slf4j.MDC; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.services.sts.StsClient; +import software.amazon.awssdk.services.sts.StsClientBuilder; public class PolarisApplication extends Application { private static final Logger LOGGER = LoggerFactory.getLogger(PolarisApplication.class); @@ -151,11 +155,17 @@ public void run(PolarisApplicationConfig configuration, Environment environment) // PolarisEntityManager will be used for Management APIs and optionally the core Catalog APIs // depending on the value of the baseCatalogType config. MetaStoreManagerFactory metaStoreManagerFactory = configuration.getMetaStoreManagerFactory(); + StsClientBuilder stsClientBuilder = StsClient.builder(); + AwsCredentialsProvider awsCredentialsProvider = configuration.credentialsProvider(); + if (awsCredentialsProvider != null) { + stsClientBuilder.credentialsProvider(awsCredentialsProvider); + } metaStoreManagerFactory.setStorageIntegrationProvider( - new PolarisStorageIntegrationProviderImpl()); + new PolarisStorageIntegrationProviderImpl(stsClientBuilder::build)); - PrometheusMeterRegistry meterRegistry = new PrometheusMeterRegistry(PrometheusConfig.DEFAULT); - metaStoreManagerFactory.setMetricRegistry(meterRegistry); + PolarisMetricRegistry polarisMetricRegistry = + new PolarisMetricRegistry(new PrometheusMeterRegistry(PrometheusConfig.DEFAULT)); + metaStoreManagerFactory.setMetricRegistry(polarisMetricRegistry); OpenTelemetry openTelemetry = setupTracing(); if (metaStoreManagerFactory instanceof OpenTelemetryAware otAware) { @@ -270,11 +280,23 @@ public void run(PolarisApplicationConfig configuration, Environment environment) Serializers.registerSerializers(objectMapper); environment.jersey().register(new IcebergJsonProcessingExceptionMapper()); environment.jersey().register(new IcebergJerseyViolationExceptionMapper()); - environment.jersey().register(new TimedApplicationEventListener(meterRegistry)); + environment.jersey().register(new TimedApplicationEventListener(polarisMetricRegistry)); + + polarisMetricRegistry.init( + IcebergRestCatalogApi.class, + IcebergRestConfigurationApi.class, + IcebergRestOAuth2Api.class, + PolarisCatalogsApi.class, + PolarisPrincipalsApi.class, + PolarisPrincipalRolesApi.class); environment .admin() - .addServlet("metrics", new PrometheusMetricsServlet(meterRegistry.getPrometheusRegistry())) + .addServlet( + "metrics", + new PrometheusMetricsServlet( + ((PrometheusMeterRegistry) polarisMetricRegistry.getMeterRegistry()) + .getPrometheusRegistry())) .addMapping("/metrics"); // For in-memory metastore we need to bootstrap Service and Service principal at startup (for diff --git a/polaris-service/src/main/java/io/polaris/service/TimedApplicationEventListener.java b/polaris-service/src/main/java/io/polaris/service/TimedApplicationEventListener.java index 9645636ebd..06ba5bccaa 100644 --- a/polaris-service/src/main/java/io/polaris/service/TimedApplicationEventListener.java +++ b/polaris-service/src/main/java/io/polaris/service/TimedApplicationEventListener.java @@ -16,10 +16,9 @@ package io.polaris.service; import com.google.common.base.Stopwatch; -import io.micrometer.core.instrument.MeterRegistry; import io.polaris.core.context.CallContext; import io.polaris.core.monitor.PolarisMetricRegistry; -import io.polaris.service.resource.TimedApi; +import io.polaris.core.resource.TimedApi; import java.lang.reflect.Method; import java.util.concurrent.TimeUnit; import javax.ws.rs.ext.Provider; @@ -29,9 +28,9 @@ import org.glassfish.jersey.server.monitoring.RequestEventListener; /** - * An ApplicationEventListener that supports timing of resource method execution and error counting. - * It uses Micrometer for metrics collection and provides detailed metrics tagged with realm - * identifiers and distinguishes between successful executions and errors. + * An ApplicationEventListener that supports timing and error counting of Jersey resource methods + * annotated by {@link TimedApi}. It uses the {@link PolarisMetricRegistry} for metric collection + * and properly times the resource on success and increments the error counter on failure. */ @Provider public class TimedApplicationEventListener implements ApplicationEventListener { @@ -39,8 +38,8 @@ public class TimedApplicationEventListener implements ApplicationEventListener { // The PolarisMetricRegistry instance used for recording metrics and error counters. private final PolarisMetricRegistry polarisMetricRegistry; - public TimedApplicationEventListener(MeterRegistry meterRegistry) { - this.polarisMetricRegistry = new PolarisMetricRegistry(meterRegistry); + public TimedApplicationEventListener(PolarisMetricRegistry polarisMetricRegistry) { + this.polarisMetricRegistry = polarisMetricRegistry; } @Override @@ -63,6 +62,7 @@ private class TimedRequestEventListener implements RequestEventListener { /** Handles various types of RequestEvents to start timing, stop timing, and record metrics. */ @Override public void onEvent(RequestEvent event) { + String realmId = CallContext.getCurrentContext().getRealmContext().getRealmIdentifier(); if (event.getType() == RequestEvent.Type.RESOURCE_METHOD_START) { Method method = event.getUriInfo().getMatchedResourceMethod().getInvocable().getHandlingMethod(); @@ -70,10 +70,10 @@ public void onEvent(RequestEvent event) { TimedApi timedApi = method.getAnnotation(TimedApi.class); metric = timedApi.value(); sw = Stopwatch.createStarted(); + polarisMetricRegistry.incrementCounter(metric, realmId); } } else if (event.getType() == RequestEvent.Type.FINISHED && metric != null) { - String realmId = CallContext.getCurrentContext().getRealmContext().getRealmIdentifier(); if (event.isSuccess()) { sw.stop(); polarisMetricRegistry.recordTimer(metric, sw.elapsed(TimeUnit.MILLISECONDS), realmId); diff --git a/polaris-service/src/main/java/io/polaris/service/admin/PolarisAdminService.java b/polaris-service/src/main/java/io/polaris/service/admin/PolarisAdminService.java index 4313df55b8..119caad618 100644 --- a/polaris-service/src/main/java/io/polaris/service/admin/PolarisAdminService.java +++ b/polaris-service/src/main/java/io/polaris/service/admin/PolarisAdminService.java @@ -16,6 +16,7 @@ package io.polaris.service.admin; import io.polaris.core.PolarisCallContext; +import io.polaris.core.PolarisConfiguration; import io.polaris.core.admin.model.CatalogGrant; import io.polaris.core.admin.model.CatalogPrivilege; import io.polaris.core.admin.model.GrantResource; @@ -60,9 +61,11 @@ import io.polaris.core.storage.azure.AzureStorageConfigurationInfo; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.function.Function; import org.apache.iceberg.catalog.Namespace; import org.apache.iceberg.catalog.TableIdentifier; @@ -73,6 +76,7 @@ import org.apache.iceberg.exceptions.NoSuchTableException; import org.apache.iceberg.exceptions.NoSuchViewException; import org.apache.iceberg.exceptions.NotFoundException; +import org.apache.iceberg.exceptions.ValidationException; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; @@ -462,10 +466,82 @@ private void authorizeGrantOnTableLikeOperationOrThrow( catalogRoleWrapper); } + /** Get all locations where data for a `CatalogEntity` may be stored */ + private Set getCatalogLocations(CatalogEntity catalogEntity) { + HashSet catalogLocations = new HashSet<>(); + catalogLocations.add(terminateWithSlash(catalogEntity.getDefaultBaseLocation())); + if (catalogEntity.getStorageConfigurationInfo() != null) { + catalogLocations.addAll( + catalogEntity.getStorageConfigurationInfo().getAllowedLocations().stream() + .map(this::terminateWithSlash) + .toList()); + } + return catalogLocations; + } + + /** Ensure a path is terminated with a `/` */ + private String terminateWithSlash(String path) { + if (path == null) { + return null; + } else if (path.endsWith("/")) { + return path; + } + return path + "/"; + } + + /** + * True if the `CatalogEntity` has a default base location or allowed location that overlaps with + * that of any existing catalog. If `ALLOW_OVERLAPPING_CATALOG_URLS` is set to true, this check + * will be skipped. + */ + private boolean catalogOverlapsWithExistingCatalog(CatalogEntity catalogEntity) { + boolean allowOverlappingCatalogUrls = + Boolean.parseBoolean( + String.valueOf( + getCurrentPolarisContext() + .getConfigurationStore() + .getConfiguration( + getCurrentPolarisContext(), + PolarisConfiguration.ALLOW_OVERLAPPING_CATALOG_URLS, + PolarisConfiguration.DEFAULT_ALLOW_OVERLAPPING_CATALOG_URLS))); + + if (allowOverlappingCatalogUrls) { + return false; + } + + Set newCatalogLocations = getCatalogLocations(catalogEntity); + return listCatalogsUnsafe().stream() + .map(CatalogEntity::new) + .anyMatch( + existingCatalog -> { + if (existingCatalog.getName().equals(catalogEntity.getName())) { + return false; + } + return getCatalogLocations(existingCatalog).stream() + .anyMatch( + existingLocation -> + newCatalogLocations.stream() + .anyMatch( + newLocation -> { + if (newLocation == null || existingLocation == null) { + return false; + } + return newLocation.startsWith(existingLocation) + || existingLocation.startsWith(newLocation); + })); + }); + } + public PolarisEntity createCatalog(PolarisEntity entity) { PolarisAuthorizableOperation op = PolarisAuthorizableOperation.CREATE_CATALOG; authorizeBasicRootOperationOrThrow(op); + if (catalogOverlapsWithExistingCatalog((CatalogEntity) entity)) { + throw new ValidationException( + "Cannot create Catalog %s. One or more of its locations overlaps with an existing catalog", + entity.getName()); + } + long id = entity.getId() <= 0 ? entityManager @@ -623,6 +699,12 @@ private void validateUpdateCatalogDiffOrThrow( validateUpdateCatalogDiffOrThrow(currentCatalogEntity, updatedEntity); + if (catalogOverlapsWithExistingCatalog(updatedEntity)) { + throw new ValidationException( + "Cannot update Catalog %s. One or more of its new locations overlaps with an existing catalog", + updatedEntity.getName()); + } + CatalogEntity returnedEntity = Optional.ofNullable( CatalogEntity.of( @@ -639,9 +721,12 @@ private void validateUpdateCatalogDiffOrThrow( } public List listCatalogs() { - PolarisAuthorizableOperation op = PolarisAuthorizableOperation.LIST_CATALOGS; - authorizeBasicRootOperationOrThrow(op); + authorizeBasicRootOperationOrThrow(PolarisAuthorizableOperation.LIST_CATALOGS); + return listCatalogsUnsafe(); + } + /** List all catalogs without checking for permission */ + private List listCatalogsUnsafe() { return entityManager .getMetaStoreManager() .listEntities( diff --git a/polaris-service/src/main/java/io/polaris/service/auth/DefaultOAuth2ApiService.java b/polaris-service/src/main/java/io/polaris/service/auth/DefaultOAuth2ApiService.java index bb43301e63..4b1e6d1fc7 100644 --- a/polaris-service/src/main/java/io/polaris/service/auth/DefaultOAuth2ApiService.java +++ b/polaris-service/src/main/java/io/polaris/service/auth/DefaultOAuth2ApiService.java @@ -23,8 +23,14 @@ import io.polaris.service.types.TokenType; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.SecurityContext; +import java.net.URLDecoder; +import java.nio.charset.Charset; +import org.apache.commons.codec.binary.Base64; import org.apache.hadoop.hdfs.web.oauth2.OAuth2Constants; +import org.apache.iceberg.rest.auth.OAuth2Properties; import org.apache.iceberg.rest.responses.OAuthTokenResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Default implementation of the {@link OAuth2ApiService} that generates a JWT token for the client @@ -32,12 +38,14 @@ */ @JsonTypeName("default") public class DefaultOAuth2ApiService implements OAuth2ApiService, HasEntityManagerFactory { + public static final Logger LOGGER = LoggerFactory.getLogger(DefaultOAuth2ApiService.class); private TokenBrokerFactory tokenBrokerFactory; public DefaultOAuth2ApiService() {} @Override public Response getToken( + String authHeader, String grantType, String scope, String clientId, @@ -57,6 +65,19 @@ public Response getToken( if (!tokenBroker.supportsRequestedTokenType(requestedTokenType)) { return OAuthUtils.getResponseFromError(OAuthTokenErrorResponse.Error.invalid_request); } + if (authHeader == null && clientId == null) { + return OAuthUtils.getResponseFromError(OAuthTokenErrorResponse.Error.invalid_client); + } + if (authHeader != null && clientId == null && authHeader.startsWith("Basic ")) { + String credentials = new String(Base64.decodeBase64(authHeader.substring(6))); + if (!credentials.contains(":")) { + return OAuthUtils.getResponseFromError(OAuthTokenErrorResponse.Error.invalid_client); + } + LOGGER.debug("Found credentials in auth header - treating as client_credentials"); + String[] parts = credentials.split(":", 2); + clientId = URLDecoder.decode(parts[0], Charset.defaultCharset()); + clientSecret = URLDecoder.decode(parts[1], Charset.defaultCharset()); + } TokenResponse tokenResponse = switch (subjectTokenType) { case TokenType.ID_TOKEN, @@ -65,11 +86,25 @@ public Response getToken( TokenType.SAML1, TokenType.SAML2 -> new TokenResponse(OAuthTokenErrorResponse.Error.invalid_request); - case TokenType.ACCESS_TOKEN -> - tokenBroker.generateFromToken(subjectTokenType, subjectToken, grantType, scope); + case TokenType.ACCESS_TOKEN -> { + // token exchange with client id and client secret means the client has previously + // attempted to refresh + // an access token, but refreshing was not supported by the token broker. Accept the + // client id and + // secret and treat it as a new token request + if (clientId != null && clientSecret != null) { + yield tokenBroker.generateFromClientSecrets( + clientId, clientSecret, OAuth2Constants.CLIENT_CREDENTIALS, scope); + } else { + yield tokenBroker.generateFromToken(subjectTokenType, subjectToken, grantType, scope); + } + } case null -> tokenBroker.generateFromClientSecrets(clientId, clientSecret, grantType, scope); }; + if (tokenResponse == null) { + return OAuthUtils.getResponseFromError(OAuthTokenErrorResponse.Error.unsupported_grant_type); + } if (!tokenResponse.isValid()) { return OAuthUtils.getResponseFromError(tokenResponse.getError()); } @@ -77,6 +112,7 @@ public Response getToken( OAuthTokenResponse.builder() .withToken(tokenResponse.getAccessToken()) .withTokenType(OAuth2Constants.BEARER) + .withIssuedTokenType(OAuth2Properties.ACCESS_TOKEN_TYPE) .setExpirationInSeconds(tokenResponse.getExpiresIn()) .build()) .build(); diff --git a/polaris-service/src/main/java/io/polaris/service/auth/TestOAuth2ApiService.java b/polaris-service/src/main/java/io/polaris/service/auth/TestOAuth2ApiService.java index ef2c1f99d0..161b886d02 100644 --- a/polaris-service/src/main/java/io/polaris/service/auth/TestOAuth2ApiService.java +++ b/polaris-service/src/main/java/io/polaris/service/auth/TestOAuth2ApiService.java @@ -42,6 +42,7 @@ public class TestOAuth2ApiService implements OAuth2ApiService, HasEntityManagerF @Override public Response getToken( + String authHeader, String grantType, String scope, String clientId, diff --git a/polaris-service/src/main/java/io/polaris/service/catalog/BasePolarisCatalog.java b/polaris-service/src/main/java/io/polaris/service/catalog/BasePolarisCatalog.java index 856bc1e53b..593fb87b2b 100644 --- a/polaris-service/src/main/java/io/polaris/service/catalog/BasePolarisCatalog.java +++ b/polaris-service/src/main/java/io/polaris/service/catalog/BasePolarisCatalog.java @@ -15,12 +15,16 @@ */ package io.polaris.service.catalog; +import static io.polaris.core.storage.StorageUtil.concatFilePrefixes; + import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Joiner; import com.google.common.base.Objects; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableMap; import io.polaris.core.PolarisCallContext; +import io.polaris.core.PolarisConfiguration; +import io.polaris.core.auth.AuthenticatedPolarisPrincipal; import io.polaris.core.catalog.PolarisCatalogHelpers; import io.polaris.core.context.CallContext; import io.polaris.core.entity.CatalogEntity; @@ -34,7 +38,10 @@ import io.polaris.core.persistence.PolarisEntityManager; import io.polaris.core.persistence.PolarisMetaStoreManager; import io.polaris.core.persistence.PolarisResolvedPathWrapper; +import io.polaris.core.persistence.resolver.PolarisResolutionManifest; import io.polaris.core.persistence.resolver.PolarisResolutionManifestCatalogView; +import io.polaris.core.persistence.resolver.ResolverPath; +import io.polaris.core.persistence.resolver.ResolverStatus; import io.polaris.core.storage.InMemoryStorageIntegration; import io.polaris.core.storage.PolarisStorageActions; import io.polaris.core.storage.PolarisStorageConfigurationInfo; @@ -46,15 +53,19 @@ import jakarta.ws.rs.BadRequestException; import java.io.Closeable; import java.io.IOException; +import java.net.URI; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Predicate; import java.util.stream.Collectors; +import java.util.stream.Stream; import org.apache.commons.lang3.exception.ExceptionUtils; import org.apache.hadoop.conf.Configuration; import org.apache.iceberg.BaseMetastoreTableOperations; @@ -127,8 +138,9 @@ public boolean test(Exception ex) { private final PolarisResolutionManifestCatalogView resolvedEntityView; private final CatalogEntity catalogEntity; private final TaskExecutor taskExecutor; + private final AuthenticatedPolarisPrincipal authenticatedPrincipal; private String ioImplClassName; - private FileIO io; + private FileIO catalogFileIO; private String catalogName; private long catalogId = -1; private String defaultBaseLocation; @@ -147,13 +159,14 @@ public BasePolarisCatalog( PolarisEntityManager entityManager, CallContext callContext, PolarisResolutionManifestCatalogView resolvedEntityView, + AuthenticatedPolarisPrincipal authenticatedPrincipal, TaskExecutor taskExecutor) { this.entityManager = entityManager; this.callContext = callContext; this.resolvedEntityView = resolvedEntityView; this.catalogEntity = CatalogEntity.of(resolvedEntityView.getResolvedReferenceCatalogEntity().getRawLeafEntity()); - + this.authenticatedPrincipal = authenticatedPrincipal; this.taskExecutor = taskExecutor; this.catalogId = catalogEntity.getId(); this.catalogName = catalogEntity.getName(); @@ -166,7 +179,7 @@ public String name() { @TestOnly FileIO getIo() { - return io; + return catalogFileIO; } @Override @@ -194,6 +207,8 @@ public void initialize(String name, Map properties) { .getConfiguration( callContext.getPolarisCallContext(), ALLOW_SPECIFYING_FILE_IO_IMPL, false); + PolarisStorageConfigurationInfo storageConfigurationInfo = + catalogEntity.getStorageConfigurationInfo(); if (properties.containsKey(CatalogProperties.FILE_IO_IMPL)) { ioImplClassName = properties.get(CatalogProperties.FILE_IO_IMPL); @@ -205,21 +220,21 @@ public void initialize(String name, Map properties) { LOG.debug( "Allowing overriding ioImplClassName to {} for storageConfiguration {}", ioImplClassName, - catalogEntity.getStorageConfigurationInfo()); + storageConfigurationInfo); } else { - ioImplClassName = catalogEntity.getStorageConfigurationInfo().getFileIoImplClassName(); + ioImplClassName = storageConfigurationInfo.getFileIoImplClassName(); LOG.debug( "Resolved ioImplClassName {} for storageConfiguration {}", ioImplClassName, - catalogEntity.getStorageConfigurationInfo()); + storageConfigurationInfo); } - this.io = loadFileIO(ioImplClassName, properties); + this.catalogFileIO = loadFileIO(ioImplClassName, properties); this.closeableGroup = CallContext.getCurrentContext().closeables(); closeableGroup.addCloseable(metricsReporter()); // TODO: FileIO initialization should should happen later depending on the operation so // we'd also add it to the closeableGroup later. - closeableGroup.addCloseable(this.io); + closeableGroup.addCloseable(this.catalogFileIO); closeableGroup.setSuppressCloseFailure(true); catalogProperties = properties; } @@ -241,13 +256,25 @@ public ViewBuilder buildView(TableIdentifier identifier) { @Override protected TableOperations newTableOps(TableIdentifier tableIdentifier) { - return new BasePolarisTableOperations(io, tableIdentifier); + return new BasePolarisTableOperations(catalogFileIO, tableIdentifier); } @Override protected String defaultWarehouseLocation(TableIdentifier tableIdentifier) { - return SLASH.join( - defaultNamespaceLocation(tableIdentifier.namespace()), tableIdentifier.name()); + if (tableIdentifier.namespace().isEmpty()) { + return SLASH.join( + defaultNamespaceLocation(tableIdentifier.namespace()), tableIdentifier.name()); + } else { + PolarisResolvedPathWrapper resolvedNamespace = + resolvedEntityView.getResolvedPath(tableIdentifier.namespace()); + if (resolvedNamespace == null) { + throw new NoSuchNamespaceException( + "Namespace does not exist: %s", tableIdentifier.namespace()); + } + List namespacePath = resolvedNamespace.getRawFullPath(); + String namespaceLocation = resolveLocationForPath(namespacePath); + return SLASH.join(namespaceLocation, tableIdentifier.name()); + } } private String defaultNamespaceLocation(Namespace namespace) { @@ -258,6 +285,22 @@ private String defaultNamespaceLocation(Namespace namespace) { } } + private Set getLocationsAllowedToBeAccessed(TableMetadata tableMetadata) { + String basicLocation = tableMetadata.location(); + Set locations = new HashSet<>(); + locations.add(concatFilePrefixes(basicLocation, "data/", "/")); + locations.add(concatFilePrefixes(basicLocation, "metadata/", "/")); + return locations; + } + + private Set getLocationsAllowedToBeAccessed(ViewMetadata viewMetadata) { + String basicLocation = viewMetadata.location(); + Set locations = new HashSet<>(); + // a view won't have a "data" location, so only allowed to access "metadata" + locations.add(concatFilePrefixes(basicLocation, "metadata/", "/")); + return locations; + } + @Override public boolean dropTable(TableIdentifier tableIdentifier, boolean purge) { TableOperations ops = newTableOps(tableIdentifier); @@ -277,15 +320,15 @@ public boolean dropTable(TableIdentifier tableIdentifier, boolean purge) { refreshCredentials( tableIdentifier, Set.of(PolarisStorageActions.READ, PolarisStorageActions.WRITE), - lastMetadata.location(), + getLocationsAllowedToBeAccessed(lastMetadata), entity)) .orElse(Map.of()); Map tableProperties = new HashMap<>(lastMetadata.properties()); tableProperties.putAll(credentialsMap); if (!tableProperties.isEmpty()) { - io = loadFileIO(ioImplClassName, tableProperties); + catalogFileIO = loadFileIO(ioImplClassName, tableProperties); // ensure the new fileIO is closed when the catalog is closed - closeableGroup.addCloseable(io); + closeableGroup.addCloseable(catalogFileIO); } } Map storageProperties = @@ -299,7 +342,7 @@ public boolean dropTable(TableIdentifier tableIdentifier, boolean purge) { Map clone = new HashMap<>(properties); clone.put(CatalogProperties.FILE_IO_IMPL, ioImplClassName); try { - clone.putAll(io.properties()); + clone.putAll(catalogFileIO.properties()); } catch (UnsupportedOperationException e) { LOG.warn("FileIO doesn't implement properties()"); } @@ -373,6 +416,7 @@ private void createNamespaceInternal( Namespace namespace, Map metadata, PolarisResolvedPathWrapper resolvedParent) { + String baseLocation = resolveNamespaceLocation(namespace, metadata); NamespaceEntity entity = new NamespaceEntity.Builder(namespace) .setCatalogId(getCatalogId()) @@ -384,7 +428,21 @@ private void createNamespaceInternal( .setParentId(resolvedParent.getRawLeafEntity().getId()) .setProperties(metadata) .setCreateTimestamp(System.currentTimeMillis()) + .setBaseLocation(baseLocation) .build(); + if (!callContext + .getPolarisCallContext() + .getConfigurationStore() + .getConfiguration( + callContext.getPolarisCallContext(), + PolarisConfiguration.ALLOW_NAMESPACE_LOCATION_OVERLAP, + PolarisConfiguration.DEFAULT_ALLOW_NAMESPACE_LOCATION_OVERLAP)) { + LOG.debug("Validating no overlap for {} with sibling tables or namespaces", namespace); + validateNoLocationOverlap( + entity.getBaseLocation(), resolvedParent.getRawFullPath(), entity.getName()); + } else { + LOG.debug("Skipping location overlap validation for namespace '{}'", namespace); + } PolarisEntity returnedEntity = PolarisEntity.of( entityManager @@ -399,6 +457,73 @@ private void createNamespaceInternal( } } + private String resolveNamespaceLocation(Namespace namespace, Map properties) { + if (properties.containsKey(PolarisEntityConstants.ENTITY_BASE_LOCATION)) { + return properties.get(PolarisEntityConstants.ENTITY_BASE_LOCATION); + } else { + List parentPath = + namespace.length() > 1 + ? getResolvedParentNamespace(namespace).getRawFullPath() + : List.of(resolvedEntityView.getResolvedReferenceCatalogEntity().getRawLeafEntity()); + + String parentLocation = resolveLocationForPath(parentPath); + + return parentLocation + "/" + namespace.level(namespace.length() - 1); + } + } + + private static @NotNull String resolveLocationForPath(List parentPath) { + // always take the first object. If it has the base-location, stop there + AtomicBoolean foundBaseLocation = new AtomicBoolean(false); + return parentPath.reversed().stream() + .takeWhile( + entity -> + !foundBaseLocation.getAndSet( + entity + .getPropertiesAsMap() + .containsKey(PolarisEntityConstants.ENTITY_BASE_LOCATION))) + .toList() + .reversed() + .stream() + .map( + entity -> { + if (entity.getType().equals(PolarisEntityType.CATALOG)) { + return CatalogEntity.of(entity).getDefaultBaseLocation(); + } else { + String baseLocation = + entity.getPropertiesAsMap().get(PolarisEntityConstants.ENTITY_BASE_LOCATION); + if (baseLocation != null) { + return baseLocation; + } else { + return entity.getName(); + } + } + }) + .map(BasePolarisCatalog::stripLeadingTrailingSlash) + .collect(Collectors.joining("/")); + } + + private static String stripLeadingTrailingSlash(String location) { + if (location.startsWith("/")) { + return stripLeadingTrailingSlash(location.substring(1)); + } + if (location.endsWith("/")) { + return location.substring(0, location.length() - 1); + } else { + return location; + } + } + + private PolarisResolvedPathWrapper getResolvedParentNamespace(Namespace namespace) { + Namespace parentNamespace = + Namespace.of(Arrays.copyOf(namespace.levels(), namespace.length() - 1)); + PolarisResolvedPathWrapper resolvedParent = resolvedEntityView.getResolvedPath(parentNamespace); + if (resolvedParent == null) { + return resolvedEntityView.getPassthroughResolvedPath(parentNamespace); + } + return resolvedParent; + } + @Override public boolean namespaceExists(Namespace namespace) { PolarisResolvedPathWrapper resolvedEntities = resolvedEntityView.getResolvedPath(namespace); @@ -455,6 +580,22 @@ public boolean setProperties(Namespace namespace, Map properties PolarisEntity updatedEntity = new PolarisEntity.Builder(entity).setProperties(newProperties).build(); + if (!callContext + .getPolarisCallContext() + .getConfigurationStore() + .getConfiguration( + callContext.getPolarisCallContext(), + PolarisConfiguration.ALLOW_NAMESPACE_LOCATION_OVERLAP, + PolarisConfiguration.DEFAULT_ALLOW_NAMESPACE_LOCATION_OVERLAP)) { + LOG.debug("Validating no overlap with sibling tables or namespaces"); + validateNoLocationOverlap( + NamespaceEntity.of(updatedEntity).getBaseLocation(), + resolvedEntities.getRawParentPath(), + updatedEntity.getName()); + } else { + LOG.debug("Skipping location overlap validation for namespace '{}'", namespace); + } + List parentPath = resolvedEntities.getRawFullPath(); PolarisEntity returnedEntity = Optional.ofNullable( @@ -564,7 +705,7 @@ public List listViews(Namespace namespace) { @Override protected BasePolarisViewOperations newViewOps(TableIdentifier identifier) { - return new BasePolarisViewOperations(io, identifier); + return new BasePolarisViewOperations(catalogFileIO, identifier); } @Override @@ -602,7 +743,10 @@ public Map getCredentialConfig( return Map.of(); } return refreshCredentials( - tableIdentifier, storageActions, tableMetadata.location(), storageInfo.get()); + tableIdentifier, + storageActions, + getLocationsAllowedToBeAccessed(tableMetadata), + storageInfo.get()); } /** @@ -644,9 +788,17 @@ private Map refreshCredentials( Set storageActions, String tableLocation, PolarisEntity entity) { + return refreshCredentials(tableIdentifier, storageActions, Set.of(tableLocation), entity); + } + + private Map refreshCredentials( + TableIdentifier tableIdentifier, + Set storageActions, + Set tableLocations, + PolarisEntity entity) { // Important: Any locations added to the set of requested locations need to be validated // prior to requested subscoped credentials. - validateLocationForTableLike(tableIdentifier, tableLocation); + tableLocations.forEach(tl -> validateLocationForTableLike(tableIdentifier, tl)); boolean allowList = storageActions.contains(PolarisStorageActions.LIST) @@ -655,7 +807,7 @@ private Map refreshCredentials( storageActions.contains(PolarisStorageActions.WRITE) || storageActions.contains(PolarisStorageActions.DELETE) || storageActions.contains(PolarisStorageActions.ALL) - ? Set.of(tableLocation) + ? tableLocations : Set.of(); Map credentialsMap = entityManager @@ -665,7 +817,7 @@ private Map refreshCredentials( callContext.getPolarisCallContext(), entity, allowList, - Set.of(tableLocation), + tableLocations, writeLocations); LOG.atDebug() .addKeyValue("tableIdentifier", tableIdentifier) @@ -687,6 +839,9 @@ private void validateLocationForTableLike(TableIdentifier identifier, String loc if (resolvedStorageEntity == null) { resolvedStorageEntity = resolvedEntityView.getResolvedPath(identifier.namespace()); } + if (resolvedStorageEntity == null) { + resolvedStorageEntity = resolvedEntityView.getPassthroughResolvedPath(identifier.namespace()); + } validateLocationForTableLike(identifier, location, resolvedStorageEntity); } @@ -699,27 +854,34 @@ private void validateLocationForTableLike( TableIdentifier identifier, String location, PolarisResolvedPathWrapper resolvedStorageEntity) { - Optional storageInfoHolder = findStorageInfoFromHierarchy(resolvedStorageEntity); - storageInfoHolder.ifPresentOrElse( - storageInfoHolderEntity -> { - // Though the storage entity may not actually be a CatalogEntity, we just use the - // CatalogEntity wrapper here for a convenient deserializer helper method. - PolarisStorageConfigurationInfo storageConfigInfo = - new CatalogEntity(storageInfoHolderEntity).getStorageConfigurationInfo(); + Optional optStorageConfiguration = + PolarisStorageConfigurationInfo.forEntityPath( + callContext.getPolarisCallContext().getDiagServices(), + resolvedStorageEntity.getRawFullPath()); + + optStorageConfiguration.ifPresentOrElse( + storageConfigInfo -> { Map> validationResults = InMemoryStorageIntegration.validateSubpathsOfAllowedLocations( storageConfigInfo, Set.of(PolarisStorageActions.ALL), Set.of(location)); - validationResults.values().stream() + validationResults + .values() .forEach( actionResult -> - actionResult.values().stream() + actionResult + .values() .forEach( result -> { if (!result.isSuccess()) { throw new ForbiddenException( "Invalid location '%s' for identifier '%s': %s", location, identifier, result.getMessage()); + } else { + LOG.debug( + "Validated location '{}' for identifier '{}'", + location, + identifier); } })); @@ -751,6 +913,158 @@ private void validateLocationForTableLike( }); } + /** + * Validates the table location has no overlap with other entities after checking the + * configuration of the service + * + * @param identifier + * @param resolvedNamespace + * @param location + */ + private void validateNoLocationOverlap( + TableIdentifier identifier, List resolvedNamespace, String location) { + if (callContext + .getPolarisCallContext() + .getConfigurationStore() + .getConfiguration( + callContext.getPolarisCallContext(), + PolarisConfiguration.ALLOW_TABLE_LOCATION_OVERLAP, + PolarisConfiguration.DEFAULT_ALLOW_TABLE_LOCATION_OVERLAP)) { + LOG.debug("Skipping location overlap validation for identifier '{}'", identifier); + } else { // if (entity.getSubType().equals(PolarisEntitySubType.TABLE)) { + // TODO - is this necessary for views? overlapping views do not expose subdirectories via the + // credential vending + // so this feels like an unnecessary restriction + LOG.debug("Validating no overlap with sibling tables or namespaces"); + validateNoLocationOverlap(location, resolvedNamespace, identifier.name()); + } + } + + /** + * Validate no location overlap exists between the entity path and its sibling entities. This + * resolves all siblings at the same level as the target entity (namespaces if the target entity + * is a namespace whose parent is the catalog, namespaces and tables otherwise) and checks the + * base-location property of each. The target entity's base location may not be a prefix or a + * suffix of any sibling entity's base location. + * + * @param location + * @param parentPath + */ + private void validateNoLocationOverlap( + String location, List parentPath, String name) { + PolarisMetaStoreManager.ListEntitiesResult siblingNamespacesResult = + entityManager + .getMetaStoreManager() + .listEntities( + callContext.getPolarisCallContext(), + parentPath.stream().map(PolarisEntity::toCore).collect(Collectors.toList()), + PolarisEntityType.NAMESPACE, + PolarisEntitySubType.ANY_SUBTYPE); + if (!siblingNamespacesResult.isSuccess()) { + throw new IllegalStateException( + "Unable to resolve siblings entities to validate location - could not list namespaces"); + } + + // if the entity path has more than just the catalog, check for tables as well as other + // namespaces + Optional parentNamespace = + parentPath.size() > 1 + ? Optional.of(NamespaceEntity.of(parentPath.get(parentPath.size() - 1))) + : Optional.empty(); + + List siblingTables = + parentNamespace + .map( + ns -> { + PolarisMetaStoreManager.ListEntitiesResult siblingTablesResult = + entityManager + .getMetaStoreManager() + .listEntities( + callContext.getPolarisCallContext(), + parentPath.stream() + .map(PolarisEntity::toCore) + .collect(Collectors.toList()), + PolarisEntityType.TABLE_LIKE, + PolarisEntitySubType.ANY_SUBTYPE); + if (!siblingTablesResult.isSuccess()) { + throw new IllegalStateException( + "Unable to resolve siblings entities to validate location - could not list tables"); + } + return siblingTablesResult.getEntities().stream() + .map(tbl -> TableIdentifier.of(ns.asNamespace(), tbl.getName())) + .collect(Collectors.toList()); + }) + .orElse(List.of()); + ; + + List siblingNamespaces = + siblingNamespacesResult.getEntities().stream() + .map( + ns -> { + String[] nsLevels = + parentNamespace + .map(parent -> parent.asNamespace().levels()) + .orElse(new String[0]); + String[] newLevels = Arrays.copyOf(nsLevels, nsLevels.length + 1); + newLevels[nsLevels.length] = ns.getName(); + return Namespace.of(newLevels); + }) + .collect(Collectors.toList()); + LOG.debug( + "Resolving {} sibling entities to validate location", + siblingTables.size() + siblingNamespaces.size()); + PolarisResolutionManifest resolutionManifest = + new PolarisResolutionManifest( + callContext, entityManager, authenticatedPrincipal, parentPath.get(0).getName()); + siblingTables.forEach( + tbl -> + resolutionManifest.addPath( + new ResolverPath( + PolarisCatalogHelpers.tableIdentifierToList(tbl), PolarisEntityType.TABLE_LIKE), + tbl)); + siblingNamespaces.forEach( + ns -> + resolutionManifest.addPath( + new ResolverPath(Arrays.asList(ns.levels()), PolarisEntityType.NAMESPACE), ns)); + ResolverStatus status = resolutionManifest.resolveAll(); + if (!status.getStatus().equals(ResolverStatus.StatusEnum.SUCCESS)) { + throw new IllegalStateException( + "Unable to resolve sibling entities to validate location - could not resolve" + + status.getFailedToResolvedEntityName()); + } + Stream.concat( + siblingTables.stream() + .filter(tbl -> !tbl.name().equals(name)) + .map( + tbl -> { + PolarisResolvedPathWrapper resolveTablePath = + resolutionManifest.getResolvedPath(tbl); + return TableLikeEntity.of(resolveTablePath.getRawLeafEntity()) + .getBaseLocation(); + }), + siblingNamespaces.stream() + .filter(ns -> !ns.level(ns.length() - 1).equals(name)) + .map( + ns -> { + PolarisResolvedPathWrapper resolveNamespacePath = + resolutionManifest.getResolvedPath(ns); + return NamespaceEntity.of(resolveNamespacePath.getRawLeafEntity()) + .getBaseLocation(); + })) + .filter(java.util.Objects::nonNull) + .forEach( + siblingLocation -> { + URI target = URI.create(location); + URI existing = URI.create(siblingLocation); + if (isUnderParentLocation(target, existing) + || isUnderParentLocation(existing, target)) { + throw new org.apache.iceberg.exceptions.BadRequestException( + "Unable to create table at location '%s' because it conflicts with existing table or namespace at location '%s'", + target, existing); + } + }); + } + private class BasePolarisCatalogTableBuilder extends BaseMetastoreViewCatalog.BaseMetastoreViewCatalogTableBuilder { private final TableIdentifier identifier; @@ -783,13 +1097,13 @@ public ViewBuilder withLocation(String newLocation) { private class BasePolarisTableOperations extends BaseMetastoreTableOperations { private final TableIdentifier tableIdentifier; private final String fullTableName; - private FileIO fileIO; + private FileIO tableFileIO; BasePolarisTableOperations(FileIO defaultFileIO, TableIdentifier tableIdentifier) { LOG.debug("new BasePolarisTableOperations for {}", tableIdentifier); this.tableIdentifier = tableIdentifier; this.fullTableName = fullTableName(catalogName, tableIdentifier); - this.fileIO = defaultFileIO; + this.tableFileIO = defaultFileIO; } @Override @@ -822,7 +1136,7 @@ public void doRefresh() { SHOULD_RETRY_REFRESH_PREDICATE, MAX_RETRIES, metadataLocation -> { - FileIO fileIO = this.fileIO; + FileIO fileIO = this.tableFileIO; boolean closeFileIO = false; PolarisResolvedPathWrapper resolvedStorageEntity = resolvedEntities == null @@ -830,30 +1144,14 @@ public void doRefresh() { : resolvedEntities; String latestLocationDir = latestLocation.substring(0, latestLocation.lastIndexOf('/')); - Optional storageInfoEntity = - findStorageInfoFromHierarchy(resolvedStorageEntity); - Map credentialsMap = - storageInfoEntity - .map( - storageInfo -> - refreshCredentials( - tableIdentifier, - Set.of(PolarisStorageActions.READ), - latestLocationDir, - storageInfo)) - .orElse(Map.of()); - if (!credentialsMap.isEmpty()) { - String ioImpl = fileIO.getClass().getName(); - fileIO = loadFileIO(ioImpl, credentialsMap); - closeFileIO = true; - } - try { - return TableMetadataParser.read(fileIO, metadataLocation); - } finally { - if (closeFileIO) { - fileIO.close(); - } - } + fileIO = + refreshIOWithCredentials( + tableIdentifier, + Set.of(latestLocationDir), + resolvedStorageEntity, + new HashMap<>(), + fileIO); + return TableMetadataParser.read(fileIO, metadataLocation); }); } } @@ -880,36 +1178,31 @@ public void doCommit(TableMetadata base, TableMetadata metadata) { ? resolvedEntityView.getResolvedPath(tableIdentifier.namespace()) : resolvedTableEntities; + // refresh credentials because we need to read the metadata file to validate its location + tableFileIO = + refreshIOWithCredentials( + tableIdentifier, + getLocationsAllowedToBeAccessed(metadata), + resolvedStorageEntity, + new HashMap<>(metadata.properties()), + tableFileIO); + + List resolvedNamespace = + resolvedTableEntities == null + ? resolvedEntityView.getResolvedPath(tableIdentifier.namespace()).getRawFullPath() + : resolvedTableEntities.getRawParentPath(); + CatalogEntity catalog = CatalogEntity.of(resolvedNamespace.get(0)); + if (base == null || !metadata.location().equals(base.location())) { // If location is changing then we must validate that the requested location is valid // for the storage configuration inherited under this entity's path. validateLocationForTableLike(tableIdentifier, metadata.location(), resolvedStorageEntity); - } - - Optional storageInfoEntity = - findStorageInfoFromHierarchy(resolvedStorageEntity); - Map credentialsMap = - storageInfoEntity - .map( - storageInfo -> - refreshCredentials( - tableIdentifier, - Set.of(PolarisStorageActions.READ, PolarisStorageActions.WRITE), - metadata.location(), - storageInfo)) - .orElse(Map.of()); - - // Update the FileIO before we write the new metadata file - // update with table properties in case there are table-level overrides - // the credentials should always override table-level properties, since - // storage configuration will be found at whatever entity defines it - Map tableProperties = new HashMap<>(metadata.properties()); - tableProperties.putAll(credentialsMap); - if (!tableProperties.isEmpty()) { - String ioImpl = fileIO.getClass().getName(); - fileIO = loadFileIO(ioImpl, tableProperties); - // ensure the new fileIO is closed when the catalog is closed - closeableGroup.addCloseable(fileIO); + // also validate that the view location doesn't overlap an existing table + validateNoLocationOverlap(tableIdentifier, resolvedNamespace, metadata.location()); + // and that the metadata file points to a location within the table's directory structure + if (metadata.metadataFileLocation() != null) { + validateMetadataFileInTableDir(tableIdentifier, metadata, catalog); + } } String newLocation = writeNewMetadataIfRequired(base == null, metadata); @@ -939,6 +1232,7 @@ public void doCommit(TableMetadata base, TableMetadata metadata) { new TableLikeEntity.Builder(tableIdentifier, newLocation) .setCatalogId(getCatalogId()) .setSubType(PolarisEntitySubType.TABLE) + .setBaseLocation(metadata.location()) .setId( entityManager .getMetaStoreManager() @@ -947,7 +1241,11 @@ public void doCommit(TableMetadata base, TableMetadata metadata) { .build(); } else { existingLocation = entity.getMetadataLocation(); - entity = new TableLikeEntity.Builder(entity).setMetadataLocation(newLocation).build(); + entity = + new TableLikeEntity.Builder(entity) + .setBaseLocation(metadata.location()) + .setMetadataLocation(newLocation) + .build(); } if (!Objects.equal(existingLocation, oldLocation)) { if (null == base) { @@ -972,7 +1270,7 @@ public void doCommit(TableMetadata base, TableMetadata metadata) { @Override public FileIO io() { - return fileIO; + return tableFileIO; } @Override @@ -981,6 +1279,35 @@ protected String tableName() { } } + private void validateMetadataFileInTableDir( + TableIdentifier identifier, TableMetadata metadata, CatalogEntity catalog) { + PolarisCallContext polarisCallContext = callContext.getPolarisCallContext(); + String allowEscape = + catalog + .getPropertiesAsMap() + .get(PolarisConfiguration.CATALOG_ALLOW_EXTERNAL_TABLE_LOCATION); + if (!Boolean.parseBoolean(allowEscape) + && !polarisCallContext + .getConfigurationStore() + .getConfiguration( + polarisCallContext, + PolarisConfiguration.ALLOW_EXTERNAL_METADATA_FILE_LOCATION, + PolarisConfiguration.DEFAULT_ALLOW_EXTERNAL_METADATA_FILE_LOCATION)) { + LOG.debug( + "Validating base location {} for table {} in metadata file {}", + metadata.location(), + identifier, + metadata.metadataFileLocation()); + if (!isUnderParentLocation( + URI.create(metadata.metadataFileLocation()), + URI.create(metadata.location() + "/metadata").normalize())) { + throw new org.apache.iceberg.exceptions.BadRequestException( + "Metadata location %s is not allowed outside of table location %s", + metadata.metadataFileLocation(), metadata.location()); + } + } + } + private static @NotNull Optional findStorageInfoFromHierarchy( PolarisResolvedPathWrapper resolvedStorageEntity) { Optional storageInfoEntity = @@ -996,10 +1323,10 @@ protected String tableName() { private class BasePolarisViewOperations extends BaseViewOperations { private final TableIdentifier identifier; private final String fullViewName; - private FileIO io; + private FileIO viewFileIO; BasePolarisViewOperations(FileIO io, TableIdentifier identifier) { - this.io = io; + this.viewFileIO = io; this.identifier = identifier; this.fullViewName = ViewUtil.fullViewName(catalogName, identifier); } @@ -1030,7 +1357,7 @@ public void doRefresh() { SHOULD_RETRY_REFRESH_PREDICATE, MAX_RETRIES, metadataLocation -> { - FileIO fileIO = this.io; + FileIO fileIO = this.viewFileIO; boolean closeFileIO = false; PolarisResolvedPathWrapper resolvedStorageEntity = resolvedEntities == null @@ -1093,37 +1420,27 @@ public void doCommit(ViewMetadata base, ViewMetadata metadata) { ? resolvedEntityView.getResolvedPath(identifier.namespace()) : resolvedEntities; + List resolvedNamespace = + resolvedEntities == null + ? resolvedEntityView.getResolvedPath(identifier.namespace()).getRawFullPath() + : resolvedEntities.getRawParentPath(); if (base == null || !metadata.location().equals(base.location())) { // If location is changing then we must validate that the requested location is valid // for the storage configuration inherited under this entity's path. validateLocationForTableLike(identifier, metadata.location(), resolvedStorageEntity); + validateNoLocationOverlap(identifier, resolvedNamespace, metadata.location()); } - Optional storageInfoEntity = - findStorageInfoFromHierarchy(resolvedStorageEntity); - Map credentialsMap = - storageInfoEntity - .map( - storageInfo -> - refreshCredentials( - identifier, - Set.of(PolarisStorageActions.READ, PolarisStorageActions.WRITE), - metadata.location(), - storageInfo)) - .orElse(Map.of()); - - // Update the FileIO before we write the new metadata file - // update with table properties in case there are table-level overrides - // the credentials should always override table-level properties, since - // storage configuration will be found at whatever entity defines it Map tableProperties = new HashMap<>(metadata.properties()); - tableProperties.putAll(credentialsMap); - if (!tableProperties.isEmpty()) { - String ioImpl = io.getClass().getName(); - io = loadFileIO(ioImpl, tableProperties); - // ensure the new fileIO is closed when the catalog is closed - closeableGroup.addCloseable(io); - } + + viewFileIO = + refreshIOWithCredentials( + identifier, + getLocationsAllowedToBeAccessed(metadata), + resolvedStorageEntity, + tableProperties, + viewFileIO); + String newLocation = writeNewMetadataIfRequired(metadata); String oldLocation = base == null ? null : currentMetadataLocation(); @@ -1175,7 +1492,7 @@ public void doCommit(ViewMetadata base, ViewMetadata metadata) { @Override public FileIO io() { - return io; + return viewFileIO; } @Override @@ -1184,6 +1501,37 @@ protected String viewName() { } } + private FileIO refreshIOWithCredentials( + TableIdentifier identifier, + Set readLocations, + PolarisResolvedPathWrapper resolvedStorageEntity, + Map tableProperties, + FileIO fileIO) { + Optional storageInfoEntity = findStorageInfoFromHierarchy(resolvedStorageEntity); + Map credentialsMap = + storageInfoEntity + .map( + storageInfo -> + refreshCredentials( + identifier, + Set.of(PolarisStorageActions.READ, PolarisStorageActions.WRITE), + readLocations, + storageInfo)) + .orElse(Map.of()); + + // Update the FileIO before we write the new metadata file + // update with table properties in case there are table-level overrides + // the credentials should always override table-level properties, since + // storage configuration will be found at whatever entity defines it + tableProperties.putAll(credentialsMap); + if (!tableProperties.isEmpty()) { + fileIO = loadFileIO(ioImplClassName, tableProperties); + // ensure the new fileIO is closed when the catalog is closed + closeableGroup.addCloseable(fileIO); + } + return fileIO; + } + private PolarisCallContext getCurrentPolarisContext() { return callContext.getPolarisCallContext(); } @@ -1359,6 +1707,10 @@ private void createTableLike( } } + private static boolean isUnderParentLocation(URI childLocation, URI expectedParentLocation) { + return !expectedParentLocation.relativize(childLocation).equals(childLocation); + } + private void updateTableLike(long catalogId, TableIdentifier identifier, PolarisEntity entity) { PolarisResolvedPathWrapper resolvedEntities = resolvedEntityView.getResolvedPath(identifier, entity.getSubType()); @@ -1459,6 +1811,28 @@ private boolean sendNotificationForTableLike( existingLocation = entity.getMetadataLocation(); entity = new TableLikeEntity.Builder(entity).setMetadataLocation(newLocation).build(); } + // first validate we can read the metadata file + validateLocationForTableLike(tableIdentifier, newLocation); + + TableOperations tableOperations = newTableOps(tableIdentifier); + String locationDir = newLocation.substring(0, newLocation.lastIndexOf("/")); + ; + FileIO fileIO = + refreshIOWithCredentials( + tableIdentifier, + Set.of(locationDir), + resolvedParent, + new HashMap<>(), + tableOperations.io()); + TableMetadata tableMetadata = TableMetadataParser.read(fileIO, newLocation); + + // then validate that it points to a valid location for this table + validateLocationForTableLike(tableIdentifier, tableMetadata.location()); + + // finally, validate that the metadata file is within the table directory + validateMetadataFileInTableDir( + tableIdentifier, tableMetadata, CatalogEntity.of(resolvedParent.getRawFullPath().get(0))); + // TODO: These might fail due to concurrent update; we need to do a retry in those cases. if (null == existingLocation) { LOG.debug( @@ -1471,6 +1845,7 @@ private boolean sendNotificationForTableLike( "Updating table {} for notification with metadataLocation {}", tableIdentifier, newLocation); + updateTableLike(catalogId, tableIdentifier, entity); } } @@ -1526,6 +1901,7 @@ private List listTableLike( * @return FileIO object */ private FileIO loadFileIO(String ioImpl, Map properties) { + blockedUserSpecifiedWriteLocation(properties); Map propertiesWithS3CustomizedClientFactory = new HashMap<>(properties); propertiesWithS3CustomizedClientFactory.put( S3FileIOProperties.CLIENT_FACTORY, PolarisS3FileIOClientFactory.class.getName()); @@ -1533,6 +1909,16 @@ private FileIO loadFileIO(String ioImpl, Map properties) { ioImpl, propertiesWithS3CustomizedClientFactory, new Configuration()); } + private void blockedUserSpecifiedWriteLocation(Map properties) { + if (properties != null + && (properties.containsKey(TableLikeEntity.USER_SPECIFIED_WRITE_DATA_LOCATION_KEY) + || properties.containsKey( + TableLikeEntity.USER_SPECIFIED_WRITE_METADATA_LOCATION_KEY))) { + throw new ForbiddenException( + "Delegate access to table with user-specified write location is temporarily not supported."); + } + } + /** * Check if the exception is retryable for the storage provider * diff --git a/polaris-service/src/main/java/io/polaris/service/catalog/PolarisCatalogHandlerWrapper.java b/polaris-service/src/main/java/io/polaris/service/catalog/PolarisCatalogHandlerWrapper.java index 9abd71dd0c..8197e5826a 100644 --- a/polaris-service/src/main/java/io/polaris/service/catalog/PolarisCatalogHandlerWrapper.java +++ b/polaris-service/src/main/java/io/polaris/service/catalog/PolarisCatalogHandlerWrapper.java @@ -136,7 +136,9 @@ public PolarisCatalogHandlerWrapper( } private void initializeCatalog() { - this.baseCatalog = catalogFactory.createCallContextCatalog(callContext, resolutionManifest); + this.baseCatalog = + catalogFactory.createCallContextCatalog( + callContext, authenticatedPrincipal, resolutionManifest); this.namespaceCatalog = (baseCatalog instanceof SupportsNamespaces) ? (SupportsNamespaces) baseCatalog : null; this.viewCatalog = (baseCatalog instanceof ViewCatalog) ? (ViewCatalog) baseCatalog : null; @@ -589,18 +591,20 @@ public LoadTableResponse createTableDirectWithWriteDelegation( LoadTableResponse.Builder responseBuilder = LoadTableResponse.builder().withTableMetadata(tableMetadata); if (baseCatalog instanceof SupportsCredentialDelegation credentialDelegation) { - LOG.atDebug() - .addKeyValue("tableIdentifier", tableIdentifier) - .addKeyValue("tableLocation", tableMetadata.location()) - .log("Fetching client credentials for table"); - responseBuilder.addAllConfig( - credentialDelegation.getCredentialConfig( - tableIdentifier, - tableMetadata, - Set.of( - PolarisStorageActions.READ, - PolarisStorageActions.WRITE, - PolarisStorageActions.LIST))); + try { + Set actionsRequested = + getValidTableActionsOrThrow(tableIdentifier); + + LOG.atDebug() + .addKeyValue("tableIdentifier", tableIdentifier) + .addKeyValue("tableLocation", tableMetadata.location()) + .log("Fetching client credentials for table"); + responseBuilder.addAllConfig( + credentialDelegation.getCredentialConfig( + tableIdentifier, tableMetadata, actionsRequested)); + } catch (ForbiddenException | NoSuchTableException e) { + // No privileges available + } } return responseBuilder.build(); } else if (table instanceof BaseMetadataTable) { @@ -702,13 +706,18 @@ public LoadTableResponse createTableStagedWithWriteDelegation( LoadTableResponse.builder().withTableMetadata(metadata); if (baseCatalog instanceof SupportsCredentialDelegation credentialDelegation) { - LOG.atDebug() - .addKeyValue("tableIdentifier", ident) - .addKeyValue("tableLocation", metadata.location()) - .log("Fetching client credentials for table"); - responseBuilder.addAllConfig( - credentialDelegation.getCredentialConfig( - ident, metadata, Set.of(PolarisStorageActions.ALL))); + try { + Set actionsRequested = getValidTableActionsOrThrow(ident); + + LOG.atDebug() + .addKeyValue("tableIdentifier", ident) + .addKeyValue("tableLocation", metadata.location()) + .log("Fetching client credentials for table"); + responseBuilder.addAllConfig( + credentialDelegation.getCredentialConfig(ident, metadata, actionsRequested)); + } catch (ForbiddenException | NoSuchTableException e) { + // No privileges available + } } return responseBuilder.build(); }); @@ -770,33 +779,38 @@ public LoadTableResponse loadTable(TableIdentifier tableIdentifier, String snaps return doCatalogOperation(() -> CatalogHandlers.loadTable(baseCatalog, tableIdentifier)); } - public LoadTableResponse loadTableWithAccessDelegation( - TableIdentifier tableIdentifier, String xIcebergAccessDelegation, String snapshots) { - // Here we have a single method that falls through multiple candidate - // PolarisAuthorizableOperations because instead of identifying the desired operation up-front - // and - // failing the authz check if grants aren't found, we find the first most-privileged authz match - // and respond according to that. - PolarisAuthorizableOperation op1 = PolarisAuthorizableOperation.LOAD_TABLE_WITH_READ_DELEGATION; - PolarisAuthorizableOperation op2 = + private Set getValidTableActionsOrThrow(TableIdentifier tableIdentifier) { + PolarisAuthorizableOperation read = + PolarisAuthorizableOperation.LOAD_TABLE_WITH_READ_DELEGATION; + PolarisAuthorizableOperation write = PolarisAuthorizableOperation.LOAD_TABLE_WITH_WRITE_DELEGATION; - Set actionsRequested = new HashSet<>(Set.of(PolarisStorageActions.READ, PolarisStorageActions.LIST)); try { // TODO: Refactor to have a boolean-return version of the helpers so we can fallthrough // easily. - authorizeBasicTableLikeOperationOrThrow(op2, PolarisEntitySubType.TABLE, tableIdentifier); + authorizeBasicTableLikeOperationOrThrow(write, PolarisEntitySubType.TABLE, tableIdentifier); actionsRequested.add(PolarisStorageActions.WRITE); - } catch (ForbiddenException e) { + } catch (ForbiddenException | NoSuchTableException e) { LOG.atDebug() .addKeyValue("tableIdentifier", tableIdentifier) .log("Authz failed for LOAD_TABLE_WITH_WRITE_DELEGATION so attempting READ only"); - authorizeBasicTableLikeOperationOrThrow(op1, PolarisEntitySubType.TABLE, tableIdentifier); + authorizeBasicTableLikeOperationOrThrow(read, PolarisEntitySubType.TABLE, tableIdentifier); } + return actionsRequested; + } + + public LoadTableResponse loadTableWithAccessDelegation( + TableIdentifier tableIdentifier, String xIcebergAccessDelegation, String snapshots) { + // Here we have a single method that falls through multiple candidate + // PolarisAuthorizableOperations because instead of identifying the desired operation up-front + // and + // failing the authz check if grants aren't found, we find the first most-privileged authz match + // and respond according to that. // TODO: Find a way for the configuration or caller to better express whether to fail or omit // when data-access is specified but access delegation grants are not found. + Set actionsRequested = getValidTableActionsOrThrow(tableIdentifier); return doCatalogOperation( () -> { diff --git a/polaris-service/src/main/java/io/polaris/service/config/PolarisApplicationConfig.java b/polaris-service/src/main/java/io/polaris/service/config/PolarisApplicationConfig.java index 25e65880dd..af5cca4867 100644 --- a/polaris-service/src/main/java/io/polaris/service/config/PolarisApplicationConfig.java +++ b/polaris-service/src/main/java/io/polaris/service/config/PolarisApplicationConfig.java @@ -26,6 +26,11 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.LoggerFactory; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; /** * Configuration specific to a Polaris REST Service. Place these entries in a YML file for them to @@ -45,6 +50,8 @@ public class PolarisApplicationConfig extends Configuration { private PolarisConfigurationStore configurationStore = new DefaultConfigurationStore(new HashMap<>()); private List defaultRealms; + private String awsAccessKey; + private String awsSecretKey; public Map getSqlLiteCatalogDirs() { return sqlLiteCatalogDirs; @@ -153,6 +160,24 @@ public List getDefaultRealms() { return defaultRealms; } + public AwsCredentialsProvider credentialsProvider() { + if (StringUtils.isNotBlank(awsAccessKey) && StringUtils.isNotBlank(awsSecretKey)) { + LoggerFactory.getLogger(PolarisApplicationConfig.class) + .warn("Using hard-coded AWS credentials - this is not recommended for production"); + return StaticCredentialsProvider.create( + AwsBasicCredentials.create(awsAccessKey, awsSecretKey)); + } + return null; + } + + public void setAwsAccessKey(String awsAccessKey) { + this.awsAccessKey = awsAccessKey; + } + + public void setAwsSecretKey(String awsSecretKey) { + this.awsSecretKey = awsSecretKey; + } + public void setDefaultRealms(List defaultRealms) { this.defaultRealms = defaultRealms; } diff --git a/polaris-service/src/main/java/io/polaris/service/context/CallContextCatalogFactory.java b/polaris-service/src/main/java/io/polaris/service/context/CallContextCatalogFactory.java index 17cd1e5654..f900081191 100644 --- a/polaris-service/src/main/java/io/polaris/service/context/CallContextCatalogFactory.java +++ b/polaris-service/src/main/java/io/polaris/service/context/CallContextCatalogFactory.java @@ -15,10 +15,14 @@ */ package io.polaris.service.context; +import io.polaris.core.auth.AuthenticatedPolarisPrincipal; import io.polaris.core.context.CallContext; import io.polaris.core.persistence.resolver.PolarisResolutionManifest; import org.apache.iceberg.catalog.Catalog; public interface CallContextCatalogFactory { - Catalog createCallContextCatalog(CallContext context, PolarisResolutionManifest resolvedManifest); + Catalog createCallContextCatalog( + CallContext context, + AuthenticatedPolarisPrincipal authenticatedPrincipal, + PolarisResolutionManifest resolvedManifest); } diff --git a/polaris-service/src/main/java/io/polaris/service/context/PolarisCallContextCatalogFactory.java b/polaris-service/src/main/java/io/polaris/service/context/PolarisCallContextCatalogFactory.java index 556c464f05..f6c1684388 100644 --- a/polaris-service/src/main/java/io/polaris/service/context/PolarisCallContextCatalogFactory.java +++ b/polaris-service/src/main/java/io/polaris/service/context/PolarisCallContextCatalogFactory.java @@ -15,6 +15,7 @@ */ package io.polaris.service.context; +import io.polaris.core.auth.AuthenticatedPolarisPrincipal; import io.polaris.core.context.CallContext; import io.polaris.core.entity.CatalogEntity; import io.polaris.core.entity.PolarisBaseEntity; @@ -48,7 +49,9 @@ public PolarisCallContextCatalogFactory( @Override public Catalog createCallContextCatalog( - CallContext context, final PolarisResolutionManifest resolvedManifest) { + CallContext context, + AuthenticatedPolarisPrincipal authenticatedPrincipal, + final PolarisResolutionManifest resolvedManifest) { PolarisBaseEntity baseCatalogEntity = resolvedManifest.getResolvedReferenceCatalogEntity().getRawLeafEntity(); String catalogName = baseCatalogEntity.getName(); @@ -61,7 +64,8 @@ public Catalog createCallContextCatalog( entityManagerFactory.getOrCreateEntityManager(context.getRealmContext()); BasePolarisCatalog catalogInstance = - new BasePolarisCatalog(entityManager, context, resolvedManifest, taskExecutor); + new BasePolarisCatalog( + entityManager, context, resolvedManifest, authenticatedPrincipal, taskExecutor); context.contextVariables().put(CallContext.REQUEST_PATH_CATALOG_INSTANCE_KEY, catalogInstance); diff --git a/polaris-service/src/main/java/io/polaris/service/context/SqlliteCallContextCatalogFactory.java b/polaris-service/src/main/java/io/polaris/service/context/SqlliteCallContextCatalogFactory.java index 546c021766..a4685d1d1c 100644 --- a/polaris-service/src/main/java/io/polaris/service/context/SqlliteCallContextCatalogFactory.java +++ b/polaris-service/src/main/java/io/polaris/service/context/SqlliteCallContextCatalogFactory.java @@ -15,6 +15,7 @@ */ package io.polaris.service.context; +import io.polaris.core.auth.AuthenticatedPolarisPrincipal; import io.polaris.core.context.CallContext; import io.polaris.core.persistence.resolver.PolarisResolutionManifest; import java.io.IOException; @@ -55,7 +56,9 @@ public SqlliteCallContextCatalogFactory(Map catalogBaseDirs) { @Override public Catalog createCallContextCatalog( - CallContext context, PolarisResolutionManifest resolvedManifest) { + CallContext context, + AuthenticatedPolarisPrincipal polarisPrincipal, + PolarisResolutionManifest resolvedManifest) { String catalogName = resolvedManifest.getResolvedReferenceCatalogEntity().getRawLeafEntity().getName(); if (catalogName == null) { diff --git a/polaris-service/src/main/java/io/polaris/service/storage/PolarisStorageIntegrationProviderImpl.java b/polaris-service/src/main/java/io/polaris/service/storage/PolarisStorageIntegrationProviderImpl.java index afc2c46e1d..b21575a968 100644 --- a/polaris-service/src/main/java/io/polaris/service/storage/PolarisStorageIntegrationProviderImpl.java +++ b/polaris-service/src/main/java/io/polaris/service/storage/PolarisStorageIntegrationProviderImpl.java @@ -32,12 +32,18 @@ import java.util.EnumMap; import java.util.Map; import java.util.Set; +import java.util.function.Supplier; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import software.amazon.awssdk.services.sts.StsClient; public class PolarisStorageIntegrationProviderImpl implements PolarisStorageIntegrationProvider { - public PolarisStorageIntegrationProviderImpl() {} + + private final Supplier stsClientSupplier; + + public PolarisStorageIntegrationProviderImpl(Supplier stsClientSupplier) { + this.stsClientSupplier = stsClientSupplier; + } @Override @SuppressWarnings("unchecked") @@ -51,7 +57,8 @@ PolarisStorageIntegration getStorageIntegrationForConfig( switch (polarisStorageConfigurationInfo.getStorageType()) { case S3: storageIntegration = - (PolarisStorageIntegration) new AwsCredentialsStorageIntegration(StsClient.create()); + (PolarisStorageIntegration) + new AwsCredentialsStorageIntegration(stsClientSupplier.get()); break; case GCS: try { diff --git a/polaris-service/src/test/java/io/polaris/service/PolarisApplicationIntegrationTest.java b/polaris-service/src/test/java/io/polaris/service/PolarisApplicationIntegrationTest.java index 4b4f4eb492..56523032d3 100644 --- a/polaris-service/src/test/java/io/polaris/service/PolarisApplicationIntegrationTest.java +++ b/polaris-service/src/test/java/io/polaris/service/PolarisApplicationIntegrationTest.java @@ -60,6 +60,7 @@ import org.apache.iceberg.catalog.SessionCatalog; import org.apache.iceberg.catalog.TableIdentifier; import org.apache.iceberg.exceptions.BadRequestException; +import org.apache.iceberg.exceptions.ForbiddenException; import org.apache.iceberg.exceptions.NoSuchNamespaceException; import org.apache.iceberg.exceptions.NoSuchTableException; import org.apache.iceberg.exceptions.RESTException; @@ -396,7 +397,11 @@ public void testIcebergCreateNamespaceInExternalCatalog(TestInfo testInfo) throw List namespaces = sessionCatalog.listNamespaces(sessionContext); assertThat(namespaces).isNotNull().hasSize(1).containsExactly(ns); Map metadata = sessionCatalog.loadNamespaceMetadata(sessionContext, ns); - assertThat(metadata).isNotNull().isEmpty(); + assertThat(metadata) + .isNotNull() + .isNotEmpty() + .containsEntry( + PolarisEntityConstants.ENTITY_BASE_LOCATION, "s3://my-bucket/path/to/data/db1"); } } @@ -446,6 +451,57 @@ public void testIcebergCreateTablesInExternalCatalog(TestInfo testInfo) throws I } } + @Test + public void testIcebergCreateTablesWithWritePathBlocked(TestInfo testInfo) throws IOException { + String catalogName = testInfo.getTestMethod().get().getName() + "Internal"; + createCatalog(catalogName, Catalog.TypeEnum.INTERNAL, PRINCIPAL_ROLE_NAME); + try (RESTSessionCatalog sessionCatalog = newSessionCatalog(catalogName)) { + SessionCatalog.SessionContext sessionContext = SessionCatalog.SessionContext.createEmpty(); + Namespace ns = Namespace.of("db1"); + sessionCatalog.createNamespace(sessionContext, ns); + try { + Assertions.assertThatThrownBy( + () -> + sessionCatalog + .buildTable( + sessionContext, + TableIdentifier.of(ns, "the_table"), + new Schema( + List.of( + Types.NestedField.of( + 1, false, "theField", Types.StringType.get())))) + .withSortOrder(SortOrder.unsorted()) + .withPartitionSpec(PartitionSpec.unpartitioned()) + .withProperties(Map.of("write.data.path", "s3://my-bucket/path/to/data")) + .create()) + .isInstanceOf(ForbiddenException.class) + .hasMessage( + "Forbidden: Delegate access to table with user-specified write location is temporarily not supported."); + + Assertions.assertThatThrownBy( + () -> + sessionCatalog + .buildTable( + sessionContext, + TableIdentifier.of(ns, "the_table"), + new Schema( + List.of( + Types.NestedField.of( + 1, false, "theField", Types.StringType.get())))) + .withSortOrder(SortOrder.unsorted()) + .withPartitionSpec(PartitionSpec.unpartitioned()) + .withProperties( + Map.of("write.metadata.path", "s3://my-bucket/path/to/data")) + .create()) + .isInstanceOf(ForbiddenException.class) + .hasMessage( + "Forbidden: Delegate access to table with user-specified write location is temporarily not supported."); + } catch (BadRequestException e) { + LOGGER.info("Received expected exception " + e.getMessage()); + } + } + } + @Test public void testIcebergRegisterTableInExternalCatalog(TestInfo testInfo) throws IOException { String catalogName = testInfo.getTestMethod().get().getName() + "External"; diff --git a/polaris-service/src/test/java/io/polaris/service/admin/PolarisAuthzTestBase.java b/polaris-service/src/test/java/io/polaris/service/admin/PolarisAuthzTestBase.java index 6cfa021a89..5f1420cb39 100644 --- a/polaris-service/src/test/java/io/polaris/service/admin/PolarisAuthzTestBase.java +++ b/polaris-service/src/test/java/io/polaris/service/admin/PolarisAuthzTestBase.java @@ -54,7 +54,6 @@ import io.polaris.service.storage.PolarisStorageIntegrationProviderImpl; import java.io.IOException; import java.time.Clock; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; @@ -146,13 +145,15 @@ public void before() { PolarisTreeMapStore backingStore = new PolarisTreeMapStore(diagServices); InMemoryPolarisMetaStoreManagerFactory managerFactory = new InMemoryPolarisMetaStoreManagerFactory(); - managerFactory.setStorageIntegrationProvider(new PolarisStorageIntegrationProviderImpl()); + managerFactory.setStorageIntegrationProvider( + new PolarisStorageIntegrationProviderImpl(Mockito::mock)); RealmContext realmContext = () -> "realm"; PolarisMetaStoreManager metaStoreManager = managerFactory.getOrCreateMetaStoreManager(realmContext); - Map configMap = new HashMap<>(); - configMap.put("ALLOW_SPECIFYING_FILE_IO_IMPL", true); + Map configMap = + Map.of( + "ALLOW_SPECIFYING_FILE_IO_IMPL", true, "ALLOW_EXTERNAL_METADATA_FILE_LOCATION", true); PolarisCallContext polarisContext = new PolarisCallContext( managerFactory.getOrCreateSessionSupplier(realmContext).get(), @@ -197,11 +198,11 @@ public String getRealmIdentifier() { this.adminService = new PolarisAdminService(callContext, entityManager, authenticatedRoot, polarisAuthorizer); - String storageLocation = "file:///tmp"; + String storageLocation = "file:///tmp/authz"; FileStorageConfigInfo storageConfigModel = FileStorageConfigInfo.builder() .setStorageType(StorageConfigInfo.StorageTypeEnum.FILE) - .setAllowedLocations(List.of(storageLocation, "file:///tmp")) + .setAllowedLocations(List.of(storageLocation, "file:///tmp/authz")) .build(); catalogEntity = adminService.createCatalog( @@ -350,7 +351,8 @@ private void initBaseCatalog() { new PolarisPassthroughResolutionView( callContext, entityManager, authenticatedRoot, CATALOG_NAME); this.baseCatalog = - new BasePolarisCatalog(entityManager, callContext, passthroughView, Mockito.mock()); + new BasePolarisCatalog( + entityManager, callContext, passthroughView, authenticatedRoot, Mockito.mock()); this.baseCatalog.initialize( CATALOG_NAME, ImmutableMap.of( @@ -371,10 +373,13 @@ public PolarisEntityManager getOrCreateEntityManager(RealmContext realmContext) @Override public Catalog createCallContextCatalog( - CallContext context, final PolarisResolutionManifest resolvedManifest) { + CallContext context, + AuthenticatedPolarisPrincipal authenticatedPolarisPrincipal, + final PolarisResolutionManifest resolvedManifest) { // This depends on the BasePolarisCatalog allowing calling initialize multiple times // to override the previous config. - Catalog catalog = super.createCallContextCatalog(context, resolvedManifest); + Catalog catalog = + super.createCallContextCatalog(context, authenticatedPolarisPrincipal, resolvedManifest); catalog.initialize( CATALOG_NAME, ImmutableMap.of( diff --git a/polaris-service/src/test/java/io/polaris/service/admin/PolarisOverlappingCatalogTest.java b/polaris-service/src/test/java/io/polaris/service/admin/PolarisOverlappingCatalogTest.java new file mode 100644 index 0000000000..3109751310 --- /dev/null +++ b/polaris-service/src/test/java/io/polaris/service/admin/PolarisOverlappingCatalogTest.java @@ -0,0 +1,192 @@ +package io.polaris.service.admin; + +import static io.polaris.service.context.DefaultContextResolver.REALM_PROPERTY_KEY; +import static org.assertj.core.api.Assertions.assertThat; + +import io.dropwizard.testing.ConfigOverride; +import io.dropwizard.testing.ResourceHelpers; +import io.dropwizard.testing.junit5.DropwizardAppExtension; +import io.dropwizard.testing.junit5.DropwizardExtensionsSupport; +import io.polaris.core.admin.model.AwsStorageConfigInfo; +import io.polaris.core.admin.model.Catalog; +import io.polaris.core.admin.model.CatalogProperties; +import io.polaris.core.admin.model.CreateCatalogRequest; +import io.polaris.core.admin.model.StorageConfigInfo; +import io.polaris.service.PolarisApplication; +import io.polaris.service.config.PolarisApplicationConfig; +import io.polaris.service.test.PolarisConnectionExtension; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.client.Invocation; +import jakarta.ws.rs.core.Response; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +@ExtendWith({DropwizardExtensionsSupport.class, PolarisConnectionExtension.class}) +public class PolarisOverlappingCatalogTest { + private static final DropwizardAppExtension EXT = + new DropwizardAppExtension<>( + PolarisApplication.class, + ResourceHelpers.resourceFilePath("polaris-server-integrationtest.yml"), + // Bind to random port to support parallelism + ConfigOverride.config("server.applicationConnectors[0].port", "0"), + ConfigOverride.config("server.adminConnectors[0].port", "0"), + // Block overlapping catalog paths: + ConfigOverride.config("featureConfiguration.ALLOW_OVERLAPPING_CATALOG_URLS", "false")); + private static String userToken; + private static String realm; + + @BeforeAll + public static void setup(PolarisConnectionExtension.PolarisToken adminToken) { + userToken = adminToken.token(); + realm = PolarisConnectionExtension.getTestRealm(PolarisServiceImplIntegrationTest.class); + } + + private Response createCatalog(String prefix, String defaultBaseLocation, boolean isExternal) { + return createCatalog(prefix, defaultBaseLocation, isExternal, new ArrayList()); + } + + private static Invocation.Builder request() { + return EXT.client() + .target(String.format("http://localhost:%d/api/management/v1/catalogs", EXT.getLocalPort())) + .request("application/json") + .header("Authorization", "Bearer " + userToken) + .header(REALM_PROPERTY_KEY, realm); + } + + private Response createCatalog( + String prefix, + String defaultBaseLocation, + boolean isExternal, + List allowedLocations) { + String uuid = UUID.randomUUID().toString(); + StorageConfigInfo config = + AwsStorageConfigInfo.builder() + .setRoleArn("arn:aws:iam::123456789012:role/my-role") + .setExternalId("externalId") + .setUserArn("userArn") + .setStorageType(StorageConfigInfo.StorageTypeEnum.S3) + .setAllowedLocations( + allowedLocations.stream() + .map( + l -> { + return String.format("s3://bucket/%s/%s", prefix, l); + }) + .toList()) + .build(); + Catalog catalog = + new Catalog( + isExternal ? Catalog.TypeEnum.EXTERNAL : Catalog.TypeEnum.INTERNAL, + String.format("overlap_catalog_%s", uuid), + new CatalogProperties(String.format("s3://bucket/%s/%s", prefix, defaultBaseLocation)), + System.currentTimeMillis(), + System.currentTimeMillis(), + 1, + config); + try (Response response = request().post(Entity.json(new CreateCatalogRequest(catalog)))) { + return response; + } + } + + @Test + public void testBasicOverlappingCatalogs() { + Arrays.asList(false, true) + .forEach( + initiallyExternal -> { + Arrays.asList(false, true) + .forEach( + laterExternal -> { + String prefix = UUID.randomUUID().toString(); + + assertThat(createCatalog(prefix, "root", initiallyExternal)) + .returns(Response.Status.CREATED.getStatusCode(), Response::getStatus); + + // OK, non-overlapping + assertThat(createCatalog(prefix, "boot", laterExternal)) + .returns(Response.Status.CREATED.getStatusCode(), Response::getStatus); + + // OK, non-overlapping due to no `/` + assertThat(createCatalog(prefix, "roo", laterExternal)) + .returns(Response.Status.CREATED.getStatusCode(), Response::getStatus); + + // Also OK due to no `/` + assertThat(createCatalog(prefix, "root.child", laterExternal)) + .returns(Response.Status.CREATED.getStatusCode(), Response::getStatus); + + // inside `root` + assertThat(createCatalog(prefix, "root/child", laterExternal)) + .returns( + Response.Status.BAD_REQUEST.getStatusCode(), Response::getStatus); + + // `root` is inside this + assertThat(createCatalog(prefix, "", laterExternal)) + .returns( + Response.Status.BAD_REQUEST.getStatusCode(), Response::getStatus); + }); + }); + } + + @Test + public void testAllowedLocationOverlappingCatalogs() { + Arrays.asList(false, true) + .forEach( + initiallyExternal -> { + Arrays.asList(false, true) + .forEach( + laterExternal -> { + String prefix = UUID.randomUUID().toString(); + + assertThat( + createCatalog( + prefix, + "animals", + initiallyExternal, + Arrays.asList("dogs", "cats"))) + .returns(Response.Status.CREATED.getStatusCode(), Response::getStatus); + + // OK, non-overlapping + assertThat( + createCatalog( + prefix, + "danimals", + laterExternal, + Arrays.asList("dan", "daniel"))) + .returns(Response.Status.CREATED.getStatusCode(), Response::getStatus); + + // This DBL overlaps with initial AL + assertThat( + createCatalog( + prefix, + "dogs", + initiallyExternal, + Arrays.asList("huskies", "labs"))) + .returns( + Response.Status.BAD_REQUEST.getStatusCode(), Response::getStatus); + + // This AL overlaps with initial DBL + assertThat( + createCatalog( + prefix, + "kingdoms", + initiallyExternal, + Arrays.asList("plants", "animals"))) + .returns( + Response.Status.BAD_REQUEST.getStatusCode(), Response::getStatus); + + // This AL overlaps with an initial AL + assertThat( + createCatalog( + prefix, + "plays", + initiallyExternal, + Arrays.asList("rent", "cats"))) + .returns( + Response.Status.BAD_REQUEST.getStatusCode(), Response::getStatus); + }); + }); + } +} diff --git a/polaris-service/src/test/java/io/polaris/service/admin/PolarisServiceImplIntegrationTest.java b/polaris-service/src/test/java/io/polaris/service/admin/PolarisServiceImplIntegrationTest.java index 63d8168a2d..8d73724362 100644 --- a/polaris-service/src/test/java/io/polaris/service/admin/PolarisServiceImplIntegrationTest.java +++ b/polaris-service/src/test/java/io/polaris/service/admin/PolarisServiceImplIntegrationTest.java @@ -1305,7 +1305,7 @@ public void testCreateListUpdateAndDeleteCatalogRole() { .setStorageConfigInfo( new AwsStorageConfigInfo( "arn:aws:iam::012345678901:role/jdoe", StorageConfigInfo.StorageTypeEnum.S3)) - .setProperties(new CatalogProperties("s3://required/base/location")) + .setProperties(new CatalogProperties("s3://required/base/other_location")) .build(); try (Response response = newRequest("http://localhost:%d/api/management/v1/catalogs") diff --git a/polaris-service/src/test/java/io/polaris/service/catalog/BasePolarisCatalogTest.java b/polaris-service/src/test/java/io/polaris/service/catalog/BasePolarisCatalogTest.java index 06ebcf1f2b..6ca1957230 100644 --- a/polaris-service/src/test/java/io/polaris/service/catalog/BasePolarisCatalogTest.java +++ b/polaris-service/src/test/java/io/polaris/service/catalog/BasePolarisCatalogTest.java @@ -20,8 +20,8 @@ import static org.mockito.Mockito.when; import com.google.common.collect.ImmutableMap; -import io.micrometer.core.instrument.MeterRegistry; import io.polaris.core.PolarisCallContext; +import io.polaris.core.PolarisConfiguration; import io.polaris.core.PolarisConfigurationStore; import io.polaris.core.PolarisDefaultDiagServiceImpl; import io.polaris.core.PolarisDiagnostics; @@ -38,12 +38,12 @@ import io.polaris.core.entity.PolarisEntityType; import io.polaris.core.entity.PrincipalEntity; import io.polaris.core.entity.TaskEntity; +import io.polaris.core.monitor.PolarisMetricRegistry; import io.polaris.core.persistence.MetaStoreManagerFactory; import io.polaris.core.persistence.PolarisEntityManager; import io.polaris.core.persistence.PolarisMetaStoreManager; import io.polaris.core.persistence.PolarisMetaStoreSession; import io.polaris.core.storage.PolarisCredentialProperty; -import io.polaris.core.storage.PolarisStorageActions; import io.polaris.core.storage.PolarisStorageIntegration; import io.polaris.core.storage.PolarisStorageIntegrationProvider; import io.polaris.core.storage.aws.AwsCredentialsStorageIntegration; @@ -72,6 +72,7 @@ import org.apache.iceberg.CatalogProperties; import org.apache.iceberg.PartitionSpec; import org.apache.iceberg.Schema; +import org.apache.iceberg.SortOrder; import org.apache.iceberg.Table; import org.apache.iceberg.TableMetadata; import org.apache.iceberg.TableMetadataParser; @@ -79,6 +80,7 @@ import org.apache.iceberg.catalog.Namespace; import org.apache.iceberg.catalog.SupportsNamespaces; import org.apache.iceberg.catalog.TableIdentifier; +import org.apache.iceberg.exceptions.BadRequestException; import org.apache.iceberg.exceptions.ForbiddenException; import org.apache.iceberg.exceptions.NoSuchNamespaceException; import org.apache.iceberg.inmemory.InMemoryFileIO; @@ -117,6 +119,7 @@ public class BasePolarisCatalogTest extends CatalogTests { private PolarisAdminService adminService; private PolarisEntityManager entityManager; private AuthenticatedPolarisPrincipal authenticatedRoot; + private PolarisEntity catalogEntity; @BeforeEach @SuppressWarnings("unchecked") @@ -178,12 +181,14 @@ public void before() { .setStorageType(StorageConfigInfo.StorageTypeEnum.S3) .setAllowedLocations(List.of(storageLocation, "s3://externally-owned-bucket")) .build(); - PolarisEntity catalogEntity = + catalogEntity = adminService.createCatalog( new CatalogEntity.Builder() .setName(CATALOG_NAME) .setDefaultBaseLocation(storageLocation) .setReplaceNewLocationPrefixWithCatalogDefault("file:") + .addProperty(PolarisConfiguration.CATALOG_ALLOW_EXTERNAL_TABLE_LOCATION, "true") + .addProperty(PolarisConfiguration.CATALOG_ALLOW_UNSTRUCTURED_TABLE_LOCATION, "true") .setStorageConfigurationInfo(storageConfigModel, storageLocation) .build()); @@ -192,7 +197,8 @@ public void before() { callContext, entityManager, authenticatedRoot, CATALOG_NAME); TaskExecutor taskExecutor = Mockito.mock(); this.catalog = - new BasePolarisCatalog(entityManager, callContext, passthroughView, taskExecutor); + new BasePolarisCatalog( + entityManager, callContext, passthroughView, authenticatedRoot, taskExecutor); this.catalog.initialize( CATALOG_NAME, ImmutableMap.of( @@ -384,6 +390,191 @@ public void testUpdateNotificationCreateTableInDisallowedLocation() { .hasMessageContaining("Invalid location"); } + @Test + public void testCreateNotificationCreateTableInExternalLocation() { + Assumptions.assumeTrue( + requiresNamespaceCreate(), + "Only applicable if namespaces must be created before adding children"); + Assumptions.assumeTrue( + supportsNestedNamespaces(), "Only applicable if nested namespaces are supported"); + Assumptions.assumeTrue( + supportsNotifications(), "Only applicable if notifications are supported"); + + // The location of the metadata JSON file specified is outside of the table's base location + // according to the + // metadata. We assume this is fraudulent and disallowed + final String tableLocation = "s3://my-bucket/path/to/data/my_table/"; + final String tableMetadataLocation = tableLocation + "metadata/v1.metadata.json"; + final String anotherTableLocation = "s3://my-bucket/path/to/data/another_table/"; + + entityManager + .getMetaStoreManager() + .updateEntityPropertiesIfNotChanged( + polarisContext, + List.of(PolarisEntity.toCore(catalogEntity)), + new CatalogEntity.Builder(CatalogEntity.of(catalogEntity)) + .addProperty(PolarisConfiguration.CATALOG_ALLOW_EXTERNAL_TABLE_LOCATION, "false") + .addProperty(PolarisConfiguration.CATALOG_ALLOW_UNSTRUCTURED_TABLE_LOCATION, "true") + .build()); + BasePolarisCatalog catalog = catalog(); + TableMetadata tableMetadata = + TableMetadata.buildFromEmpty() + .assignUUID() + .setLocation(anotherTableLocation) + .addSchema(SCHEMA, 4) + .addPartitionSpec(PartitionSpec.unpartitioned()) + .addSortOrder(SortOrder.unsorted()) + .build(); + TableMetadataParser.write(tableMetadata, catalog.getIo().newOutputFile(tableMetadataLocation)); + + Namespace namespace = Namespace.of("parent", "child1"); + TableIdentifier table = TableIdentifier.of(namespace, "my_table"); + + NotificationRequest request = new NotificationRequest(); + request.setNotificationType(NotificationType.CREATE); + TableUpdateNotification update = new TableUpdateNotification(); + update.setMetadataLocation(tableMetadataLocation); + update.setTableName(table.name()); + update.setTableUuid(UUID.randomUUID().toString()); + update.setTimestamp(230950845L); + request.setPayload(update); + + Assertions.assertThatThrownBy(() -> catalog.sendNotification(table, request)) + .isInstanceOf(BadRequestException.class) + .hasMessageContaining("is not allowed outside of table location"); + } + + @Test + public void testCreateNotificationCreateTableOutsideOfMetadataLocation() { + Assumptions.assumeTrue( + requiresNamespaceCreate(), + "Only applicable if namespaces must be created before adding children"); + Assumptions.assumeTrue( + supportsNestedNamespaces(), "Only applicable if nested namespaces are supported"); + Assumptions.assumeTrue( + supportsNotifications(), "Only applicable if notifications are supported"); + + // The location of the metadata JSON file specified is outside of the table's metadata directory + // according to the + // metadata. We assume this is fraudulent and disallowed + final String tableLocation = "s3://my-bucket/path/to/data/my_table/"; + final String tableMetadataLocation = tableLocation + "metadata/v3.metadata.json"; + + // this passes the first validation, since it's within the namespace subdirectory, but + // the location is in another table's subdirectory + final String anotherTableLocation = "s3://my-bucket/path/to/data/another_table"; + + entityManager + .getMetaStoreManager() + .updateEntityPropertiesIfNotChanged( + polarisContext, + List.of(PolarisEntity.toCore(catalogEntity)), + new CatalogEntity.Builder(CatalogEntity.of(catalogEntity)) + .addProperty(PolarisConfiguration.CATALOG_ALLOW_EXTERNAL_TABLE_LOCATION, "false") + .addProperty(PolarisConfiguration.CATALOG_ALLOW_UNSTRUCTURED_TABLE_LOCATION, "true") + .build()); + BasePolarisCatalog catalog = catalog(); + TableMetadata tableMetadata = + TableMetadata.buildFromEmpty() + .assignUUID() + .setLocation(anotherTableLocation) + .addSchema(SCHEMA, 4) + .addPartitionSpec(PartitionSpec.unpartitioned()) + .addSortOrder(SortOrder.unsorted()) + .build(); + TableMetadataParser.write(tableMetadata, catalog.getIo().newOutputFile(tableMetadataLocation)); + + Namespace namespace = Namespace.of("parent", "child1"); + TableIdentifier table = TableIdentifier.of(namespace, "my_table"); + + NotificationRequest request = new NotificationRequest(); + request.setNotificationType(NotificationType.CREATE); + TableUpdateNotification update = new TableUpdateNotification(); + update.setMetadataLocation(tableMetadataLocation); + update.setTableName(table.name()); + update.setTableUuid(UUID.randomUUID().toString()); + update.setTimestamp(230950845L); + request.setPayload(update); + + Assertions.assertThatThrownBy(() -> catalog.sendNotification(table, request)) + .isInstanceOf(BadRequestException.class) + .hasMessageContaining("is not allowed outside of table location"); + } + + @Test + public void testUpdateNotificationCreateTableInExternalLocation() { + Assumptions.assumeTrue( + requiresNamespaceCreate(), + "Only applicable if namespaces must be created before adding children"); + Assumptions.assumeTrue( + supportsNestedNamespaces(), "Only applicable if nested namespaces are supported"); + Assumptions.assumeTrue( + supportsNotifications(), "Only applicable if notifications are supported"); + + // The location of the metadata JSON file specified is outside of the table's base location + // according to the + // metadata. We assume this is fraudulent and disallowed + final String tableLocation = "s3://my-bucket/path/to/data/my_table/"; + final String tableMetadataLocation = tableLocation + "metadata/v1.metadata.json"; + final String anotherTableLocation = "s3://my-bucket/path/to/data/another_table/"; + + entityManager + .getMetaStoreManager() + .updateEntityPropertiesIfNotChanged( + polarisContext, + List.of(PolarisEntity.toCore(catalogEntity)), + new CatalogEntity.Builder(CatalogEntity.of(catalogEntity)) + .addProperty(PolarisConfiguration.CATALOG_ALLOW_EXTERNAL_TABLE_LOCATION, "false") + .addProperty(PolarisConfiguration.CATALOG_ALLOW_UNSTRUCTURED_TABLE_LOCATION, "true") + .build()); + BasePolarisCatalog catalog = catalog(); + InMemoryFileIO fileIO = (InMemoryFileIO) catalog.getIo(); + + fileIO.addFile( + tableMetadataLocation, + TableMetadataParser.toJson(createSampleTableMetadata(tableLocation)).getBytes()); + + Namespace namespace = Namespace.of("parent", "child1"); + TableIdentifier table = TableIdentifier.of(namespace, "my_table"); + + NotificationRequest createRequest = new NotificationRequest(); + createRequest.setNotificationType(NotificationType.CREATE); + TableUpdateNotification create = new TableUpdateNotification(); + create.setMetadataLocation(tableMetadataLocation); + create.setTableName(table.name()); + create.setTableUuid(UUID.randomUUID().toString()); + create.setTimestamp(230950845L); + createRequest.setPayload(create); + + // the create should succeed + catalog.sendNotification(table, createRequest); + + // now craft the malicious metadata file + final String maliciousMetadataFile = tableLocation + "metadata/v2.metadata.json"; + TableMetadata tableMetadata = + TableMetadata.buildFromEmpty() + .assignUUID() + .setLocation(anotherTableLocation) + .addSchema(SCHEMA, 4) + .addPartitionSpec(PartitionSpec.unpartitioned()) + .addSortOrder(SortOrder.unsorted()) + .build(); + TableMetadataParser.write(tableMetadata, catalog.getIo().newOutputFile(maliciousMetadataFile)); + + NotificationRequest updateRequest = new NotificationRequest(); + updateRequest.setNotificationType(NotificationType.UPDATE); + TableUpdateNotification update = new TableUpdateNotification(); + update.setMetadataLocation(maliciousMetadataFile); + update.setTableName(table.name()); + update.setTableUuid(UUID.randomUUID().toString()); + update.setTimestamp(230950845L); + updateRequest.setPayload(update); + + Assertions.assertThatThrownBy(() -> catalog.sendNotification(table, updateRequest)) + .isInstanceOf(BadRequestException.class) + .hasMessageContaining("is not allowed outside of table location"); + } + @Test public void testUpdateNotificationCreateTableWithLocalFilePrefix() { Assumptions.assumeTrue( @@ -399,7 +590,10 @@ public void testUpdateNotificationCreateTableWithLocalFilePrefix() { String catalogWithoutStorage = "catalogWithoutStorage"; PolarisEntity catalogEntity = adminService.createCatalog( - new CatalogEntity.Builder().setName(catalogWithoutStorage).build()); + new CatalogEntity.Builder() + .setDefaultBaseLocation("file://") + .setName(catalogWithoutStorage) + .build()); CallContext callContext = CallContext.getCurrentContext(); PolarisPassthroughResolutionView passthroughView = @@ -407,7 +601,8 @@ public void testUpdateNotificationCreateTableWithLocalFilePrefix() { callContext, entityManager, authenticatedRoot, catalogWithoutStorage); TaskExecutor taskExecutor = Mockito.mock(); BasePolarisCatalog catalog = - new BasePolarisCatalog(entityManager, callContext, passthroughView, taskExecutor); + new BasePolarisCatalog( + entityManager, callContext, passthroughView, authenticatedRoot, taskExecutor); catalog.initialize( catalogWithoutStorage, ImmutableMap.of( @@ -448,7 +643,11 @@ public void testUpdateNotificationCreateTableWithHttpPrefix() { String catalogName = "catalogForMaliciousDomain"; PolarisEntity catalogEntity = - adminService.createCatalog(new CatalogEntity.Builder().setName(catalogName).build()); + adminService.createCatalog( + new CatalogEntity.Builder() + .setDefaultBaseLocation("http://maliciousdomain.com") + .setName(catalogName) + .build()); CallContext callContext = CallContext.getCurrentContext(); PolarisPassthroughResolutionView passthroughView = @@ -456,7 +655,8 @@ public void testUpdateNotificationCreateTableWithHttpPrefix() { callContext, entityManager, authenticatedRoot, catalogName); TaskExecutor taskExecutor = Mockito.mock(); BasePolarisCatalog catalog = - new BasePolarisCatalog(entityManager, callContext, passthroughView, taskExecutor); + new BasePolarisCatalog( + entityManager, callContext, passthroughView, authenticatedRoot, taskExecutor); catalog.initialize( catalogName, ImmutableMap.of( @@ -691,17 +891,7 @@ public void testUpdateNotificationWhenTableExistsFileSpecifiesDisallowedLocation createSampleTableMetadata("s3://forbidden-table-location/table/"); fileIO.addFile(tableMetadataLocation, TableMetadataParser.toJson(forbiddenMetadata).getBytes()); - // TODO: Once we prefetch or prevalidate json contents, we should perform location validation - // proactively and this sendNotification should throw. For now, if we only validate the path - // later when trying to read it, we'll wait until something tries to get a credential to error - // out. - Assertions.assertThat(catalog.sendNotification(table, request)) - .as("Notification should be sent successfully") - .isTrue(); - Assertions.assertThatThrownBy( - () -> - catalog.getCredentialConfig( - table, forbiddenMetadata, Set.of(PolarisStorageActions.READ))) + Assertions.assertThatThrownBy(() -> catalog.sendNotification(table, request)) .isInstanceOf(ForbiddenException.class) .hasMessageContaining("Invalid location"); } @@ -905,7 +1095,7 @@ public StorageCredentialCache getOrCreateStorageCredentialCache( } @Override - public void setMetricRegistry(MeterRegistry metricRegistry) {} + public void setMetricRegistry(PolarisMetricRegistry metricRegistry) {} @Override public Map diff --git a/polaris-service/src/test/java/io/polaris/service/catalog/BasePolarisCatalogViewTest.java b/polaris-service/src/test/java/io/polaris/service/catalog/BasePolarisCatalogViewTest.java index c173d25bc9..4d7ba998e8 100644 --- a/polaris-service/src/test/java/io/polaris/service/catalog/BasePolarisCatalogViewTest.java +++ b/polaris-service/src/test/java/io/polaris/service/catalog/BasePolarisCatalogViewTest.java @@ -17,6 +17,7 @@ import com.google.common.collect.ImmutableMap; import io.polaris.core.PolarisCallContext; +import io.polaris.core.PolarisConfiguration; import io.polaris.core.PolarisConfigurationStore; import io.polaris.core.PolarisDefaultDiagServiceImpl; import io.polaris.core.PolarisDiagnostics; @@ -60,7 +61,8 @@ public void before() { RealmContext realmContext = () -> "realm"; InMemoryPolarisMetaStoreManagerFactory managerFactory = new InMemoryPolarisMetaStoreManagerFactory(); - managerFactory.setStorageIntegrationProvider(new PolarisStorageIntegrationProviderImpl()); + managerFactory.setStorageIntegrationProvider( + new PolarisStorageIntegrationProviderImpl(Mockito::mock)); PolarisMetaStoreManager metaStoreManager = managerFactory.getOrCreateMetaStoreManager(realmContext); Map configMap = new HashMap<>(); @@ -110,6 +112,9 @@ public void before() { adminService.createCatalog( new CatalogEntity.Builder() .setName(CATALOG_NAME) + .addProperty(PolarisConfiguration.CATALOG_ALLOW_EXTERNAL_TABLE_LOCATION, "true") + .addProperty(PolarisConfiguration.CATALOG_ALLOW_UNSTRUCTURED_TABLE_LOCATION, "true") + .setDefaultBaseLocation("file://tmp") .setStorageConfigurationInfo( new FileStorageConfigInfo( StorageConfigInfo.StorageTypeEnum.FILE, List.of("file://", "/", "*")), @@ -120,7 +125,8 @@ public void before() { new PolarisPassthroughResolutionView( callContext, entityManager, authenticatedRoot, CATALOG_NAME); this.catalog = - new BasePolarisCatalog(entityManager, callContext, passthroughView, Mockito.mock()); + new BasePolarisCatalog( + entityManager, callContext, passthroughView, authenticatedRoot, Mockito.mock()); this.catalog.initialize( CATALOG_NAME, ImmutableMap.of( diff --git a/polaris-service/src/test/java/io/polaris/service/catalog/PolarisCatalogHandlerWrapperAuthzTest.java b/polaris-service/src/test/java/io/polaris/service/catalog/PolarisCatalogHandlerWrapperAuthzTest.java index 7bd3f752e1..b4a3c370ed 100644 --- a/polaris-service/src/test/java/io/polaris/service/catalog/PolarisCatalogHandlerWrapperAuthzTest.java +++ b/polaris-service/src/test/java/io/polaris/service/catalog/PolarisCatalogHandlerWrapperAuthzTest.java @@ -40,10 +40,15 @@ import java.util.Set; import java.util.UUID; import org.apache.iceberg.CatalogProperties; +import org.apache.iceberg.PartitionSpec; +import org.apache.iceberg.SortOrder; +import org.apache.iceberg.TableMetadata; +import org.apache.iceberg.TableMetadataParser; import org.apache.iceberg.catalog.Catalog; import org.apache.iceberg.catalog.Namespace; import org.apache.iceberg.catalog.TableIdentifier; import org.apache.iceberg.exceptions.ForbiddenException; +import org.apache.iceberg.io.FileIO; import org.apache.iceberg.rest.requests.CommitTransactionRequest; import org.apache.iceberg.rest.requests.CreateNamespaceRequest; import org.apache.iceberg.rest.requests.CreateTableRequest; @@ -688,14 +693,15 @@ public void testRegisterTableAllSufficientPrivileges() { .isTrue(); // To get a handy metadata file we can use one from another table. + // to avoid overlapping directories, drop the original table and recreate it via registerTable final String metadataLocation = newWrapper().loadTable(TABLE_NS1_1, "all").metadataLocation(); + newWrapper(Set.of(PRINCIPAL_ROLE2)).dropTableWithoutPurge(TABLE_NS1_1); - final TableIdentifier newtable = TableIdentifier.of(NS2, "newtable"); final RegisterTableRequest registerRequest = new RegisterTableRequest() { @Override public String name() { - return "newtable"; + return TABLE_NS1_1.name(); } @Override @@ -711,10 +717,10 @@ public String metadataLocation() { PolarisPrivilege.TABLE_FULL_METADATA, PolarisPrivilege.CATALOG_MANAGE_CONTENT), () -> { - newWrapper(Set.of(PRINCIPAL_ROLE1)).registerTable(NS2, registerRequest); + newWrapper(Set.of(PRINCIPAL_ROLE1)).registerTable(NS1, registerRequest); }, () -> { - newWrapper(Set.of(PRINCIPAL_ROLE2)).dropTableWithoutPurge(newtable); + newWrapper(Set.of(PRINCIPAL_ROLE2)).dropTableWithoutPurge(TABLE_NS1_1); }); } @@ -1541,12 +1547,12 @@ public void testRenameViewPrivilegesOnWrongSourceOrDestination() { @Test public void testSendNotificationSufficientPrivileges() { String externalCatalog = "externalCatalog"; + String storageLocation = + "file:///tmp/send_notification_sufficient_privileges_" + System.currentTimeMillis(); - String storageLocation = "file:///tmp"; FileStorageConfigInfo storageConfigModel = FileStorageConfigInfo.builder() .setStorageType(StorageConfigInfo.StorageTypeEnum.FILE) - .setAllowedLocations(List.of(storageLocation, "file:///tmp")) .build(); adminService.createCatalog( new CatalogEntity.Builder() @@ -1580,7 +1586,8 @@ public void testSendNotificationSufficientPrivileges() { NotificationRequest request = new NotificationRequest(); request.setNotificationType(NotificationType.CREATE); TableUpdateNotification update = new TableUpdateNotification(); - update.setMetadataLocation("file:///tmp/bucket/table/metadata/v1.metadata.json"); + update.setMetadataLocation( + String.format("%s/bucket/table/metadata/v1.metadata.json", storageLocation)); update.setTableName(table.name()); update.setTableUuid(tableUuid); update.setTimestamp(230950845L); @@ -1589,7 +1596,8 @@ public void testSendNotificationSufficientPrivileges() { NotificationRequest request2 = new NotificationRequest(); request2.setNotificationType(NotificationType.UPDATE); TableUpdateNotification update2 = new TableUpdateNotification(); - update2.setMetadataLocation("file:///tmp/bucket/table/metadata/v2.metadata.json"); + update2.setMetadataLocation( + String.format("%s/bucket/table/metadata/v2.metadata.json", storageLocation)); update2.setTableName(table.name()); update2.setTableUuid(tableUuid); update2.setTimestamp(330950845L); @@ -1614,12 +1622,31 @@ public PolarisEntityManager getOrCreateEntityManager(RealmContext realmContext) Mockito.mock()) { @Override public Catalog createCallContextCatalog( - CallContext context, PolarisResolutionManifest resolvedManifest) { - Catalog catalog = super.createCallContextCatalog(context, resolvedManifest); + CallContext context, + AuthenticatedPolarisPrincipal authenticatedPolarisPrincipal, + PolarisResolutionManifest resolvedManifest) { + Catalog catalog = + super.createCallContextCatalog( + context, authenticatedPolarisPrincipal, resolvedManifest); catalog.initialize( externalCatalog, ImmutableMap.of( CatalogProperties.FILE_IO_IMPL, "org.apache.iceberg.inmemory.InMemoryFileIO")); + + FileIO fileIO = ((BasePolarisCatalog) catalog).newTableOps(table).io(); + TableMetadata tableMetadata = + TableMetadata.buildFromEmpty() + .addSchema(SCHEMA, SCHEMA.highestFieldId()) + .setLocation( + String.format("%s/bucket/table/metadata/v1.metadata.json", storageLocation)) + .addPartitionSpec(PartitionSpec.unpartitioned()) + .addSortOrder(SortOrder.unsorted()) + .assignUUID() + .build(); + TableMetadataParser.overwrite( + tableMetadata, fileIO.newOutputFile(update.getMetadataLocation())); + TableMetadataParser.overwrite( + tableMetadata, fileIO.newOutputFile(update2.getMetadataLocation())); return catalog; } }; diff --git a/polaris-service/src/test/java/io/polaris/service/catalog/PolarisRestCatalogIntegrationTest.java b/polaris-service/src/test/java/io/polaris/service/catalog/PolarisRestCatalogIntegrationTest.java index afe180dab5..a60d9f404b 100644 --- a/polaris-service/src/test/java/io/polaris/service/catalog/PolarisRestCatalogIntegrationTest.java +++ b/polaris-service/src/test/java/io/polaris/service/catalog/PolarisRestCatalogIntegrationTest.java @@ -18,12 +18,14 @@ import static io.polaris.service.context.DefaultContextResolver.REALM_PROPERTY_KEY; import static org.apache.iceberg.types.Types.NestedField.required; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import com.google.common.collect.ImmutableMap; import io.dropwizard.testing.ConfigOverride; import io.dropwizard.testing.ResourceHelpers; import io.dropwizard.testing.junit5.DropwizardAppExtension; import io.dropwizard.testing.junit5.DropwizardExtensionsSupport; +import io.polaris.core.PolarisConfiguration; import io.polaris.core.admin.model.AwsStorageConfigInfo; import io.polaris.core.admin.model.Catalog; import io.polaris.core.admin.model.CatalogGrant; @@ -38,9 +40,11 @@ import io.polaris.core.admin.model.StorageConfigInfo; import io.polaris.core.admin.model.TableGrant; import io.polaris.core.admin.model.TablePrivilege; +import io.polaris.core.admin.model.UpdateCatalogRequest; import io.polaris.core.admin.model.ViewGrant; import io.polaris.core.admin.model.ViewPrivilege; import io.polaris.core.entity.CatalogEntity; +import io.polaris.core.entity.PolarisEntityConstants; import io.polaris.service.PolarisApplication; import io.polaris.service.auth.BasePolarisAuthenticator; import io.polaris.service.auth.TokenUtils; @@ -58,20 +62,27 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.Comparator; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.UUID; +import org.apache.iceberg.BaseTable; import org.apache.iceberg.CatalogProperties; import org.apache.iceberg.Schema; +import org.apache.iceberg.Table; import org.apache.iceberg.catalog.CatalogTests; import org.apache.iceberg.catalog.Namespace; import org.apache.iceberg.catalog.SessionCatalog; import org.apache.iceberg.catalog.TableIdentifier; +import org.apache.iceberg.exceptions.BadRequestException; +import org.apache.iceberg.exceptions.ForbiddenException; import org.apache.iceberg.rest.HTTPClient; import org.apache.iceberg.rest.RESTCatalog; import org.apache.iceberg.rest.auth.OAuth2Properties; import org.apache.iceberg.rest.responses.ErrorResponse; import org.apache.iceberg.types.Types; +import org.assertj.core.api.Assertions; import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; @@ -113,6 +124,9 @@ public class PolarisRestCatalogIntegrationTest extends CatalogTests private String userToken; private static String realm; + private final String catalogBaseLocation = + S3_BUCKET_BASE + "/" + System.getenv("USER") + "/path/to/data"; + @BeforeAll public static void setup() throws IOException { realm = PolarisConnectionExtension.getTestRealm(PolarisRestCatalogIntegrationTest.class); @@ -161,18 +175,21 @@ public void before( .setStorageType(StorageConfigInfo.StorageTypeEnum.S3) .setAllowedLocations(List.of("s3://my-old-bucket/path/to/data")) .build(); + io.polaris.core.admin.model.CatalogProperties.Builder catalogPropsBuilder = + io.polaris.core.admin.model.CatalogProperties.builder(catalogBaseLocation) + .addProperty( + PolarisConfiguration.CATALOG_ALLOW_UNSTRUCTURED_TABLE_LOCATION, "true") + .addProperty( + PolarisConfiguration.CATALOG_ALLOW_EXTERNAL_TABLE_LOCATION, "true"); + if (!S3_BUCKET_BASE.startsWith("file:/")) { + catalogPropsBuilder.addProperty( + CatalogEntity.REPLACE_NEW_LOCATION_PREFIX_WITH_CATALOG_DEFAULT_KEY, "file:"); + } Catalog catalog = PolarisCatalog.builder() .setType(Catalog.TypeEnum.INTERNAL) .setName(currentCatalogName) - .setProperties( - io.polaris.core.admin.model.CatalogProperties.builder( - S3_BUCKET_BASE + "/" + System.getenv("USER") + "/path/to/data") - .addProperty( - CatalogEntity - .REPLACE_NEW_LOCATION_PREFIX_WITH_CATALOG_DEFAULT_KEY, - "file:") - .build()) + .setProperties(catalogPropsBuilder.build()) .setStorageConfigInfo( S3_BUCKET_BASE.startsWith("file:/") ? new FileStorageConfigInfo( @@ -561,6 +578,179 @@ public void testListGrantsAfterRename() { } } + @Test + public void testCreateTableWithOverriddenBaseLocation(PolarisToken adminToken) { + try (Response response = + EXT.client() + .target( + String.format( + "http://localhost:%d/api/management/v1/catalogs/%s", + EXT.getLocalPort(), currentCatalogName)) + .request("application/json") + .header("Authorization", "Bearer " + adminToken.token()) + .header(REALM_PROPERTY_KEY, realm) + .get()) { + assertThat(response).returns(Response.Status.OK.getStatusCode(), Response::getStatus); + Catalog catalog = response.readEntity(Catalog.class); + Map catalogProps = new HashMap<>(catalog.getProperties().toMap()); + catalogProps.put(PolarisConfiguration.CATALOG_ALLOW_UNSTRUCTURED_TABLE_LOCATION, "false"); + try (Response updateResponse = + EXT.client() + .target( + String.format( + "http://localhost:%d/api/management/v1/catalogs/%s", + EXT.getLocalPort(), catalog.getName())) + .request("application/json") + .header("Authorization", "Bearer " + adminToken.token()) + .header(REALM_PROPERTY_KEY, realm) + .put( + Entity.json( + new UpdateCatalogRequest( + catalog.getEntityVersion(), + catalogProps, + catalog.getStorageConfigInfo())))) { + assertThat(updateResponse).returns(Response.Status.OK.getStatusCode(), Response::getStatus); + } + } + + restCatalog.createNamespace(Namespace.of("ns1")); + restCatalog.createNamespace( + Namespace.of("ns1", "ns1a"), + ImmutableMap.of( + PolarisEntityConstants.ENTITY_BASE_LOCATION, + catalogBaseLocation + "/ns1/ns1a-override")); + + TableIdentifier tableIdentifier = TableIdentifier.of(Namespace.of("ns1", "ns1a"), "tbl1"); + restCatalog + .buildTable(tableIdentifier, SCHEMA) + .withLocation(catalogBaseLocation + "/ns1/ns1a-override/tbl1-override") + .create(); + Table table = restCatalog.loadTable(tableIdentifier); + assertThat(table) + .isNotNull() + .isInstanceOf(BaseTable.class) + .asInstanceOf(InstanceOfAssertFactories.type(BaseTable.class)) + .returns(catalogBaseLocation + "/ns1/ns1a-override/tbl1-override", BaseTable::location); + } + + @Test + public void testCreateTableWithOverriddenBaseLocationCannotOverlapSibling( + PolarisToken adminToken) { + try (Response response = + EXT.client() + .target( + String.format( + "http://localhost:%d/api/management/v1/catalogs/%s", + EXT.getLocalPort(), currentCatalogName)) + .request("application/json") + .header("Authorization", "Bearer " + adminToken.token()) + .header(REALM_PROPERTY_KEY, realm) + .get()) { + assertThat(response).returns(Response.Status.OK.getStatusCode(), Response::getStatus); + Catalog catalog = response.readEntity(Catalog.class); + Map catalogProps = new HashMap<>(catalog.getProperties().toMap()); + catalogProps.put(PolarisConfiguration.CATALOG_ALLOW_UNSTRUCTURED_TABLE_LOCATION, "false"); + try (Response updateResponse = + EXT.client() + .target( + String.format( + "http://localhost:%d/api/management/v1/catalogs/%s", + EXT.getLocalPort(), catalog.getName())) + .request("application/json") + .header("Authorization", "Bearer " + adminToken.token()) + .header(REALM_PROPERTY_KEY, realm) + .put( + Entity.json( + new UpdateCatalogRequest( + catalog.getEntityVersion(), + catalogProps, + catalog.getStorageConfigInfo())))) { + assertThat(updateResponse).returns(Response.Status.OK.getStatusCode(), Response::getStatus); + } + } + + restCatalog.createNamespace(Namespace.of("ns1")); + restCatalog.createNamespace( + Namespace.of("ns1", "ns1a"), + ImmutableMap.of( + PolarisEntityConstants.ENTITY_BASE_LOCATION, + catalogBaseLocation + "/ns1/ns1a-override")); + + TableIdentifier tableIdentifier = TableIdentifier.of(Namespace.of("ns1", "ns1a"), "tbl1"); + restCatalog + .buildTable(tableIdentifier, SCHEMA) + .withLocation(catalogBaseLocation + "/ns1/ns1a-override/tbl1-override") + .create(); + Table table = restCatalog.loadTable(tableIdentifier); + assertThat(table) + .isNotNull() + .isInstanceOf(BaseTable.class) + .asInstanceOf(InstanceOfAssertFactories.type(BaseTable.class)) + .returns(catalogBaseLocation + "/ns1/ns1a-override/tbl1-override", BaseTable::location); + + Assertions.assertThatThrownBy( + () -> + restCatalog + .buildTable(TableIdentifier.of(Namespace.of("ns1", "ns1a"), "tbl2"), SCHEMA) + .withLocation(catalogBaseLocation + "/ns1/ns1a-override/tbl1-override") + .create()) + .isInstanceOf(BadRequestException.class) + .hasMessageContaining("because it conflicts with existing table or namespace"); + } + + @Test + public void testCreateTableWithOverriddenBaseLocationMustResideInNsDirectory( + PolarisToken adminToken) { + try (Response response = + EXT.client() + .target( + String.format( + "http://localhost:%d/api/management/v1/catalogs/%s", + EXT.getLocalPort(), currentCatalogName)) + .request("application/json") + .header("Authorization", "Bearer " + adminToken.token()) + .header(REALM_PROPERTY_KEY, realm) + .get()) { + assertThat(response).returns(Response.Status.OK.getStatusCode(), Response::getStatus); + Catalog catalog = response.readEntity(Catalog.class); + Map catalogProps = new HashMap<>(catalog.getProperties().toMap()); + catalogProps.put(PolarisConfiguration.CATALOG_ALLOW_UNSTRUCTURED_TABLE_LOCATION, "false"); + try (Response updateResponse = + EXT.client() + .target( + String.format( + "http://localhost:%d/api/management/v1/catalogs/%s", + EXT.getLocalPort(), catalog.getName())) + .request("application/json") + .header("Authorization", "Bearer " + adminToken.token()) + .header(REALM_PROPERTY_KEY, realm) + .put( + Entity.json( + new UpdateCatalogRequest( + catalog.getEntityVersion(), + catalogProps, + catalog.getStorageConfigInfo())))) { + assertThat(updateResponse).returns(Response.Status.OK.getStatusCode(), Response::getStatus); + } + } + + restCatalog.createNamespace(Namespace.of("ns1")); + restCatalog.createNamespace( + Namespace.of("ns1", "ns1a"), + ImmutableMap.of( + PolarisEntityConstants.ENTITY_BASE_LOCATION, + catalogBaseLocation + "/ns1/ns1a-override")); + + TableIdentifier tableIdentifier = TableIdentifier.of(Namespace.of("ns1", "ns1a"), "tbl1"); + assertThatThrownBy( + () -> + restCatalog + .buildTable(tableIdentifier, SCHEMA) + .withLocation(catalogBaseLocation + "/ns1/ns1a/tbl1-override") + .create()) + .isInstanceOf(ForbiddenException.class); + } + @Test public void testSendNotificationInternalCatalog() { NotificationRequest notification = new NotificationRequest(); diff --git a/polaris-service/src/test/java/io/polaris/service/catalog/PolarisRestCatalogViewIntegrationTest.java b/polaris-service/src/test/java/io/polaris/service/catalog/PolarisRestCatalogViewIntegrationTest.java index c0c24633e0..f99cbb32b1 100644 --- a/polaris-service/src/test/java/io/polaris/service/catalog/PolarisRestCatalogViewIntegrationTest.java +++ b/polaris-service/src/test/java/io/polaris/service/catalog/PolarisRestCatalogViewIntegrationTest.java @@ -23,6 +23,7 @@ import io.dropwizard.testing.ResourceHelpers; import io.dropwizard.testing.junit5.DropwizardAppExtension; import io.dropwizard.testing.junit5.DropwizardExtensionsSupport; +import io.polaris.core.PolarisConfiguration; import io.polaris.core.admin.model.AwsStorageConfigInfo; import io.polaris.core.admin.model.Catalog; import io.polaris.core.admin.model.CatalogGrant; @@ -149,10 +150,16 @@ public void before( .setAllowedLocations(List.of("s3://my-old-bucket/path/to/data")) .build(); io.polaris.core.admin.model.CatalogProperties props = - new io.polaris.core.admin.model.CatalogProperties( - S3_BUCKET_BASE + "/" + System.getenv("USER") + "/path/to/data"); - props.put( - CatalogEntity.REPLACE_NEW_LOCATION_PREFIX_WITH_CATALOG_DEFAULT_KEY, "file:"); + io.polaris.core.admin.model.CatalogProperties.builder( + S3_BUCKET_BASE + "/" + System.getenv("USER") + "/path/to/data") + .addProperty( + CatalogEntity.REPLACE_NEW_LOCATION_PREFIX_WITH_CATALOG_DEFAULT_KEY, + "file:") + .addProperty( + PolarisConfiguration.CATALOG_ALLOW_EXTERNAL_TABLE_LOCATION, "true") + .addProperty( + PolarisConfiguration.CATALOG_ALLOW_UNSTRUCTURED_TABLE_LOCATION, "true") + .build(); Catalog catalog = PolarisCatalog.builder() .setType(Catalog.TypeEnum.INTERNAL) diff --git a/polaris-service/src/test/resources/polaris-server-integrationtest.yml b/polaris-service/src/test/resources/polaris-server-integrationtest.yml index 77008e80a1..78ff8190de 100644 --- a/polaris-service/src/test/resources/polaris-server-integrationtest.yml +++ b/polaris-service/src/test/resources/polaris-server-integrationtest.yml @@ -71,6 +71,7 @@ featureConfiguration: DISABLE_TOKEN_GENERATION_FOR_USER_PRINCIPALS: true ALLOW_WILDCARD_LOCATION: true ALLOW_SPECIFYING_FILE_IO_IMPL: true + ALLOW_OVERLAPPING_CATALOG_URLS: true SUPPORTED_CATALOG_STORAGE_TYPES: - FILE - S3 diff --git a/regtests/setup.sh b/regtests/setup.sh index be80478c7f..e28a6fe052 100755 --- a/regtests/setup.sh +++ b/regtests/setup.sh @@ -70,7 +70,7 @@ else echo 'Verified azure bundle jar already installed' fi if ! [ -f ${SPARK_HOME}/jars/iceberg-gcp-bundle-1.5.2.jar ]; then - echo 'Download azure bundle jar...' + echo 'Download gcp bundle jar...' wget -O ${SPARK_HOME}/jars/iceberg-gcp-bundle-1.5.2.jar https://search.maven.org/remotecontent?filepath=org/apache/iceberg/iceberg-gcp-bundle/1.5.2/iceberg-gcp-bundle-1.5.2.jar if ! [ -f ${SPARK_HOME}/jars/iceberg-gcp-bundle-1.5.2.jar ]; then if [[ "${OSTYPE}" == "darwin"* ]]; then diff --git a/regtests/t_pyspark/src/conftest.py b/regtests/t_pyspark/src/conftest.py index 9bea00f06d..2b3fce2ac7 100644 --- a/regtests/t_pyspark/src/conftest.py +++ b/regtests/t_pyspark/src/conftest.py @@ -73,7 +73,9 @@ def snowflake_catalog(root_client, catalog_client, test_bucket, aws_role_arn): role_arn=aws_role_arn) catalog_name = 'snowflake' catalog = Catalog(name=catalog_name, type='INTERNAL', properties={ - "default-base-location": f"s3://{test_bucket}/polaris_test/snowflake_catalog"}, + "default-base-location": f"s3://{test_bucket}/polaris_test/snowflake_catalog", + "client.credentials-provider": "software.amazon.awssdk.auth.credentials.SystemPropertyCredentialsProvider" + }, storage_config_info=storage_conf) catalog.storage_config_info = storage_conf try: diff --git a/regtests/t_pyspark/src/test_spark_sql_s3_with_privileges.py b/regtests/t_pyspark/src/test_spark_sql_s3_with_privileges.py index 22b078b374..313cd4334b 100644 --- a/regtests/t_pyspark/src/test_spark_sql_s3_with_privileges.py +++ b/regtests/t_pyspark/src/test_spark_sql_s3_with_privileges.py @@ -16,21 +16,25 @@ import os import time import uuid +from urllib.parse import unquote +import boto3 import botocore import pytest -import boto3 -from urllib.parse import unquote +from py4j.protocol import Py4JJavaError + +from botocore.exceptions import ClientError from iceberg_spark import IcebergSparkSession +from polaris.catalog import CreateNamespaceRequest, CreateTableRequest, ModelSchema, StructField from polaris.catalog.api.iceberg_catalog_api import IcebergCatalogAPI from polaris.catalog.api.iceberg_o_auth2_api import IcebergOAuth2API from polaris.catalog.api_client import ApiClient as CatalogApiClient from polaris.catalog.configuration import Configuration +from polaris.management import ApiClient as ManagementApiClient from polaris.management import PolarisDefaultApi, Principal, PrincipalRole, CatalogRole, \ CatalogGrant, CatalogPrivilege, ApiException, CreateCatalogRoleRequest, CreatePrincipalRoleRequest, \ CreatePrincipalRequest, AddGrantRequest, GrantCatalogRoleRequest, GrantPrincipalRoleRequest -from polaris.management import ApiClient as ManagementApiClient @pytest.fixture @@ -150,6 +154,60 @@ def snowman_catalog_client(polaris_catalog_url, snowman): return IcebergCatalogAPI(CatalogApiClient(Configuration(access_token=token.access_token, host=polaris_catalog_url))) +@pytest.fixture +def creator_catalog_client(polaris_catalog_url, creator): + """ + Create an iceberg catalog client with TABLE_CREATE credentials + :param polaris_catalog_url: + :param creator: + :return: + """ + client = CatalogApiClient(Configuration(username=creator.principal.client_id, + password=creator.credentials.client_secret, + host=polaris_catalog_url)) + oauth_api = IcebergOAuth2API(client) + token = oauth_api.get_token(scope='PRINCIPAL_ROLE:ALL', client_id=creator.principal.client_id, + client_secret=creator.credentials.client_secret, + grant_type='client_credentials', + _headers={'realm': 'default-realm'}) + + return IcebergCatalogAPI(CatalogApiClient(Configuration(access_token=token.access_token, + host=polaris_catalog_url))) + + +@pytest.fixture +def creator(polaris_url, polaris_catalog_url, root_client, snowflake_catalog): + """ + create the creator principal with only TABLE_CREATE privileges + :param root_client: + :param snowflake_catalog: + :return: + """ + creator_name = "creator" + principal_role = "creator_principal_role" + catalog_role = "creator_catalog_role" + try: + creator = create_principal(polaris_url, polaris_catalog_url, root_client, creator_name) + creator_principal_role = create_principal_role(root_client, principal_role) + creator_catalog_role = create_catalog_role(root_client, snowflake_catalog, catalog_role) + + root_client.assign_catalog_role_to_principal_role(principal_role_name=creator_principal_role.name, + catalog_name=snowflake_catalog.name, + grant_catalog_role_request=GrantCatalogRoleRequest( + catalog_role=creator_catalog_role)) + root_client.add_grant_to_catalog_role(snowflake_catalog.name, creator_catalog_role.name, + AddGrantRequest(grant=CatalogGrant(catalog_name=snowflake_catalog.name, + type='catalog', + privilege=CatalogPrivilege.TABLE_CREATE))) + root_client.assign_principal_role(creator.principal.name, + grant_principal_role_request=GrantPrincipalRoleRequest( + principal_role=creator_principal_role)) + yield creator + finally: + root_client.delete_principal(creator_name) + root_client.delete_principal_role(principal_role_name=principal_role) + root_client.delete_catalog_role(catalog_role_name=catalog_role, catalog_name=snowflake_catalog.name) + @pytest.fixture def reader_catalog_client(polaris_catalog_url, reader): @@ -234,6 +292,119 @@ def test_spark_credentials(root_client, snowflake_catalog, polaris_catalog_url, spark.sql('DROP NAMESPACE db1') +@pytest.mark.skipif(os.environ.get('AWS_TEST_ENABLED', 'False').lower() != 'true', reason='AWS_TEST_ENABLED is not set or is false') +def test_spark_cannot_create_table_outside_of_namespace_dir(root_client, snowflake_catalog, polaris_catalog_url, snowman, reader): + """ + Basic spark test - using snowman, create a namespace and try to create a table outside of the namespace. This should fail + + Using the reader principal's credentials verify read access. Validate the reader cannot insert into the table. + :param root_client: + :param snowflake_catalog: + :param polaris_catalog_url: + :param snowman: + :param reader: + :return: + """ + with IcebergSparkSession(credentials=f'{snowman.principal.client_id}:{snowman.credentials.client_secret}', + catalog_name=snowflake_catalog.name, + polaris_url=polaris_catalog_url) as spark: + table_location = snowflake_catalog.properties.default_base_location + '/db1/outside_schema/table_outside_namespace' + spark.sql(f'USE {snowflake_catalog.name}') + spark.sql('CREATE NAMESPACE db1') + spark.sql('CREATE NAMESPACE db1.schema') + spark.sql('SHOW NAMESPACES') + spark.sql('USE db1.schema') + try: + spark.sql(f"CREATE TABLE iceberg_table_outside_namespace (col1 int, col2 string) LOCATION '{table_location}'") + pytest.fail("Expected to fail when creating table outside of namespace directory") + except Py4JJavaError as e: + assert "is not in the list of allowed locations" in e.java_exception.getMessage() + + +@pytest.mark.skipif(os.environ.get('AWS_TEST_ENABLED', 'False').lower() != 'true', reason='AWS_TEST_ENABLED is not set or is false') +def test_spark_creates_table_in_custom_namespace_dir(root_client, snowflake_catalog, polaris_catalog_url, snowman, reader): + """ + Basic spark test - using snowman, create a namespace and try to create a table outside of the namespace. This should fail + + Using the reader principal's credentials verify read access. Validate the reader cannot insert into the table. + :param root_client: + :param snowflake_catalog: + :param polaris_catalog_url: + :param snowman: + :param reader: + :return: + """ + with IcebergSparkSession(credentials=f'{snowman.principal.client_id}:{snowman.credentials.client_secret}', + catalog_name=snowflake_catalog.name, + polaris_url=polaris_catalog_url) as spark: + namespace_location = snowflake_catalog.properties.default_base_location + '/db1/custom_location' + spark.sql(f'USE {snowflake_catalog.name}') + spark.sql('CREATE NAMESPACE db1') + spark.sql(f"CREATE NAMESPACE db1.schema LOCATION '{namespace_location}'") + spark.sql('USE db1.schema') + spark.sql(f"CREATE TABLE table_in_custom_namespace_location (col1 int, col2 string)") + assert spark.sql("SELECT * FROM table_in_custom_namespace_location").count() == 0 + # check the metadata and assert the custom namespace location is used + entries = spark.sql(f"SELECT file FROM db1.schema.table_in_custom_namespace_location.metadata_log_entries").collect() + assert namespace_location in entries[0][0] + + +@pytest.mark.skipif(os.environ.get('AWS_TEST_ENABLED', 'False').lower() != 'true', reason='AWS_TEST_ENABLED is not set or is false') +def test_spark_can_create_table_in_custom_allowed_dir(root_client, snowflake_catalog, polaris_catalog_url, snowman, reader): + """ + Basic spark test - using snowman, create a namespace and try to create a table outside of the namespace. This should fail + + Using the reader principal's credentials verify read access. Validate the reader cannot insert into the table. + :param root_client: + :param snowflake_catalog: + :param polaris_catalog_url: + :param snowman: + :param reader: + :return: + """ + with IcebergSparkSession(credentials=f'{snowman.principal.client_id}:{snowman.credentials.client_secret}', + catalog_name=snowflake_catalog.name, + polaris_url=polaris_catalog_url) as spark: + table_location = snowflake_catalog.properties.default_base_location + '/db1/custom_schema_location/table_outside_namespace' + spark.sql(f'USE {snowflake_catalog.name}') + spark.sql('CREATE NAMESPACE db1') + spark.sql(f"CREATE NAMESPACE db1.schema LOCATION '{snowflake_catalog.properties.default_base_location}/db1/custom_schema_location'") + spark.sql('SHOW NAMESPACES') + spark.sql('USE db1.schema') + # this is supported because it is inside of the custom namespace location + spark.sql(f"CREATE TABLE iceberg_table_outside_namespace (col1 int, col2 string) LOCATION '{table_location}'") + + +@pytest.mark.skipif(os.environ.get('AWS_TEST_ENABLED', 'False').lower() != 'true', reason='AWS_TEST_ENABLED is not set or is false') +def test_spark_cannot_create_view_overlapping_table(root_client, snowflake_catalog, polaris_catalog_url, snowman, reader): + """ + Basic spark test - using snowman, create a namespace and try to create a table outside of the namespace. This should fail + + Using the reader principal's credentials verify read access. Validate the reader cannot insert into the table. + :param root_client: + :param snowflake_catalog: + :param polaris_catalog_url: + :param snowman: + :param reader: + :return: + """ + with IcebergSparkSession(credentials=f'{snowman.principal.client_id}:{snowman.credentials.client_secret}', + catalog_name=snowflake_catalog.name, + polaris_url=polaris_catalog_url) as spark: + table_location = snowflake_catalog.properties.default_base_location + '/db1/schema/table_dir' + spark.sql(f'USE {snowflake_catalog.name}') + spark.sql('CREATE NAMESPACE db1') + spark.sql(f"CREATE NAMESPACE db1.schema LOCATION '{snowflake_catalog.properties.default_base_location}/db1/schema'") + spark.sql('SHOW NAMESPACES') + spark.sql('USE db1.schema') + spark.sql(f"CREATE TABLE my_iceberg_table (col1 int, col2 string) LOCATION '{table_location}'") + try: + spark.sql(f"CREATE VIEW disallowed_view (int, string) TBLPROPERTIES ('location'= '{table_location}') AS SELECT * FROM my_iceberg_table") + pytest.fail("Expected to fail when creating table outside of namespace directory") + except Py4JJavaError as e: + assert "conflicts with existing table or namespace at location" in e.java_exception.getMessage() + + @pytest.mark.skipif(os.environ.get('AWS_TEST_ENABLED', 'False').lower() != 'true', reason='AWS_TEST_ENABLED is not set or is false') def test_spark_credentials_can_delete_after_purge(root_client, snowflake_catalog, polaris_catalog_url, snowman, snowman_catalog_client, test_bucket): @@ -567,6 +738,46 @@ def test_spark_credentials_s3_direct_without_write(root_client, snowflake_catalo spark.sql('DROP NAMESPACE db1') +@pytest.mark.skipif(os.environ.get('AWS_TEST_ENABLED', 'false').lower() != 'true', reason='AWS_TEST_ENABLED is not set or is false') +def test_spark_credentials_s3_direct_without_read( + snowflake_catalog, snowman_catalog_client, creator_catalog_client, test_bucket): + """ + Create a table using `creator`, which does not have TABLE_READ_DATA and ensure that credentials to read the table + are not vended. + """ + snowman_catalog_client.create_namespace( + prefix=snowflake_catalog.name, + create_namespace_request=CreateNamespaceRequest( + namespace=["some_schema"] + ) + ) + + response = creator_catalog_client.create_table( + prefix=snowflake_catalog.name, + namespace="some_schema", + x_iceberg_access_delegation="true", + create_table_request=CreateTableRequest( + name="some_table", + var_schema=ModelSchema( + type = 'struct', + fields = [], + ) + ) + ) + + assert not response.config + + snowman_catalog_client.drop_table( + prefix=snowflake_catalog.name, + namespace="some_schema", + table="some_table" + ) + snowman_catalog_client.drop_namespace( + prefix=snowflake_catalog.name, + namespace="some_schema" + ) + + def create_principal(polaris_url, polaris_catalog_url, api, principal_name): principal = Principal(name=principal_name, type="SERVICE") try: @@ -592,6 +803,85 @@ def create_principal(polaris_url, polaris_catalog_url, api, principal_name): else: raise e +@pytest.mark.skipif(os.environ.get('AWS_TEST_ENABLED', 'False').lower() != 'true', reason='AWS_TEST_ENABLED is not set or is false') +def test_spark_credentials_s3_scoped_to_metadata_data_locations(root_client, snowflake_catalog, polaris_catalog_url, + snowman, snowman_catalog_client, test_bucket): + """ + Create a table using Spark. Then call the loadTable api directly with snowman token to fetch the vended credentials + for the table. + Verify that the credentials returned to snowman can only work for the location that ending with metadata or data directory + :param root_client: + :param snowflake_catalog: + :param polaris_catalog_url: + :param snowman_catalog_client: + :param reader_catalog_client: + :return: + """ + with IcebergSparkSession(credentials=f'{snowman.principal.client_id}:{snowman.credentials.client_secret}', + catalog_name=snowflake_catalog.name, + polaris_url=polaris_catalog_url) as spark: + spark.sql(f'USE {snowflake_catalog.name}') + spark.sql('CREATE NAMESPACE db1') + spark.sql('CREATE NAMESPACE db1.schema') + spark.sql('USE db1.schema') + spark.sql('CREATE TABLE iceberg_table_scope_loc(col1 int, col2 string)') + spark.sql(f'''CREATE TABLE iceberg_table_scope_loc_slashes (col1 int, col2 string) LOCATION \'s3://{test_bucket}/polaris_test/snowflake_catalog/db1/schema/iceberg_table_scope_loc_slashes/path_with_slashes///////\'''') + + prefix1 = 'polaris_test/snowflake_catalog/db1/schema/iceberg_table_scope_loc' + prefix2 = 'polaris_test/snowflake_catalog/db1/schema/iceberg_table_scope_loc_slashes/path_with_slashes' + response1 = snowman_catalog_client.load_table(snowflake_catalog.name, unquote('db1%1Fschema'), + "iceberg_table_scope_loc", + "s3_scoped_table_locations") + response2 = snowman_catalog_client.load_table(snowflake_catalog.name, unquote('db1%1Fschema'), + "iceberg_table_scope_loc_slashes", + "s3_scoped_table_locations_with_slashes") + assert response1 is not None + assert response2 is not None + assert response1.metadata_location.startswith(f"s3://{test_bucket}/{prefix1}/metadata/") + # ensure that the slashes are removed before "/metadata/" + assert response2.metadata_location.startswith(f"s3://{test_bucket}/{prefix2}/metadata/") + + s3_1 = boto3.client('s3', + aws_access_key_id=response1.config['s3.access-key-id'], + aws_secret_access_key=response1.config['s3.secret-access-key'], + aws_session_token=response1.config['s3.session-token']) + + s3_2 = boto3.client('s3', + aws_access_key_id=response2.config['s3.access-key-id'], + aws_secret_access_key=response2.config['s3.secret-access-key'], + aws_session_token=response2.config['s3.session-token']) + for client,prefix in [(s3_1,prefix1), (s3_2, prefix2)]: + objects = client.list_objects(Bucket=test_bucket, Delimiter='/', + Prefix=f'{prefix}/metadata/') + assert objects is not None + assert 'Contents' in objects , f'list medata files failed in prefix: {prefix}/metadata/' + + objects = client.list_objects(Bucket=test_bucket, Delimiter='/', + Prefix=f'{prefix}/data/') + assert objects is not None + # no insert executed, so should not have any data files + assert 'Contents' not in objects , f'No contents should be in prefix: {prefix}/data/' + + # list files fail in the same table's other directory. The access policy should restrict this + # even metadata and data, it needs an ending `/` + for invalidPrefix in [f'{prefix}/other_directory/', f'{prefix}/metadata', f'{prefix}/data']: + try: + client.list_objects(Bucket=test_bucket, Delimiter='/', + Prefix=invalidPrefix) + pytest.fail(f'Expected exception listing files outside of allowed table directories, but succeeds on location: {invalidPrefix}') + except botocore.exceptions.ClientError as error: + assert error.response['Error']['Code'] == 'AccessDenied', 'Expected exception AccessDenied, but got: ' + error.response['Error']['Code'] + ' on location: ' + invalidPrefix + + with IcebergSparkSession(credentials=f'{snowman.principal.client_id}:{snowman.credentials.client_secret}', + catalog_name=snowflake_catalog.name, + polaris_url=polaris_catalog_url) as spark: + spark.sql(f'USE {snowflake_catalog.name}') + spark.sql('USE db1.schema') + spark.sql('DROP TABLE iceberg_table_scope_loc PURGE') + spark.sql('DROP TABLE iceberg_table_scope_loc_slashes PURGE') + spark.sql(f'USE {snowflake_catalog.name}') + spark.sql('DROP NAMESPACE db1.schema') + spark.sql('DROP NAMESPACE db1') def create_catalog_role(api, catalog, role_name): catalog_role = CatalogRole(name=role_name) diff --git a/server-templates/api.mustache b/server-templates/api.mustache index 49064a11ab..951b036f39 100644 --- a/server-templates/api.mustache +++ b/server-templates/api.mustache @@ -19,7 +19,7 @@ package {{package}}; import {{import}}; {{/imports}} -import io.polaris.service.resource.TimedApi; +import io.polaris.core.resource.TimedApi; import java.util.Map; import java.util.List; diff --git a/spec/rest-catalog-open-api.yaml b/spec/rest-catalog-open-api.yaml index 1b7ec745a2..9c7e61e75f 100644 --- a/spec/rest-catalog-open-api.yaml +++ b/spec/rest-catalog-open-api.yaml @@ -169,6 +169,12 @@ paths: Clients may also use the token exchange flow to refresh a token that is about to expire by sending a token exchange request (3). The request's "subject" token should be the expiring token. This request should use the subject token in the "Authorization" header. + parameters: + - name: Authorization + in: header + schema: + type: string + required: false requestBody: required: true content: From 1ea3cc990e1691e08c76cb4d9607ea2c1dd92a62 Mon Sep 17 00:00:00 2001 From: Robert Stupp Date: Tue, 30 Jul 2024 05:15:00 +0200 Subject: [PATCH 21/27] Use `bom`s where possible (#37) ... to avoid repeating dependency version. Also update all strings in `.gradle` files to use double quotes instead of single quotes and use bracketed syntax. A follow-up's going to introduce Gradle version catalogs. --- build.gradle | 68 +++++----- .../persistence/eclipselink/build.gradle | 11 +- polaris-core/build.gradle | 119 +++++++++--------- polaris-service/build.gradle | 115 +++++++++-------- 4 files changed, 163 insertions(+), 150 deletions(-) diff --git a/build.gradle b/build.gradle index b6d01be0f9..a86baf2b31 100644 --- a/build.gradle +++ b/build.gradle @@ -17,17 +17,17 @@ buildscript { repositories { maven { - url 'https://plugins.gradle.org/m2/' + url "https://plugins.gradle.org/m2/" } } dependencies { - classpath 'com.diffplug.spotless:spotless-plugin-gradle:6.25.0' + classpath "com.diffplug.spotless:spotless-plugin-gradle:6.25.0" } } plugins { - id 'idea' - id 'eclipse' + id "idea" + id "eclipse" } allprojects { @@ -44,48 +44,50 @@ allprojects { } subprojects { - apply plugin: 'jacoco' - apply plugin: 'java' - apply plugin: 'com.diffplug.spotless' - apply plugin: 'jacoco-report-aggregation' - apply plugin: 'groovy' + apply plugin: "jacoco" + apply plugin: "java" + apply plugin: "com.diffplug.spotless" + apply plugin: "jacoco-report-aggregation" + apply plugin: "groovy" ext { - jacksonVersion = '2.17.2' - icebergVersion = '1.5.0' - hadoopVersion = '3.3.6' - dropwizardVersion = '4.0.7' - assertJVersion = '3.25.3' + jacksonVersion = "2.17.2" + icebergVersion = "1.5.0" + hadoopVersion = "3.3.6" + dropwizardVersion = "4.0.7" + assertJVersion = "3.25.3" } tasks.withType(JavaCompile) { - options.compilerArgs << '-Xlint:unchecked' - options.compilerArgs << '-Xlint:deprecation' + options.compilerArgs << "-Xlint:unchecked" + options.compilerArgs << "-Xlint:deprecation" } - project(':polaris-service') { - apply plugin: 'application' + project(":polaris-service") { + apply plugin: "application" } - project(':polaris-core') { - apply plugin: 'java-library' + project(":polaris-core") { + apply plugin: "java-library" } dependencies { - implementation "com.fasterxml.jackson.core:jackson-annotations:${jacksonVersion}" - implementation 'com.google.guava:guava:33.0.0-jre' - implementation "org.jetbrains:annotations:24.0.0" - implementation 'org.slf4j:slf4j-api:2.0.12' - compileOnly "com.github.spotbugs:spotbugs-annotations:4.8.5" - - testImplementation 'org.junit.jupiter:junit-jupiter:5.7.1' - testImplementation 'org.assertj:assertj-core:3.25.3' - testImplementation "org.mockito:mockito-core:5.11.0" - - testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + implementation(platform("com.fasterxml.jackson:jackson-bom:${jacksonVersion}")) + implementation("com.fasterxml.jackson.core:jackson-annotations") + implementation("com.google.guava:guava:33.0.0-jre") + implementation("org.jetbrains:annotations:24.0.0") + implementation("org.slf4j:slf4j-api:2.0.12") + compileOnly("com.github.spotbugs:spotbugs-annotations:4.8.5") + + testImplementation(platform("org.junit:junit-bom:5.10.3")) + testImplementation("org.junit.jupiter:junit-jupiter") + testImplementation("org.assertj:assertj-core:3.26.3") + testImplementation("org.mockito:mockito-core:5.11.0") + + testRuntimeOnly("org.junit.platform:junit-platform-launcher") } task format { - dependsOn 'spotlessApply' + dependsOn "spotlessApply" } test { @@ -106,7 +108,7 @@ subprojects { targetExclude 'build/**' googleJavaFormat() endWithNewline() - custom 'disallowWildcardImports', disallowWildcardImports + custom "disallowWildcardImports", disallowWildcardImports } } } diff --git a/extension/persistence/eclipselink/build.gradle b/extension/persistence/eclipselink/build.gradle index 1bbad8223a..f872994358 100644 --- a/extension/persistence/eclipselink/build.gradle +++ b/extension/persistence/eclipselink/build.gradle @@ -13,12 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + dependencies { - implementation project(":polaris-core") - implementation project(":polaris-service") - implementation "org.eclipse.persistence:eclipselink:4.0.3" - implementation "io.dropwizard:dropwizard-jackson:${dropwizardVersion}" + implementation(project(":polaris-core")) + implementation(project(":polaris-service")) + implementation("org.eclipse.persistence:eclipselink:4.0.3") + implementation("io.dropwizard:dropwizard-jackson:${dropwizardVersion}") - testImplementation 'com.h2database:h2:2.2.224' + testImplementation("com.h2database:h2:2.2.224") testImplementation(testFixtures(project(":polaris-core"))) } diff --git a/polaris-core/build.gradle b/polaris-core/build.gradle index 5ef84b5843..64536c1c05 100644 --- a/polaris-core/build.gradle +++ b/polaris-core/build.gradle @@ -15,7 +15,7 @@ */ plugins { - id 'org.openapi.generator' version '7.6.0' + id "org.openapi.generator" version "7.6.0" id("java-library") id("java-test-fixtures") } @@ -26,92 +26,97 @@ compileJava { } dependencies { - implementation "org.apache.iceberg:iceberg-api:${icebergVersion}" - implementation "org.apache.iceberg:iceberg-core:${icebergVersion}" + implementation(platform("org.apache.iceberg:iceberg-bom:${icebergVersion}")) + implementation("org.apache.iceberg:iceberg-api:${icebergVersion}") + implementation("org.apache.iceberg:iceberg-core:${icebergVersion}") constraints { implementation("io.airlift:aircompressor:0.27") { - because 'Vulnerability detected in 0.25' + because "Vulnerability detected in 0.25" } } // TODO - this is only here for the Discoverable interface // We should use a different mechanism to discover the plugin implementations - implementation "io.dropwizard:dropwizard-jackson:${dropwizardVersion}" + implementation("io.dropwizard:dropwizard-jackson:${dropwizardVersion}") - implementation "com.fasterxml.jackson.core:jackson-annotations:${jacksonVersion}" - implementation "com.fasterxml.jackson.core:jackson-core:${jacksonVersion}" - implementation "com.fasterxml.jackson.core:jackson-databind:${jacksonVersion}" - implementation 'com.github.ben-manes.caffeine:caffeine:3.1.8' - implementation 'org.apache.commons:commons-lang3:3.14.0' - implementation 'commons-codec:commons-codec:1.17.0' + implementation(platform("com.fasterxml.jackson:jackson-bom:${jacksonVersion}")) + implementation("com.fasterxml.jackson.core:jackson-annotations") + implementation("com.fasterxml.jackson.core:jackson-core") + implementation("com.fasterxml.jackson.core:jackson-databind") + implementation("com.github.ben-manes.caffeine:caffeine:3.1.8") + implementation("org.apache.commons:commons-lang3:3.14.0") + implementation("commons-codec:commons-codec:1.17.0") implementation("org.apache.hadoop:hadoop-common:${hadoopVersion}") { - exclude group: 'org.slf4j', module: 'slf4j-reload4j' - exclude group: 'org.slf4j', module: 'slf4j-log4j12' - exclude group: 'ch.qos.reload4j', module: 'reload4j' - exclude group: 'log4j', module: 'log4j' - exclude group: 'org.apache.zookeeper', module: 'zookeeper' + exclude group: "org.slf4j", module: "slf4j-reload4j" + exclude group: "org.slf4j", module: "slf4j-log4j12" + exclude group: "ch.qos.reload4j", module: "reload4j" + exclude group: "log4j", module: "log4j" + exclude group: "org.apache.zookeeper", module: "zookeeper" } constraints { implementation("org.xerial.snappy:snappy-java:1.1.10.4") { - because 'Vulnerability detected in 1.1.8.2' + because "Vulnerability detected in 1.1.8.2" } implementation("org.codehaus.jettison:jettison:1.5.4") { - because 'Vulnerability detected in 1.1' + because "Vulnerability detected in 1.1" } implementation("org.apache.commons:commons-configuration2:2.10.1") { - because 'Vulnerability detected in 2.8.0' + because "Vulnerability detected in 2.8.0" } implementation("org.apache.commons:commons-compress:1.26.0") { - because 'Vulnerability detected in 1.21' + because "Vulnerability detected in 1.21" } implementation("com.nimbusds:nimbus-jose-jwt:9.37.2") { - because 'Vulnerability detected in 9.8.1' + because "Vulnerability detected in 9.8.1" } } - implementation "org.apache.hadoop:hadoop-hdfs-client:${hadoopVersion}" - - implementation "javax.inject:javax.inject:1" - implementation "io.swagger:swagger-annotations:1.6.14" - implementation "io.swagger:swagger-jaxrs:1.6.14" - implementation "jakarta.validation:jakarta.validation-api:3.0.2" - - implementation "org.apache.iceberg:iceberg-aws:${icebergVersion}" - implementation "software.amazon.awssdk:sts:2.25.61" - implementation "software.amazon.awssdk:iam-policy-builder:2.25.61" - implementation "software.amazon.awssdk:s3:2.25.61" - - implementation "org.apache.iceberg:iceberg-azure:${icebergVersion}" - implementation "com.azure:azure-storage-blob:12.18.0" - implementation "com.azure:azure-storage-common:12.14.2" - implementation "com.azure:azure-identity:1.12.2" - implementation "com.azure:azure-storage-file-datalake:12.19.0" + implementation("org.apache.hadoop:hadoop-hdfs-client:${hadoopVersion}") + + implementation("javax.inject:javax.inject:1") + implementation("io.swagger:swagger-annotations:1.6.14") + implementation("io.swagger:swagger-jaxrs:1.6.14") + implementation("jakarta.validation:jakarta.validation-api:3.0.2") + + implementation("org.apache.iceberg:iceberg-aws") + implementation(platform("software.amazon.awssdk:bom:2.26.25")) + implementation("software.amazon.awssdk:sts") + implementation("software.amazon.awssdk:iam-policy-builder") + implementation("software.amazon.awssdk:s3") + + implementation("org.apache.iceberg:iceberg-azure") + implementation("com.azure:azure-storage-blob:12.18.0") + implementation("com.azure:azure-storage-common:12.14.2") + implementation("com.azure:azure-identity:1.12.2") + implementation("com.azure:azure-storage-file-datalake:12.19.0") constraints { implementation("io.netty:netty-codec-http2:4.1.100") { - because 'Vulnerability detected in 4.1.72' + because "Vulnerability detected in 4.1.72" } implementation("io.projectreactor.netty:reactor-netty-http:1.1.13") { - because 'Vulnerability detected in 1.0.45' + because "Vulnerability detected in 1.0.45" } } - implementation "org.apache.iceberg:iceberg-gcp:${icebergVersion}" - implementation "com.fasterxml.jackson.core:jackson-core:${jacksonVersion}" - implementation "com.fasterxml.jackson.core:jackson-databind:${jacksonVersion}" - implementation "com.google.cloud:google-cloud-storage:2.39.0" - - implementation 'io.micrometer:micrometer-core:1.13.2' - - testFixturesApi 'org.junit.jupiter:junit-jupiter:5.7.1' - testFixturesApi 'org.assertj:assertj-core:3.25.3' - testFixturesApi "org.mockito:mockito-core:5.11.0" - testFixturesApi "com.fasterxml.jackson.core:jackson-core:${jacksonVersion}" - testFixturesApi "com.fasterxml.jackson.core:jackson-databind:${jacksonVersion}" - testFixturesApi 'org.apache.commons:commons-lang3:3.14.0' - testFixturesApi "org.jetbrains:annotations:24.0.0" - - compileOnly "jakarta.annotation:jakarta.annotation-api:2.1.1" - compileOnly "jakarta.persistence:jakarta.persistence-api:3.1.0" + implementation("org.apache.iceberg:iceberg-gcp") + implementation(platform("com.google.cloud:google-cloud-storage-bom:2.39.0")) + implementation("com.google.cloud:google-cloud-storage") + + implementation(platform("io.micrometer:micrometer-bom:1.13.2")) + implementation("io.micrometer:micrometer-core") + + testFixturesApi(platform("org.junit:junit-bom:5.10.3")) + testFixturesApi("org.junit.jupiter:junit-jupiter") + testFixturesApi("org.assertj:assertj-core:3.25.3") + testFixturesApi("org.mockito:mockito-core:5.11.0") + testFixturesApi("com.fasterxml.jackson.core:jackson-core") + testFixturesApi("com.fasterxml.jackson.core:jackson-databind") + testFixturesApi("org.apache.commons:commons-lang3:3.14.0") + testFixturesApi("org.jetbrains:annotations:24.0.0") + testFixturesApi(platform("com.fasterxml.jackson:jackson-bom:${jacksonVersion}")) + + compileOnly("jakarta.annotation:jakarta.annotation-api:2.1.1") + compileOnly("jakarta.persistence:jakarta.persistence-api:3.1.0") } openApiValidate { diff --git a/polaris-service/build.gradle b/polaris-service/build.gradle index 3dc0dfb364..fb99ef01ef 100644 --- a/polaris-service/build.gradle +++ b/polaris-service/build.gradle @@ -15,71 +15,76 @@ */ plugins { - id 'com.github.johnrengelman.shadow' version '8.1.1' - id 'org.openapi.generator' version '7.6.0' + id "com.github.johnrengelman.shadow" version "8.1.1" + id "org.openapi.generator" version "7.6.0" } dependencies { - implementation project(':polaris-core') + implementation(project(":polaris-core")) - implementation "org.apache.iceberg:iceberg-api:${icebergVersion}" - implementation "org.apache.iceberg:iceberg-core:${icebergVersion}" - implementation "org.apache.iceberg:iceberg-aws:${icebergVersion}" + implementation(platform("org.apache.iceberg:iceberg-bom:${icebergVersion}")) + implementation("org.apache.iceberg:iceberg-api") + implementation("org.apache.iceberg:iceberg-core") + implementation("org.apache.iceberg:iceberg-aws") - implementation "io.dropwizard:dropwizard-core:${dropwizardVersion}" - implementation "io.dropwizard:dropwizard-auth:${dropwizardVersion}" - implementation "io.dropwizard:dropwizard-json-logging:${dropwizardVersion}" + implementation(platform("io.dropwizard:dropwizard-bom:${dropwizardVersion}")) + implementation("io.dropwizard:dropwizard-core") + implementation("io.dropwizard:dropwizard-auth") + implementation("io.dropwizard:dropwizard-json-logging") - implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.16.2' + implementation(platform("com.fasterxml.jackson:jackson-bom:${jacksonVersion}")) + implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-xml") - implementation "io.opentelemetry:opentelemetry-api:1.38.0" - implementation "io.opentelemetry:opentelemetry-sdk-trace:1.38.0" - implementation "io.opentelemetry:opentelemetry-exporter-logging:1.38.0"; - implementation "io.opentelemetry.semconv:opentelemetry-semconv:1.25.0-alpha"; + implementation(platform("io.opentelemetry:opentelemetry-bom:1.38.0")) + implementation("io.opentelemetry:opentelemetry-api") + implementation("io.opentelemetry:opentelemetry-sdk-trace") + implementation("io.opentelemetry:opentelemetry-exporter-logging") + implementation("io.opentelemetry.semconv:opentelemetry-semconv:1.25.0-alpha") - implementation 'com.github.ben-manes.caffeine:caffeine:3.1.8' + implementation("com.github.ben-manes.caffeine:caffeine:3.1.8") - implementation 'io.prometheus:prometheus-metrics-exporter-servlet-jakarta:1.3.0' - implementation 'io.micrometer:micrometer-core:1.13.2' - implementation 'io.micrometer:micrometer-registry-prometheus:1.13.2' + implementation("io.prometheus:prometheus-metrics-exporter-servlet-jakarta:1.3.0") + implementation(platform("io.micrometer:micrometer-bom:1.13.2")) + implementation("io.micrometer:micrometer-core") + implementation("io.micrometer:micrometer-registry-prometheus") - implementation "io.swagger:swagger-annotations:1.6.14" - implementation "io.swagger:swagger-jaxrs:1.6.14" - implementation "javax.annotation:javax.annotation-api:1.3.2" + implementation("io.swagger:swagger-annotations:1.6.14") + implementation("io.swagger:swagger-jaxrs:1.6.14") + implementation("javax.annotation:javax.annotation-api:1.3.2") - implementation "org.apache.hadoop:hadoop-client-api:${hadoopVersion}" + implementation("org.apache.hadoop:hadoop-client-api:${hadoopVersion}") - implementation 'org.xerial:sqlite-jdbc:3.45.1.0' - implementation 'com.auth0:java-jwt:4.2.1' + implementation("org.xerial:sqlite-jdbc:3.45.1.0") + implementation("com.auth0:java-jwt:4.2.1") - implementation "ch.qos.logback:logback-core:1.4.14" - implementation group: 'org.bouncycastle', name: 'bcprov-jdk18on', version: '1.78' + implementation("ch.qos.logback:logback-core:1.4.14") + implementation("org.bouncycastle:bcprov-jdk18on:1.78") - implementation "com.google.cloud:google-cloud-storage:2.39.0" - implementation "software.amazon.awssdk:sts:2.25.61" - implementation "software.amazon.awssdk:sts:2.25.61" - implementation "software.amazon.awssdk:iam-policy-builder:2.25.61" - implementation "software.amazon.awssdk:s3:2.25.61" + implementation("com.google.cloud:google-cloud-storage:2.39.0") + implementation(platform("software.amazon.awssdk:bom:2.26.25")) + implementation("software.amazon.awssdk:sts") + implementation("software.amazon.awssdk:sts") + implementation("software.amazon.awssdk:iam-policy-builder") + implementation("software.amazon.awssdk:s3") + testImplementation("org.apache.iceberg:iceberg-api:${icebergVersion}:tests") + testImplementation("org.apache.iceberg:iceberg-core:${icebergVersion}:tests") + testImplementation("io.dropwizard:dropwizard-testing") + testImplementation("org.testcontainers:testcontainers:1.19.8") + testImplementation("com.adobe.testing:s3mock-testcontainers:3.9.1") - testImplementation "org.apache.iceberg:iceberg-api:${icebergVersion}:tests" - testImplementation "org.apache.iceberg:iceberg-core:${icebergVersion}:tests" - testImplementation "io.dropwizard:dropwizard-testing:${dropwizardVersion}" - testImplementation "org.testcontainers:testcontainers:1.19.8" - testImplementation "com.adobe.testing:s3mock-testcontainers:3.9.1" - - testImplementation "org.apache.iceberg:iceberg-spark-3.5_2.12:1.5.0" - testImplementation "org.apache.iceberg:iceberg-spark-extensions-3.5_2.12:1.5.0" + testImplementation("org.apache.iceberg:iceberg-spark-3.5_2.12") + testImplementation("org.apache.iceberg:iceberg-spark-extensions-3.5_2.12") testImplementation("org.apache.spark:spark-sql_2.12:3.5.1") { // exclude log4j dependencies - exclude group: 'org.apache.logging.log4j', module: 'log4j-slf4j2-impl' - exclude group: 'org.apache.logging.log4j', module: 'log4j-api' - exclude group: 'org.apache.logging.log4j', module: 'log4j-1.2-api' + exclude group: "org.apache.logging.log4j", module: "log4j-slf4j2-impl" + exclude group: "org.apache.logging.log4j", module: "log4j-api" + exclude group: "org.apache.logging.log4j", module: "log4j-1.2-api" } - testImplementation "software.amazon.awssdk:glue:2.25.61" - testImplementation "software.amazon.awssdk:kms:2.25.61" - testImplementation "software.amazon.awssdk:dynamodb:2.25.61" + testImplementation("software.amazon.awssdk:glue") + testImplementation("software.amazon.awssdk:kms") + testImplementation("software.amazon.awssdk:dynamodb") } openApiGenerate { @@ -171,35 +176,35 @@ compileJava.dependsOn tasks.openApiGenerate, tasks.generatePolarisService sourceSets.main.java.srcDirs += ["$buildDir/generated/src/main/java"] test { - if (System.getenv('AWS_REGION') == null) { - environment 'AWS_REGION', 'us-west-2' + if (System.getenv("AWS_REGION") == null) { + environment "AWS_REGION", "us-west-2" } - jvmArgs '--add-exports', 'java.base/sun.nio.ch=ALL-UNNAMED' + jvmArgs "--add-exports", "java.base/sun.nio.ch=ALL-UNNAMED" useJUnitPlatform() maxParallelForks = 4 } task runApp(type: JavaExec) { - if (System.getenv('AWS_REGION') == null) { - environment 'AWS_REGION', 'us-west-2' + if (System.getenv("AWS_REGION") == null) { + environment "AWS_REGION", "us-west-2" } classpath = sourceSets.main.runtimeClasspath - mainClass = 'io.polaris.service.PolarisApplication' - args 'server', "$rootDir/polaris-server.yml" + mainClass = "io.polaris.service.PolarisApplication" + args "server", "$rootDir/polaris-server.yml" } application { - mainClass = 'io.polaris.service.PolarisApplication' + mainClass = "io.polaris.service.PolarisApplication" } jar { manifest { - attributes 'Main-Class': 'io.polaris.service.PolarisApplication' + attributes "Main-Class": "io.polaris.service.PolarisApplication" } } shadowJar { - mainClassName = 'io.polaris.service.PolarisApplication' + mainClassName = "io.polaris.service.PolarisApplication" mergeServiceFiles() zip64 true } From 8aea03a87142de6255a3d097b404c9c17626fa1b Mon Sep 17 00:00:00 2001 From: Robert Stupp Date: Tue, 30 Jul 2024 05:34:42 +0200 Subject: [PATCH 22/27] Add Logo for IntelliJ (#44) Co-authored-by: Michael Collado <40346148+collado-mike@users.noreply.github.com> --- build.gradle | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/build.gradle b/build.gradle index a86baf2b31..66efa62226 100644 --- a/build.gradle +++ b/build.gradle @@ -122,6 +122,11 @@ if (System.getProperty("idea.sync.active").asBoolean()) { def ideaDir = rootProject.layout.projectDirectory.dir(".idea") ideaDir.asFile.mkdirs() ideaDir.file(".name").asFile.text = ideName + def icon = ideaDir.file("icon.png").asFile + if (!icon.exists()) { + def img = new URI("https://avatars.githubusercontent.com/u/173406119?s=200&v=4").toURL().openConnection().getInputStream().bytes + ideaDir.file("icon.png").asFile.newOutputStream().with { out -> out.write(img) } + } } eclipse { project { name = ideName } } From c6ad51f4ab42df94f1a2dc5997b3f0068f1fc7ae Mon Sep 17 00:00:00 2001 From: Michael Collado <40346148+collado-mike@users.noreply.github.com> Date: Mon, 29 Jul 2024 22:23:21 -0700 Subject: [PATCH 23/27] Fixed demo notebook and updated test auth service to include role in generated token (#47) --- docker-compose-jupyter.yml | 6 +++--- notebooks/SparkPolaris.ipynb | 7 +++---- .../auth/TestInlineBearerTokenPolarisAuthenticator.java | 9 ++++++++- .../io/polaris/service/auth/TestOAuth2ApiService.java | 7 +++++-- 4 files changed, 19 insertions(+), 10 deletions(-) diff --git a/docker-compose-jupyter.yml b/docker-compose-jupyter.yml index 758cc85668..b336d736a6 100644 --- a/docker-compose-jupyter.yml +++ b/docker-compose-jupyter.yml @@ -17,7 +17,7 @@ services: polaris: build: - context: ../iceberg-rest-server + context: . network: host ports: - "8181:8181" @@ -34,8 +34,8 @@ services: retries: 5 jupyter: build: - context: .. - dockerfile: ../notebooks/Dockerfile + context: . + dockerfile: ./notebooks/Dockerfile network: host ports: - "8888:8888" diff --git a/notebooks/SparkPolaris.ipynb b/notebooks/SparkPolaris.ipynb index 888b8b7e59..b4fe58edff 100644 --- a/notebooks/SparkPolaris.ipynb +++ b/notebooks/SparkPolaris.ipynb @@ -21,8 +21,8 @@ "from polaris.catalog.api_client import ApiClient as CatalogApiClient\n", "from polaris.catalog.api_client import Configuration as CatalogApiClientConfiguration\n", "\n", - "client_id = 'bcbf464af92c43e7'\n", - "client_secret = '85b887ccbecf498a61d192019bb4c153' # pragma: allowlist secret\n", + "client_id = 'b3b6497353b33ea7'\n", + "client_secret = '623a67ee71d75825238e3e269df5cdac' # pragma: allowlist secret\n", "client = CatalogApiClient(CatalogApiClientConfiguration(username=client_id,\n", " password=client_secret,\n", " host='http://polaris:8181/api/catalog'))\n", @@ -91,8 +91,7 @@ " principal = Principal(name=principal_name, type=\"SERVICE\")\n", " try:\n", " principal_result = api.create_principal(CreatePrincipalRequest(principal=principal))\n", - " rotate_credentials = api.rotate_credentials(principal_name=principal_name)\n", - " return rotate_credentials\n", + " return principal_result\n", " except ApiException as e:\n", " if e.status == 409:\n", " return api.rotate_credentials(principal_name=principal_name)\n", diff --git a/polaris-service/src/main/java/io/polaris/service/auth/TestInlineBearerTokenPolarisAuthenticator.java b/polaris-service/src/main/java/io/polaris/service/auth/TestInlineBearerTokenPolarisAuthenticator.java index 16ea0dbc92..e4b1a7d984 100644 --- a/polaris-service/src/main/java/io/polaris/service/auth/TestInlineBearerTokenPolarisAuthenticator.java +++ b/polaris-service/src/main/java/io/polaris/service/auth/TestInlineBearerTokenPolarisAuthenticator.java @@ -22,9 +22,11 @@ import io.polaris.core.context.CallContext; import io.polaris.core.entity.PolarisPrincipalSecrets; import io.polaris.core.persistence.PolarisMetaStoreManager; +import java.util.Arrays; import java.util.HashMap; import java.util.Map; import java.util.Optional; +import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -59,7 +61,12 @@ public Optional authenticate(String credentials) TokenInfoExchangeResponse tokenInfo = new TokenInfoExchangeResponse(); tokenInfo.setSub(principal); - tokenInfo.setScope(properties.get("role")); + if (properties.get("role") != null) { + tokenInfo.setScope( + Arrays.stream(properties.get("role").split(" ")) + .map(r -> PRINCIPAL_ROLE_PREFIX + r) + .collect(Collectors.joining(" "))); + } PolarisPrincipalSecrets secrets = metaStoreManager.loadPrincipalSecrets(callContext, principal).getPrincipalSecrets(); diff --git a/polaris-service/src/main/java/io/polaris/service/auth/TestOAuth2ApiService.java b/polaris-service/src/main/java/io/polaris/service/auth/TestOAuth2ApiService.java index 161b886d02..aab6526c07 100644 --- a/polaris-service/src/main/java/io/polaris/service/auth/TestOAuth2ApiService.java +++ b/polaris-service/src/main/java/io/polaris/service/auth/TestOAuth2ApiService.java @@ -30,6 +30,7 @@ import jakarta.ws.rs.core.SecurityContext; import java.util.HashMap; import java.util.Map; +import java.util.Objects; import org.apache.iceberg.exceptions.NotAuthorizedException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -62,10 +63,12 @@ public Response getToken( + ";password:" + clientSecret + ";realm:" - + CallContext.getCurrentContext().getRealmContext().getRealmIdentifier()); + + CallContext.getCurrentContext().getRealmContext().getRealmIdentifier() + + ";role:" + + scope.replaceAll(BasePolarisAuthenticator.PRINCIPAL_ROLE_PREFIX, "")); response.put("token_type", "bearer"); response.put("expires_in", 3600); - response.put("scope", "catalog"); + response.put("scope", Objects.requireNonNullElse(scope, "catalog")); return Response.ok(response).build(); } From 01347b645b7f16936805a1dd1cf8f16e5b96484a Mon Sep 17 00:00:00 2001 From: Anna Filippova <7892219+annafil@users.noreply.github.com> Date: Mon, 29 Jul 2024 22:57:10 -0700 Subject: [PATCH 24/27] Add CLAs, security policy and expand contributing guide (#28) --- CCLA.md | 34 ++++++++++++++++++++++++++++ CONTRIBUTING.md | 59 ++++++++++++++++++++++++++++++++++++------------- ICLA.md | 31 ++++++++++++++++++++++++++ SECURITY.md | 6 +++++ 4 files changed, 115 insertions(+), 15 deletions(-) create mode 100644 CCLA.md create mode 100644 ICLA.md create mode 100644 SECURITY.md diff --git a/CCLA.md b/CCLA.md new file mode 100644 index 0000000000..edcc41ff55 --- /dev/null +++ b/CCLA.md @@ -0,0 +1,34 @@ +# Snowflake Corporate Contributor License Agreement + +This version of the contributor license agreement allows an entity (the “Corporation”) to submit Contributions (as defined below) to Snowflake, to authorize Contributions submitted by its designated employees to Snowflake, and to grant copyright and patent licenses thereto. + +1. DEFINITIONS. "You" (or "Your") shall mean the copyright owner or legal entity authorized by the copyright owner that is making this Agreement with Snowflake. For legal entities, the entity making a Contribution and all other entities that control, are controlled by, or are under common control with that entity are considered to be a single Contributor. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + "Contribution" shall mean the code, documentation, or other original work of authorship, including any modifications or additions to an existing work, that is intentionally submitted by You to Snowflake for inclusion in, or documentation of, any of the products owned or managed by Snowflake (the “Work”). For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to Snowflake or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, Snowflake for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by You as "Not a Contribution." + +2. GRANT OF COPYRIGHT LICENSE. Subject to the terms and conditions of this Agreement, You hereby grant to Snowflake and to recipients of software distributed by Snowflake a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, and distribute Your Contributions and such derivative works. + +3. GRANT OF PATENT LICENSE. Subject to the terms and conditions of this Agreement, You hereby grant to Snowflake and to recipients of software distributed by Snowflake a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by You that are necessarily infringed by Your Contribution(s) alone or by combination of Your Contribution(s) with the Work to which such Contribution(s) was submitted. If any entity institutes patent litigation against You or any other entity (including a cross-claim or counterclaim in a lawsuit) alleging that your Contribution, or the Work to which you have contributed, constitutes direct or contributory patent infringement, then any patent licenses granted to that entity under this Agreement for that Contribution or Work shall terminate as of the date such litigation is filed. + +4. You represent that You are legally entitled to grant the above licenses. You represent further that each employee of the Corporation designated by You is authorized to submit Contributions on behalf of the Corporation. + +5. You represent that each of Your Contributions is Your original creation (see Section 7 for submissions on behalf of others). + +6. NO SUPPORT. You are not expected to provide support for Your Contributions, except to the extent You desire to provide support. You may provide support for free, for a fee, or not at all. Unless required by applicable law or agreed to in writing, You provide Your Contributions on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. + +7. Should You wish to submit work that is not Your original creation, You may submit it to Snowflake separately from any Contribution, identifying the complete details of its source and of any license or other restriction (including, but not limited to, related patents, trademarks, and license agreements) of which you are personally aware, and conspicuously marking the work as "Submitted on behalf of a third-party: [named here]". + +8. NOTICE TO SNOWFLAKE. It is your responsibility to notify Snowflake when any change is required to the designated employees authorized to submit Contributions on behalf of the Corporation, or to the Corporation’s point of contact with Snowflake. + + +Name: _________________________________________ + +Signature: _________________________________________ + +Title: _________________________________________ + +Corporation: _________________________________________ + +Date: _________________________________________ + +Notices: _________________________________________ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5402c68808..fdbceb1fe7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,23 +14,55 @@ limitations under the License. --> -# Contributing to Polaris +# Contributing to Polaris Catalog -You want to contribute to Polaris: thank you! -Any contribution (code, test cases, documentation, use cases, ...) is valuable. +Thank you for considering contributing to the Polaris Catalog. Any contribution (code, test cases, documentation, use cases, ...) is valuable! -This documentation will help you to start your contribution. +This documentation will help you get started. -## Report bugs and feature requests +## Contribute bug reports and feature requests -You can report an issue in Polaris [issue tracker](https://github.com/polaris-catalog/polaris-dev/issues). +You can report an issue in the Polaris Catalog [issue tracker](https://github.com/polaris-catalog/polaris/issues). -When reporting a bug make sure you document the steps to reproduce the issue and provide all necessary information (Apache Iceberg version, Catalog capabilities enabled, ...). -When creating a feature request document your requirements first. Please, try to not directly describe the solution. +### How to report a bug +Note: If you find a **security vulnerability**, do _NOT_ open an issue. Please email security [at] polaris.io instead to get advice from maintainers. -If you want to dive into development yourself then you can also browse for open issues or features that need to be implemented. Take ownership of an issue and try fix it. Before doing a bigger change, please describe the concept/design of what you plan to do. If unsure if the design is good or will be accepted, discuss it as issue comments. +When filing an [issue](https://github.com/polaris-catalog/polaris/issues), make sure to answer these five questions: +1. What version of Polaris Catalog are you using? +2. What operating system and processor architecture are you using? +3. What did you do? +4. What did you expect to see? +5. What did you see instead? -## Provide changes in a Pull Request +Troubleshooting questions should be posted in [GitHub Discussions](https://github.com/polaris-catalog/polaris/discussions/categories/q-a) instead of the issue tracker. Maintainers and community members will answer your questions there or ask you to file an issue if you’ve encountered a bug. + +### How to suggest a feature or enhancement + +Polaris Catalog aims to provide the Iceberg community with new levels of choice, flexibility and control over their data, with full enterprise security and Apache Iceberg interoperability with Amazon Web Services (AWS), Confluent, Dremio, Google Cloud, Microsoft Azure, Salesforce and more. + +If you're looking for a feature that doesn't exist in Polaris Catalog, you're probably not alone. Others likely have similar needs. Please open a [GitHub Issue](https://github.com/polaris-catalog/polaris/issues) describing the feature you'd like to see, why you need it, and how it should work. + +When creating your feature request, document your requirements first. Please, try to not directly describe the solution. + + +## Before you begin contributing code + +### Review the License + +When contributing to this project, you agree that your contributions use the Apache License version 2. Please ensure you have permission to do this if required by your employer. + +### Sign the CLA +When you submit your first PR to Polaris Catalog, you will need to sign an [Individual Contributor License Agreement (ICLA)](./ICLA.md). If your employer agreement requires you to do so, you may also need someone from your company to also sign the [Corporate Contributor License Agreement (CCLA)](./CCLA.md). Make sure they have the legal authority to enter into contracts on behalf of the company. Please send over your ICLA and CCLA to community [at] polaris.io in order for your pull request to be considered. + +You can download a copy of the ICLA [here](./ICLA.md) and the CCLA [here](./CCLA.md). + +### Review open issues and discuss your approach + +If you want to dive into development yourself then you can check out existing open issues or requests for features that need to be implemented. Take ownership of an issue and try fix it. + +Before starting on a large code change, please describe the concept/design of what you plan to do on the issue/feature request you intend to address. If unsure if the design is good or will be accepted, discuss it with the community in the respective issue first, before you do too much active development. + +### Provide your changes in a Pull Request The best way to provide changes is to fork Polaris repository on GitHub and provide a Pull Request with your changes. To make it easy to apply your changes please use the following conventions: @@ -38,8 +70,8 @@ The best way to provide changes is to fork Polaris repository on GitHub and prov * Create a branch that will house your change: ```bash -git clone https://github.com/polaris/polaris-dev -cd polaris-dev +git clone https://github.com/polaris-catalog/polaris +cd polaris git fetch --all git checkout -b my-branch origin/main ``` @@ -62,6 +94,3 @@ The Polaris build currently requires Java 21 or later. There are a few tools tha * [SDKMAN!](https://sdkman.io/) follow the installation instructions, then run `sdk list java` to see the available distributions and versions, then run `sdk install java ` using the identifier for the distribution and version (>= 21) of your choice. * [jenv](https://www.jenv.be/) If on a Mac you can use jenv to set the appropriate SDK. -## License - -When contributing to this project, you agree that your contributions use the Apache License version 2. Please ensure you have permission to do this if required by your employer. diff --git a/ICLA.md b/ICLA.md new file mode 100644 index 0000000000..711626d78e --- /dev/null +++ b/ICLA.md @@ -0,0 +1,31 @@ +# Snowflake Individual Contributor License Agreement + +By signing this contributor license agreement, You understand and agree that Your Contribution (as defined below) is public and that a record of the Contribution, including Your full name and email address among other information, will be maintained indefinitely and may be redistributed consistent with this project, compliance with the open source license(s) involved, and maintenance of authorship attribution. + +1. DEFINITIONS. "You" (or "Your") shall mean the copyright owner or legal entity authorized by the copyright owner that is making this Agreement with Snowflake. For legal entities, the entity making a Contribution and all other entities that control, are controlled by, or are under common control with that entity are considered to be a single Contributor. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + "Contribution" shall mean any original work of authorship, including any modifications or additions to an existing work, that is intentionally submitted by You to Snowflake for inclusion in, or documentation of, any of the products owned or managed by Snowflake (the “Work”). For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to Snowflake or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, Snowflake for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by You as "Not a Contribution." + +2. GRANT OF COPYRIGHT LICENSE. Subject to the terms and conditions of this Agreement, You hereby grant to Snowflake and to recipients of software distributed by Snowflake a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, and distribute Your Contributions and such derivative works. + +3. GRANT OF PATENT LICENSE. Subject to the terms and conditions of this Agreement, You hereby grant to Snowflake and to recipients of software distributed by Snowflake a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by You that are necessarily infringed by Your Contribution(s) alone or by combination of Your Contribution(s) with the Work to which such Contribution(s) was submitted. If any entity institutes patent litigation against You or any other entity (including a cross-claim or counterclaim in a lawsuit) alleging that your Contribution, or the Work to which you have contributed, constitutes direct or contributory patent infringement, then any patent licenses granted to that entity under this Agreement for that Contribution or Work shall terminate as of the date such litigation is filed. + +4. You represent that You are legally entitled to grant the above licenses. If Your employer(s) has rights to intellectual property that You create that includes Your Contributions, You represent that You have received permission to make Contributions on behalf of Your employer, that Your employer has waived such rights for Your Contributions to Snowflake, or that your employer has executed a separate corporate CLA with Snowflake. + +5. You represent that each of Your Contributions is Your original creation (see Section 7 for submissions on behalf of others). You represent that Your Contribution submissions include complete details of any third-party license or other restriction (including, but not limited to, related patents and trademarks) of which you are personally aware and which are associated with any part of Your Contributions. + +6. NO SUPPORT. You are not expected to provide support for Your Contributions, except to the extent You desire to provide support. You may provide support for free, for a fee, or not at all. Unless required by applicable law or agreed to in writing, You provide Your Contributions on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. + +7. Should You wish to submit work that is not Your original creation, You may submit it to Snowflake separately from any Contribution, identifying the complete details of its source and of any license or other restriction (including, but not limited to, related patents, trademarks, and license agreements) of which you are personally aware, and conspicuously marking the work as "Submitted on behalf of a third-party: [named here]". + +8. NOTICE TO SNOWFLAKE. You agree to notify Snowflake of any facts or circumstances of which you become aware that would make these representations inaccurate in any respect. + + + +Name (“You”): _________________________________________ + +Signature: _________________________________________ + +Date: _________________________________________ + +Email: _________________________________________ diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000000..c94c618e1b --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,6 @@ +# Security Policy +If you discover a security issue, please bring it to our attention right away! + +## Reporting a Vulnerability + +Please DO NOT file a public issue to report a security vulberability, instead send your report privately to security@polaris.io. This will help ensure that any vulnerabilities that are found can be disclosed responsibly to any affected parties. From 2b28361f6d3f7d4023cb38ad6088c9630254664a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?JB=20Onofr=C3=A9?= Date: Tue, 30 Jul 2024 11:25:11 +0200 Subject: [PATCH 25/27] Suppress warning (unchecked) (#48) --- .../io/polaris/core/storage/InMemoryStorageIntegrationTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/polaris-core/src/test/java/io/polaris/core/storage/InMemoryStorageIntegrationTest.java b/polaris-core/src/test/java/io/polaris/core/storage/InMemoryStorageIntegrationTest.java index 3b1291f614..f4749fcbe4 100644 --- a/polaris-core/src/test/java/io/polaris/core/storage/InMemoryStorageIntegrationTest.java +++ b/polaris-core/src/test/java/io/polaris/core/storage/InMemoryStorageIntegrationTest.java @@ -79,6 +79,7 @@ public void testValidateAccessToLocationsWithWildcard() { Mockito.mock(), new PolarisDefaultDiagServiceImpl(), new PolarisConfigurationStore() { + @SuppressWarnings("unchecked") @Override public @Nullable T getConfiguration(PolarisCallContext ctx, String configName) { return (T) config.get(configName); From 7bf67eeca3dbcc788042a53bbddc2ba4ac4e4bc9 Mon Sep 17 00:00:00 2001 From: Robert Stupp Date: Tue, 30 Jul 2024 11:54:55 +0200 Subject: [PATCH 26/27] Gradle wrapper download / macOS + broken gradle-wrapper.properties (#51) * Gradle wrapper download / macOS Fixes #49 * Also fix #50 * shasum + sha256sum --- gradle/gradlew-include.sh | 16 +++++++++++++--- gradle/wrapper/gradle-wrapper.properties | 2 +- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/gradle/gradlew-include.sh b/gradle/gradlew-include.sh index 815e5d17c5..19cb46059c 100644 --- a/gradle/gradlew-include.sh +++ b/gradle/gradlew-include.sh @@ -5,6 +5,16 @@ GRADLE_DIST_VERSION="$(grep distributionUrl= "$APP_HOME/gradle/wrapper/gradle-wrapper.properties" | sed 's/^.*gradle-\([0-9.]*\)-[a-z]*.zip$/\1/')" GRADLE_WRAPPER_SHA256="$APP_HOME/gradle/wrapper/gradle-wrapper-${GRADLE_DIST_VERSION}.jar.sha256" GRADLE_WRAPPER_JAR="$APP_HOME/gradle/wrapper/gradle-wrapper.jar" +if [ -x "$(command -v sha256sum)" ] ; then + SHASUM="sha256sum" +else + if [ -x "$(command -v shasum)" ] ; then + SHASUM="shasum -a 256" + else + echo "Neither sha256sum nor shasum are available, install either." > /dev/stderr + exit 1 + fi +fi if [ ! -e "${GRADLE_WRAPPER_SHA256}" ]; then # Delete the wrapper jar, if the checksum file does not exist. rm -f "${GRADLE_WRAPPER_JAR}" @@ -12,7 +22,7 @@ fi if [ -e "${GRADLE_WRAPPER_JAR}" ]; then # Verify the wrapper jar, if it exists, delete wrapper jar and checksum file, if the checksums # do not match. - JAR_CHECKSUM="$(sha256sum "${GRADLE_WRAPPER_JAR}" | cut -d\ -f1)" + JAR_CHECKSUM="$(${SHASUM} "${GRADLE_WRAPPER_JAR}" | cut -d\ -f1)" EXPECTED="$(cat "${GRADLE_WRAPPER_SHA256}")" if [ "${JAR_CHECKSUM}" != "${EXPECTED}" ]; then rm -f "${GRADLE_WRAPPER_JAR}" "${GRADLE_WRAPPER_SHA256}" @@ -26,12 +36,12 @@ if [ ! -e "${GRADLE_WRAPPER_JAR}" ]; then # versions. Need to append a ".0" in that case to download the wrapper jar. GRADLE_VERSION="$(echo "$GRADLE_DIST_VERSION" | sed 's/^\([0-9]*[.][0-9]*\)$/\1.0/')" curl --location --output "${GRADLE_WRAPPER_JAR}" https://raw.githubusercontent.com/gradle/gradle/v${GRADLE_VERSION}/gradle/wrapper/gradle-wrapper.jar || exit 1 - JAR_CHECKSUM="$(sha256sum "${GRADLE_WRAPPER_JAR}" | cut -d\ -f1)" + JAR_CHECKSUM="$(${SHASUM} "${GRADLE_WRAPPER_JAR}" | cut -d\ -f1)" EXPECTED="$(cat "${GRADLE_WRAPPER_SHA256}")" if [ "${JAR_CHECKSUM}" != "${EXPECTED}" ]; then # If the (just downloaded) checksum and the downloaded wrapper jar do not match, something # really bad is going on. - echo "Expected sha256 of the downloaded gradle-wrapper.jar does not match the downloaded sha256!" + echo "Expected sha256 of the downloaded gradle-wrapper.jar does not match the downloaded sha256!" > /dev/stderr exit 1 fi fi diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index abc941ef79..f24d7559ef 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -17,7 +17,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists # See https://gradle.org/release-checksums/ for valid checksums -distributionSha256Sum=d725d707bfabd4dfdc958c624003b3c80accc03f7037b5122c4b1d0ef15cecab # pragma: allowlist secret +distributionSha256Sum=d725d707bfabd4dfdc958c624003b3c80accc03f7037b5122c4b1d0ef15cecab distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip networkTimeout=10000 validateDistributionUrl=true From 1a6b3eb3963355f78c5ca916cc1d66ecd1493092 Mon Sep 17 00:00:00 2001 From: Robert Stupp Date: Tue, 30 Jul 2024 13:38:08 +0200 Subject: [PATCH 27/27] Spotless, license header, add xml (#25) --- .idea/google-java-format.xml | 6 +++ CONTRIBUTING.md | 6 +++ build.gradle | 46 ++++++++++++++++--- codestyle/copyright-header-java.txt | 15 ++++++ codestyle/copyright-header.txt | 13 ++++++ codestyle/copyright-header.xml | 16 +++++++ codestyle/org.eclipse.wst.xml.core.prefs | 24 ++++++++++ .../PolarisEclipseLinkMetaStoreTest.java | 2 - .../io/polaris/core/PolarisCallContext.java | 2 +- .../io/polaris/core/PolarisConfiguration.java | 2 +- .../core/PolarisConfigurationStore.java | 2 +- .../core/PolarisDefaultDiagServiceImpl.java | 2 +- .../io/polaris/core/PolarisDiagnostics.java | 2 +- .../io/polaris/core/resource/TimedApi.java | 15 ++++++ .../storage/StorageConfigurationOverride.java | 15 ++++++ .../io/polaris/core/storage/StorageUtil.java | 15 ++++++ .../main/resources/META-INF/persistence.xml | 17 +++---- .../admin/PolarisOverlappingCatalogTest.java | 15 ++++++ .../test/resources/META-INF/persistence.xml | 17 +++---- 19 files changed, 203 insertions(+), 29 deletions(-) create mode 100644 .idea/google-java-format.xml create mode 100644 codestyle/copyright-header-java.txt create mode 100644 codestyle/copyright-header.txt create mode 100644 codestyle/copyright-header.xml create mode 100644 codestyle/org.eclipse.wst.xml.core.prefs diff --git a/.idea/google-java-format.xml b/.idea/google-java-format.xml new file mode 100644 index 0000000000..8b57f4527a --- /dev/null +++ b/.idea/google-java-format.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fdbceb1fe7..6ff623b926 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -81,6 +81,12 @@ git checkout -b my-branch origin/main ```bash git pull --rebase git push GitHubUser my-branch --force +``` + + Ensure the code is properly formatted: + +```bash +./gradlew format ``` * Pull Requests should be based on the `main` branch. diff --git a/build.gradle b/build.gradle index 66efa62226..38c650bdd2 100644 --- a/build.gradle +++ b/build.gradle @@ -28,6 +28,7 @@ buildscript { plugins { id "idea" id "eclipse" + id "org.jetbrains.gradle.plugin.idea-ext" version "1.1.8" } allprojects { @@ -103,12 +104,23 @@ subprojects { throw new AssertionError("Wildcard imports disallowed - ${m.findAll()}") } } - java { - target 'src/*/java/**/*.java' - targetExclude 'build/**' - googleJavaFormat() - endWithNewline() - custom "disallowWildcardImports", disallowWildcardImports + format("xml") { + target("src/**/*.xml", "src/**/*.xsd") + targetExclude("codestyle/copyright-header.xml") + eclipseWtp(com.diffplug.spotless.extra.wtp.EclipseWtpFormatterStep.XML) + .configFile(rootProject.file("codestyle/org.eclipse.wst.xml.core.prefs")) + // getting the license-header delimiter right is a bit tricky. + //licenseHeaderFile(rootProject.file("codestyle/copyright-header.xml"), '<^[!?].*$') + } + if (project.plugins.hasPlugin("java-base")) { + java { + target "src/*/java/**/*.java" + targetExclude "build/**" + licenseHeaderFile(rootProject.file("codestyle/copyright-header-java.txt")) + googleJavaFormat() + endWithNewline() + custom "disallowWildcardImports", disallowWildcardImports + } } } } @@ -127,6 +139,28 @@ if (System.getProperty("idea.sync.active").asBoolean()) { def img = new URI("https://avatars.githubusercontent.com/u/173406119?s=200&v=4").toURL().openConnection().getInputStream().bytes ideaDir.file("icon.png").asFile.newOutputStream().with { out -> out.write(img) } } + + idea { + module { + name = ideName + downloadSources = true // this is the default BTW + inheritOutputDirs = true + } + } + + idea.project.settings { + copyright { + useDefault = "ApacheLicense-v2" + profiles.create("ApacheLicense-v2") { + // strip trailing LF + def copyrightText = rootProject.file("codestyle/copyright-header.txt").text + notice = copyrightText + } + } + + encodings.encoding = "UTF-8" + encodings.properties.encoding = "UTF-8" + } } eclipse { project { name = ideName } } diff --git a/codestyle/copyright-header-java.txt b/codestyle/copyright-header-java.txt new file mode 100644 index 0000000000..fdd7c41a38 --- /dev/null +++ b/codestyle/copyright-header-java.txt @@ -0,0 +1,15 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ diff --git a/codestyle/copyright-header.txt b/codestyle/copyright-header.txt new file mode 100644 index 0000000000..b0b441c85c --- /dev/null +++ b/codestyle/copyright-header.txt @@ -0,0 +1,13 @@ +Copyright (c) 2024 Snowflake Computing 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. diff --git a/codestyle/copyright-header.xml b/codestyle/copyright-header.xml new file mode 100644 index 0000000000..d3838526b8 --- /dev/null +++ b/codestyle/copyright-header.xml @@ -0,0 +1,16 @@ + + diff --git a/codestyle/org.eclipse.wst.xml.core.prefs b/codestyle/org.eclipse.wst.xml.core.prefs new file mode 100644 index 0000000000..bc2f15da16 --- /dev/null +++ b/codestyle/org.eclipse.wst.xml.core.prefs @@ -0,0 +1,24 @@ +# +# Copyright (c) 2024 Snowflake Computing 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. +# +# + +eclipse.preferences.version=1 +formatCommentJoinLines=false +formatCommentText=false +indentationChar=space +indentationSize=2 +lineWidth=100 +spaceBeforeEmptyCloseTag=false diff --git a/extension/persistence/eclipselink/src/test/java/com/snowflake/polaris/persistence/impl/eclipselink/PolarisEclipseLinkMetaStoreTest.java b/extension/persistence/eclipselink/src/test/java/com/snowflake/polaris/persistence/impl/eclipselink/PolarisEclipseLinkMetaStoreTest.java index c791b2b86f..03cc1026e9 100644 --- a/extension/persistence/eclipselink/src/test/java/com/snowflake/polaris/persistence/impl/eclipselink/PolarisEclipseLinkMetaStoreTest.java +++ b/extension/persistence/eclipselink/src/test/java/com/snowflake/polaris/persistence/impl/eclipselink/PolarisEclipseLinkMetaStoreTest.java @@ -1,5 +1,4 @@ /* - * * Copyright (c) 2024 Snowflake Computing Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -13,7 +12,6 @@ * 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. - * */ package com.snowflake.polaris.persistence.impl.eclipselink; diff --git a/polaris-core/src/main/java/io/polaris/core/PolarisCallContext.java b/polaris-core/src/main/java/io/polaris/core/PolarisCallContext.java index 88209769dd..a64663d650 100644 --- a/polaris-core/src/main/java/io/polaris/core/PolarisCallContext.java +++ b/polaris-core/src/main/java/io/polaris/core/PolarisCallContext.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2024 Snowflake Computing Inc. + * Copyright (c) 2024 Snowflake Computing Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/polaris-core/src/main/java/io/polaris/core/PolarisConfiguration.java b/polaris-core/src/main/java/io/polaris/core/PolarisConfiguration.java index 177ab5afdd..ceefb83ff9 100644 --- a/polaris-core/src/main/java/io/polaris/core/PolarisConfiguration.java +++ b/polaris-core/src/main/java/io/polaris/core/PolarisConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2024 Snowflake Computing Inc. + * Copyright (c) 2024 Snowflake Computing Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/polaris-core/src/main/java/io/polaris/core/PolarisConfigurationStore.java b/polaris-core/src/main/java/io/polaris/core/PolarisConfigurationStore.java index a4f48e9099..f2e38c2ddd 100644 --- a/polaris-core/src/main/java/io/polaris/core/PolarisConfigurationStore.java +++ b/polaris-core/src/main/java/io/polaris/core/PolarisConfigurationStore.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2024 Snowflake Computing Inc. + * Copyright (c) 2024 Snowflake Computing Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/polaris-core/src/main/java/io/polaris/core/PolarisDefaultDiagServiceImpl.java b/polaris-core/src/main/java/io/polaris/core/PolarisDefaultDiagServiceImpl.java index 39c0d130a4..74acd06cd7 100644 --- a/polaris-core/src/main/java/io/polaris/core/PolarisDefaultDiagServiceImpl.java +++ b/polaris-core/src/main/java/io/polaris/core/PolarisDefaultDiagServiceImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2024 Snowflake Computing Inc. + * Copyright (c) 2024 Snowflake Computing Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/polaris-core/src/main/java/io/polaris/core/PolarisDiagnostics.java b/polaris-core/src/main/java/io/polaris/core/PolarisDiagnostics.java index 6a5a0c39fd..f85c31b844 100644 --- a/polaris-core/src/main/java/io/polaris/core/PolarisDiagnostics.java +++ b/polaris-core/src/main/java/io/polaris/core/PolarisDiagnostics.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2024 Snowflake Computing Inc. + * Copyright (c) 2024 Snowflake Computing Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/polaris-core/src/main/java/io/polaris/core/resource/TimedApi.java b/polaris-core/src/main/java/io/polaris/core/resource/TimedApi.java index b4bea7bc17..d7514be3fa 100644 --- a/polaris-core/src/main/java/io/polaris/core/resource/TimedApi.java +++ b/polaris-core/src/main/java/io/polaris/core/resource/TimedApi.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.core.resource; import io.polaris.core.monitor.PolarisMetricRegistry; diff --git a/polaris-core/src/main/java/io/polaris/core/storage/StorageConfigurationOverride.java b/polaris-core/src/main/java/io/polaris/core/storage/StorageConfigurationOverride.java index 00cb88a55e..1116a7e553 100644 --- a/polaris-core/src/main/java/io/polaris/core/storage/StorageConfigurationOverride.java +++ b/polaris-core/src/main/java/io/polaris/core/storage/StorageConfigurationOverride.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.core.storage; import java.util.List; diff --git a/polaris-core/src/main/java/io/polaris/core/storage/StorageUtil.java b/polaris-core/src/main/java/io/polaris/core/storage/StorageUtil.java index 8278c943f4..3e16ced537 100644 --- a/polaris-core/src/main/java/io/polaris/core/storage/StorageUtil.java +++ b/polaris-core/src/main/java/io/polaris/core/storage/StorageUtil.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.core.storage; import org.jetbrains.annotations.NotNull; diff --git a/polaris-service/src/main/resources/META-INF/persistence.xml b/polaris-service/src/main/resources/META-INF/persistence.xml index 59fb601130..db8c1c4bd3 100644 --- a/polaris-service/src/main/resources/META-INF/persistence.xml +++ b/polaris-service/src/main/resources/META-INF/persistence.xml @@ -16,9 +16,9 @@ limitations under the License. --> - + org.eclipse.persistence.jpa.PersistenceProvider @@ -31,13 +31,14 @@ io.polaris.core.persistence.models.ModelSequenceId NONE - - - - + + + + - + \ No newline at end of file diff --git a/polaris-service/src/test/java/io/polaris/service/admin/PolarisOverlappingCatalogTest.java b/polaris-service/src/test/java/io/polaris/service/admin/PolarisOverlappingCatalogTest.java index 3109751310..f4cba62197 100644 --- a/polaris-service/src/test/java/io/polaris/service/admin/PolarisOverlappingCatalogTest.java +++ b/polaris-service/src/test/java/io/polaris/service/admin/PolarisOverlappingCatalogTest.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2024 Snowflake Computing 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. + */ package io.polaris.service.admin; import static io.polaris.service.context.DefaultContextResolver.REALM_PROPERTY_KEY; diff --git a/polaris-service/src/test/resources/META-INF/persistence.xml b/polaris-service/src/test/resources/META-INF/persistence.xml index db27cb8f45..11828b2848 100644 --- a/polaris-service/src/test/resources/META-INF/persistence.xml +++ b/polaris-service/src/test/resources/META-INF/persistence.xml @@ -16,9 +16,9 @@ limitations under the License. --> - + org.eclipse.persistence.jpa.PersistenceProvider @@ -31,13 +31,14 @@ io.polaris.core.persistence.models.ModelSequenceId NONE - - - - + + + + - + \ No newline at end of file

dZ(@Cu}k&M%G8EM7=Ymx)i@4>Q1gr8L;j0q2C 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 P0SG)@{an^LB{Ts5)YlGU literal 0 HcmV?d00001 diff --git a/docs/img/logos/polaris-catalog-stacked-logo.svg b/docs/img/logos/polaris-catalog-stacked-logo.svg new file mode 100644 index 0000000000..b44b0a5d7d --- /dev/null +++ b/docs/img/logos/polaris-catalog-stacked-logo.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/docs/img/logos/polaris-favicon.png b/docs/img/logos/polaris-favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..bb92271f76fc5a24c4aca74908390308698bb1ae GIT binary patch literal 12969 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 literal 0 HcmV?d00001 diff --git a/docs/img/overview.svg b/docs/img/overview.svg new file mode 100644 index 0000000000..dbe577490d --- /dev/null +++ b/docs/img/overview.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/img/rbac-example.svg b/docs/img/rbac-example.svg new file mode 100644 index 0000000000..431e30ffbe --- /dev/null +++ b/docs/img/rbac-example.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/img/rbac-model.svg b/docs/img/rbac-model.svg new file mode 100644 index 0000000000..7c7323d32c --- /dev/null +++ b/docs/img/rbac-model.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/img/sample-catalog-structure.svg b/docs/img/sample-catalog-structure.svg new file mode 100644 index 0000000000..efecec6ba4 --- /dev/null +++ b/docs/img/sample-catalog-structure.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/index.html b/docs/index.html index 683725b549..ac8987d906 100644 --- a/docs/index.html +++ b/docs/index.html @@ -1,397 +1,3481 @@ - + + + + + + + + +