diff --git a/.github/workflows/python-test.yml b/.github/workflows/python-test.yml
index c2d0f41..36e6e0f 100644
--- a/.github/workflows/python-test.yml
+++ b/.github/workflows/python-test.yml
@@ -44,10 +44,6 @@ jobs:
- name: Test with pytest
run: |
python -m hatch --data-dir=.hatch --cache-dir=.hatch_cache run test:testcov
- python -m hatch --data-dir=.hatch --cache-dir=.hatch_cache run test:pytest tests/test_migrations_engine_hooks_lifetime.py
- python -m hatch --data-dir=.hatch --cache-dir=.hatch_cache run test:pytest tests/test_migrations_engine_hooks_filters_lifetime.py
- python -m hatch --data-dir=.hatch --cache-dir=.hatch_cache run test:pytest tests/test_migrations_engine_hooks_mappings_lifetime.py
- python -m hatch --data-dir=.hatch --cache-dir=.hatch_cache run test:pytest tests/test_migrations_engine_hooks_transformers_lifetime.py
- name: Test TestApplication with pytest
run: |
diff --git a/.gitignore b/.gitignore
index 3e6a492..c0e14ea 100644
--- a/.gitignore
+++ b/.gitignore
@@ -176,8 +176,8 @@ appsettings.Development.json
clean-server-settings.dev.json
launchSettings.json
UpgradeLog.htm
-*.DEV.ini
-*.DEV.json
+*.*.ini
+*.*.json
/src/Python/Documentation/_build
/src/Python/Documentation/_static
/src/Python/Python.pyproj.user
diff --git a/Directory.Build.props b/Directory.Build.props
index 1c24409..d009a18 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -4,9 +4,10 @@
enable
true
true
- 3.0.1
+ 4.0.0
Salesforce, Inc.
Salesforce, Inc.
Copyright (c) 2024, Salesforce, Inc. and its licensors
+ Apache-2.0
\ No newline at end of file
diff --git a/LICENSE.txt b/LICENSE.txt
index b2949c4..ae7332a 100644
--- a/LICENSE.txt
+++ b/LICENSE.txt
@@ -1,6 +1,6 @@
Apache License Version 2.0
-Copyright (c) 2023 Salesforce, Inc.
+Copyright (c) 2024 Salesforce, Inc.
All rights reserved.
Apache License
diff --git a/Migration SDK.sln b/Migration SDK.sln
index 19e5b58..6ad00fc 100644
--- a/Migration SDK.sln
+++ b/Migration SDK.sln
@@ -18,6 +18,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
CONTRIBUTING.md = CONTRIBUTING.md
Directory.Build.props = Directory.Build.props
global.json = global.json
+ LICENSE.txt = LICENSE.txt
README.md = README.md
SECURITY.md = SECURITY.md
EndProjectSection
@@ -82,6 +83,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "setup-dotnet", "setup-dotne
.github\actions\setup-dotnet\action.yml = .github\actions\setup-dotnet\action.yml
EndProjectSection
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tableau.Migration.PythonGenerator", "src\Tableau.Migration.PythonGenerator\Tableau.Migration.PythonGenerator.csproj", "{F20029C7-4514-4668-8941-B2C3BC245CCB}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -124,6 +127,10 @@ Global
{99DA12FB-BB16-4EE1-9C9C-047755210255}.Debug|Any CPU.Build.0 = Debug|Any CPU
{99DA12FB-BB16-4EE1-9C9C-047755210255}.Release|Any CPU.ActiveCfg = Release|Any CPU
{99DA12FB-BB16-4EE1-9C9C-047755210255}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F20029C7-4514-4668-8941-B2C3BC245CCB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F20029C7-4514-4668-8941-B2C3BC245CCB}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F20029C7-4514-4668-8941-B2C3BC245CCB}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F20029C7-4514-4668-8941-B2C3BC245CCB}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/examples/Csharp.ExampleApplication/Hooks/Mappings/ChangeProjectMapping.cs b/examples/Csharp.ExampleApplication/Hooks/Mappings/ChangeProjectMapping.cs
index 8593aa5..6ca4e66 100644
--- a/examples/Csharp.ExampleApplication/Hooks/Mappings/ChangeProjectMapping.cs
+++ b/examples/Csharp.ExampleApplication/Hooks/Mappings/ChangeProjectMapping.cs
@@ -5,23 +5,24 @@
using Tableau.Migration;
using Tableau.Migration.Content;
using Tableau.Migration.Engine.Hooks.Mappings;
+using Tableau.Migration.Resources;
namespace Csharp.ExampleApplication.Hooks.Mappings
{
#region class
- public class ChangeProjectMapping : IContentMapping, IContentMapping
+ public class ChangeProjectMapping : ContentMappingBase
+ where T : IContentReference, IMappableContainerContent
{
private static readonly StringComparer StringComparer = StringComparer.OrdinalIgnoreCase;
- private readonly ILogger _logger;
+ private readonly ILogger>? _logger;
- public ChangeProjectMapping(ILogger logger)
+ public ChangeProjectMapping(ISharedResourcesLocalizer? localizer, ILogger>? logger) : base(localizer, logger)
{
_logger = logger;
}
- private async Task?> ExecuteAsync(ContentMappingContext ctx)
- where T : IContentReference, IMappableContainerContent
+ public override Task?> MapAsync(ContentMappingContext ctx, CancellationToken cancel)
{
// Get the container (project) location for the content item.
var containerLocation = ctx.ContentItem.Location.Parent();
@@ -29,7 +30,7 @@ public ChangeProjectMapping(ILogger logger)
// We only want to map content items whose project name is "Test".
if (!StringComparer.Equals("Test", containerLocation.Name))
{
- return ctx;
+ return ctx.ToTask();
}
// Build the new project location.
@@ -41,20 +42,20 @@ public ChangeProjectMapping(ILogger logger)
// Map the new content item location.
ctx = ctx.MapTo(newLocation);
- _logger.LogInformation(
+ _logger?.LogInformation(
"{ContentType} mapped from {OldLocation} to {NewLocation}.",
typeof(T).Name,
ctx.ContentItem.Location,
ctx.MappedLocation);
- return await ctx.ToTask();
+ return ctx.ToTask();
}
- public async Task?> ExecuteAsync(ContentMappingContext ctx, CancellationToken cancel)
- => await ExecuteAsync(ctx);
+ public async Task?> MapAsync(ContentMappingContext ctx, CancellationToken cancel)
+ => await MapAsync(ctx, cancel);
- public async Task?> ExecuteAsync(ContentMappingContext ctx, CancellationToken cancel)
- => await ExecuteAsync(ctx);
+ public async Task?> MapAsync(ContentMappingContext ctx, CancellationToken cancel)
+ => await MapAsync(ctx, cancel);
}
#endregion
}
diff --git a/examples/Csharp.ExampleApplication/Hooks/Mappings/EmailDomainMapping.cs b/examples/Csharp.ExampleApplication/Hooks/Mappings/EmailDomainMapping.cs
index 6c8441a..710be68 100644
--- a/examples/Csharp.ExampleApplication/Hooks/Mappings/EmailDomainMapping.cs
+++ b/examples/Csharp.ExampleApplication/Hooks/Mappings/EmailDomainMapping.cs
@@ -42,11 +42,12 @@ public EmailDomainMapping(
public override Task?> MapAsync(ContentMappingContext userMappingContext, CancellationToken cancel)
{
var domain = userMappingContext.MappedLocation.Parent();
+
// Re-use an existing email if it already exists.
if (!string.IsNullOrEmpty(userMappingContext.ContentItem.Email))
return userMappingContext.MapTo(domain.Append(userMappingContext.ContentItem.Email)).ToTask();
- // Takes the existing username and appends the default domain to build the email
+ // Takes the existing username and appends the domain to build the email
var testEmail = $"{userMappingContext.ContentItem.Name}@{_domain}";
return userMappingContext.MapTo(domain.Append(testEmail)).ToTask();
}
diff --git a/examples/Csharp.ExampleApplication/Hooks/Transformers/EncryptExtractTransformer.cs b/examples/Csharp.ExampleApplication/Hooks/Transformers/EncryptExtractTransformer.cs
new file mode 100644
index 0000000..855df7c
--- /dev/null
+++ b/examples/Csharp.ExampleApplication/Hooks/Transformers/EncryptExtractTransformer.cs
@@ -0,0 +1,42 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Tableau.Migration;
+using Tableau.Migration.Content;
+using Tableau.Migration.Engine.Hooks.Transformers;
+using Tableau.Migration.Resources;
+
+namespace Csharp.ExampleApplication.Hooks.Transformers
+{
+ #region class
+ public class EncryptExtractsTransformer : ContentTransformerBase where T : IContentReference, IFileContent, IExtractContent
+ {
+ private readonly ILogger>? _logger;
+
+ public EncryptExtractsTransformer(ISharedResourcesLocalizer? localizer, ILogger>? logger) : base(localizer, logger)
+ {
+ _logger = logger;
+ }
+
+ public override async Task TransformAsync(T itemToTransform, CancellationToken cancel)
+ {
+ itemToTransform.EncryptExtracts = true;
+
+ _logger?.LogInformation(
+ @"Setting encrypt extract to true for {ContentType} {ContentLocation}",
+ typeof(T).Name,
+ itemToTransform.Location);
+
+ return await Task.FromResult(itemToTransform);
+ }
+
+ public async Task TransformAsync(IPublishableWorkbook ctx, CancellationToken cancel)
+ => await TransformAsync(ctx, cancel);
+
+ public async Task TransformAsync(IPublishableDataSource ctx, CancellationToken cancel)
+ => await TransformAsync(ctx, cancel);
+ }
+ #endregion
+}
diff --git a/examples/Csharp.ExampleApplication/Hooks/Transformers/MigratedTagTransformer.cs b/examples/Csharp.ExampleApplication/Hooks/Transformers/MigratedTagTransformer.cs
index 701ee92..f48beff 100644
--- a/examples/Csharp.ExampleApplication/Hooks/Transformers/MigratedTagTransformer.cs
+++ b/examples/Csharp.ExampleApplication/Hooks/Transformers/MigratedTagTransformer.cs
@@ -6,41 +6,41 @@
using Tableau.Migration;
using Tableau.Migration.Content;
using Tableau.Migration.Engine.Hooks.Transformers;
+using Tableau.Migration.Resources;
namespace Csharp.ExampleApplication.Hooks.Transformers
{
#region class
- public class MigratedTagTransformer : IContentTransformer, IContentTransformer
+ public class MigratedTagTransformer : ContentTransformerBase where T : IContentReference, IWithTags
{
- private readonly ILogger _logger;
+ private readonly ILogger>? _logger;
- public MigratedTagTransformer(ILogger logger)
+ public MigratedTagTransformer(ISharedResourcesLocalizer? localizer, ILogger>? logger) : base(localizer, logger)
{
_logger = logger;
}
- protected async Task ExecuteAsync(T ctx)
- where T : IContentReference, IWithTags
+ public override async Task TransformAsync(T itemToTransform, CancellationToken cancel)
{
var tag = "Migrated";
// Add the tag to the content item.
- ctx.Tags.Add(new Tag(tag));
+ itemToTransform.Tags.Add(new Tag(tag));
- _logger.LogInformation(
+ _logger?.LogInformation(
@"Added ""{Tag}"" tag to {ContentType} {ContentLocation}.",
tag,
typeof(T).Name,
- ctx.Location);
+ itemToTransform.Location);
- return await Task.FromResult(ctx);
+ return await Task.FromResult(itemToTransform);
}
- public async Task ExecuteAsync(IPublishableWorkbook ctx, CancellationToken cancel)
- => await ExecuteAsync(ctx);
+ public async Task TransformAsync(IPublishableWorkbook ctx, CancellationToken cancel)
+ => await TransformAsync(ctx, cancel);
- public async Task ExecuteAsync(IPublishableDataSource ctx, CancellationToken cancel)
- => await ExecuteAsync(ctx);
+ public async Task TransformAsync(IPublishableDataSource ctx, CancellationToken cancel)
+ => await TransformAsync(ctx, cancel);
}
#endregion
}
diff --git a/examples/Csharp.ExampleApplication/MyMigrationApplication.cs b/examples/Csharp.ExampleApplication/MyMigrationApplication.cs
index f337e0d..1746c79 100644
--- a/examples/Csharp.ExampleApplication/MyMigrationApplication.cs
+++ b/examples/Csharp.ExampleApplication/MyMigrationApplication.cs
@@ -86,8 +86,8 @@ public async Task StartAsync(CancellationToken cancel)
#endregion
#region ChangeProjectMapping-Registration
- _planBuilder.Mappings.Add();
- _planBuilder.Mappings.Add();
+ _planBuilder.Mappings.Add, IDataSource>();
+ _planBuilder.Mappings.Add, IWorkbook>();
#endregion
// Add filters
@@ -111,8 +111,13 @@ public async Task StartAsync(CancellationToken cancel)
// Add transformers
#region MigratedTagTransformer-Registration
- _planBuilder.Transformers.Add();
- _planBuilder.Transformers.Add();
+ _planBuilder.Transformers.Add, IPublishableDataSource>();
+ _planBuilder.Transformers.Add, IPublishableWorkbook>();
+ #endregion
+
+ #region EncryptExtractTransformer-Registration
+ _planBuilder.Transformers.Add, IPublishableDataSource>();
+ _planBuilder.Transformers.Add, IPublishableWorkbook>();
#endregion
// Add migration action completed hooks
diff --git a/examples/Csharp.ExampleApplication/Program.cs b/examples/Csharp.ExampleApplication/Program.cs
index 8803e19..e524e05 100644
--- a/examples/Csharp.ExampleApplication/Program.cs
+++ b/examples/Csharp.ExampleApplication/Program.cs
@@ -9,6 +9,7 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Tableau.Migration;
+using Tableau.Migration.Content;
#region namespace
namespace Csharp.ExampleApplication
@@ -56,7 +57,8 @@ public static IServiceCollection AddCustomizations(this IServiceCollection servi
#endregion
#region ChangeProjectMapping-DI
- services.AddScoped();
+ services.AddScoped>();
+ services.AddScoped>();
#endregion
#region DefaultProjectsFilter-DI
@@ -76,7 +78,13 @@ public static IServiceCollection AddCustomizations(this IServiceCollection servi
#endregion
#region MigratedTagTransformer-DI
- services.AddScoped();
+ services.AddScoped>();
+ services.AddScoped>();
+ #endregion
+
+ #region EncryptExtractTransformer-DI
+ services.AddScoped>();
+ services.AddScoped>();
#endregion
#region LogMigrationActionsHook-DI
diff --git a/examples/Python.ExampleApplication/Hooks/Filters/default_project_filter.py b/examples/Python.ExampleApplication/Hooks/Filters/default_project_filter.py
new file mode 100644
index 0000000..803d863
--- /dev/null
+++ b/examples/Python.ExampleApplication/Hooks/Filters/default_project_filter.py
@@ -0,0 +1,11 @@
+from tableau_migration import (
+ IProject,
+ ContentMigrationItem,
+ ContentFilterBase)
+
+
+class DefaultProjectFilter(ContentFilterBase[IProject]):
+ def should_migrate(self, item: ContentMigrationItem[IProject]) -> bool:
+ if item.source_item.name.casefold() == 'Default'.casefold():
+ return False
+ return True
\ No newline at end of file
diff --git a/examples/Python.ExampleApplication/Hooks/Filters/unlicensed_user_filter.py b/examples/Python.ExampleApplication/Hooks/Filters/unlicensed_user_filter.py
new file mode 100644
index 0000000..2a02ab3
--- /dev/null
+++ b/examples/Python.ExampleApplication/Hooks/Filters/unlicensed_user_filter.py
@@ -0,0 +1,12 @@
+from tableau_migration import (
+ IUser,
+ ContentMigrationItem,
+ ContentFilterBase,
+ SiteRoles)
+
+
+class UnlicensedUserFilter(ContentFilterBase[IUser]):
+ def should_migrate(self, item: ContentMigrationItem[IUser]) -> bool:
+ if item.source_item.license_level.casefold() == SiteRoles.UNLICENSED.casefold():
+ return False
+ return True
\ No newline at end of file
diff --git a/examples/Python.ExampleApplication/Hooks/batch_migration_completed/log_migration_batches_hook.py b/examples/Python.ExampleApplication/Hooks/batch_migration_completed/log_migration_batches_hook.py
new file mode 100644
index 0000000..3a5bf53
--- /dev/null
+++ b/examples/Python.ExampleApplication/Hooks/batch_migration_completed/log_migration_batches_hook.py
@@ -0,0 +1,34 @@
+import logging
+from typing import TypeVar
+from tableau_migration import(
+ ContentBatchMigrationCompletedHookBase,
+ IContentBatchMigrationResult,
+ IUser
+ )
+
+T = TypeVar("T")
+
+class LogMigrationBatchesHook(ContentBatchMigrationCompletedHookBase[T]):
+ def __init__(self) -> None:
+ super().__init__()
+ self._logger = logging.getLogger(__name__)
+
+ def execute(self, ctx: IContentBatchMigrationResult[T]) -> IContentBatchMigrationResult[T]:
+
+ item_status = ""
+ for item in ctx.item_results:
+ item_status += "%s: %s".format(item.manifest_entry.source.location, item.manifest_entry.status)
+
+ self._logger.info("%s batch of %d item(s) completed:\n%s", ctx._content_type, ctx.item_results.count, item_status)
+
+ pass
+
+class LogMigrationBatchesHookForUsers(ContentBatchMigrationCompletedHookBase[IUser]):
+ def __init__(self) -> None:
+ super().__init__()
+ self._content_type = "User";
+
+class LogMigrationBatchesHookForGoups(ContentBatchMigrationCompletedHookBase[IUser]):
+ def __init__(self) -> None:
+ super().__init__()
+ self._content_type = "Group";
\ No newline at end of file
diff --git a/examples/Python.ExampleApplication/Hooks/mappings/change_project_mapping.py b/examples/Python.ExampleApplication/Hooks/mappings/change_project_mapping.py
new file mode 100644
index 0000000..fe8f18f
--- /dev/null
+++ b/examples/Python.ExampleApplication/Hooks/mappings/change_project_mapping.py
@@ -0,0 +1,38 @@
+from typing import TypeVar
+from tableau_migration import(
+ IWorkbook,
+ IDataSource,
+ ContentMappingContext,
+ ContentMappingBase)
+
+T = TypeVar("T")
+
+class ChangeProjectMapping(ContentMappingBase[T]):
+
+ def map(self, ctx: ContentMappingContext[T]) -> ContentMappingContext[T]:
+ # Get the container (project) location for the content item.
+ container_location = ctx.content_item.location.parent()
+
+ # We only want to map content items whose project name is "Test".
+ if not container_location.name.casefold() == "Test".casefold():
+ return ctx
+
+ # Build the new project location.
+ new_container_location = container_location.rename("Production")
+
+ # Build the new content item location.
+ new_location = new_container_location.append(ctx.content_item.name)
+
+ # Map the new content item location.
+ ctx = ctx.map_to(new_location)
+
+ return ctx
+
+
+# Create the workbook version of the templated ChangeProjectMapping class
+class ChangeProjectMappingForWorkbooks(ChangeProjectMapping[IWorkbook]):
+ pass
+
+# Create the datasource version of the templated ChangeProjectMapping class
+class ChangeProjectMappingForDataSources(ChangeProjectMapping[IDataSource]):
+ pass
\ No newline at end of file
diff --git a/examples/Python.ExampleApplication/Hooks/mappings/email_domain_mapping.py b/examples/Python.ExampleApplication/Hooks/mappings/email_domain_mapping.py
new file mode 100644
index 0000000..24c3a82
--- /dev/null
+++ b/examples/Python.ExampleApplication/Hooks/mappings/email_domain_mapping.py
@@ -0,0 +1,19 @@
+from tableau_migration import(
+ IUser,
+ TableauCloudUsernameMappingBase,
+ ContentMappingContext)
+
+
+class EmailDomainMapping(TableauCloudUsernameMappingBase):
+ def map(self, ctx: ContentMappingContext[IUser]) -> ContentMappingContext[IUser]:
+ _email_domain: str = "@mycompany.com"
+
+ _tableau_user_domain = ctx.mapped_location.parent()
+
+ # Re-use an existing email if it already exists.
+ if not ctx.content_item.email:
+ return ctx.map_to(_tableau_user_domain.append(ctx.content_item.email))
+
+ # Takes the existing username and appends the domain to build the email
+ new_email = ctx.content_item.name + _email_domain
+ return ctx.map_to(_tableau_user_domain.append(new_email))
\ No newline at end of file
diff --git a/examples/Python.ExampleApplication/Hooks/mappings/project_rename_mapping.py b/examples/Python.ExampleApplication/Hooks/mappings/project_rename_mapping.py
new file mode 100644
index 0000000..5607abb
--- /dev/null
+++ b/examples/Python.ExampleApplication/Hooks/mappings/project_rename_mapping.py
@@ -0,0 +1,16 @@
+from tableau_migration import(
+ IProject,
+ ContentMappingBase,
+ ContentMappingContext)
+
+
+class ProjectRenameMapping(ContentMappingBase[IProject]):
+ def map(self, ctx: ContentMappingContext[IProject]) -> ContentMappingContext[IProject]:
+ if not ctx.content_item.name.casefold() == "Test".casefold():
+ return ctx
+
+ new_location = ctx.content_item.location.rename("Production")
+
+ ctx.map_to(new_location)
+
+ return ctx
\ No newline at end of file
diff --git a/examples/Python.ExampleApplication/Hooks/migration_action_completed/log_migration_actions_hook.py b/examples/Python.ExampleApplication/Hooks/migration_action_completed/log_migration_actions_hook.py
new file mode 100644
index 0000000..968852c
--- /dev/null
+++ b/examples/Python.ExampleApplication/Hooks/migration_action_completed/log_migration_actions_hook.py
@@ -0,0 +1,22 @@
+import logging
+from tableau_migration import(
+ MigrationActionCompletedHookBase,
+ IMigrationActionResult
+ )
+
+
+class LogMigrationActionsHook(MigrationActionCompletedHookBase):
+ def __init__(self) -> None:
+ super().__init__()
+
+ # Create a logger for this class
+ self._logger = logging.getLogger(__name__)
+
+ def execute(self, ctx: IMigrationActionResult) -> IMigrationActionResult:
+ if(ctx.success):
+ self._logger.info("Migration action completed successfully.")
+ else:
+ all_errors = "\n".join(ctx.errors)
+ self._logger.warning("Migration action completed with errors:\n%s", all_errors)
+
+ return None
diff --git a/examples/Python.ExampleApplication/Hooks/post_publish/bulk_logging_hook.py b/examples/Python.ExampleApplication/Hooks/post_publish/bulk_logging_hook.py
new file mode 100644
index 0000000..d228114
--- /dev/null
+++ b/examples/Python.ExampleApplication/Hooks/post_publish/bulk_logging_hook.py
@@ -0,0 +1,19 @@
+import logging
+from tableau_migration import (
+ BulkPostPublishHookBase,
+ BulkPostPublishContext,
+ IDataSource)
+
+
+class BulkLoggingHookForDataSources(BulkPostPublishHookBase[IDataSource]):
+ def __init__(self) -> None:
+ super().__init__()
+
+ # Create a logger for this class
+ self._logger = logging.getLogger(__name__)
+
+ def execute(self, ctx: BulkPostPublishContext[IDataSource]) -> BulkPostPublishContext[IDataSource]:
+ # Log the number of items published in the batch.
+ self._logger.info("Published %d IDataSource item(s).", ctx.published_items.count)
+ return None
+
\ No newline at end of file
diff --git a/examples/Python.ExampleApplication/Hooks/transformers/encrypt_extracts_transformer.py b/examples/Python.ExampleApplication/Hooks/transformers/encrypt_extracts_transformer.py
new file mode 100644
index 0000000..c0d1339
--- /dev/null
+++ b/examples/Python.ExampleApplication/Hooks/transformers/encrypt_extracts_transformer.py
@@ -0,0 +1,19 @@
+from typing import TypeVar
+from tableau_migration import (
+ ContentTransformerBase,
+ IPublishableWorkbook,
+ IPublishableDataSource)
+
+T = TypeVar("T")
+
+class EncryptExtractTransformer(ContentTransformerBase[T]):
+ def transform(self, itemToTransform: T) -> T:
+ itemToTransform.encrypt_extracts = True
+
+ return itemToTransform
+
+class EncryptExtractTransformerForDataSources(EncryptExtractTransformer[IPublishableDataSource]):
+ pass
+
+class EncryptExtractTransformerForWorkbooks(EncryptExtractTransformer[IPublishableWorkbook]):
+ pass
\ No newline at end of file
diff --git a/examples/Python.ExampleApplication/Hooks/transformers/migrated_tag_transformer.py b/examples/Python.ExampleApplication/Hooks/transformers/migrated_tag_transformer.py
new file mode 100644
index 0000000..f51418a
--- /dev/null
+++ b/examples/Python.ExampleApplication/Hooks/transformers/migrated_tag_transformer.py
@@ -0,0 +1,23 @@
+from typing import TypeVar
+from tableau_migration import (
+ ContentTransformerBase,
+ IDataSource,
+ ITag,
+ IWorkbook)
+
+T = TypeVar("T")
+
+
+class MigratedTagTransformer(ContentTransformerBase[T]):
+ def transform(self, itemToTransform: T) -> T:
+ tag: str = "Migrated"
+
+ itemToTransform.tags.append(ITag(tag))
+
+ return itemToTransform
+
+class MigratedTagTransformerForDataSources(MigratedTagTransformer[IDataSource]):
+ pass
+
+class MigratedTagTransformerForWorkbooks(MigratedTagTransformer[IWorkbook]):
+ pass
\ No newline at end of file
diff --git a/examples/Python.ExampleApplication/Python.ExampleApplication.py b/examples/Python.ExampleApplication/Python.ExampleApplication.py
index 4850fe8..a98fcd4 100644
--- a/examples/Python.ExampleApplication/Python.ExampleApplication.py
+++ b/examples/Python.ExampleApplication/Python.ExampleApplication.py
@@ -6,6 +6,7 @@
import os # environment variables
import sys # system utility
import tableau_migration # Tableau Migration SDK
+import print_result
from threading import Thread # threading
@@ -32,7 +33,7 @@ def migrate():
access_token = os.environ.get('TABLEAU_MIGRATION_DESTINATION_TOKEN', config['DESTINATION']['ACCESS_TOKEN'])) \
.for_server_to_cloud() \
.with_tableau_id_authentication_type() \
- .with_tableau_cloud_usernames(config['USERS']['EMAIL_DOMAIN'])
+ .with_tableau_cloud_usernames(config['USERS']['EMAIL_DOMAIN'])
# TODO: add filters, mappings, transformers, etc. here.
@@ -47,6 +48,7 @@ def migrate():
results = migration.execute(plan)
# TODO: Handle results here.
+ print_result(results)
print("All done.")
diff --git a/examples/Python.ExampleApplication/Python.ExampleApplication.pyproj b/examples/Python.ExampleApplication/Python.ExampleApplication.pyproj
index 3f9cb7f..0af8ded 100644
--- a/examples/Python.ExampleApplication/Python.ExampleApplication.pyproj
+++ b/examples/Python.ExampleApplication/Python.ExampleApplication.pyproj
@@ -5,8 +5,7 @@
9c94fbc9-ae67-4a26-bdda-eb2ce9fe5c25
.
Python.ExampleApplication.py
-
-
+ ..\..\src\Python\dist
.
.
Python.ExampleApplication
@@ -23,8 +22,17 @@
-
-
+
+
+
+
+
+
+
+
+
+
+
@@ -43,6 +51,15 @@
X64
+
+
+
+
+
+
+
+
+
+
+
+
+
+"""
+
+_expected_twb = """
+
+
+
+
+
+
+
+"""
+
+def _transform_xml_content(xml: ElementTree.Element) -> None:
+ xml.find("user:test", {"user": "http://www.tableausoftware.com/xml/user"}).tail = "\n "
+ sub = ElementTree.SubElement(xml, "test2", { "a": "b" })
+ sub.tail = "\n"
+
+class PyWorkbookXmlTransformer(PyXmlTransformer[PyPublishableWorkbook]):
+
+ def needs_xml_transforming(self, ctx: PyPublishableWorkbook) -> bool:
+ return ctx.description == "mark"
+
+ def transform(self, ctx: PyPublishableWorkbook, xml: ElementTree.Element) -> None:
+ ctx.description = xml.get("version")
+ _transform_xml_content(xml)
+
+def transform_workbook_xml(ctx: PyPublishableWorkbook, xml: ElementTree.Element) -> None:
+ ctx.description = xml.get("version")
+ _transform_xml_content(xml)
+
+def transform_workbook_xml_services(ctx: PyPublishableWorkbook, xml: ElementTree.Element, services: ScopedMigrationServices) -> None:
+ ctx.description = xml.get("version")
+ _transform_xml_content(xml)
+
+class TestXmlTransformerInterop(AutoFixtureTestBase):
+
+ def _clean_xml_text(self, xml_text: str) -> str:
+ return xml_text.replace("\r", "")
+
+ # Helper method to save XDocument to string that includes the XML declaration.
+ def _save_xml(self, xdoc: XDocument) -> str:
+ stream = MemoryStream()
+ writer = XmlWriter.Create(stream)
+ xdoc.Save(writer)
+ writer.Flush()
+ stream.Position = 0
+ reader = StreamReader(stream)
+ return self._clean_xml_text(reader.ReadToEnd())
+
+ def test_transformer_interop_class(self):
+ hook_builder = PyContentTransformerBuilder(ContentTransformerBuilder())
+
+ result = hook_builder.add(PyWorkbookXmlTransformer)
+ assert result is hook_builder
+
+ hook_factories = hook_builder.build().get_hooks(IContentTransformer[IPublishableWorkbook])
+ assert len(hook_factories) == 1
+
+ services = self.create(IServiceProvider)
+ ctx = self.create(IPublishableWorkbook)
+ xml = XDocument.Parse(_test_twb, LoadOptions.PreserveWhitespace)
+
+ hook = hook_factories[0].Create[IXmlContentTransformer[IPublishableWorkbook]](services)
+ hook.TransformAsync(ctx, xml, CancellationToken(False)).GetAwaiter().GetResult()
+
+ assert ctx.Description == "18.1"
+
+ saved_xml = self._save_xml(xml)
+ assert saved_xml == self._clean_xml_text(_expected_twb)
+
+ def test_transformer_needs_transforming(self):
+ hook_builder = PyContentTransformerBuilder(ContentTransformerBuilder())
+
+ result = hook_builder.add(PyWorkbookXmlTransformer)
+ assert result is hook_builder
+
+ hook_factories = hook_builder.build().get_hooks(IContentTransformer[IPublishableWorkbook])
+ assert len(hook_factories) == 1
+
+ services = self.create(IServiceProvider)
+ ctx = self.create(IPublishableWorkbook)
+
+ hook = hook_factories[0].Create[IXmlContentTransformer[IPublishableWorkbook]](services)
+
+ ctx.Description = "notmark"
+
+ assert hook.NeedsXmlTransforming(ctx) == False
+
+ ctx.Description = "mark"
+
+ assert hook.NeedsXmlTransforming(ctx) == True
+
+ def test_transformer_interop_callback(self):
+ hook_builder = PyContentTransformerBuilder(ContentTransformerBuilder())
+
+ ctx = self.create(IPublishableWorkbook)
+ xml = XDocument.Parse(_test_twb, LoadOptions.PreserveWhitespace)
+
+ result = hook_builder.add(PyPublishableWorkbook, transform_workbook_xml, is_xml = True)
+ assert result is hook_builder
+
+ hook_factories = hook_builder.build().get_hooks(IContentTransformer[IPublishableWorkbook])
+ assert len(hook_factories) == 1
+
+ services = self.create(IServiceProvider)
+
+ hook = hook_factories[0].Create[IXmlContentTransformer[IPublishableWorkbook]](services)
+ hook.TransformAsync(ctx, xml, CancellationToken(False)).GetAwaiter().GetResult()
+
+ assert ctx.Description == "18.1"
+ assert self._save_xml(xml) == self._clean_xml_text(_expected_twb)
+
+ def test_transformer_interop_callback_services(self):
+ hook_builder = PyContentTransformerBuilder(ContentTransformerBuilder())
+
+ ctx = self.create(IPublishableWorkbook)
+ xml = XDocument.Parse(_test_twb, LoadOptions.PreserveWhitespace)
+
+ result = hook_builder.add(PyPublishableWorkbook, transform_workbook_xml_services, is_xml = True)
+ assert result is hook_builder
+
+ hook_factories = hook_builder.build().get_hooks(IContentTransformer[IPublishableWorkbook])
+ assert len(hook_factories) == 1
+
+ services = self.create(IServiceProvider)
+
+ hook = hook_factories[0].Create[IXmlContentTransformer[IPublishableWorkbook]](services)
+ hook.TransformAsync(ctx, xml, CancellationToken(False)).GetAwaiter().GetResult()
+
+ assert ctx.Description == "18.1"
+ assert self._save_xml(xml) == self._clean_xml_text(_expected_twb)
+
\ No newline at end of file
diff --git a/src/Python/tests/test_migration_engine_migrators_batch.py b/src/Python/tests/test_migration_engine_migrators_batch.py
new file mode 100644
index 0000000..daeef4e
--- /dev/null
+++ b/src/Python/tests/test_migration_engine_migrators_batch.py
@@ -0,0 +1,35 @@
+# Copyright (c) 2024, Salesforce, Inc.
+# SPDX-License-Identifier: Apache-2
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import pytest
+import uuid
+
+from tableau_migration.migration_content import PyUser
+from tableau_migration.migration_engine_migrators_batch import PyContentBatchMigrationResult
+from tests.helpers.autofixture import AutoFixtureTestBase
+
+from Tableau.Migration.Content import IUser
+from Tableau.Migration.Engine.Migrators.Batch import IContentBatchMigrationResult
+
+class TestPyContentBatchMigrationResult(AutoFixtureTestBase):
+
+ def test_item_results(self):
+ dotnet = self.create(IContentBatchMigrationResult[IUser])
+
+ py = PyContentBatchMigrationResult[PyUser](dotnet)
+
+ assert len(dotnet.ItemResults) != 0
+ assert len(py.item_results) == len(dotnet.ItemResults)
+ assert py.item_results[0].manifest_entry.source.id == uuid.UUID(dotnet.ItemResults[0].ManifestEntry.Source.Id.ToString())
\ No newline at end of file
diff --git a/src/Python/tests/test_migrations_engine_options.py b/src/Python/tests/test_migration_engine_options.py
similarity index 95%
rename from src/Python/tests/test_migrations_engine_options.py
rename to src/Python/tests/test_migration_engine_options.py
index 26b361b..c7166ce 100644
--- a/src/Python/tests/test_migrations_engine_options.py
+++ b/src/Python/tests/test_migration_engine_options.py
@@ -21,11 +21,13 @@
import pytest
from tableau_migration.migration import (
get_service_provider,
- get_service )
+ get_service
+)
from tableau_migration.migration_engine_options import (
PyMigrationPlanOptionsBuilder,
- PyMigrationPlanOptionsCollection)
+ PyMigrationPlanOptionsCollection
+)
import System
@@ -35,7 +37,8 @@
from Tableau.Migration.Engine.Options import (
IMigrationPlanOptionsBuilder,
- IMigrationPlanOptionsCollection)
+ IMigrationPlanOptionsCollection
+)
import Moq
_dist_path = abspath(Path(__file__).parent.resolve().__str__() + "/../src/tableau_migration")
diff --git a/src/Python/tests/test_migration_services.py b/src/Python/tests/test_migration_services.py
new file mode 100644
index 0000000..a02354f
--- /dev/null
+++ b/src/Python/tests/test_migration_services.py
@@ -0,0 +1,112 @@
+# Copyright (c) 2024, Salesforce, Inc.
+# SPDX-License-Identifier: Apache-2
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import pytest
+
+from System import IServiceProvider
+
+from tableau_migration.migration import (
+ PyMigrationManifest
+)
+from tableau_migration.migration_content import (
+ PyUser,
+ PyGroup,
+ PyProject,
+ PyDataSource,
+ PyWorkbook
+)
+from tableau_migration.migration_engine import (
+ PyMigrationPlan
+)
+
+from tableau_migration.migration_engine_endpoints_search import (
+ PyDestinationContentReferenceFinderFactory,
+ PyDestinationContentReferenceFinder,
+ PySourceContentReferenceFinderFactory,
+ PySourceContentReferenceFinder
+)
+
+from tableau_migration.migration_services import (
+ ScopedMigrationServices
+)
+
+from tests.helpers.autofixture import AutoFixtureTestBase
+
+class TestPySourceContentReferenceFinder(AutoFixtureTestBase):
+ _test_data = [
+ PyUser,
+ PyGroup,
+ PyProject,
+ PyDataSource,
+ PyWorkbook
+ ]
+
+ def test_get_manifest(self):
+ dotnet_provider = self.create(IServiceProvider)
+ scoped_services = ScopedMigrationServices(dotnet_provider)
+
+ result = scoped_services.get_manifest()
+
+ assert result is not None
+ assert isinstance(result, PyMigrationManifest)
+
+ def test_get_plan(self):
+ dotnet_provider = self.create(IServiceProvider)
+ scoped_services = ScopedMigrationServices(dotnet_provider)
+
+ result = scoped_services.get_plan()
+
+ assert result is not None
+ assert isinstance(result, PyMigrationPlan)
+
+ def test_get_source_finder_factory(self):
+ dotnet_provider = self.create(IServiceProvider)
+ scoped_services = ScopedMigrationServices(dotnet_provider)
+
+ result = scoped_services.get_source_finder_factory()
+
+ assert result is not None
+ assert isinstance(result, PySourceContentReferenceFinderFactory)
+
+ @pytest.mark.parametrize("searched_type", _test_data)
+ def test_get_source_finder(self, searched_type):
+ dotnet_provider = self.create(IServiceProvider)
+ scoped_services = ScopedMigrationServices(dotnet_provider)
+
+ result = scoped_services.get_source_finder(searched_type)
+
+ assert result is not None
+ assert isinstance(result, PySourceContentReferenceFinder)
+ assert result._content_type is searched_type
+
+ def test_get_destination_finder_factory(self):
+ dotnet_provider = self.create(IServiceProvider)
+ scoped_services = ScopedMigrationServices(dotnet_provider)
+
+ result = scoped_services.get_destination_finder_factory()
+
+ assert result is not None
+ assert isinstance(result, PyDestinationContentReferenceFinderFactory)
+
+ @pytest.mark.parametrize("searched_type", _test_data)
+ def test_get_destination_finder(self, searched_type):
+ dotnet_provider = self.create(IServiceProvider)
+ scoped_services = ScopedMigrationServices(dotnet_provider)
+
+ result = scoped_services.get_destination_finder(searched_type)
+
+ assert result is not None
+ assert isinstance(result, PyDestinationContentReferenceFinder)
+ assert result._content_type is searched_type
diff --git a/src/Python/tests/test_migrations_engine_hooks.py b/src/Python/tests/test_migrations_engine_hooks.py
deleted file mode 100644
index 7dda9ed..0000000
--- a/src/Python/tests/test_migrations_engine_hooks.py
+++ /dev/null
@@ -1,272 +0,0 @@
-# Copyright (c) 2024, Salesforce, Inc.
-# SPDX-License-Identifier: Apache-2
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-# Make sure the test can find the module
-from tableau_migration.migration import get_service
-from tableau_migration.migration_engine_hooks import PyMigrationHookBuilder
-from System import IServiceProvider, Func, ArgumentException, NotImplementedException
-from System.Threading import CancellationToken
-from Microsoft.Extensions.DependencyInjection import (
- ServiceCollection,
- ServiceCollectionContainerBuilderExtensions)
-from Tableau.Migration.Interop.Hooks import ISyncMigrationHook
-from Tableau.Migration.Engine.Hooks import MigrationHookBuilder, IMigrationHook
-
-class ClassImplementation(ISyncMigrationHook[bool]):
- __namespace__ = "Tableau.Migration.Custom.Hooks"
-
- def Execute(self, ctx) -> bool:
- return not ctx
-
-class SubClassImplementation(ClassImplementation):
- __namespace__ = "Tableau.Migration.Custom.Hooks"
-
- def Execute(self, ctx) -> bool:
- return not ctx
-
-class SubSubClassImplementation(SubClassImplementation):
- __namespace__ = "Tableau.Migration.Custom.Hooks"
-
- def Execute(self, ctx) -> bool:
- return not ctx
-
-class RawHook(IMigrationHook[str]):
- __namespace__ = "Tableau.Migration.Custom.Hooks"
-
- def Execute(self, ctx) -> str:
- return ctx
-
-class WithoutImplementation(ISyncMigrationHook[bool]):
- __namespace__ = "Tableau.Migration.Custom.Hooks"
-
-class TestMigrationHookBuilderTests():
- def test_clear_class_hook(self):
- hook_builder = PyMigrationHookBuilder(MigrationHookBuilder())
-
- result = hook_builder.add(ClassImplementation()).clear()
-
- assert result is hook_builder
- assert len(result.build().get_hooks(ISyncMigrationHook[bool])) == 0
-
- def test_clear_subclass_hook(self):
- hook_builder = PyMigrationHookBuilder(MigrationHookBuilder())
-
- result = hook_builder.add(SubClassImplementation()).clear()
-
- assert result is hook_builder
- assert len(result.build().get_hooks(ISyncMigrationHook[int])) == 0
-
- def test_clear_subsubclass_hook(self):
- hook_builder = PyMigrationHookBuilder(MigrationHookBuilder())
-
- result = hook_builder.add(SubSubClassImplementation()).clear()
-
- assert result is hook_builder
- assert len(result.build().get_hooks(ISyncMigrationHook[int])) == 0
-
- def test_add_object(self):
- hook_builder = PyMigrationHookBuilder(MigrationHookBuilder())
-
- result = hook_builder.add(SubSubClassImplementation())
-
- assert result is hook_builder
-
- def test_add_type_noinitializer(self):
- hook_builder = PyMigrationHookBuilder(MigrationHookBuilder())
-
- result = hook_builder.add(SubSubClassImplementation)
-
- assert result is hook_builder
-
- def test_add_type_initializer(self):
- hook_builder = PyMigrationHookBuilder(MigrationHookBuilder())
-
- def subclassinitialize(provider: IServiceProvider):
- return get_service(provider, SubClassImplementation)
-
- result = hook_builder.add(SubClassImplementation, Func[IServiceProvider, SubClassImplementation](subclassinitialize))
-
- assert result is hook_builder
-
- def test_add_callback(self):
- hook_builder = PyMigrationHookBuilder(MigrationHookBuilder())
-
- def classcallback(context: str):
- return context
-
- result = hook_builder.add(ISyncMigrationHook[str], str, Func[str, str](classcallback))
-
- assert result is hook_builder
-
- def test_add_ignores_raw_hook_interface(self):
- try:
- PyMigrationHookBuilder(MigrationHookBuilder()).add(RawHook())
- except ArgumentException as ae:
- assert ae.args[0] == "Type Tableau.Migration.Custom.Hooks.RawHook does not implement any migration hook types."
- else:
- assert False, "This test must generate an ArgumentException."
-
- def test_add_withoutimplementation(self):
- provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(ServiceCollection())
- hookFactory = PyMigrationHookBuilder(MigrationHookBuilder()).add(WithoutImplementation()).build().get_hooks(ISyncMigrationHook[bool])
- assert len(hookFactory) == 1
- hook = hookFactory[0].Create[IMigrationHook[bool]](provider)
- try:
- hook.ExecuteAsync(False, CancellationToken(False))
- except NotImplementedException as nie:
- assert nie.args[0] == "Python object does not have a 'Execute' method"
- else:
- assert False, "This test must generate an NotImplementedException."
- finally:
- provider.Dispose()
-
- def test_add_withimplementation(self):
- provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(ServiceCollection())
- hookFactory = PyMigrationHookBuilder(MigrationHookBuilder()).add(SubClassImplementation()).build().get_hooks(ISyncMigrationHook[bool])
- assert len(hookFactory) == 1
- hook = hookFactory[0].Create[IMigrationHook[bool]](provider)
- try:
- result = hook.ExecuteAsync(False, CancellationToken(False))
-
- assert result
- except:
- assert False, "This test must not generate an Exception."
- finally:
- provider.Dispose()
-
- def test_build_detects_interface_from_concrete_class(self):
- collection = PyMigrationHookBuilder(MigrationHookBuilder()).add(ClassImplementation()).build()
- hooks = collection.get_hooks(ISyncMigrationHook[bool])
- internalhooks = collection.get_hooks(IMigrationHook[bool])
- otherhooks = collection.get_hooks(ISyncMigrationHook[int])
- classhooks = collection.get_hooks(ClassImplementation)
- subclasshooks = collection.get_hooks(SubClassImplementation)
- subsubclasshooks = collection.get_hooks(SubSubClassImplementation)
-
- # Can't do a direct object comparison, so let's just count the right number is returned
- assert len(hooks) == 1
- assert len(internalhooks) == 0
- assert len(otherhooks) == 0
- assert len(classhooks) == 0
- assert len(subclasshooks) == 0
- assert len(subsubclasshooks) == 0
-
- def test_build_detects_interface_from_concrete_subclass(self):
- collection = PyMigrationHookBuilder(MigrationHookBuilder()).add(SubClassImplementation()).build()
- hooks = collection.get_hooks(ISyncMigrationHook[bool])
- internalhooks = collection.get_hooks(IMigrationHook[bool])
- otherhooks = collection.get_hooks(ISyncMigrationHook[int])
- classhooks = collection.get_hooks(ClassImplementation)
- subclasshooks = collection.get_hooks(SubClassImplementation)
- subsubclasshooks = collection.get_hooks(SubSubClassImplementation)
-
- # Can't do a direct object comparison, so let's just count the right number is returned
- assert len(hooks) == 1
- assert len(internalhooks) == 0
- assert len(otherhooks) == 0
- assert len(classhooks) == 0
- assert len(subclasshooks) == 0
- assert len(subsubclasshooks) == 0
-
- def test_build_detects_interface_from_concrete_subsubclass(self):
- collection = PyMigrationHookBuilder(MigrationHookBuilder()).add(SubSubClassImplementation()).build()
- hooks = collection.get_hooks(ISyncMigrationHook[bool])
- internalhooks = collection.get_hooks(IMigrationHook[bool])
- otherhooks = collection.get_hooks(ISyncMigrationHook[int])
- classhooks = collection.get_hooks(ClassImplementation)
- subclasshooks = collection.get_hooks(SubClassImplementation)
- subsubclasshooks = collection.get_hooks(SubSubClassImplementation)
-
- # Can't do a direct object comparison, so let's just count the right number is returned
- assert len(hooks) == 1
- assert len(internalhooks) == 0
- assert len(otherhooks) == 0
- assert len(classhooks) == 0
- assert len(subclasshooks) == 0
- assert len(subsubclasshooks) == 0
-
- def test_build_factory_class(self):
- collection = PyMigrationHookBuilder(MigrationHookBuilder()).add(ClassImplementation).build()
- hooks = collection.get_hooks(ISyncMigrationHook[bool])
- internalhooks = collection.get_hooks(IMigrationHook[bool])
- otherhooks = collection.get_hooks(ISyncMigrationHook[int])
- classhooks = collection.get_hooks(ClassImplementation)
- subclasshooks = collection.get_hooks(SubClassImplementation)
- subsubclasshooks = collection.get_hooks(SubSubClassImplementation)
-
- # Can't do a direct object comparison, so let's just count the right number is returned
- assert len(hooks) == 1
- assert len(internalhooks) == 0
- assert len(otherhooks) == 0
- assert len(classhooks) == 0
- assert len(subclasshooks) == 0
- assert len(subsubclasshooks) == 0
-
- def test_build_factory_subclass_withinitializer(self):
- def subclassinitialize(provider: IServiceProvider):
- return get_service(provider, SubClassImplementation)
-
- collection = PyMigrationHookBuilder(MigrationHookBuilder()).add(SubClassImplementation, Func[IServiceProvider, SubClassImplementation](subclassinitialize)).build()
- hooks = collection.get_hooks(ISyncMigrationHook[bool])
- internalhooks = collection.get_hooks(IMigrationHook[bool])
- otherhooks = collection.get_hooks(ISyncMigrationHook[int])
- classhooks = collection.get_hooks(ClassImplementation)
- subclasshooks = collection.get_hooks(SubClassImplementation)
- subsubclasshooks = collection.get_hooks(SubSubClassImplementation)
-
- # Can't do a direct object comparison, so let's just count the right number is returned
- assert len(hooks) == 1
- assert len(internalhooks) == 0
- assert len(otherhooks) == 0
- assert len(classhooks) == 0
- assert len(subclasshooks) == 0
- assert len(subsubclasshooks) == 0
-
- def test_build_factory_multipleclasses(self):
- collection = PyMigrationHookBuilder(MigrationHookBuilder()).add(ClassImplementation).add(SubClassImplementation).add(SubSubClassImplementation).build()
- hooks = collection.get_hooks(ISyncMigrationHook[bool])
- internalhooks = collection.get_hooks(IMigrationHook[bool])
- otherhooks = collection.get_hooks(ISyncMigrationHook[int])
- classhooks = collection.get_hooks(ClassImplementation)
- subclasshooks = collection.get_hooks(SubClassImplementation)
- subsubclasshooks = collection.get_hooks(SubSubClassImplementation)
-
- # Can't do a direct object comparison, so let's just count the right number is returned
- assert len(hooks) == 3
- assert len(internalhooks) == 0
- assert len(otherhooks) == 0
- assert len(classhooks) == 0
- assert len(subclasshooks) == 0
- assert len(subsubclasshooks) == 0
-
- def test_build_callback(self):
- def classcallback(context: str):
- return context
-
- collection = PyMigrationHookBuilder(MigrationHookBuilder()).add(ISyncMigrationHook[str], str, Func[str, str](classcallback)).build()
- hooks = collection.get_hooks(ISyncMigrationHook[str])
- internalhooks = collection.get_hooks(IMigrationHook[str])
- otherhooks = collection.get_hooks(ISyncMigrationHook[int])
- classhooks = collection.get_hooks(ClassImplementation)
- subclasshooks = collection.get_hooks(SubClassImplementation)
- subsubclasshooks = collection.get_hooks(SubSubClassImplementation)
-
- # Can't do a direct object comparison, so let's just count the right number is returned
- assert len(hooks) == 1
- assert len(internalhooks) == 0
- assert len(otherhooks) == 0
- assert len(classhooks) == 0
- assert len(subclasshooks) == 0
- assert len(subsubclasshooks) == 0
diff --git a/src/Python/tests/test_migrations_engine_hooks_filters.py b/src/Python/tests/test_migrations_engine_hooks_filters.py
deleted file mode 100644
index 4afc1cc..0000000
--- a/src/Python/tests/test_migrations_engine_hooks_filters.py
+++ /dev/null
@@ -1,243 +0,0 @@
-# Copyright (c) 2024, Salesforce, Inc.
-# SPDX-License-Identifier: Apache-2
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-# Make sure the test can find the module
-from tableau_migration.migration import get_service
-from tableau_migration.migration_engine_hooks_filters import PyContentFilterBuilder
-from System import IServiceProvider, Func, ArgumentException, NotImplementedException
-from System.Collections.Generic import IEnumerable, List
-from System.Threading import CancellationToken
-from Microsoft.Extensions.DependencyInjection import (
- ServiceCollection,
- ServiceCollectionContainerBuilderExtensions)
-from Tableau.Migration.Content import IUser,IProject
-from Tableau.Migration.Engine import ContentMigrationItem
-from Tableau.Migration.Engine.Hooks import IMigrationHook
-from Tableau.Migration.Engine.Hooks.Filters import ContentFilterBuilder,IContentFilter,ContentFilterBase
-from Tableau.Migration.Interop.Hooks.Filters import ISyncContentFilter
-
-class ClassImplementation(ISyncContentFilter[IUser]):
- __namespace__ = "Tableau.Migration.Custom.Hooks.Filters"
-
- def Execute(self, ctx: IEnumerable[ContentMigrationItem[IUser]]) -> IEnumerable[ContentMigrationItem[IUser]]:
- return ctx
-
-
-class SubClassImplementation(ClassImplementation):
- __namespace__ = "Tableau.Migration.Custom.Hooks.Filters"
-
- def Execute(self, ctx: IEnumerable[ContentMigrationItem[IUser]]) -> IEnumerable[ContentMigrationItem[IUser]]:
- print("In Execute of SubClassImplementation(ClassImplementation)" )
- return ctx
-
-
-class RawHook():
- __namespace__ = "Tableau.Migration.Custom.Hooks.Filters"
-
- def ShouldMigrate(self, ctx) -> bool:
- return True
-
-
-class WithoutImplementation(ISyncContentFilter[IUser]):
- __namespace__ = "Tableau.Migration.Custom.Hooks.Filters"
-
-
-class TestContentFilterBuilderTests():
- def test_clear_class_hook(self):
- hook_builder = PyContentFilterBuilder(ContentFilterBuilder())
-
- result = hook_builder.add(IUser,ClassImplementation()).clear()
-
- assert result is hook_builder
- assert len(result.build().get_hooks(ISyncContentFilter[IUser])) == 0
-
- def test_clear_subclass_hook(self):
- hook_builder = PyContentFilterBuilder(ContentFilterBuilder())
-
- result = hook_builder.add(IUser,SubClassImplementation()).clear()
-
- assert result is hook_builder
- assert len(result.build().get_hooks(ISyncContentFilter[IUser])) == 0
-
- def test_clear_raw_hook(self):
- try:
- hook_builder = PyContentFilterBuilder(ContentFilterBuilder())
-
- result = hook_builder.add(IProject,RawHook()).clear()
-
- assert result is hook_builder
- assert len(result.build().get_hooks(ISyncContentFilter[IUser])) == 0
- except Exception as e:
- print(e);
-
-
- def test_add_object(self):
- hook_builder = PyContentFilterBuilder(ContentFilterBuilder())
-
- result = hook_builder.add(IUser,SubClassImplementation())
-
- assert result is hook_builder
-
- def test_add_type_noinitializer(self):
- hook_builder = PyContentFilterBuilder(ContentFilterBuilder())
-
- result = hook_builder.add(ClassImplementation,IUser)
-
- assert result is hook_builder
-
- def test_add_type_initializer(self):
- hook_builder = PyContentFilterBuilder(ContentFilterBuilder())
-
- def subclassinitialize(provider: IServiceProvider):
- return get_service(provider, SubClassImplementation)
-
- result = hook_builder.add(SubClassImplementation, IUser, Func[IServiceProvider, SubClassImplementation](subclassinitialize))
-
- assert result is hook_builder
-
- def test_add_callback(self):
- hook_builder = PyContentFilterBuilder(ContentFilterBuilder())
-
- def classcallback(context: IEnumerable[ContentMigrationItem[IProject]]):
- return context
-
- result = hook_builder.add(IProject, Func[IEnumerable[ContentMigrationItem[IProject]], IEnumerable[ContentMigrationItem[IProject]]](classcallback))
-
- assert result is hook_builder
-
- def test_add_withoutimplementation(self):
- users = List[ContentMigrationItem[IUser]]();
- provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(ServiceCollection())
- hookFactory = PyContentFilterBuilder(ContentFilterBuilder()).add(IUser, WithoutImplementation()).build().get_hooks(IContentFilter[IUser])
- assert len(hookFactory) == 1
- hook = hookFactory[0].Create[IMigrationHook[IEnumerable[ContentMigrationItem[IUser]]]](provider)
- try:
- hook.ExecuteAsync(users, CancellationToken(False))
- except NotImplementedException as nie:
- assert nie.args[0] == "Python object does not have a 'Execute' method"
- else:
- assert False, "This test must generate a NotImplementedException."
- finally:
- provider.Dispose()
-
- def test_add_withimplementation(self):
- users = List[ContentMigrationItem[IUser]]();
- provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(ServiceCollection())
- hookFactory = PyContentFilterBuilder(ContentFilterBuilder()).add(IUser, SubClassImplementation()).build().get_hooks(IContentFilter[IUser])
- assert len(hookFactory) == 1
- hook = hookFactory[0].Create[IMigrationHook[IEnumerable[ContentMigrationItem[IUser]]]](provider)
- try:
- result = hook.ExecuteAsync(users, CancellationToken(False))
- print(result)
- except:
- assert False, "This test must not generate an Exception."
- finally:
- provider.Dispose()
-
- def test_build_detects_interface_from_concrete_class(self):
- collection = PyContentFilterBuilder(ContentFilterBuilder()).add(IUser,ClassImplementation()).build()
- hooks = collection.get_hooks(IContentFilter[IUser])
- internalhooks = collection.get_hooks(IMigrationHook[IEnumerable[ContentMigrationItem[IUser]]])
- otherhooks = collection.get_hooks(IContentFilter[IProject])
- classhooks = collection.get_hooks(ClassImplementation)
- subclasshooks = collection.get_hooks(SubClassImplementation)
-
- # Can't do a direct object comparison, so let's just count the right number is returned
- assert len(hooks) == 1
- assert len(internalhooks) == 0
- assert len(otherhooks) == 0
- assert len(classhooks) == 0
- assert len(subclasshooks) == 0
-
- def test_build_detects_interface_from_concrete_subclass(self):
- collection = PyContentFilterBuilder(ContentFilterBuilder()).add(IUser,SubClassImplementation()).build()
- hooks = collection.get_hooks(IContentFilter[IUser])
- internalhooks = collection.get_hooks(IMigrationHook[IEnumerable[ContentMigrationItem[IUser]]])
- otherhooks = collection.get_hooks(IContentFilter[IProject])
- classhooks = collection.get_hooks(ClassImplementation)
- subclasshooks = collection.get_hooks(SubClassImplementation)
-
- # Can't do a direct object comparison, so let's just count the right number is returned
- assert len(hooks) == 1
- assert len(internalhooks) == 0
- assert len(otherhooks) == 0
- assert len(classhooks) == 0
- assert len(subclasshooks) == 0
-
- def test_build_factory_class(self):
- collection = PyContentFilterBuilder(ContentFilterBuilder()).add(ClassImplementation,IUser).build()
- hooks = collection.get_hooks(IContentFilter[IUser])
- internalhooks = collection.get_hooks(IMigrationHook[IEnumerable[ContentMigrationItem[IUser]]])
- otherhooks = collection.get_hooks(IContentFilter[IProject])
- classhooks = collection.get_hooks(ClassImplementation)
- subclasshooks = collection.get_hooks(SubClassImplementation)
-
- # Can't do a direct object comparison, so let's just count the right number is returned
- assert len(hooks) == 1
- assert len(internalhooks) == 0
- assert len(otherhooks) == 0
- assert len(classhooks) == 0
- assert len(subclasshooks) == 0
-
- def test_build_factory_subclass_withinitializer(self):
- def subclassinitialize(provider: IServiceProvider):
- return get_service(provider, SubClassImplementation)
-
- collection = PyContentFilterBuilder(ContentFilterBuilder()).add(SubClassImplementation, IUser, Func[IServiceProvider, SubClassImplementation](subclassinitialize)).build()
- hooks = collection.get_hooks(IContentFilter[IUser])
- internalhooks = collection.get_hooks(IMigrationHook[IEnumerable[ContentMigrationItem[IUser]]])
- otherhooks = collection.get_hooks(IContentFilter[IProject])
- classhooks = collection.get_hooks(ClassImplementation)
- subclasshooks = collection.get_hooks(SubClassImplementation)
-
- # Can't do a direct object comparison, so let's just count the right number is returned
- assert len(hooks) == 1
- assert len(internalhooks) == 0
- assert len(otherhooks) == 0
- assert len(classhooks) == 0
- assert len(subclasshooks) == 0
-
- def test_build_factory_multipleclasses(self):
- collection = PyContentFilterBuilder(ContentFilterBuilder()).add(ClassImplementation,IUser).add(SubClassImplementation,IUser).build()
- hooks = collection.get_hooks(IContentFilter[IUser])
- internalhooks = collection.get_hooks(IMigrationHook[IEnumerable[ContentMigrationItem[IUser]]])
- otherhooks = collection.get_hooks(IContentFilter[IProject])
- classhooks = collection.get_hooks(ClassImplementation)
- subclasshooks = collection.get_hooks(SubClassImplementation)
-
- # Can't do a direct object comparison, so let's just count the right number is returned
- assert len(hooks) == 2
- assert len(internalhooks) == 0
- assert len(otherhooks) == 0
- assert len(classhooks) == 0
- assert len(subclasshooks) == 0
-
- def test_build_callback(self):
- def classcallback(context: IEnumerable[ContentMigrationItem[IProject]]):
- return context
-
- collection = PyContentFilterBuilder(ContentFilterBuilder()).add(IProject, Func[IEnumerable[ContentMigrationItem[IProject]], IEnumerable[ContentMigrationItem[IProject]]](classcallback)).build()
- hooks = collection.get_hooks(IContentFilter[IProject])
- internalhooks = collection.get_hooks(IMigrationHook[IEnumerable[ContentMigrationItem[IProject]]])
- otherhooks = collection.get_hooks(IContentFilter[IUser])
- classhooks = collection.get_hooks(ClassImplementation)
- subclasshooks = collection.get_hooks(SubClassImplementation)
-
- # Can't do a direct object comparison, so let's just count the right number is returned
- assert len(hooks) == 1
- assert len(internalhooks) == 0
- assert len(otherhooks) == 0
- assert len(classhooks) == 0
- assert len(subclasshooks) == 0
diff --git a/src/Python/tests/test_migrations_engine_hooks_filters_lifetime.py b/src/Python/tests/test_migrations_engine_hooks_filters_lifetime.py
deleted file mode 100644
index c5a046d..0000000
--- a/src/Python/tests/test_migrations_engine_hooks_filters_lifetime.py
+++ /dev/null
@@ -1,373 +0,0 @@
-# Copyright (c) 2024, Salesforce, Inc.
-# SPDX-License-Identifier: Apache-2
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-# Make sure the test can find the module
-import pytest
-from tableau_migration.migration import get_service
-from tableau_migration.migration_engine_hooks_filters import PyContentFilterBuilder
-from System import IServiceProvider, Func
-from Microsoft.Extensions.DependencyInjection import (
- ServiceProviderServiceExtensions,
- ServiceCollectionServiceExtensions,
- ServiceCollection,
- ServiceCollectionContainerBuilderExtensions)
-from System.Collections.Generic import IEnumerable
-from Tableau.Migration.Content import IUser,IProject
-from Tableau.Migration.Engine import ContentMigrationItem
-from Tableau.Migration.Engine.Hooks import IMigrationHook
-from Tableau.Migration.Engine.Hooks.Filters import ContentFilterBuilder,IContentFilter
-from Tableau.Migration.Interop.Hooks.Filters import ISyncContentFilter
-
-class FilterImplementation(ISyncContentFilter[IUser]):
- __namespace__ = "Tableau.Migration.Custom.Hooks.Filters.Lifetime"
-
- def Execute(self, ctx: IEnumerable[ContentMigrationItem[IUser]]) -> IEnumerable[ContentMigrationItem[IUser]]:
- return ctx
-
-class SubFilterImplementation(FilterImplementation):
- __namespace__ = "Tableau.Migration.Custom.Hooks.Filters.Lifetime"
-
- def Execute(self, ctx: IEnumerable[ContentMigrationItem[IUser]]) -> IEnumerable[ContentMigrationItem[IUser]]:
- return ctx
-
-class TestContentFilterBuilderLifetimeTests():
- @pytest.fixture(autouse=True, scope="class")
- def setup(self):
- TestContentFilterBuilderLifetimeTests.FilterImplementation = FilterImplementation
- TestContentFilterBuilderLifetimeTests.SubFilterImplementation = SubFilterImplementation
-
- yield
-
- del TestContentFilterBuilderLifetimeTests.SubFilterImplementation
- del TestContentFilterBuilderLifetimeTests.FilterImplementation
-
- def test_lifetime_buildandcreate_object(self, skip_by_python_lifetime_env_var):
- try:
- # Arrange
- provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(ServiceCollection())
- hook = TestContentFilterBuilderLifetimeTests.SubFilterImplementation()
- hookFactory = PyContentFilterBuilder(ContentFilterBuilder()).add(IUser,hook).build().get_hooks(IContentFilter[IUser])
-
- assert len(hookFactory) == 1
-
- # Act
- try:
- scope1 = ServiceProviderServiceExtensions.CreateScope(provider)
- firstScopeHook1 = hookFactory[0].Create[IContentFilter[IUser]](scope1.ServiceProvider)
- firstScopeHook2 = hookFactory[0].Create[IContentFilter[IUser]](scope1.ServiceProvider)
- finally:
- scope1.Dispose()
-
- try:
- scope2 = ServiceProviderServiceExtensions.CreateScope(provider)
- lastScopeHook = hookFactory[0].Create[IContentFilter[IUser]](scope2.ServiceProvider)
- finally:
- scope2.Dispose()
-
- # Assert
- assert hook == firstScopeHook1
- assert hook is not firstScopeHook1
- assert hook == firstScopeHook2
- assert hook is not firstScopeHook2
- assert hook == lastScopeHook
- assert hook is not lastScopeHook
- finally:
- provider.Dispose()
-
- def test_lifetime_buildandcreate_noinitializersingleton(self, skip_by_python_lifetime_env_var):
- try:
- # Arrange
- servicecollection = ServiceCollection()
- ServiceCollectionServiceExtensions.AddSingleton[TestContentFilterBuilderLifetimeTests.SubFilterImplementation](servicecollection)
- provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(servicecollection)
- hookFactory = PyContentFilterBuilder(ContentFilterBuilder()).add(TestContentFilterBuilderLifetimeTests.SubFilterImplementation,IUser).build().get_hooks(IContentFilter[IUser])
-
- assert len(hookFactory) == 1
-
- # Act
- try:
- scope1 = ServiceProviderServiceExtensions.CreateScope(provider)
- firstScopeHook1 = hookFactory[0].Create[IContentFilter[IUser]](scope1.ServiceProvider)
- firstScopeHook2 = hookFactory[0].Create[IContentFilter[IUser]](scope1.ServiceProvider)
- finally:
- scope1.Dispose()
-
- try:
- scope2 = ServiceProviderServiceExtensions.CreateScope(provider)
- lastScopeHook = hookFactory[0].Create[IContentFilter[IUser]](scope2.ServiceProvider)
- finally:
- scope2.Dispose()
-
- # Assert
- assert firstScopeHook1 == firstScopeHook2
- assert firstScopeHook1 is not firstScopeHook2
- assert firstScopeHook1 == lastScopeHook
- assert firstScopeHook1 is not lastScopeHook
- finally:
- provider.Dispose()
-
- def test_lifetime_buildandcreate_noinitializerscoped(self, skip_by_python_lifetime_env_var):
- try:
- # Arrange
- servicecollection = ServiceCollection()
- ServiceCollectionServiceExtensions.AddScoped[TestContentFilterBuilderLifetimeTests.SubFilterImplementation](servicecollection)
- provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(servicecollection)
- hookFactory = PyContentFilterBuilder(ContentFilterBuilder()).add(TestContentFilterBuilderLifetimeTests.SubFilterImplementation,IUser).build().get_hooks(IContentFilter[IUser])
-
- assert len(hookFactory) == 1
-
- # Act
- try:
- scope1 = ServiceProviderServiceExtensions.CreateScope(provider)
- firstScopeHook1 = hookFactory[0].Create[IContentFilter[IUser]](scope1.ServiceProvider)
- firstScopeHook2 = hookFactory[0].Create[IContentFilter[IUser]](scope1.ServiceProvider)
- finally:
- scope1.Dispose()
-
- try:
- scope2 = ServiceProviderServiceExtensions.CreateScope(provider)
- lastScopeHook = hookFactory[0].Create[IContentFilter[IUser]](scope2.ServiceProvider)
- finally:
- scope2.Dispose()
-
- # Assert
- assert firstScopeHook1 == firstScopeHook2
- assert firstScopeHook1 is not firstScopeHook2
- assert firstScopeHook1 != lastScopeHook
- finally:
- provider.Dispose()
-
- def test_lifetime_buildandcreate_noinitializertransient(self, skip_by_python_lifetime_env_var):
- try:
- # Arrange
- servicecollection = ServiceCollection()
- ServiceCollectionServiceExtensions.AddTransient[TestContentFilterBuilderLifetimeTests.SubFilterImplementation](servicecollection)
- provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(servicecollection)
- hookFactory = PyContentFilterBuilder(ContentFilterBuilder()).add(TestContentFilterBuilderLifetimeTests.SubFilterImplementation,IUser).build().get_hooks(IContentFilter[IUser])
-
- assert len(hookFactory) == 1
-
- # Act
- try:
- scope1 = ServiceProviderServiceExtensions.CreateScope(provider)
- firstScopeHook1 = hookFactory[0].Create[IContentFilter[IUser]](scope1.ServiceProvider)
- firstScopeHook2 = hookFactory[0].Create[IContentFilter[IUser]](scope1.ServiceProvider)
- finally:
- scope1.Dispose()
-
- try:
- scope2 = ServiceProviderServiceExtensions.CreateScope(provider)
- lastScopeHook = hookFactory[0].Create[IContentFilter[IUser]](scope2.ServiceProvider)
- finally:
- scope2.Dispose()
-
- # Assert
- assert firstScopeHook1 != firstScopeHook2
- assert firstScopeHook1 != lastScopeHook
- finally:
- provider.Dispose()
-
- def test_lifetime_buildandcreate_initializerobjectreference(self, skip_by_python_lifetime_env_var):
- try:
- # Arrange
- hook = TestContentFilterBuilderLifetimeTests.SubFilterImplementation()
- def subsubclassinitialize(provider: IServiceProvider):
- return hook
- provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(ServiceCollection())
- hookFactory = PyContentFilterBuilder(ContentFilterBuilder()).add(TestContentFilterBuilderLifetimeTests.SubFilterImplementation, IUser, Func[IServiceProvider, TestContentFilterBuilderLifetimeTests.SubFilterImplementation](subsubclassinitialize)).build().get_hooks(IContentFilter[IUser])
-
- assert len(hookFactory) == 1
-
- # Act
- try:
- scope1 = ServiceProviderServiceExtensions.CreateScope(provider)
- firstScopeHook1 = hookFactory[0].Create[IContentFilter[IUser]](scope1.ServiceProvider)
- firstScopeHook2 = hookFactory[0].Create[IContentFilter[IUser]](scope1.ServiceProvider)
- finally:
- scope1.Dispose()
-
- try:
- scope2 = ServiceProviderServiceExtensions.CreateScope(provider)
- lastScopeHook = hookFactory[0].Create[IContentFilter[IUser]](scope2.ServiceProvider)
- finally:
- scope2.Dispose()
-
- # Assert
- assert firstScopeHook1 == firstScopeHook2
- assert firstScopeHook1 is not firstScopeHook2
- assert firstScopeHook1 == lastScopeHook
- assert firstScopeHook1 is not lastScopeHook
- finally:
- provider.Dispose()
-
- def test_lifetime_buildandcreate_initializernewobject(self, skip_by_python_lifetime_env_var):
- try:
- # Arrange
- def subsubclassinitialize(provider: IServiceProvider):
- return TestContentFilterBuilderLifetimeTests.SubFilterImplementation()
- provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(ServiceCollection())
- hookFactory = PyContentFilterBuilder(ContentFilterBuilder()).add(TestContentFilterBuilderLifetimeTests.SubFilterImplementation, IUser, Func[IServiceProvider, TestContentFilterBuilderLifetimeTests.SubFilterImplementation](subsubclassinitialize)).build().get_hooks(IContentFilter[IUser])
-
- assert len(hookFactory) == 1
-
- # Act
- try:
- scope1 = ServiceProviderServiceExtensions.CreateScope(provider)
- firstScopeHook1 = hookFactory[0].Create[IContentFilter[IUser]](scope1.ServiceProvider)
- firstScopeHook2 = hookFactory[0].Create[IContentFilter[IUser]](scope1.ServiceProvider)
- finally:
- scope1.Dispose()
-
- try:
- scope2 = ServiceProviderServiceExtensions.CreateScope(provider)
- lastScopeHook = hookFactory[0].Create[IContentFilter[IUser]](scope2.ServiceProvider)
- finally:
- scope2.Dispose()
-
- # Assert
- assert firstScopeHook1 != firstScopeHook2
- assert firstScopeHook1 != lastScopeHook
- finally:
- provider.Dispose()
-
- def test_lifetime_buildandcreate_initializersingleton(self, skip_by_python_lifetime_env_var):
- try:
- # Arrange
- def subsubclassinitialize(provider: IServiceProvider):
- return get_service(provider,TestContentFilterBuilderLifetimeTests.SubFilterImplementation)
- servicecollection = ServiceCollection()
- ServiceCollectionServiceExtensions.AddSingleton[TestContentFilterBuilderLifetimeTests.SubFilterImplementation](servicecollection)
- provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(servicecollection)
- hookFactory = PyContentFilterBuilder(ContentFilterBuilder()).add(TestContentFilterBuilderLifetimeTests.SubFilterImplementation, IUser, Func[IServiceProvider, TestContentFilterBuilderLifetimeTests.SubFilterImplementation](subsubclassinitialize)).build().get_hooks(IContentFilter[IUser])
-
- assert len(hookFactory) == 1
-
- # Act
- try:
- scope1 = ServiceProviderServiceExtensions.CreateScope(provider)
- firstScopeHook1 = hookFactory[0].Create[IContentFilter[IUser]](scope1.ServiceProvider)
- firstScopeHook2 = hookFactory[0].Create[IContentFilter[IUser]](scope1.ServiceProvider)
- finally:
- scope1.Dispose()
-
- try:
- scope2 = ServiceProviderServiceExtensions.CreateScope(provider)
- lastScopeHook = hookFactory[0].Create[IContentFilter[IUser]](scope2.ServiceProvider)
- finally:
- scope2.Dispose()
-
- # Assert
- assert firstScopeHook1 == firstScopeHook2
- assert firstScopeHook1 is not firstScopeHook2
- assert firstScopeHook1 == lastScopeHook
- assert firstScopeHook1 is not lastScopeHook
- finally:
- provider.Dispose()
-
- def test_lifetime_buildandcreate_initializerscoped(self, skip_by_python_lifetime_env_var):
- try:
- # Arrange
- def classinitialize(provider: IServiceProvider):
- return get_service(provider,TestContentFilterBuilderLifetimeTests.FilterImplementation)
- servicecollection = ServiceCollection()
- ServiceCollectionServiceExtensions.AddScoped[TestContentFilterBuilderLifetimeTests.FilterImplementation](servicecollection)
- provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(servicecollection)
- hookFactory = PyContentFilterBuilder(ContentFilterBuilder()).add(TestContentFilterBuilderLifetimeTests.FilterImplementation, IUser, Func[IServiceProvider, TestContentFilterBuilderLifetimeTests.FilterImplementation](classinitialize)).build().get_hooks(IContentFilter[IUser])
-
- assert len(hookFactory) == 1
-
- # Act
- try:
- scope1 = ServiceProviderServiceExtensions.CreateScope(provider)
- firstScopeHook1 = hookFactory[0].Create[IContentFilter[IUser]](scope1.ServiceProvider)
- firstScopeHook2 = hookFactory[0].Create[IContentFilter[IUser]](scope1.ServiceProvider)
- finally:
- scope1.Dispose()
-
- try:
- scope2 = ServiceProviderServiceExtensions.CreateScope(provider)
- lastScopeHook = hookFactory[0].Create[IContentFilter[IUser]](scope2.ServiceProvider)
- finally:
- scope2.Dispose()
-
- # Assert
- assert firstScopeHook1 == firstScopeHook2
- assert firstScopeHook1 is not firstScopeHook2
- assert firstScopeHook1 != lastScopeHook
- finally:
- provider.Dispose()
-
- def test_lifetime_buildandcreate_initializertransient(self, skip_by_python_lifetime_env_var):
- try:
- # Arrange
- def subclassinitialize(provider: IServiceProvider):
- return get_service(provider,TestContentFilterBuilderLifetimeTests.SubFilterImplementation)
- servicecollection = ServiceCollection()
- ServiceCollectionServiceExtensions.AddTransient[TestContentFilterBuilderLifetimeTests.SubFilterImplementation](servicecollection)
- provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(servicecollection)
- hookFactory = PyContentFilterBuilder(ContentFilterBuilder()).add(TestContentFilterBuilderLifetimeTests.SubFilterImplementation, IUser, Func[IServiceProvider, TestContentFilterBuilderLifetimeTests.SubFilterImplementation](subclassinitialize)).build().get_hooks(IContentFilter[IUser])
-
- assert len(hookFactory) == 1
-
- # Act
- try:
- scope1 = ServiceProviderServiceExtensions.CreateScope(provider)
- firstScopeHook1 = hookFactory[0].Create[IContentFilter[IUser]](scope1.ServiceProvider)
- firstScopeHook2 = hookFactory[0].Create[IContentFilter[IUser]](scope1.ServiceProvider)
- finally:
- scope1.Dispose()
-
- try:
- scope2 = ServiceProviderServiceExtensions.CreateScope(provider)
- lastScopeHook = hookFactory[0].Create[IContentFilter[IUser]](scope2.ServiceProvider)
- finally:
- scope2.Dispose()
-
- # Assert
- assert firstScopeHook1 != firstScopeHook2
- assert firstScopeHook1 != lastScopeHook
- finally:
- provider.Dispose()
-
- def test_lifetime_buildandcreate_callback(self, skip_by_python_lifetime_env_var):
- try:
- # Arrange
- def classcallback(context: IEnumerable[ContentMigrationItem[IProject]]):
- return context
-
- provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(ServiceCollection())
- hookFactory = PyContentFilterBuilder(ContentFilterBuilder()).add(IProject, Func[IEnumerable[ContentMigrationItem[IProject]], IEnumerable[ContentMigrationItem[IProject]]](classcallback)).build().get_hooks(IContentFilter[IProject])
-
- assert len(hookFactory) == 1
-
- # Act
- try:
- scope1 = ServiceProviderServiceExtensions.CreateScope(provider)
- firstScopeHook1 = hookFactory[0].Create[IMigrationHook[IEnumerable[ContentMigrationItem[IProject]]]](scope1.ServiceProvider)
- firstScopeHook2 = hookFactory[0].Create[IMigrationHook[IEnumerable[ContentMigrationItem[IProject]]]](scope1.ServiceProvider)
- finally:
- scope1.Dispose()
-
- try:
- scope2 = ServiceProviderServiceExtensions.CreateScope(provider)
- lastScopeHook = hookFactory[0].Create[IMigrationHook[IEnumerable[ContentMigrationItem[IProject]]]](scope2.ServiceProvider)
- finally:
- scope2.Dispose()
-
- # Assert
- assert firstScopeHook1 != firstScopeHook2
- assert firstScopeHook1 != lastScopeHook
- finally:
- provider.Dispose()
diff --git a/src/Python/tests/test_migrations_engine_hooks_lifetime.py b/src/Python/tests/test_migrations_engine_hooks_lifetime.py
deleted file mode 100644
index 458bb89..0000000
--- a/src/Python/tests/test_migrations_engine_hooks_lifetime.py
+++ /dev/null
@@ -1,378 +0,0 @@
-# Copyright (c) 2024, Salesforce, Inc.
-# SPDX-License-Identifier: Apache-2
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-# Make sure the test can find the module
-import pytest
-from tableau_migration.migration import get_service
-from tableau_migration.migration_engine_hooks import PyMigrationHookBuilder
-from System import IServiceProvider, Func
-from Microsoft.Extensions.DependencyInjection import (
- ServiceProviderServiceExtensions,
- ServiceCollectionServiceExtensions,
- ServiceCollection,
- ServiceCollectionContainerBuilderExtensions)
-from Tableau.Migration.Engine.Hooks import MigrationHookBuilder, IMigrationHook
-from Tableau.Migration.Interop.Hooks import ISyncMigrationHook
-
-class HookImplementation(ISyncMigrationHook[bool]):
- __namespace__ = "Tableau.Migration.Custom.Hooks.Lifetime"
-
- def Execute(self, ctx) -> bool:
- return ctx
-
-class SubHookImplementation(HookImplementation):
- __namespace__ = "Tableau.Migration.Custom.Hooks.Lifetime"
-
- def Execute(self, ctx) -> bool:
- return ctx
-
-class SubSubHookImplementation(SubHookImplementation):
- __namespace__ = "Tableau.Migration.Custom.Hooks.Lifetime"
-
- def Execute(self, ctx) -> bool:
- return ctx
-
-class TestMigrationHookBuilderLifetimeTests():
- @pytest.fixture(autouse=True, scope="class")
- def setup(self):
- TestMigrationHookBuilderLifetimeTests.HookImplementation = HookImplementation
- TestMigrationHookBuilderLifetimeTests.SubHookImplementation = SubHookImplementation
- TestMigrationHookBuilderLifetimeTests.SubSubHookImplementation = SubSubHookImplementation
-
- yield
-
- del TestMigrationHookBuilderLifetimeTests.SubSubHookImplementation
- del TestMigrationHookBuilderLifetimeTests.SubHookImplementation
- del TestMigrationHookBuilderLifetimeTests.HookImplementation
-
- def test_lifetime_buildandcreate_object(self, skip_by_python_lifetime_env_var):
- try:
- # Arrange
- provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(ServiceCollection())
- hook = TestMigrationHookBuilderLifetimeTests.SubHookImplementation()
- hookFactory = PyMigrationHookBuilder(MigrationHookBuilder()).add(hook).build().get_hooks(ISyncMigrationHook[bool])
-
- assert len(hookFactory) == 1
-
- # Act
- try:
- scope1 = ServiceProviderServiceExtensions.CreateScope(provider)
- firstScopeHook1 = hookFactory[0].Create[IMigrationHook[bool]](scope1.ServiceProvider)
- firstScopeHook2 = hookFactory[0].Create[IMigrationHook[bool]](scope1.ServiceProvider)
- finally:
- scope1.Dispose()
-
- try:
- scope2 = ServiceProviderServiceExtensions.CreateScope(provider)
- lastScopeHook = hookFactory[0].Create[IMigrationHook[bool]](scope2.ServiceProvider)
- finally:
- scope2.Dispose()
-
- # Assert
- assert hook == firstScopeHook1
- assert hook is not firstScopeHook1
- assert hook == firstScopeHook2
- assert hook is not firstScopeHook2
- assert hook == lastScopeHook
- assert hook is not lastScopeHook
- finally:
- provider.Dispose()
-
- def test_lifetime_buildandcreate_noinitializersingleton(self, skip_by_python_lifetime_env_var):
- try:
- # Arrange
- servicecollection = ServiceCollection()
- ServiceCollectionServiceExtensions.AddSingleton[TestMigrationHookBuilderLifetimeTests.SubHookImplementation](servicecollection)
- provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(servicecollection)
- hookFactory = PyMigrationHookBuilder(MigrationHookBuilder()).add(TestMigrationHookBuilderLifetimeTests.SubHookImplementation).build().get_hooks(ISyncMigrationHook[bool])
-
- assert len(hookFactory) == 1
-
- # Act
- try:
- scope1 = ServiceProviderServiceExtensions.CreateScope(provider)
- firstScopeHook1 = hookFactory[0].Create[IMigrationHook[bool]](scope1.ServiceProvider)
- firstScopeHook2 = hookFactory[0].Create[IMigrationHook[bool]](scope1.ServiceProvider)
- finally:
- scope1.Dispose()
-
- try:
- scope2 = ServiceProviderServiceExtensions.CreateScope(provider)
- lastScopeHook = hookFactory[0].Create[IMigrationHook[bool]](scope2.ServiceProvider)
- finally:
- scope2.Dispose()
-
- # Assert
- assert firstScopeHook1 == firstScopeHook2
- assert firstScopeHook1 is not firstScopeHook2
- assert firstScopeHook1 == lastScopeHook
- assert firstScopeHook1 is not lastScopeHook
- finally:
- provider.Dispose()
-
- def test_lifetime_buildandcreate_noinitializerscoped(self, skip_by_python_lifetime_env_var):
- try:
- # Arrange
- servicecollection = ServiceCollection()
- ServiceCollectionServiceExtensions.AddScoped[TestMigrationHookBuilderLifetimeTests.SubHookImplementation](servicecollection)
- provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(servicecollection)
- hookFactory = PyMigrationHookBuilder(MigrationHookBuilder()).add(TestMigrationHookBuilderLifetimeTests.SubHookImplementation).build().get_hooks(ISyncMigrationHook[bool])
-
- assert len(hookFactory) == 1
-
- # Act
- try:
- scope1 = ServiceProviderServiceExtensions.CreateScope(provider)
- firstScopeHook1 = hookFactory[0].Create[IMigrationHook[bool]](scope1.ServiceProvider)
- firstScopeHook2 = hookFactory[0].Create[IMigrationHook[bool]](scope1.ServiceProvider)
- finally:
- scope1.Dispose()
-
- try:
- scope2 = ServiceProviderServiceExtensions.CreateScope(provider)
- lastScopeHook = hookFactory[0].Create[IMigrationHook[bool]](scope2.ServiceProvider)
- finally:
- scope2.Dispose()
-
- # Assert
- assert firstScopeHook1 == firstScopeHook2
- assert firstScopeHook1 is not firstScopeHook2
- assert firstScopeHook1 != lastScopeHook
- finally:
- provider.Dispose()
-
- def test_lifetime_buildandcreate_noinitializertransient(self, skip_by_python_lifetime_env_var):
- try:
- # Arrange
- servicecollection = ServiceCollection()
- ServiceCollectionServiceExtensions.AddTransient[TestMigrationHookBuilderLifetimeTests.SubHookImplementation](servicecollection)
- provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(servicecollection)
- hookFactory = PyMigrationHookBuilder(MigrationHookBuilder()).add(TestMigrationHookBuilderLifetimeTests.SubHookImplementation).build().get_hooks(ISyncMigrationHook[bool])
-
- assert len(hookFactory) == 1
-
- # Act
- try:
- scope1 = ServiceProviderServiceExtensions.CreateScope(provider)
- firstScopeHook1 = hookFactory[0].Create[IMigrationHook[bool]](scope1.ServiceProvider)
- firstScopeHook2 = hookFactory[0].Create[IMigrationHook[bool]](scope1.ServiceProvider)
- finally:
- scope1.Dispose()
-
- try:
- scope2 = ServiceProviderServiceExtensions.CreateScope(provider)
- lastScopeHook = hookFactory[0].Create[IMigrationHook[bool]](scope2.ServiceProvider)
- finally:
- scope2.Dispose()
-
- # Assert
- assert firstScopeHook1 != firstScopeHook2
- assert firstScopeHook1 != lastScopeHook
- finally:
- provider.Dispose()
-
-
- def test_lifetime_buildandcreate_initializerobjectreference(self, skip_by_python_lifetime_env_var):
- try:
- # Arrange
- hook = TestMigrationHookBuilderLifetimeTests.SubSubHookImplementation()
- def subsubclassinitialize(provider: IServiceProvider):
- return hook
- provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(ServiceCollection())
- hookFactory = PyMigrationHookBuilder(MigrationHookBuilder()).add(TestMigrationHookBuilderLifetimeTests.SubSubHookImplementation, Func[IServiceProvider, TestMigrationHookBuilderLifetimeTests.SubSubHookImplementation](subsubclassinitialize)).build().get_hooks(ISyncMigrationHook[bool])
-
- assert len(hookFactory) == 1
-
- # Act
- try:
- scope1 = ServiceProviderServiceExtensions.CreateScope(provider)
- firstScopeHook1 = hookFactory[0].Create[IMigrationHook[bool]](scope1.ServiceProvider)
- firstScopeHook2 = hookFactory[0].Create[IMigrationHook[bool]](scope1.ServiceProvider)
- finally:
- scope1.Dispose()
-
- try:
- scope2 = ServiceProviderServiceExtensions.CreateScope(provider)
- lastScopeHook = hookFactory[0].Create[IMigrationHook[bool]](scope2.ServiceProvider)
- finally:
- scope2.Dispose()
-
- # Assert
- assert firstScopeHook1 == firstScopeHook2
- assert firstScopeHook1 is not firstScopeHook2
- assert firstScopeHook1 == lastScopeHook
- assert firstScopeHook1 is not lastScopeHook
- finally:
- provider.Dispose()
-
- def test_lifetime_buildandcreate_initializernewobject(self, skip_by_python_lifetime_env_var):
- try:
- # Arrange
- def subsubclassinitialize(provider: IServiceProvider):
- return TestMigrationHookBuilderLifetimeTests.SubSubHookImplementation()
- provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(ServiceCollection())
- hookFactory = PyMigrationHookBuilder(MigrationHookBuilder()).add(TestMigrationHookBuilderLifetimeTests.SubSubHookImplementation, Func[IServiceProvider, TestMigrationHookBuilderLifetimeTests.SubSubHookImplementation](subsubclassinitialize)).build().get_hooks(ISyncMigrationHook[bool])
-
- assert len(hookFactory) == 1
-
- # Act
- try:
- scope1 = ServiceProviderServiceExtensions.CreateScope(provider)
- firstScopeHook1 = hookFactory[0].Create[IMigrationHook[bool]](scope1.ServiceProvider)
- firstScopeHook2 = hookFactory[0].Create[IMigrationHook[bool]](scope1.ServiceProvider)
- finally:
- scope1.Dispose()
-
- try:
- scope2 = ServiceProviderServiceExtensions.CreateScope(provider)
- lastScopeHook = hookFactory[0].Create[IMigrationHook[bool]](scope2.ServiceProvider)
- finally:
- scope2.Dispose()
-
- # Assert
- assert firstScopeHook1 != firstScopeHook2
- assert firstScopeHook1 != lastScopeHook
- finally:
- provider.Dispose()
-
- def test_lifetime_buildandcreate_initializersingleton(self, skip_by_python_lifetime_env_var):
- try:
- # Arrange
- def subsubclassinitialize(provider: IServiceProvider):
- return get_service(provider,TestMigrationHookBuilderLifetimeTests.SubSubHookImplementation)
- servicecollection = ServiceCollection()
- ServiceCollectionServiceExtensions.AddSingleton[TestMigrationHookBuilderLifetimeTests.SubSubHookImplementation](servicecollection)
- provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(servicecollection)
- hookFactory = PyMigrationHookBuilder(MigrationHookBuilder()).add(TestMigrationHookBuilderLifetimeTests.SubSubHookImplementation, Func[IServiceProvider, TestMigrationHookBuilderLifetimeTests.SubSubHookImplementation](subsubclassinitialize)).build().get_hooks(ISyncMigrationHook[bool])
-
- assert len(hookFactory) == 1
-
- # Act
- try:
- scope1 = ServiceProviderServiceExtensions.CreateScope(provider)
- firstScopeHook1 = hookFactory[0].Create[IMigrationHook[bool]](scope1.ServiceProvider)
- firstScopeHook2 = hookFactory[0].Create[IMigrationHook[bool]](scope1.ServiceProvider)
- finally:
- scope1.Dispose()
-
- try:
- scope2 = ServiceProviderServiceExtensions.CreateScope(provider)
- lastScopeHook = hookFactory[0].Create[IMigrationHook[bool]](scope2.ServiceProvider)
- finally:
- scope2.Dispose()
-
- # Assert
- assert firstScopeHook1 == firstScopeHook2
- assert firstScopeHook1 is not firstScopeHook2
- assert firstScopeHook1 == lastScopeHook
- assert firstScopeHook1 is not lastScopeHook
- finally:
- provider.Dispose()
-
- def test_lifetime_buildandcreate_initializerscoped(self, skip_by_python_lifetime_env_var):
- try:
- # Arrange
- def subsubclassinitialize(provider: IServiceProvider):
- return get_service(provider,TestMigrationHookBuilderLifetimeTests.SubSubHookImplementation)
- servicecollection = ServiceCollection()
- ServiceCollectionServiceExtensions.AddScoped[TestMigrationHookBuilderLifetimeTests.SubSubHookImplementation](servicecollection)
- provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(servicecollection)
- hookFactory = PyMigrationHookBuilder(MigrationHookBuilder()).add(TestMigrationHookBuilderLifetimeTests.SubSubHookImplementation, Func[IServiceProvider, TestMigrationHookBuilderLifetimeTests.SubSubHookImplementation](subsubclassinitialize)).build().get_hooks(ISyncMigrationHook[bool])
-
- assert len(hookFactory) == 1
-
- # Act
- try:
- scope1 = ServiceProviderServiceExtensions.CreateScope(provider)
- firstScopeHook1 = hookFactory[0].Create[IMigrationHook[bool]](scope1.ServiceProvider)
- firstScopeHook2 = hookFactory[0].Create[IMigrationHook[bool]](scope1.ServiceProvider)
- finally:
- scope1.Dispose()
-
- try:
- scope2 = ServiceProviderServiceExtensions.CreateScope(provider)
- lastScopeHook = hookFactory[0].Create[IMigrationHook[bool]](scope2.ServiceProvider)
- finally:
- scope2.Dispose()
-
- # Assert
- assert firstScopeHook1 == firstScopeHook2
- assert firstScopeHook1 is not firstScopeHook2
- assert firstScopeHook1 != lastScopeHook
- finally:
- provider.Dispose()
-
- def test_lifetime_buildandcreate_initializertransient(self, skip_by_python_lifetime_env_var):
- try:
- # Arrange
- def subsubclassinitialize(provider: IServiceProvider):
- return get_service(provider,TestMigrationHookBuilderLifetimeTests.SubSubHookImplementation)
- servicecollection = ServiceCollection()
- ServiceCollectionServiceExtensions.AddTransient[TestMigrationHookBuilderLifetimeTests.SubSubHookImplementation](servicecollection)
- provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(servicecollection)
- hookFactory = PyMigrationHookBuilder(MigrationHookBuilder()).add(TestMigrationHookBuilderLifetimeTests.SubSubHookImplementation, Func[IServiceProvider, TestMigrationHookBuilderLifetimeTests.SubSubHookImplementation](subsubclassinitialize)).build().get_hooks(ISyncMigrationHook[bool])
-
- assert len(hookFactory) == 1
-
- # Act
- try:
- scope1 = ServiceProviderServiceExtensions.CreateScope(provider)
- firstScopeHook1 = hookFactory[0].Create[IMigrationHook[bool]](scope1.ServiceProvider)
- firstScopeHook2 = hookFactory[0].Create[IMigrationHook[bool]](scope1.ServiceProvider)
- finally:
- scope1.Dispose()
-
- try:
- scope2 = ServiceProviderServiceExtensions.CreateScope(provider)
- lastScopeHook = hookFactory[0].Create[IMigrationHook[bool]](scope2.ServiceProvider)
- finally:
- scope2.Dispose()
-
- # Assert
- assert firstScopeHook1 != firstScopeHook2
- assert firstScopeHook1 != lastScopeHook
- finally:
- provider.Dispose()
-
- def test_lifetime_buildandcreate_callback(self, skip_by_python_lifetime_env_var):
- try:
- # Arrange
- def classcallback(context: str):
- return context
-
- provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(ServiceCollection())
- hookFactory = PyMigrationHookBuilder(MigrationHookBuilder()).add(ISyncMigrationHook[str], str, Func[str, str](classcallback)).build().get_hooks(ISyncMigrationHook[str])
-
- assert len(hookFactory) == 1
-
- # Act
- try:
- scope1 = ServiceProviderServiceExtensions.CreateScope(provider)
- firstScopeHook1 = hookFactory[0].Create[IMigrationHook[str]](scope1.ServiceProvider)
- firstScopeHook2 = hookFactory[0].Create[IMigrationHook[str]](scope1.ServiceProvider)
- finally:
- scope1.Dispose()
-
- try:
- scope2 = ServiceProviderServiceExtensions.CreateScope(provider)
- lastScopeHook = hookFactory[0].Create[IMigrationHook[str]](scope2.ServiceProvider)
- finally:
- scope2.Dispose()
-
- # Assert
- assert firstScopeHook1 != firstScopeHook2
- assert firstScopeHook1 != lastScopeHook
- finally:
- provider.Dispose()
diff --git a/src/Python/tests/test_migrations_engine_hooks_mappings.py b/src/Python/tests/test_migrations_engine_hooks_mappings.py
deleted file mode 100644
index 26cd620..0000000
--- a/src/Python/tests/test_migrations_engine_hooks_mappings.py
+++ /dev/null
@@ -1,188 +0,0 @@
-# Copyright (c) 2024, Salesforce, Inc.
-# SPDX-License-Identifier: Apache-2
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-# Make sure the test can find the module
-from tableau_migration.migration import get_service
-from tableau_migration.migration_engine_hooks_mappings import PyContentMappingBuilder
-from System import IServiceProvider, Func, ArgumentException
-from Tableau.Migration.Content import IUser, IProject
-from Tableau.Migration.Engine.Hooks import IMigrationHook
-from Tableau.Migration.Engine.Hooks.Mappings import ContentMappingBuilder, IContentMapping, ContentMappingBase, ContentMappingContext
-from Tableau.Migration.Interop.Hooks.Mappings import ISyncContentMapping
-
-class ClassImplementation(ISyncContentMapping[IUser]):
- __namespace__ = "Tableau.Migration.Custom.Hooks.Mappings"
-
- def Execute(self, ctx: ContentMappingContext[IUser]) -> ContentMappingContext[IUser]:
- return ctx
-
-class SubClassImplementation(ClassImplementation):
- __namespace__ = "Tableau.Migration.Custom.Hooks.Mappings"
-
- def Execute(self, ctx: ContentMappingContext[IUser]) -> ContentMappingContext[IUser]:
- return ctx
-
-class RawHook(ContentMappingBase[IProject]):
- __namespace__ = "Tableau.Migration.Custom.Hooks.Mappings"
-
- def ShouldMigrate(self, ctx) -> bool:
- return True
-
-class TestContentMappingBuilderTests():
- def test_clear_class_hook(self):
- hook_builder = PyContentMappingBuilder(ContentMappingBuilder())
-
- result = hook_builder.add(IUser,ClassImplementation()).clear()
-
- assert result is hook_builder
- assert len(result.build().get_hooks(ISyncContentMapping[IUser])) == 0
-
- def test_clear_subclass_hook(self):
- hook_builder = PyContentMappingBuilder(ContentMappingBuilder())
-
- result = hook_builder.add(IUser,SubClassImplementation()).clear()
-
- assert result is hook_builder
- assert len(result.build().get_hooks(ISyncContentMapping[IUser])) == 0
-
- def test_add_object(self):
- hook_builder = PyContentMappingBuilder(ContentMappingBuilder())
-
- result = hook_builder.add(IUser,SubClassImplementation())
-
- assert result is hook_builder
-
- def test_add_type_noinitializer(self):
- hook_builder = PyContentMappingBuilder(ContentMappingBuilder())
-
- result = hook_builder.add(ClassImplementation,IUser)
-
- assert result is hook_builder
-
- def test_add_type_initializer(self):
- hook_builder = PyContentMappingBuilder(ContentMappingBuilder())
-
- def subclassinitialize(provider: IServiceProvider):
- return get_service(provider, SubClassImplementation)
-
- result = hook_builder.add(SubClassImplementation, IUser, Func[IServiceProvider, SubClassImplementation](subclassinitialize))
-
- assert result is hook_builder
-
- def test_add_callback(self):
- hook_builder = PyContentMappingBuilder(ContentMappingBuilder())
-
- def classcallback(context: ContentMappingContext[IProject]):
- return context
-
- result = hook_builder.add(IProject, Func[ContentMappingContext[IProject], ContentMappingContext[IProject]](classcallback))
-
- assert result is hook_builder
-
- def test_build_detects_interface_from_concrete_class(self):
- collection = PyContentMappingBuilder(ContentMappingBuilder()).add(IUser,ClassImplementation()).build()
- hooks = collection.get_hooks(IContentMapping[IUser])
- internalhooks = collection.get_hooks(IMigrationHook[ContentMappingContext[IUser]])
- otherhooks = collection.get_hooks(IContentMapping[IProject])
- classhooks = collection.get_hooks(ClassImplementation)
- subclasshooks = collection.get_hooks(SubClassImplementation)
-
- # Can't do a direct object comparison, so let's just count the right number is returned
- assert len(hooks) == 1
- assert len(internalhooks) == 0
- assert len(otherhooks) == 0
- assert len(classhooks) == 0
- assert len(subclasshooks) == 0
-
- def test_build_detects_interface_from_concrete_subclass(self):
- collection = PyContentMappingBuilder(ContentMappingBuilder()).add(IUser,SubClassImplementation()).build()
- hooks = collection.get_hooks(IContentMapping[IUser])
- internalhooks = collection.get_hooks(IMigrationHook[ContentMappingContext[IUser]])
- otherhooks = collection.get_hooks(IContentMapping[IProject])
- classhooks = collection.get_hooks(ClassImplementation)
- subclasshooks = collection.get_hooks(SubClassImplementation)
-
- # Can't do a direct object comparison, so let's just count the right number is returned
- assert len(hooks) == 1
- assert len(internalhooks) == 0
- assert len(otherhooks) == 0
- assert len(classhooks) == 0
- assert len(subclasshooks) == 0
-
- def test_build_factory_class(self):
- collection = PyContentMappingBuilder(ContentMappingBuilder()).add(ClassImplementation,IUser).build()
- hooks = collection.get_hooks(IContentMapping[IUser])
- internalhooks = collection.get_hooks(IMigrationHook[ContentMappingContext[IUser]])
- otherhooks = collection.get_hooks(IContentMapping[IProject])
- classhooks = collection.get_hooks(ClassImplementation)
- subclasshooks = collection.get_hooks(SubClassImplementation)
-
- # Can't do a direct object comparison, so let's just count the right number is returned
- assert len(hooks) == 1
- assert len(internalhooks) == 0
- assert len(otherhooks) == 0
- assert len(classhooks) == 0
- assert len(subclasshooks) == 0
-
- def test_build_factory_subclass_withinitializer(self):
- def subclassinitialize(provider: IServiceProvider):
- return get_service(provider, SubClassImplementation)
-
- collection = PyContentMappingBuilder(ContentMappingBuilder()).add(SubClassImplementation, IUser, Func[IServiceProvider, SubClassImplementation](subclassinitialize)).build()
- hooks = collection.get_hooks(IContentMapping[IUser])
- internalhooks = collection.get_hooks(IMigrationHook[ContentMappingContext[IUser]])
- otherhooks = collection.get_hooks(IContentMapping[IProject])
- classhooks = collection.get_hooks(ClassImplementation)
- subclasshooks = collection.get_hooks(SubClassImplementation)
-
- # Can't do a direct object comparison, so let's just count the right number is returned
- assert len(hooks) == 1
- assert len(internalhooks) == 0
- assert len(otherhooks) == 0
- assert len(classhooks) == 0
- assert len(subclasshooks) == 0
-
- def test_build_factory_multipleclasses(self):
- collection = PyContentMappingBuilder(ContentMappingBuilder()).add(ClassImplementation,IUser).add(SubClassImplementation,IUser).build()
- hooks = collection.get_hooks(IContentMapping[IUser])
- internalhooks = collection.get_hooks(IMigrationHook[ContentMappingContext[IUser]])
- otherhooks = collection.get_hooks(IContentMapping[IProject])
- classhooks = collection.get_hooks(ClassImplementation)
- subclasshooks = collection.get_hooks(SubClassImplementation)
-
- # Can't do a direct object comparison, so let's just count the right number is returned
- assert len(hooks) == 2
- assert len(internalhooks) == 0
- assert len(otherhooks) == 0
- assert len(classhooks) == 0
- assert len(subclasshooks) == 0
-
- def test_build_callback(self):
- def classcallback(context: ContentMappingContext[IProject]):
- return context
-
- collection = PyContentMappingBuilder(ContentMappingBuilder()).add(IProject, Func[ContentMappingContext[IProject], ContentMappingContext[IProject]](classcallback)).build()
- hooks = collection.get_hooks(IContentMapping[IProject])
- internalhooks = collection.get_hooks(IMigrationHook[ContentMappingContext[IProject]])
- otherhooks = collection.get_hooks(IContentMapping[IUser])
- classhooks = collection.get_hooks(ClassImplementation)
- subclasshooks = collection.get_hooks(SubClassImplementation)
-
- # Can't do a direct object comparison, so let's just count the right number is returned
- assert len(hooks) == 1
- assert len(internalhooks) == 0
- assert len(otherhooks) == 0
- assert len(classhooks) == 0
- assert len(subclasshooks) == 0
diff --git a/src/Python/tests/test_migrations_engine_hooks_mappings_lifetime.py b/src/Python/tests/test_migrations_engine_hooks_mappings_lifetime.py
deleted file mode 100644
index a09b5e1..0000000
--- a/src/Python/tests/test_migrations_engine_hooks_mappings_lifetime.py
+++ /dev/null
@@ -1,371 +0,0 @@
-# Copyright (c) 2024, Salesforce, Inc.
-# SPDX-License-Identifier: Apache-2
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-# Make sure the test can find the module
-import pytest
-from tableau_migration.migration import get_service
-from tableau_migration.migration_engine_hooks_mappings import PyContentMappingBuilder
-from System import IServiceProvider, Func
-from Microsoft.Extensions.DependencyInjection import (
- ServiceProviderServiceExtensions,
- ServiceCollectionServiceExtensions,
- ServiceCollection,
- ServiceCollectionContainerBuilderExtensions)
-from Tableau.Migration.Content import IUser,IProject
-from Tableau.Migration.Engine.Hooks import IMigrationHook
-from Tableau.Migration.Engine.Hooks.Mappings import ContentMappingBuilder, IContentMapping, ContentMappingContext
-from Tableau.Migration.Interop.Hooks.Mappings import ISyncContentMapping
-
-class MappingImplementation(ISyncContentMapping[IUser]):
- __namespace__ = "Tableau.Migration.Custom.Hooks.Mappings.Lifetime"
-
- def Execute(self, ctx: ContentMappingContext[IUser]) -> ContentMappingContext[IUser]:
- return ctx
-
-class SubMappingImplementation(MappingImplementation):
- __namespace__ = "Tableau.Migration.Custom.Hooks.Mappings.Lifetime"
-
- def Execute(self, ctx: ContentMappingContext[IUser]) -> ContentMappingContext[IUser]:
- return ctx
-
-class TestContentMappingBuilderLifetimeTests():
- @pytest.fixture(autouse=True, scope="class")
- def setup(self):
- TestContentMappingBuilderLifetimeTests.MappingImplementation = MappingImplementation
- TestContentMappingBuilderLifetimeTests.SubMappingImplementation = SubMappingImplementation
-
- yield
-
- del TestContentMappingBuilderLifetimeTests.SubMappingImplementation
- del TestContentMappingBuilderLifetimeTests.MappingImplementation
-
- def test_lifetime_buildandcreate_object(self, skip_by_python_lifetime_env_var):
- try:
- # Arrange
- provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(ServiceCollection())
- hook = TestContentMappingBuilderLifetimeTests.SubMappingImplementation()
- hookFactory = PyContentMappingBuilder(ContentMappingBuilder()).add(IUser,hook).build().get_hooks(IContentMapping[IUser])
-
- assert len(hookFactory) == 1
-
- # Act
- try:
- scope1 = ServiceProviderServiceExtensions.CreateScope(provider)
- firstScopeHook1 = hookFactory[0].Create[IContentMapping[IUser]](scope1.ServiceProvider)
- firstScopeHook2 = hookFactory[0].Create[IContentMapping[IUser]](scope1.ServiceProvider)
- finally:
- scope1.Dispose()
-
- try:
- scope2 = ServiceProviderServiceExtensions.CreateScope(provider)
- lastScopeHook = hookFactory[0].Create[IContentMapping[IUser]](scope2.ServiceProvider)
- finally:
- scope2.Dispose()
-
- # Assert
- assert hook == firstScopeHook1
- assert hook is not firstScopeHook1
- assert hook == firstScopeHook2
- assert hook is not firstScopeHook2
- assert hook == lastScopeHook
- assert hook is not lastScopeHook
- finally:
- provider.Dispose()
-
- def test_lifetime_buildandcreate_noinitializersingleton(self, skip_by_python_lifetime_env_var):
- try:
- # Arrange
- servicecollection = ServiceCollection()
- ServiceCollectionServiceExtensions.AddSingleton[TestContentMappingBuilderLifetimeTests.SubMappingImplementation](servicecollection)
- provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(servicecollection)
- hookFactory = PyContentMappingBuilder(ContentMappingBuilder()).add(TestContentMappingBuilderLifetimeTests.SubMappingImplementation,IUser).build().get_hooks(IContentMapping[IUser])
-
- assert len(hookFactory) == 1
-
- # Act
- try:
- scope1 = ServiceProviderServiceExtensions.CreateScope(provider)
- firstScopeHook1 = hookFactory[0].Create[IContentMapping[IUser]](scope1.ServiceProvider)
- firstScopeHook2 = hookFactory[0].Create[IContentMapping[IUser]](scope1.ServiceProvider)
- finally:
- scope1.Dispose()
-
- try:
- scope2 = ServiceProviderServiceExtensions.CreateScope(provider)
- lastScopeHook = hookFactory[0].Create[IContentMapping[IUser]](scope2.ServiceProvider)
- finally:
- scope2.Dispose()
-
- # Assert
- assert firstScopeHook1 == firstScopeHook2
- assert firstScopeHook1 is not firstScopeHook2
- assert firstScopeHook1 == lastScopeHook
- assert firstScopeHook1 is not lastScopeHook
- finally:
- provider.Dispose()
-
- def test_lifetime_buildandcreate_noinitializerscoped(self, skip_by_python_lifetime_env_var):
- try:
- # Arrange
- servicecollection = ServiceCollection()
- ServiceCollectionServiceExtensions.AddScoped[TestContentMappingBuilderLifetimeTests.SubMappingImplementation](servicecollection)
- provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(servicecollection)
- hookFactory = PyContentMappingBuilder(ContentMappingBuilder()).add(TestContentMappingBuilderLifetimeTests.SubMappingImplementation,IUser).build().get_hooks(IContentMapping[IUser])
-
- assert len(hookFactory) == 1
-
- # Act
- try:
- scope1 = ServiceProviderServiceExtensions.CreateScope(provider)
- firstScopeHook1 = hookFactory[0].Create[IContentMapping[IUser]](scope1.ServiceProvider)
- firstScopeHook2 = hookFactory[0].Create[IContentMapping[IUser]](scope1.ServiceProvider)
- finally:
- scope1.Dispose()
-
- try:
- scope2 = ServiceProviderServiceExtensions.CreateScope(provider)
- lastScopeHook = hookFactory[0].Create[IContentMapping[IUser]](scope2.ServiceProvider)
- finally:
- scope2.Dispose()
-
- # Assert
- assert firstScopeHook1 == firstScopeHook2
- assert firstScopeHook1 is not firstScopeHook2
- assert firstScopeHook1 != lastScopeHook
- finally:
- provider.Dispose()
-
- def test_lifetime_buildandcreate_noinitializertransient(self, skip_by_python_lifetime_env_var):
- try:
- # Arrange
- servicecollection = ServiceCollection()
- ServiceCollectionServiceExtensions.AddTransient[TestContentMappingBuilderLifetimeTests.SubMappingImplementation](servicecollection)
- provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(servicecollection)
- hookFactory = PyContentMappingBuilder(ContentMappingBuilder()).add(TestContentMappingBuilderLifetimeTests.SubMappingImplementation,IUser).build().get_hooks(IContentMapping[IUser])
-
- assert len(hookFactory) == 1
-
- # Act
- try:
- scope1 = ServiceProviderServiceExtensions.CreateScope(provider)
- firstScopeHook1 = hookFactory[0].Create[IContentMapping[IUser]](scope1.ServiceProvider)
- firstScopeHook2 = hookFactory[0].Create[IContentMapping[IUser]](scope1.ServiceProvider)
- finally:
- scope1.Dispose()
-
- try:
- scope2 = ServiceProviderServiceExtensions.CreateScope(provider)
- lastScopeHook = hookFactory[0].Create[IContentMapping[IUser]](scope2.ServiceProvider)
- finally:
- scope2.Dispose()
-
- # Assert
- assert firstScopeHook1 != firstScopeHook2
- assert firstScopeHook1 != lastScopeHook
- finally:
- provider.Dispose()
-
- def test_lifetime_buildandcreate_initializerobjectreference(self, skip_by_python_lifetime_env_var):
- try:
- # Arrange
- hook = TestContentMappingBuilderLifetimeTests.SubMappingImplementation()
- def subsubclassinitialize(provider: IServiceProvider):
- return hook
- provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(ServiceCollection())
- hookFactory = PyContentMappingBuilder(ContentMappingBuilder()).add(TestContentMappingBuilderLifetimeTests.SubMappingImplementation, IUser, Func[IServiceProvider, TestContentMappingBuilderLifetimeTests.SubMappingImplementation](subsubclassinitialize)).build().get_hooks(IContentMapping[IUser])
-
- assert len(hookFactory) == 1
-
- # Act
- try:
- scope1 = ServiceProviderServiceExtensions.CreateScope(provider)
- firstScopeHook1 = hookFactory[0].Create[IContentMapping[IUser]](scope1.ServiceProvider)
- firstScopeHook2 = hookFactory[0].Create[IContentMapping[IUser]](scope1.ServiceProvider)
- finally:
- scope1.Dispose()
-
- try:
- scope2 = ServiceProviderServiceExtensions.CreateScope(provider)
- lastScopeHook = hookFactory[0].Create[IContentMapping[IUser]](scope2.ServiceProvider)
- finally:
- scope2.Dispose()
-
- # Assert
- assert firstScopeHook1 == firstScopeHook2
- assert firstScopeHook1 is not firstScopeHook2
- assert firstScopeHook1 == lastScopeHook
- assert firstScopeHook1 is not lastScopeHook
- finally:
- provider.Dispose()
-
- def test_lifetime_buildandcreate_initializernewobject(self, skip_by_python_lifetime_env_var):
- try:
- # Arrange
- def subsubclassinitialize(provider: IServiceProvider):
- return TestContentMappingBuilderLifetimeTests.SubMappingImplementation()
- provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(ServiceCollection())
- hookFactory = PyContentMappingBuilder(ContentMappingBuilder()).add(TestContentMappingBuilderLifetimeTests.SubMappingImplementation, IUser, Func[IServiceProvider, TestContentMappingBuilderLifetimeTests.SubMappingImplementation](subsubclassinitialize)).build().get_hooks(IContentMapping[IUser])
-
- assert len(hookFactory) == 1
-
- # Act
- try:
- scope1 = ServiceProviderServiceExtensions.CreateScope(provider)
- firstScopeHook1 = hookFactory[0].Create[IContentMapping[IUser]](scope1.ServiceProvider)
- firstScopeHook2 = hookFactory[0].Create[IContentMapping[IUser]](scope1.ServiceProvider)
- finally:
- scope1.Dispose()
-
- try:
- scope2 = ServiceProviderServiceExtensions.CreateScope(provider)
- lastScopeHook = hookFactory[0].Create[IContentMapping[IUser]](scope2.ServiceProvider)
- finally:
- scope2.Dispose()
-
- # Assert
- assert firstScopeHook1 != firstScopeHook2
- assert firstScopeHook1 != lastScopeHook
- finally:
- provider.Dispose()
-
- def test_lifetime_buildandcreate_initializersingleton(self, skip_by_python_lifetime_env_var):
- try:
- # Arrange
- def subsubclassinitialize(provider: IServiceProvider):
- return get_service(provider,TestContentMappingBuilderLifetimeTests.SubMappingImplementation)
- servicecollection = ServiceCollection()
- ServiceCollectionServiceExtensions.AddSingleton[TestContentMappingBuilderLifetimeTests.SubMappingImplementation](servicecollection)
- provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(servicecollection)
- hookFactory = PyContentMappingBuilder(ContentMappingBuilder()).add(TestContentMappingBuilderLifetimeTests.SubMappingImplementation, IUser, Func[IServiceProvider, TestContentMappingBuilderLifetimeTests.SubMappingImplementation](subsubclassinitialize)).build().get_hooks(IContentMapping[IUser])
-
- assert len(hookFactory) == 1
-
- # Act
- try:
- scope1 = ServiceProviderServiceExtensions.CreateScope(provider)
- firstScopeHook1 = hookFactory[0].Create[IContentMapping[IUser]](scope1.ServiceProvider)
- firstScopeHook2 = hookFactory[0].Create[IContentMapping[IUser]](scope1.ServiceProvider)
- finally:
- scope1.Dispose()
-
- try:
- scope2 = ServiceProviderServiceExtensions.CreateScope(provider)
- lastScopeHook = hookFactory[0].Create[IContentMapping[IUser]](scope2.ServiceProvider)
- finally:
- scope2.Dispose()
-
- # Assert
- assert firstScopeHook1 == firstScopeHook2
- assert firstScopeHook1 is not firstScopeHook2
- assert firstScopeHook1 == lastScopeHook
- assert firstScopeHook1 is not lastScopeHook
- finally:
- provider.Dispose()
-
- def test_lifetime_buildandcreate_initializerscoped(self, skip_by_python_lifetime_env_var):
- try:
- # Arrange
- def classinitialize(provider: IServiceProvider):
- return get_service(provider,TestContentMappingBuilderLifetimeTests.MappingImplementation)
- servicecollection = ServiceCollection()
- ServiceCollectionServiceExtensions.AddScoped[TestContentMappingBuilderLifetimeTests.MappingImplementation](servicecollection)
- provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(servicecollection)
- hookFactory = PyContentMappingBuilder(ContentMappingBuilder()).add(TestContentMappingBuilderLifetimeTests.MappingImplementation, IUser, Func[IServiceProvider, TestContentMappingBuilderLifetimeTests.MappingImplementation](classinitialize)).build().get_hooks(IContentMapping[IUser])
-
- assert len(hookFactory) == 1
-
- # Act
- try:
- scope1 = ServiceProviderServiceExtensions.CreateScope(provider)
- firstScopeHook1 = hookFactory[0].Create[IContentMapping[IUser]](scope1.ServiceProvider)
- firstScopeHook2 = hookFactory[0].Create[IContentMapping[IUser]](scope1.ServiceProvider)
- finally:
- scope1.Dispose()
-
- try:
- scope2 = ServiceProviderServiceExtensions.CreateScope(provider)
- lastScopeHook = hookFactory[0].Create[IContentMapping[IUser]](scope2.ServiceProvider)
- finally:
- scope2.Dispose()
-
- # Assert
- assert firstScopeHook1 == firstScopeHook2
- assert firstScopeHook1 is not firstScopeHook2
- assert firstScopeHook1 != lastScopeHook
- finally:
- provider.Dispose()
-
- def test_lifetime_buildandcreate_initializertransient(self, skip_by_python_lifetime_env_var):
- try:
- # Arrange
- def subclassinitialize(provider: IServiceProvider):
- return get_service(provider,TestContentMappingBuilderLifetimeTests.SubMappingImplementation)
- servicecollection = ServiceCollection()
- ServiceCollectionServiceExtensions.AddTransient[TestContentMappingBuilderLifetimeTests.SubMappingImplementation](servicecollection)
- provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(servicecollection)
- hookFactory = PyContentMappingBuilder(ContentMappingBuilder()).add(TestContentMappingBuilderLifetimeTests.SubMappingImplementation, IUser, Func[IServiceProvider, TestContentMappingBuilderLifetimeTests.SubMappingImplementation](subclassinitialize)).build().get_hooks(IContentMapping[IUser])
-
- assert len(hookFactory) == 1
-
- # Act
- try:
- scope1 = ServiceProviderServiceExtensions.CreateScope(provider)
- firstScopeHook1 = hookFactory[0].Create[IContentMapping[IUser]](scope1.ServiceProvider)
- firstScopeHook2 = hookFactory[0].Create[IContentMapping[IUser]](scope1.ServiceProvider)
- finally:
- scope1.Dispose()
-
- try:
- scope2 = ServiceProviderServiceExtensions.CreateScope(provider)
- lastScopeHook = hookFactory[0].Create[IContentMapping[IUser]](scope2.ServiceProvider)
- finally:
- scope2.Dispose()
-
- # Assert
- assert firstScopeHook1 != firstScopeHook2
- assert firstScopeHook1 != lastScopeHook
- finally:
- provider.Dispose()
-
- def test_lifetime_buildandcreate_callback(self, skip_by_python_lifetime_env_var):
- try:
- # Arrange
- def classcallback(context: ContentMappingContext[IProject]):
- return context
-
- provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(ServiceCollection())
- hookFactory = PyContentMappingBuilder(ContentMappingBuilder()).add(IProject, Func[ContentMappingContext[IProject], ContentMappingContext[IProject]](classcallback)).build().get_hooks(IContentMapping[IProject])
-
- assert len(hookFactory) == 1
-
- # Act
- try:
- scope1 = ServiceProviderServiceExtensions.CreateScope(provider)
- firstScopeHook1 = hookFactory[0].Create[IMigrationHook[ContentMappingContext[IProject]]](scope1.ServiceProvider)
- firstScopeHook2 = hookFactory[0].Create[IMigrationHook[ContentMappingContext[IProject]]](scope1.ServiceProvider)
- finally:
- scope1.Dispose()
-
- try:
- scope2 = ServiceProviderServiceExtensions.CreateScope(provider)
- lastScopeHook = hookFactory[0].Create[IMigrationHook[ContentMappingContext[IProject]]](scope2.ServiceProvider)
- finally:
- scope2.Dispose()
-
- # Assert
- assert firstScopeHook1 != firstScopeHook2
- assert firstScopeHook1 != lastScopeHook
- finally:
- provider.Dispose()
diff --git a/src/Python/tests/test_migrations_engine_hooks_transformers.py b/src/Python/tests/test_migrations_engine_hooks_transformers.py
deleted file mode 100644
index e908235..0000000
--- a/src/Python/tests/test_migrations_engine_hooks_transformers.py
+++ /dev/null
@@ -1,258 +0,0 @@
-# Copyright (c) 2024, Salesforce, Inc.
-# SPDX-License-Identifier: Apache-2
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-# Make sure the test can find the module
-from tableau_migration.migration import get_service
-from tableau_migration.migration_engine_hooks_transformers import PyContentTransformerBuilder
-from System import IServiceProvider, Func, ArgumentException, NotImplementedException
-from System.Threading import CancellationToken
-from Microsoft.Extensions.DependencyInjection import (
- ServiceCollection,
- ServiceCollectionContainerBuilderExtensions)
-from Tableau.Migration.Content import IUser, IProject, IWorkbook
-from Tableau.Migration.Engine.Hooks import IMigrationHook
-from Tableau.Migration.Engine.Hooks.Transformers import ContentTransformerBuilder, IContentTransformer, ContentTransformerBase
-from Tableau.Migration.Interop.Hooks.Transformers import ISyncContentTransformer, ISyncXmlContentTransformer
-from Tableau.Migration.Tests import TestFileContentType as PyTestFileContentType # Needed as this class name starts with Test, which means pytest wants to pick it up
-
-class ClassImplementation(ISyncContentTransformer[IUser]):
- __namespace__ = "Tableau.Migration.Custom.Hooks.Transformers"
-
- def Execute(self, ctx: IUser) -> IUser:
- return ctx
-
-class SubClassImplementation(ClassImplementation):
- __namespace__ = "Tableau.Migration.Custom.Hooks.Transformers"
-
- def Execute(self, ctx: IUser) -> IUser:
- return ctx
-
-class RawHook(IMigrationHook[float]):
- __namespace__ = "Tableau.Migration.Custom.Hooks.Transformers"
-
-class WithoutImplementation(ISyncContentTransformer[str]):
- __namespace__ = "Tableau.Migration.Custom.Hooks.Transformers"
-
-class WithImplementation(ISyncContentTransformer[str]):
- __namespace__ = "Tableau.Migration.Custom.Hooks.Transformers"
-
- def Execute(self, ctx: str) -> str:
- return ctx
-
-class TestXmlTransformer(ISyncXmlContentTransformer[PyTestFileContentType]):
- __namespace__ = "Tableau.Migration.Custom.Hooks.Transformers"
- __test__ = False # Needed as this class name starts with Test, which means pytest wants to pick it up
-
- def __init__(self):
- self.called = False
-
- def NeedsXmlTransforming(self, ctx: PyTestFileContentType) -> bool:
- return True
-
- def Execute(self, ctx: PyTestFileContentType, xml) -> None:
- self.called = True
-
-class TestContentTransformerBuilderTests():
- def test_clear_class_hook(self):
- hook_builder = PyContentTransformerBuilder(ContentTransformerBuilder())
-
- result = hook_builder.add(IUser,ClassImplementation()).clear()
-
- assert result is hook_builder
- assert len(result.build().get_hooks(ISyncContentTransformer[IUser])) == 0
-
- def test_clear_subclass_hook(self):
- hook_builder = PyContentTransformerBuilder(ContentTransformerBuilder())
-
- result = hook_builder.add(IUser,SubClassImplementation()).clear()
-
- assert result is hook_builder
- assert len(result.build().get_hooks(ISyncContentTransformer[IUser])) == 0
-
- def test_add_object(self):
- hook_builder = PyContentTransformerBuilder(ContentTransformerBuilder())
-
- result = hook_builder.add(IUser,SubClassImplementation())
-
- assert result is hook_builder
-
- def test_add_type_noinitializer(self):
- hook_builder = PyContentTransformerBuilder(ContentTransformerBuilder())
-
- result = hook_builder.add(ClassImplementation,IUser)
-
- assert result is hook_builder
-
- def test_add_type_initializer(self):
- hook_builder = PyContentTransformerBuilder(ContentTransformerBuilder())
-
- def subclassinitialize(provider: IServiceProvider):
- return get_service(provider, SubClassImplementation)
-
- result = hook_builder.add(SubClassImplementation, IUser, Func[IServiceProvider, SubClassImplementation](subclassinitialize))
-
- assert result is hook_builder
-
- def test_add_callback(self):
- hook_builder = PyContentTransformerBuilder(ContentTransformerBuilder())
-
- def classcallback(context: IProject):
- return context
-
- result = hook_builder.add(IProject, Func[IProject, IProject](classcallback))
-
- assert result is hook_builder
-
- def test_add_withoutimplementation(self):
- value = "testvalue";
- provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(ServiceCollection())
- hookFactory = PyContentTransformerBuilder(ContentTransformerBuilder()).add(str,WithoutImplementation()).build().get_hooks(IContentTransformer[str])
- assert len(hookFactory) == 1
- hook = hookFactory[0].Create[IMigrationHook[str]](provider)
- try:
- hook.ExecuteAsync(value, CancellationToken(False))
- except NotImplementedException as nie:
- assert nie.args[0] == "Python object does not have a 'Execute' method"
- else:
- assert False, "This test must generate a NotImplementedException."
- finally:
- provider.Dispose()
-
- def test_add_withimplementation(self):
- value = "testvalue";
- provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(ServiceCollection())
- hookFactory = PyContentTransformerBuilder(ContentTransformerBuilder()).add(str,WithImplementation()).build().get_hooks(IContentTransformer[str])
- assert len(hookFactory) == 1
- hook = hookFactory[0].Create[IMigrationHook[str]](provider)
- try:
- result = hook.ExecuteAsync(value, CancellationToken(False))
- except:
- assert False, "This test must not generate an Exception."
- finally:
- provider.Dispose()
-
- def test_build_detects_interface_from_concrete_class(self):
- collection = PyContentTransformerBuilder(ContentTransformerBuilder()).add(IUser,ClassImplementation()).build()
- hooks = collection.get_hooks(IContentTransformer[IUser])
- internalhooks = collection.get_hooks(IMigrationHook[IUser])
- otherhooks = collection.get_hooks(IContentTransformer[IProject])
- classhooks = collection.get_hooks(ClassImplementation)
- subclasshooks = collection.get_hooks(SubClassImplementation)
-
- # Can't do a direct object comparison, so let's just count the right number is returned
- assert len(hooks) == 1
- assert len(internalhooks) == 0
- assert len(otherhooks) == 0
- assert len(classhooks) == 0
- assert len(subclasshooks) == 0
-
- def test_build_detects_interface_from_concrete_subclass(self):
- collection = PyContentTransformerBuilder(ContentTransformerBuilder()).add(IUser,SubClassImplementation()).build()
- hooks = collection.get_hooks(IContentTransformer[IUser])
- internalhooks = collection.get_hooks(IMigrationHook[IUser])
- otherhooks = collection.get_hooks(IContentTransformer[IProject])
- classhooks = collection.get_hooks(ClassImplementation)
- subclasshooks = collection.get_hooks(SubClassImplementation)
-
- # Can't do a direct object comparison, so let's just count the right number is returned
- assert len(hooks) == 1
- assert len(internalhooks) == 0
- assert len(otherhooks) == 0
- assert len(classhooks) == 0
- assert len(subclasshooks) == 0
-
- def test_build_factory_class(self):
- collection = PyContentTransformerBuilder(ContentTransformerBuilder()).add(ClassImplementation,IUser).build()
- hooks = collection.get_hooks(IContentTransformer[IUser])
- internalhooks = collection.get_hooks(IMigrationHook[IUser])
- otherhooks = collection.get_hooks(IContentTransformer[IProject])
- classhooks = collection.get_hooks(ClassImplementation)
- subclasshooks = collection.get_hooks(SubClassImplementation)
-
- # Can't do a direct object comparison, so let's just count the right number is returned
- assert len(hooks) == 1
- assert len(internalhooks) == 0
- assert len(otherhooks) == 0
- assert len(classhooks) == 0
- assert len(subclasshooks) == 0
-
- def test_build_factory_subclass_withinitializer(self):
- def subclassinitialize(provider: IServiceProvider):
- return get_service(provider, SubClassImplementation)
-
- collection = PyContentTransformerBuilder(ContentTransformerBuilder()).add(SubClassImplementation, IUser, Func[IServiceProvider, SubClassImplementation](subclassinitialize)).build()
- hooks = collection.get_hooks(IContentTransformer[IUser])
- internalhooks = collection.get_hooks(IMigrationHook[IUser])
- otherhooks = collection.get_hooks(IContentTransformer[IProject])
- classhooks = collection.get_hooks(ClassImplementation)
- subclasshooks = collection.get_hooks(SubClassImplementation)
-
- # Can't do a direct object comparison, so let's just count the right number is returned
- assert len(hooks) == 1
- assert len(internalhooks) == 0
- assert len(otherhooks) == 0
- assert len(classhooks) == 0
- assert len(subclasshooks) == 0
-
- def test_build_factory_multipleclasses(self):
- collection = PyContentTransformerBuilder(ContentTransformerBuilder()).add(ClassImplementation,IUser).add(SubClassImplementation,IUser).build()
- hooks = collection.get_hooks(IContentTransformer[IUser])
- internalhooks = collection.get_hooks(IMigrationHook[IUser])
- otherhooks = collection.get_hooks(IContentTransformer[IProject])
- classhooks = collection.get_hooks(ClassImplementation)
- subclasshooks = collection.get_hooks(SubClassImplementation)
-
- # Can't do a direct object comparison, so let's just count the right number is returned
- assert len(hooks) == 2
- assert len(internalhooks) == 0
- assert len(otherhooks) == 0
- assert len(classhooks) == 0
- assert len(subclasshooks) == 0
-
- def test_build_callback(self):
- def classcallback(context: IProject):
- return context
-
- collection = PyContentTransformerBuilder(ContentTransformerBuilder()).add(IProject, Func[IProject, IProject](classcallback)).build()
- hooks = collection.get_hooks(IContentTransformer[IProject])
- internalhooks = collection.get_hooks(IMigrationHook[IProject])
- otherhooks = collection.get_hooks(IContentTransformer[IUser])
- classhooks = collection.get_hooks(ClassImplementation)
- subclasshooks = collection.get_hooks(SubClassImplementation)
-
- # Can't do a direct object comparison, so let's just count the right number is returned
- assert len(hooks) == 1
- assert len(internalhooks) == 0
- assert len(otherhooks) == 0
- assert len(classhooks) == 0
- assert len(subclasshooks) == 0
-
- def test_xml_execute(self):
-
- content = PyTestFileContentType()
-
- provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(ServiceCollection())
- pyTransformer = TestXmlTransformer()
- hookFactory = PyContentTransformerBuilder(ContentTransformerBuilder()).add(PyTestFileContentType, pyTransformer).build().get_hooks(IContentTransformer[PyTestFileContentType])
- assert len(hookFactory) == 1
- hook = hookFactory[0].Create[IMigrationHook[PyTestFileContentType]](provider)
- try:
- result = hook.ExecuteAsync(content, CancellationToken(False)).GetAwaiter().GetResult()
- except Exception:
- assert False, "This test must not generate an Exception."
- finally:
- provider.Dispose()
-
- assert pyTransformer.called
diff --git a/src/Python/tests/test_migrations_engine_hooks_transformers_lifetime.py b/src/Python/tests/test_migrations_engine_hooks_transformers_lifetime.py
deleted file mode 100644
index 002b870..0000000
--- a/src/Python/tests/test_migrations_engine_hooks_transformers_lifetime.py
+++ /dev/null
@@ -1,372 +0,0 @@
-# Copyright (c) 2024, Salesforce, Inc.
-# SPDX-License-Identifier: Apache-2
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-# Make sure the test can find the module
-import pytest
-from tableau_migration.migration import get_service
-from tableau_migration.migration_engine_hooks_transformers import PyContentTransformerBuilder
-from System import IServiceProvider, Func
-from Microsoft.Extensions.DependencyInjection import (
- ServiceProviderServiceExtensions,
- ServiceCollectionServiceExtensions,
- ServiceCollection,
- ServiceCollectionContainerBuilderExtensions)
-from Tableau.Migration.Content import IUser, IProject
-from Tableau.Migration.Engine.Hooks import IMigrationHook
-from Tableau.Migration.Engine.Hooks.Transformers import ContentTransformerBuilder, IContentTransformer
-from Tableau.Migration.Interop.Hooks.Transformers import ISyncContentTransformer
-
-class TransformerImplementation(ISyncContentTransformer[IUser]):
- __namespace__ = "Tableau.Migration.Custom.Hooks.Transformers.Lifetime"
-
- def Execute(self, ctx: IUser) -> IUser:
- return ctx
-
-class SubTransformerImplementation(TransformerImplementation):
- __namespace__ = "Tableau.Migration.Custom.Hooks.Transformers.Lifetime"
-
- def Execute(self, ctx: IUser) -> IUser:
- return ctx
-
-class TestContentTransformerBuilderLifetimeTests():
- @pytest.fixture(autouse=True, scope="class")
- def setup(self):
- TestContentTransformerBuilderLifetimeTests.TransformerImplementation = TransformerImplementation
- TestContentTransformerBuilderLifetimeTests.SubTransformerImplementation = SubTransformerImplementation
-
- yield
-
- del TestContentTransformerBuilderLifetimeTests.SubTransformerImplementation
- del TestContentTransformerBuilderLifetimeTests.TransformerImplementation
-
-
- def test_lifetime_buildandcreate_object(self, skip_by_python_lifetime_env_var):
- try:
- # Arrange
- provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(ServiceCollection())
- hook = TestContentTransformerBuilderLifetimeTests.SubTransformerImplementation()
- hookFactory = PyContentTransformerBuilder(ContentTransformerBuilder()).add(IUser,hook).build().get_hooks(IContentTransformer[IUser])
-
- assert len(hookFactory) == 1
-
- # Act
- try:
- scope1 = ServiceProviderServiceExtensions.CreateScope(provider)
- firstScopeHook1 = hookFactory[0].Create[IContentTransformer[IUser]](scope1.ServiceProvider)
- firstScopeHook2 = hookFactory[0].Create[IContentTransformer[IUser]](scope1.ServiceProvider)
- finally:
- scope1.Dispose()
-
- try:
- scope2 = ServiceProviderServiceExtensions.CreateScope(provider)
- lastScopeHook = hookFactory[0].Create[IContentTransformer[IUser]](scope2.ServiceProvider)
- finally:
- scope2.Dispose()
-
- # Assert
- assert hook == firstScopeHook1
- assert hook is not firstScopeHook1
- assert hook == firstScopeHook2
- assert hook is not firstScopeHook2
- assert hook == lastScopeHook
- assert hook is not lastScopeHook
- finally:
- provider.Dispose()
-
- def test_lifetime_buildandcreate_noinitializersingleton(self, skip_by_python_lifetime_env_var):
- try:
- # Arrange
- servicecollection = ServiceCollection()
- ServiceCollectionServiceExtensions.AddSingleton[TestContentTransformerBuilderLifetimeTests.SubTransformerImplementation](servicecollection)
- provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(servicecollection)
- hookFactory = PyContentTransformerBuilder(ContentTransformerBuilder()).add(TestContentTransformerBuilderLifetimeTests.SubTransformerImplementation,IUser).build().get_hooks(IContentTransformer[IUser])
-
- assert len(hookFactory) == 1
-
- # Act
- try:
- scope1 = ServiceProviderServiceExtensions.CreateScope(provider)
- firstScopeHook1 = hookFactory[0].Create[IContentTransformer[IUser]](scope1.ServiceProvider)
- firstScopeHook2 = hookFactory[0].Create[IContentTransformer[IUser]](scope1.ServiceProvider)
- finally:
- scope1.Dispose()
-
- try:
- scope2 = ServiceProviderServiceExtensions.CreateScope(provider)
- lastScopeHook = hookFactory[0].Create[IContentTransformer[IUser]](scope2.ServiceProvider)
- finally:
- scope2.Dispose()
-
- # Assert
- assert firstScopeHook1 == firstScopeHook2
- assert firstScopeHook1 is not firstScopeHook2
- assert firstScopeHook1 == lastScopeHook
- assert firstScopeHook1 is not lastScopeHook
- finally:
- provider.Dispose()
-
- def test_lifetime_buildandcreate_noinitializerscoped(self, skip_by_python_lifetime_env_var):
- try:
- # Arrange
- servicecollection = ServiceCollection()
- ServiceCollectionServiceExtensions.AddScoped[TestContentTransformerBuilderLifetimeTests.SubTransformerImplementation](servicecollection)
- provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(servicecollection)
- hookFactory = PyContentTransformerBuilder(ContentTransformerBuilder()).add(TestContentTransformerBuilderLifetimeTests.SubTransformerImplementation,IUser).build().get_hooks(IContentTransformer[IUser])
-
- assert len(hookFactory) == 1
-
- # Act
- try:
- scope1 = ServiceProviderServiceExtensions.CreateScope(provider)
- firstScopeHook1 = hookFactory[0].Create[IContentTransformer[IUser]](scope1.ServiceProvider)
- firstScopeHook2 = hookFactory[0].Create[IContentTransformer[IUser]](scope1.ServiceProvider)
- finally:
- scope1.Dispose()
-
- try:
- scope2 = ServiceProviderServiceExtensions.CreateScope(provider)
- lastScopeHook = hookFactory[0].Create[IContentTransformer[IUser]](scope2.ServiceProvider)
- finally:
- scope2.Dispose()
-
- # Assert
- assert firstScopeHook1 == firstScopeHook2
- assert firstScopeHook1 is not firstScopeHook2
- assert firstScopeHook1 != lastScopeHook
- finally:
- provider.Dispose()
-
- def test_lifetime_buildandcreate_noinitializertransient(self, skip_by_python_lifetime_env_var):
- try:
- # Arrange
- servicecollection = ServiceCollection()
- ServiceCollectionServiceExtensions.AddTransient[TestContentTransformerBuilderLifetimeTests.SubTransformerImplementation](servicecollection)
- provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(servicecollection)
- hookFactory = PyContentTransformerBuilder(ContentTransformerBuilder()).add(TestContentTransformerBuilderLifetimeTests.SubTransformerImplementation,IUser).build().get_hooks(IContentTransformer[IUser])
-
- assert len(hookFactory) == 1
-
- # Act
- try:
- scope1 = ServiceProviderServiceExtensions.CreateScope(provider)
- firstScopeHook1 = hookFactory[0].Create[IContentTransformer[IUser]](scope1.ServiceProvider)
- firstScopeHook2 = hookFactory[0].Create[IContentTransformer[IUser]](scope1.ServiceProvider)
- finally:
- scope1.Dispose()
-
- try:
- scope2 = ServiceProviderServiceExtensions.CreateScope(provider)
- lastScopeHook = hookFactory[0].Create[IContentTransformer[IUser]](scope2.ServiceProvider)
- finally:
- scope2.Dispose()
-
- # Assert
- assert firstScopeHook1 != firstScopeHook2
- assert firstScopeHook1 != lastScopeHook
- finally:
- provider.Dispose()
-
- def test_lifetime_buildandcreate_initializerobjectreference(self, skip_by_python_lifetime_env_var):
- try:
- # Arrange
- hook = TestContentTransformerBuilderLifetimeTests.SubTransformerImplementation()
- def subsubclassinitialize(provider: IServiceProvider):
- return hook
- provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(ServiceCollection())
- hookFactory = PyContentTransformerBuilder(ContentTransformerBuilder()).add(TestContentTransformerBuilderLifetimeTests.SubTransformerImplementation, IUser, Func[IServiceProvider, TestContentTransformerBuilderLifetimeTests.SubTransformerImplementation](subsubclassinitialize)).build().get_hooks(IContentTransformer[IUser])
-
- assert len(hookFactory) == 1
-
- # Act
- try:
- scope1 = ServiceProviderServiceExtensions.CreateScope(provider)
- firstScopeHook1 = hookFactory[0].Create[IContentTransformer[IUser]](scope1.ServiceProvider)
- firstScopeHook2 = hookFactory[0].Create[IContentTransformer[IUser]](scope1.ServiceProvider)
- finally:
- scope1.Dispose()
-
- try:
- scope2 = ServiceProviderServiceExtensions.CreateScope(provider)
- lastScopeHook = hookFactory[0].Create[IContentTransformer[IUser]](scope2.ServiceProvider)
- finally:
- scope2.Dispose()
-
- # Assert
- assert firstScopeHook1 == firstScopeHook2
- assert firstScopeHook1 is not firstScopeHook2
- assert firstScopeHook1 == lastScopeHook
- assert firstScopeHook1 is not lastScopeHook
- finally:
- provider.Dispose()
-
- def test_lifetime_buildandcreate_initializernewobject(self, skip_by_python_lifetime_env_var):
- try:
- # Arrange
- def subsubclassinitialize(provider: IServiceProvider):
- return TestContentTransformerBuilderLifetimeTests.SubTransformerImplementation()
- provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(ServiceCollection())
- hookFactory = PyContentTransformerBuilder(ContentTransformerBuilder()).add(TestContentTransformerBuilderLifetimeTests.SubTransformerImplementation, IUser, Func[IServiceProvider, TestContentTransformerBuilderLifetimeTests.SubTransformerImplementation](subsubclassinitialize)).build().get_hooks(IContentTransformer[IUser])
-
- assert len(hookFactory) == 1
-
- # Act
- try:
- scope1 = ServiceProviderServiceExtensions.CreateScope(provider)
- firstScopeHook1 = hookFactory[0].Create[IContentTransformer[IUser]](scope1.ServiceProvider)
- firstScopeHook2 = hookFactory[0].Create[IContentTransformer[IUser]](scope1.ServiceProvider)
- finally:
- scope1.Dispose()
-
- try:
- scope2 = ServiceProviderServiceExtensions.CreateScope(provider)
- lastScopeHook = hookFactory[0].Create[IContentTransformer[IUser]](scope2.ServiceProvider)
- finally:
- scope2.Dispose()
-
- # Assert
- assert firstScopeHook1 != firstScopeHook2
- assert firstScopeHook1 != lastScopeHook
- finally:
- provider.Dispose()
-
- def test_lifetime_buildandcreate_initializersingleton(self, skip_by_python_lifetime_env_var):
- try:
- # Arrange
- def subsubclassinitialize(provider: IServiceProvider):
- return get_service(provider,TestContentTransformerBuilderLifetimeTests.SubTransformerImplementation)
- servicecollection = ServiceCollection()
- ServiceCollectionServiceExtensions.AddSingleton[TestContentTransformerBuilderLifetimeTests.SubTransformerImplementation](servicecollection)
- provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(servicecollection)
- hookFactory = PyContentTransformerBuilder(ContentTransformerBuilder()).add(TestContentTransformerBuilderLifetimeTests.SubTransformerImplementation, IUser, Func[IServiceProvider, TestContentTransformerBuilderLifetimeTests.SubTransformerImplementation](subsubclassinitialize)).build().get_hooks(IContentTransformer[IUser])
-
- assert len(hookFactory) == 1
-
- # Act
- try:
- scope1 = ServiceProviderServiceExtensions.CreateScope(provider)
- firstScopeHook1 = hookFactory[0].Create[IContentTransformer[IUser]](scope1.ServiceProvider)
- firstScopeHook2 = hookFactory[0].Create[IContentTransformer[IUser]](scope1.ServiceProvider)
- finally:
- scope1.Dispose()
-
- try:
- scope2 = ServiceProviderServiceExtensions.CreateScope(provider)
- lastScopeHook = hookFactory[0].Create[IContentTransformer[IUser]](scope2.ServiceProvider)
- finally:
- scope2.Dispose()
-
- # Assert
- assert firstScopeHook1 == firstScopeHook2
- assert firstScopeHook1 is not firstScopeHook2
- assert firstScopeHook1 == lastScopeHook
- assert firstScopeHook1 is not lastScopeHook
- finally:
- provider.Dispose()
-
- def test_lifetime_buildandcreate_initializerscoped(self, skip_by_python_lifetime_env_var):
- try:
- # Arrange
- def classinitialize(provider: IServiceProvider):
- return get_service(provider,TestContentTransformerBuilderLifetimeTests.TransformerImplementation)
- servicecollection = ServiceCollection()
- ServiceCollectionServiceExtensions.AddScoped[TestContentTransformerBuilderLifetimeTests.TransformerImplementation](servicecollection)
- provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(servicecollection)
- hookFactory = PyContentTransformerBuilder(ContentTransformerBuilder()).add(TestContentTransformerBuilderLifetimeTests.TransformerImplementation, IUser, Func[IServiceProvider, TestContentTransformerBuilderLifetimeTests.TransformerImplementation](classinitialize)).build().get_hooks(IContentTransformer[IUser])
-
- assert len(hookFactory) == 1
-
- # Act
- try:
- scope1 = ServiceProviderServiceExtensions.CreateScope(provider)
- firstScopeHook1 = hookFactory[0].Create[IContentTransformer[IUser]](scope1.ServiceProvider)
- firstScopeHook2 = hookFactory[0].Create[IContentTransformer[IUser]](scope1.ServiceProvider)
- finally:
- scope1.Dispose()
-
- try:
- scope2 = ServiceProviderServiceExtensions.CreateScope(provider)
- lastScopeHook = hookFactory[0].Create[IContentTransformer[IUser]](scope2.ServiceProvider)
- finally:
- scope2.Dispose()
-
- # Assert
- assert firstScopeHook1 == firstScopeHook2
- assert firstScopeHook1 is not firstScopeHook2
- assert firstScopeHook1 != lastScopeHook
- finally:
- provider.Dispose()
-
- def test_lifetime_buildandcreate_initializertransient(self, skip_by_python_lifetime_env_var):
- try:
- # Arrange
- def subclassinitialize(provider: IServiceProvider):
- return get_service(provider,TestContentTransformerBuilderLifetimeTests.SubTransformerImplementation)
- servicecollection = ServiceCollection()
- ServiceCollectionServiceExtensions.AddTransient[TestContentTransformerBuilderLifetimeTests.SubTransformerImplementation](servicecollection)
- provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(servicecollection)
- hookFactory = PyContentTransformerBuilder(ContentTransformerBuilder()).add(TestContentTransformerBuilderLifetimeTests.SubTransformerImplementation, IUser, Func[IServiceProvider, TestContentTransformerBuilderLifetimeTests.SubTransformerImplementation](subclassinitialize)).build().get_hooks(IContentTransformer[IUser])
-
- assert len(hookFactory) == 1
-
- # Act
- try:
- scope1 = ServiceProviderServiceExtensions.CreateScope(provider)
- firstScopeHook1 = hookFactory[0].Create[IContentTransformer[IUser]](scope1.ServiceProvider)
- firstScopeHook2 = hookFactory[0].Create[IContentTransformer[IUser]](scope1.ServiceProvider)
- finally:
- scope1.Dispose()
-
- try:
- scope2 = ServiceProviderServiceExtensions.CreateScope(provider)
- lastScopeHook = hookFactory[0].Create[IContentTransformer[IUser]](scope2.ServiceProvider)
- finally:
- scope2.Dispose()
-
- # Assert
- assert firstScopeHook1 != firstScopeHook2
- assert firstScopeHook1 != lastScopeHook
- finally:
- provider.Dispose()
-
- def test_lifetime_buildandcreate_callback(self, skip_by_python_lifetime_env_var):
- try:
- # Arrange
- def classcallback(context: IProject):
- return context
-
- provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(ServiceCollection())
- hookFactory = PyContentTransformerBuilder(ContentTransformerBuilder()).add(IProject, Func[IProject, IProject](classcallback)).build().get_hooks(IContentTransformer[IProject])
-
- assert len(hookFactory) == 1
-
- # Act
- try:
- scope1 = ServiceProviderServiceExtensions.CreateScope(provider)
- firstScopeHook1 = hookFactory[0].Create[IMigrationHook[IProject]](scope1.ServiceProvider)
- firstScopeHook2 = hookFactory[0].Create[IMigrationHook[IProject]](scope1.ServiceProvider)
- finally:
- scope1.Dispose()
-
- try:
- scope2 = ServiceProviderServiceExtensions.CreateScope(provider)
- lastScopeHook = hookFactory[0].Create[IMigrationHook[IProject]](scope2.ServiceProvider)
- finally:
- scope2.Dispose()
-
- # Assert
- assert firstScopeHook1 != firstScopeHook2
- assert firstScopeHook1 != lastScopeHook
- finally:
- provider.Dispose()
diff --git a/src/Python/tests/test_other.py b/src/Python/tests/test_other.py
index c5a9b3a..4632898 100644
--- a/src/Python/tests/test_other.py
+++ b/src/Python/tests/test_other.py
@@ -15,7 +15,11 @@
import os
import logging
-import tableau_migration
+from tableau_migration import _logger_names
+from tableau_migration.migration import (
+ get_service_provider,
+ get_service
+)
from tableau_migration.migration_engine import (
PyMigrationPlanBuilder)
@@ -46,9 +50,9 @@ def test_logging(self):
PyMigrationPlanBuilder()
# tableau_migration module keeps track of instaniated loggers. Verify that we have at least one
- assert len(tableau_migration._logger_names) > 0
+ assert len(_logger_names) > 0
- for name in tableau_migration._logger_names:
+ for name in _logger_names:
# Given that we have a name, we should have a logger
assert logging.getLogger(name)
@@ -62,8 +66,8 @@ def test_config(self):
process starts and hence the env variables take and that way an int can be set to a env var.
'''
- services = tableau_migration.migration.get_service_provider()
- config_reader = tableau_migration.migration.get_service(services, IConfigReader)
+ services = get_service_provider()
+ config_reader = get_service(services, IConfigReader)
batch_size = config_reader.Get[IUser]().BatchSize
diff --git a/src/Tableau.Migration.PythonGenerator/Config/Hints/GeneratorHints.cs b/src/Tableau.Migration.PythonGenerator/Config/Hints/GeneratorHints.cs
new file mode 100644
index 0000000..b71af0e
--- /dev/null
+++ b/src/Tableau.Migration.PythonGenerator/Config/Hints/GeneratorHints.cs
@@ -0,0 +1,40 @@
+//
+// Copyright (c) 2024, Salesforce, Inc.
+// SPDX-License-Identifier: Apache-2
+//
+// Licensed under the Apache License, Version 2.0 (the "License")
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+using System;
+using System.Linq;
+using Microsoft.CodeAnalysis;
+
+namespace Tableau.Migration.PythonGenerator.Config.Hints
+{
+ internal sealed class GeneratorHints
+ {
+ public NamespaceHints[] Namespaces { get; set; } = Array.Empty();
+
+ public TypeHints? ForType(ITypeSymbol type)
+ {
+ var ns = type.ContainingNamespace.ToDisplayString();
+ var nsHint = Namespaces.FirstOrDefault(nh => string.Equals(nh.Namespace, ns, StringComparison.Ordinal));
+ if(nsHint is null)
+ {
+ return null;
+ }
+
+ return nsHint.Types.FirstOrDefault(th => string.Equals(th.Type, type.Name, StringComparison.Ordinal));
+ }
+ }
+}
diff --git a/src/Tableau.Migration.PythonGenerator/Config/Hints/NamespaceHints.cs b/src/Tableau.Migration.PythonGenerator/Config/Hints/NamespaceHints.cs
new file mode 100644
index 0000000..12632e7
--- /dev/null
+++ b/src/Tableau.Migration.PythonGenerator/Config/Hints/NamespaceHints.cs
@@ -0,0 +1,28 @@
+//
+// Copyright (c) 2024, Salesforce, Inc.
+// SPDX-License-Identifier: Apache-2
+//
+// Licensed under the Apache License, Version 2.0 (the "License")
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+using System;
+
+namespace Tableau.Migration.PythonGenerator.Config.Hints
+{
+ internal sealed class NamespaceHints
+ {
+ public string Namespace { get; set; } = string.Empty;
+
+ public TypeHints[] Types { get; set; } = Array.Empty();
+ }
+}
diff --git a/src/Tableau.Migration.PythonGenerator/Config/Hints/TypeHints.cs b/src/Tableau.Migration.PythonGenerator/Config/Hints/TypeHints.cs
new file mode 100644
index 0000000..b47e276
--- /dev/null
+++ b/src/Tableau.Migration.PythonGenerator/Config/Hints/TypeHints.cs
@@ -0,0 +1,28 @@
+//
+// Copyright (c) 2024, Salesforce, Inc.
+// SPDX-License-Identifier: Apache-2
+//
+// Licensed under the Apache License, Version 2.0 (the "License")
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+using System;
+
+namespace Tableau.Migration.PythonGenerator.Config.Hints
+{
+ internal sealed class TypeHints
+ {
+ public string Type { get; set; } = string.Empty;
+
+ public string[] ExcludeMembers { get; set; } = Array.Empty();
+ }
+}
diff --git a/src/Tableau.Migration.PythonGenerator/Config/PythonGeneratorOptions.cs b/src/Tableau.Migration.PythonGenerator/Config/PythonGeneratorOptions.cs
new file mode 100644
index 0000000..ce97794
--- /dev/null
+++ b/src/Tableau.Migration.PythonGenerator/Config/PythonGeneratorOptions.cs
@@ -0,0 +1,30 @@
+//
+// Copyright (c) 2024, Salesforce, Inc.
+// SPDX-License-Identifier: Apache-2
+//
+// Licensed under the Apache License, Version 2.0 (the "License")
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+using Tableau.Migration.PythonGenerator.Config.Hints;
+
+namespace Tableau.Migration.PythonGenerator.Config
+{
+ internal sealed class PythonGeneratorOptions
+ {
+ public string ImportPath { get; set; } = string.Empty;
+
+ public string OutputPath { get; set; } = string.Empty;
+
+ public GeneratorHints Hints { get; set; } = new();
+ }
+}
diff --git a/src/Tableau.Migration.PythonGenerator/ConversionMode.cs b/src/Tableau.Migration.PythonGenerator/ConversionMode.cs
new file mode 100644
index 0000000..e069ad4
--- /dev/null
+++ b/src/Tableau.Migration.PythonGenerator/ConversionMode.cs
@@ -0,0 +1,38 @@
+//
+// Copyright (c) 2024, Salesforce, Inc.
+// SPDX-License-Identifier: Apache-2
+//
+// Licensed under the Apache License, Version 2.0 (the "License")
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+namespace Tableau.Migration.PythonGenerator
+{
+ internal enum ConversionMode
+ {
+ Direct = 0,
+
+ Wrap,
+
+ WrapSerialized,
+
+ WrapGeneric,
+
+ Enum,
+
+ WrapImmutableCollection,
+
+ WrapMutableCollection,
+
+ WrapArray
+ }
+}
diff --git a/src/Tableau.Migration.PythonGenerator/Generators/IPythonDocstringGenerator.cs b/src/Tableau.Migration.PythonGenerator/Generators/IPythonDocstringGenerator.cs
new file mode 100644
index 0000000..efbfb61
--- /dev/null
+++ b/src/Tableau.Migration.PythonGenerator/Generators/IPythonDocstringGenerator.cs
@@ -0,0 +1,26 @@
+//
+// Copyright (c) 2024, Salesforce, Inc.
+// SPDX-License-Identifier: Apache-2
+//
+// Licensed under the Apache License, Version 2.0 (the "License")
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+using Microsoft.CodeAnalysis;
+
+namespace Tableau.Migration.PythonGenerator.Generators
+{
+ internal interface IPythonDocstringGenerator
+ {
+ PythonDocstring? Generate(ISymbol dotNetSymbol, bool ignoreArgs = false);
+ }
+}
diff --git a/src/Tableau.Migration.PythonGenerator/Generators/IPythonEnumValueGenerator.cs b/src/Tableau.Migration.PythonGenerator/Generators/IPythonEnumValueGenerator.cs
new file mode 100644
index 0000000..2949148
--- /dev/null
+++ b/src/Tableau.Migration.PythonGenerator/Generators/IPythonEnumValueGenerator.cs
@@ -0,0 +1,27 @@
+//
+// Copyright (c) 2024, Salesforce, Inc.
+// SPDX-License-Identifier: Apache-2
+//
+// Licensed under the Apache License, Version 2.0 (the "License")
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+using System.Collections.Immutable;
+using Microsoft.CodeAnalysis;
+
+namespace Tableau.Migration.PythonGenerator.Generators
+{
+ internal interface IPythonEnumValueGenerator
+ {
+ ImmutableArray GenerateEnumValues(INamedTypeSymbol dotNetType);
+ }
+}
diff --git a/src/Tableau.Migration.PythonGenerator/Generators/IPythonGenerator.cs b/src/Tableau.Migration.PythonGenerator/Generators/IPythonGenerator.cs
new file mode 100644
index 0000000..84a864f
--- /dev/null
+++ b/src/Tableau.Migration.PythonGenerator/Generators/IPythonGenerator.cs
@@ -0,0 +1,27 @@
+//
+// Copyright (c) 2024, Salesforce, Inc.
+// SPDX-License-Identifier: Apache-2
+//
+// Licensed under the Apache License, Version 2.0 (the "License")
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+using System.Collections.Generic;
+using Microsoft.CodeAnalysis;
+
+namespace Tableau.Migration.PythonGenerator.Generators
+{
+ internal interface IPythonGenerator
+ {
+ PythonTypeCache Generate(IEnumerable dotNetTypes);
+ }
+}
diff --git a/src/Tableau.Migration.PythonGenerator/Generators/IPythonMethodGenerator.cs b/src/Tableau.Migration.PythonGenerator/Generators/IPythonMethodGenerator.cs
new file mode 100644
index 0000000..9000d52
--- /dev/null
+++ b/src/Tableau.Migration.PythonGenerator/Generators/IPythonMethodGenerator.cs
@@ -0,0 +1,27 @@
+//
+// Copyright (c) 2024, Salesforce, Inc.
+// SPDX-License-Identifier: Apache-2
+//
+// Licensed under the Apache License, Version 2.0 (the "License")
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+using System.Collections.Immutable;
+using Microsoft.CodeAnalysis;
+
+namespace Tableau.Migration.PythonGenerator.Generators
+{
+ internal interface IPythonMethodGenerator
+ {
+ ImmutableArray GenerateMethods(INamedTypeSymbol dotNetType);
+ }
+}
diff --git a/src/Tableau.Migration.PythonGenerator/Generators/IPythonPropertyGenerator.cs b/src/Tableau.Migration.PythonGenerator/Generators/IPythonPropertyGenerator.cs
new file mode 100644
index 0000000..76cb66d
--- /dev/null
+++ b/src/Tableau.Migration.PythonGenerator/Generators/IPythonPropertyGenerator.cs
@@ -0,0 +1,27 @@
+//
+// Copyright (c) 2024, Salesforce, Inc.
+// SPDX-License-Identifier: Apache-2
+//
+// Licensed under the Apache License, Version 2.0 (the "License")
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+using System.Collections.Immutable;
+using Microsoft.CodeAnalysis;
+
+namespace Tableau.Migration.PythonGenerator.Generators
+{
+ internal interface IPythonPropertyGenerator
+ {
+ ImmutableArray GenerateProperties(INamedTypeSymbol dotNetType);
+ }
+}
diff --git a/src/Tableau.Migration.PythonGenerator/Generators/IPythonTypeGenerator.cs b/src/Tableau.Migration.PythonGenerator/Generators/IPythonTypeGenerator.cs
new file mode 100644
index 0000000..f8ef6c1
--- /dev/null
+++ b/src/Tableau.Migration.PythonGenerator/Generators/IPythonTypeGenerator.cs
@@ -0,0 +1,27 @@
+//
+// Copyright (c) 2024, Salesforce, Inc.
+// SPDX-License-Identifier: Apache-2
+//
+// Licensed under the Apache License, Version 2.0 (the "License")
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+using System.Collections.Immutable;
+using Microsoft.CodeAnalysis;
+
+namespace Tableau.Migration.PythonGenerator.Generators
+{
+ internal interface IPythonTypeGenerator
+ {
+ PythonType Generate(ImmutableHashSet dotNetTypeNames, INamedTypeSymbol dotNetType);
+ }
+}
diff --git a/src/Tableau.Migration.PythonGenerator/Generators/PythonDocstringGenerator.cs b/src/Tableau.Migration.PythonGenerator/Generators/PythonDocstringGenerator.cs
new file mode 100644
index 0000000..1a6ef05
--- /dev/null
+++ b/src/Tableau.Migration.PythonGenerator/Generators/PythonDocstringGenerator.cs
@@ -0,0 +1,111 @@
+//
+// Copyright (c) 2024, Salesforce, Inc.
+// SPDX-License-Identifier: Apache-2
+//
+// Licensed under the Apache License, Version 2.0 (the "License")
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+using System.Collections.Immutable;
+using System.Diagnostics.CodeAnalysis;
+using System.Text.RegularExpressions;
+using System.Xml;
+using Microsoft.CodeAnalysis;
+
+namespace Tableau.Migration.PythonGenerator.Generators
+{
+ internal sealed class PythonDocstringGenerator : IPythonDocstringGenerator
+ {
+ [return: NotNullIfNotNull(nameof(s))]
+ private static string? CleanText(string? s)
+ {
+ if (s is null)
+ {
+ return null;
+ }
+
+ var result = Regex.Replace(s.Trim().ReplaceLineEndings(""), @"\s+", " ");
+
+ if (result.Contains("\w+)(?:`\d)*\""\s+/\>", "$1");
+ result = Regex.Replace(result, @"\.*)\""\s*\>(?.+)\", "$1");
+ }
+
+ if (result.Contains("\w+)\""\s+/\>", "$1");
+ }
+
+ if (result.Contains("para>"))
+ {
+ result = Regex.Replace(result, @"\<(/*)para\>", string.Empty);
+ }
+
+ return result.Trim();
+ }
+
+ private static PythonDocstring FromXml(string xmlDoc, bool ignoreArgs)
+ {
+ var xml = new XmlDocument();
+ xml.LoadXml(xmlDoc);
+
+ string summary = string.Empty;
+ var summaryEls = xml.DocumentElement?.GetElementsByTagName("summary");
+ if (summaryEls is not null && summaryEls.Count > 0)
+ {
+ summary = CleanText(summaryEls[0]?.InnerXml) ?? string.Empty;
+ }
+
+ string? returnDoc = null;
+ var returnsEls = xml.DocumentElement?.GetElementsByTagName("returns");
+ if (returnsEls is not null && returnsEls.Count > 0)
+ {
+ returnDoc = CleanText(returnsEls[0]?.InnerXml);
+ }
+
+ var args = ImmutableArray.CreateBuilder();
+
+ if(!ignoreArgs)
+ {
+ var argEls = xml.DocumentElement?.GetElementsByTagName("param");
+ if (argEls is not null && argEls.Count > 0)
+ {
+ for (int i = 0; i < argEls.Count; i++)
+ {
+ var arg = argEls[i];
+ var name = arg?.Attributes?["name"]?.Value?.ToSnakeCase();
+ if (arg is null || name is null)
+ {
+ continue;
+ }
+
+ args.Add(new(name, CleanText(arg.InnerXml)));
+ }
+ }
+ }
+
+ return new(summary, args.ToImmutable(), returnDoc);
+ }
+
+ public PythonDocstring? Generate(ISymbol dotNetSymbol, bool ignoreArgs = false)
+ {
+ var xml = dotNetSymbol.GetDocumentationCommentXml();
+ if (string.IsNullOrEmpty(xml))
+ {
+ return null;
+ }
+
+ return FromXml(xml, ignoreArgs);
+ }
+ }
+}
diff --git a/src/Tableau.Migration.PythonGenerator/Generators/PythonEnumValueGenerator.cs b/src/Tableau.Migration.PythonGenerator/Generators/PythonEnumValueGenerator.cs
new file mode 100644
index 0000000..916a4e7
--- /dev/null
+++ b/src/Tableau.Migration.PythonGenerator/Generators/PythonEnumValueGenerator.cs
@@ -0,0 +1,65 @@
+//
+// Copyright (c) 2024, Salesforce, Inc.
+// SPDX-License-Identifier: Apache-2
+//
+// Licensed under the Apache License, Version 2.0 (the "License")
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+using System.Collections.Immutable;
+using Microsoft.CodeAnalysis;
+using Microsoft.Extensions.Options;
+using Tableau.Migration.PythonGenerator.Config;
+
+namespace Tableau.Migration.PythonGenerator.Generators
+{
+ internal sealed class PythonEnumValueGenerator : PythonMemberGenerator, IPythonEnumValueGenerator
+ {
+ private readonly IPythonDocstringGenerator _docGenerator;
+
+ public PythonEnumValueGenerator(IPythonDocstringGenerator docGenerator,
+ IOptions options)
+ : base(options)
+ {
+ _docGenerator = docGenerator;
+ }
+
+ public ImmutableArray GenerateEnumValues(INamedTypeSymbol dotNetType)
+ {
+ if(!dotNetType.IsAnyEnum())
+ {
+ return ImmutableArray.Empty;
+ }
+
+ var results = ImmutableArray.CreateBuilder();
+
+ foreach (var dotNetMember in dotNetType.GetMembers())
+ {
+ if (!(dotNetMember is IFieldSymbol dotNetField) ||
+ !dotNetField.IsConst ||
+ IgnoreMember(dotNetType, dotNetField))
+ {
+ continue;
+ }
+
+ var value = dotNetField.ConstantValue!;
+ var docs = _docGenerator.Generate(dotNetField);
+
+ var pyValue = new PythonEnumValue(dotNetField.Name.ToConstantCase(), value, docs);
+
+ results.Add(pyValue);
+ }
+
+ return results.ToImmutable();
+ }
+ }
+}
diff --git a/src/Tableau.Migration.PythonGenerator/Generators/PythonGenerator.cs b/src/Tableau.Migration.PythonGenerator/Generators/PythonGenerator.cs
new file mode 100644
index 0000000..687aaa6
--- /dev/null
+++ b/src/Tableau.Migration.PythonGenerator/Generators/PythonGenerator.cs
@@ -0,0 +1,49 @@
+//
+// Copyright (c) 2024, Salesforce, Inc.
+// SPDX-License-Identifier: Apache-2
+//
+// Licensed under the Apache License, Version 2.0 (the "License")
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Linq;
+using Microsoft.CodeAnalysis;
+
+namespace Tableau.Migration.PythonGenerator.Generators
+{
+ internal sealed class PythonGenerator : IPythonGenerator
+ {
+ private readonly IPythonTypeGenerator _typeGenerator;
+
+ public PythonGenerator(IPythonTypeGenerator typeGenerator)
+ {
+ _typeGenerator = typeGenerator;
+ }
+
+ public PythonTypeCache Generate(IEnumerable dotNetTypes)
+ {
+ var dotNetTypeNames = dotNetTypes.Select(t => t.ToDisplayString()).ToImmutableHashSet();
+
+ var pyTypes = new List(dotNetTypes.Count());
+
+ foreach(var dotnetType in dotNetTypes)
+ {
+ var pyType = _typeGenerator.Generate(dotNetTypeNames,dotnetType);
+ pyTypes.Add(pyType);
+ }
+
+ return new PythonTypeCache(pyTypes);
+ }
+ }
+}
diff --git a/src/Tableau.Migration.PythonGenerator/Generators/PythonMemberGenerator.cs b/src/Tableau.Migration.PythonGenerator/Generators/PythonMemberGenerator.cs
new file mode 100644
index 0000000..1eef6e5
--- /dev/null
+++ b/src/Tableau.Migration.PythonGenerator/Generators/PythonMemberGenerator.cs
@@ -0,0 +1,195 @@
+//
+// Copyright (c) 2024, Salesforce, Inc.
+// SPDX-License-Identifier: Apache-2
+//
+// Licensed under the Apache License, Version 2.0 (the "License")
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Linq;
+using Microsoft.CodeAnalysis;
+using Microsoft.Extensions.Options;
+using Tableau.Migration.PythonGenerator.Config;
+using Dotnet = Tableau.Migration.PythonGenerator.Keywords.Dotnet;
+using Py = Tableau.Migration.PythonGenerator.Keywords.Python;
+
+namespace Tableau.Migration.PythonGenerator.Generators
+{
+ internal abstract class PythonMemberGenerator
+ {
+ private static readonly PythonTypeReference BOOL = new(Py.Types.BOOL, ImportModule: null, ConversionMode.Direct);
+ private static readonly PythonTypeReference INT = new(Py.Types.INT, ImportModule: null, ConversionMode.Direct);
+ private static readonly PythonTypeReference STRING = new(Py.Types.STR, ImportModule: null, ConversionMode.Direct);
+ private static readonly PythonTypeReference UUID = new(
+ Py.Types.UUID,
+ ImportModule: Py.Modules.UUID,
+ ConversionMode.WrapSerialized,
+ DotNetParseFunction: "Guid.Parse",
+ ExtraImports: ImmutableArray.Create(
+ new PythonTypeReference(Dotnet.Types.GUID, ImportModule: Dotnet.Namespaces.SYSTEM, ConversionMode.Direct)));
+
+ internal static readonly PythonTypeReference LIST_REFERENCE = new PythonTypeReference(
+ Dotnet.Types.LIST,
+ ImportModule: Dotnet.Namespaces.SYSTEM_COLLECTIONS_GENERIC,
+ ConversionMode: ConversionMode.Direct,
+ ImportAlias: Dotnet.TypeAliases.LIST);
+
+ internal static readonly PythonTypeReference HASH_SET_REFERENCE = new PythonTypeReference(
+ Dotnet.Types.HASH_SET,
+ ImportModule: Dotnet.Namespaces.SYSTEM_COLLECTIONS_GENERIC,
+ ConversionMode: ConversionMode.Direct,
+ ImportAlias: Dotnet.TypeAliases.HASH_SET);
+
+ private static readonly PythonTypeReference STRING_REFERENCE = new PythonTypeReference(
+ Dotnet.Types.STRING,
+ ImportModule: Dotnet.Namespaces.SYSTEM,
+ ConversionMode: ConversionMode.Direct,
+ ImportAlias: Dotnet.TypeAliases.STRING);
+
+ private static readonly PythonTypeReference EXCEPTION = new(
+ Dotnet.Namespaces.SYSTEM_EXCEPTION,
+ Dotnet.Namespaces.SYSTEM,
+ ConversionMode.Direct);
+
+ private readonly PythonGeneratorOptions _options;
+
+ protected PythonMemberGenerator(IOptions options)
+ {
+ _options = options.Value;
+ }
+
+ protected bool IgnoreMember(ITypeSymbol type, ISymbol member)
+ {
+ var typeHints = _options.Hints.ForType(type);
+ if (typeHints is null)
+ {
+ return false;
+ }
+
+ return typeHints.ExcludeMembers.Any(m => string.Equals(m, member.Name, StringComparison.Ordinal));
+ }
+
+ protected ImmutableArray? GetGenericTypes(ITypeSymbol t)
+ {
+ if (t is INamedTypeSymbol nt)
+ {
+ return nt.TypeArguments.Select(ToPythonType).ToImmutableArray();
+ }
+
+ if (t is IArrayTypeSymbol at)
+ {
+ var pyType = ToPythonType(at.ElementType);
+ return ImmutableArray.Create(pyType);
+ }
+
+ return null;
+ }
+
+ protected ImmutableArray? GetDotnetGenericTypes(ITypeSymbol t)
+ {
+ if (t is INamedTypeSymbol nt)
+ {
+ return nt.TypeArguments;
+ }
+
+ if (t is IArrayTypeSymbol at)
+ {
+ return ImmutableArray.Create(at.ElementType);
+ }
+ return null;
+ }
+
+ protected PythonTypeReference ToPythonType(ITypeSymbol t)
+ {
+ if (t.Kind is SymbolKind.TypeParameter)
+ {
+ return PythonTypeReference.ForGenericType(t);
+ }
+
+ switch (t.Name)
+ {
+ case "bool":
+ case nameof(Boolean):
+ return BOOL;
+ case nameof(Exception):
+ return EXCEPTION;
+ case nameof(Guid):
+ return UUID;
+ case nameof(IList):
+ return new(
+ Py.Types.LIST_WRAPPED,
+ ImportModule: Py.Modules.TYPING,
+ ConversionMode.WrapMutableCollection,
+ GenericTypes: GetGenericTypes(t),
+ ExtraImports: ImmutableArray.Create(LIST_REFERENCE),
+ DotnetTypes: GetDotnetGenericTypes(t));
+ case nameof(HashSet):
+ return new(
+ Py.Types.SET,
+ ImportModule: Py.Modules.TYPING,
+ ConversionMode.WrapMutableCollection,
+ GenericTypes: GetGenericTypes(t),
+ ExtraImports: ImmutableArray.Create(HASH_SET_REFERENCE),
+ DotnetTypes: GetDotnetGenericTypes(t));
+ case nameof(ISet):
+ return new(
+ Py.Types.SEQUENCE,
+ ImportModule: Py.Modules.TYPING,
+ ConversionMode.WrapMutableCollection,
+ GenericTypes: GetGenericTypes(t),
+ ExtraImports: ImmutableArray.Create(LIST_REFERENCE, STRING_REFERENCE, HASH_SET_REFERENCE),
+ DotnetTypes: GetDotnetGenericTypes(t));
+
+ case nameof(ImmutableArray):
+ case nameof(IImmutableList):
+ case nameof(IReadOnlyList):
+ case nameof(IEnumerable):
+ return new(
+ Py.Types.SEQUENCE,
+ ImportModule: Py.Modules.TYPING,
+ ConversionMode.WrapImmutableCollection,
+ WrapType: "list",
+ GenericTypes: GetGenericTypes(t));
+ case Dotnet.Types.INT:
+ case nameof(Int32):
+ case Dotnet.Types.LONG:
+ case nameof(Int64):
+ return INT;
+ case nameof(Nullable):
+ return GetGenericTypes(t)!.Value.Single();
+ case Dotnet.Types.STRING_SIMPLIFIED:
+ case nameof(String):
+ return STRING;
+ default:
+ if (t is IArrayTypeSymbol symbol)
+ {
+ return new(
+ Py.Types.SEQUENCE,
+ ImportModule: Py.Modules.TYPING,
+ ConversionMode.WrapArray,
+ WrapType: "list",
+ GenericTypes: GetGenericTypes(t),
+ ExtraImports: ImmutableArray.Create(LIST_REFERENCE),
+ DotnetTypes: GetDotnetGenericTypes(t));
+ }
+ else
+ {
+ return PythonTypeReference.ForDotNetType(t);
+ }
+ }
+ }
+
+ }
+}
diff --git a/src/Tableau.Migration.PythonGenerator/Generators/PythonMethodGenerator.cs b/src/Tableau.Migration.PythonGenerator/Generators/PythonMethodGenerator.cs
new file mode 100644
index 0000000..014c9ee
--- /dev/null
+++ b/src/Tableau.Migration.PythonGenerator/Generators/PythonMethodGenerator.cs
@@ -0,0 +1,99 @@
+//
+// Copyright (c) 2024, Salesforce, Inc.
+// SPDX-License-Identifier: Apache-2
+//
+// Licensed under the Apache License, Version 2.0 (the "License")
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+using System;
+using System.Collections.Immutable;
+using Microsoft.CodeAnalysis;
+using Microsoft.Extensions.Options;
+using Tableau.Migration.PythonGenerator.Config;
+
+namespace Tableau.Migration.PythonGenerator.Generators
+{
+ internal class PythonMethodGenerator : PythonMemberGenerator, IPythonMethodGenerator
+ {
+ private static readonly ImmutableHashSet IGNORED_METHODS = new[]
+ {
+ nameof(object.Equals),
+ nameof(object.GetHashCode),
+ nameof(object.ToString),
+ nameof(IComparable.CompareTo),
+ "Deconstruct",
+ "PrintMembers",
+ "$"
+ }.ToImmutableHashSet();
+
+ private readonly IPythonDocstringGenerator _docGenerator;
+
+ public PythonMethodGenerator(IPythonDocstringGenerator docGenerator,
+ IOptions options)
+ : base(options)
+ {
+ _docGenerator = docGenerator;
+ }
+
+ private PythonTypeReference? GetPythonReturnType(IMethodSymbol dotNetMethod)
+ {
+ if(dotNetMethod.ReturnsVoid)
+ {
+ return null;
+ }
+
+ return ToPythonType(dotNetMethod.ReturnType);
+ }
+
+ private ImmutableArray GenerateArguments(IMethodSymbol dotNetMethod)
+ {
+ var results = ImmutableArray.CreateBuilder();
+
+ foreach(var dotNetParam in dotNetMethod.Parameters)
+ {
+ var pyArgument = new PythonMethodArgument(dotNetParam.Name.ToSnakeCase(), ToPythonType(dotNetParam.Type));
+ results.Add(pyArgument);
+ }
+
+ return results.ToImmutable();
+ }
+
+ public ImmutableArray GenerateMethods(INamedTypeSymbol dotNetType)
+ {
+ var results = ImmutableArray.CreateBuilder();
+
+ foreach (var dotNetMember in dotNetType.GetMembers())
+ {
+ if (!(dotNetMember is IMethodSymbol dotNetMethod) || IgnoreMember(dotNetType, dotNetMethod))
+ {
+ continue;
+ }
+
+ if(dotNetMethod.MethodKind is not MethodKind.Ordinary || IGNORED_METHODS.Contains(dotNetMethod.Name))
+ {
+ continue;
+ }
+
+ var docs = _docGenerator.Generate(dotNetMethod);
+ var returnType = GetPythonReturnType(dotNetMethod);
+ var arguments = GenerateArguments(dotNetMethod);
+
+ var pyMethod = new PythonMethod(dotNetMethod.Name.ToSnakeCase(), returnType, arguments, dotNetMethod.IsStatic, docs, dotNetMethod);
+
+ results.Add(pyMethod);
+ }
+
+ return results.ToImmutable();
+ }
+ }
+}
diff --git a/src/Tableau.Migration.PythonGenerator/Generators/PythonPropertyGenerator.cs b/src/Tableau.Migration.PythonGenerator/Generators/PythonPropertyGenerator.cs
new file mode 100644
index 0000000..4ba1133
--- /dev/null
+++ b/src/Tableau.Migration.PythonGenerator/Generators/PythonPropertyGenerator.cs
@@ -0,0 +1,70 @@
+//
+// Copyright (c) 2024, Salesforce, Inc.
+// SPDX-License-Identifier: Apache-2
+//
+// Licensed under the Apache License, Version 2.0 (the "License")
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+using System;
+using System.Collections.Immutable;
+using Microsoft.CodeAnalysis;
+using Microsoft.Extensions.Options;
+using Tableau.Migration.PythonGenerator.Config;
+
+namespace Tableau.Migration.PythonGenerator.Generators
+{
+ internal sealed class PythonPropertyGenerator : PythonMemberGenerator, IPythonPropertyGenerator
+ {
+ private static readonly ImmutableHashSet IGNORED_PROPERTIES = new[]
+ {
+ "EqualityContract"
+ }.ToImmutableHashSet();
+
+ private readonly IPythonDocstringGenerator _docGenerator;
+
+ public PythonPropertyGenerator(IPythonDocstringGenerator docGenerator,
+ IOptions options)
+ : base(options)
+ {
+ _docGenerator = docGenerator;
+ }
+
+ public ImmutableArray GenerateProperties(INamedTypeSymbol dotNetType)
+ {
+ var results = ImmutableArray.CreateBuilder();
+
+ foreach (var dotNetMember in dotNetType.GetMembers())
+ {
+ if(dotNetMember.IsStatic ||
+ !(dotNetMember is IPropertySymbol dotNetProperty) ||
+ IgnoreMember(dotNetType, dotNetProperty) ||
+ IGNORED_PROPERTIES.Contains(dotNetProperty.Name))
+ {
+ continue;
+ }
+
+ var type = ToPythonType(dotNetProperty.Type);
+ var docs = _docGenerator.Generate(dotNetProperty);
+
+ var pyProperty = new PythonProperty(dotNetProperty.Name.ToSnakeCase(), type,
+ !dotNetProperty.IsWriteOnly,
+ !(dotNetProperty.IsReadOnly || (dotNetProperty.SetMethod is not null && dotNetProperty.SetMethod.IsInitOnly)),
+ docs, dotNetProperty);
+
+ results.Add(pyProperty);
+ }
+
+ return results.ToImmutable();
+ }
+ }
+}
diff --git a/src/Tableau.Migration.PythonGenerator/Generators/PythonTypeGenerator.cs b/src/Tableau.Migration.PythonGenerator/Generators/PythonTypeGenerator.cs
new file mode 100644
index 0000000..3282bea
--- /dev/null
+++ b/src/Tableau.Migration.PythonGenerator/Generators/PythonTypeGenerator.cs
@@ -0,0 +1,137 @@
+//
+// Copyright (c) 2024, Salesforce, Inc.
+// SPDX-License-Identifier: Apache-2
+//
+// Licensed under the Apache License, Version 2.0 (the "License")
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Linq;
+using Microsoft.CodeAnalysis;
+
+namespace Tableau.Migration.PythonGenerator.Generators
+{
+ internal sealed class PythonTypeGenerator : IPythonTypeGenerator
+ {
+ private readonly IPythonPropertyGenerator _propertyGenerator;
+ private readonly IPythonMethodGenerator _methodGenerator;
+ private readonly IPythonEnumValueGenerator _enumValueGenerator;
+ private readonly IPythonDocstringGenerator _docGenerator;
+
+ public PythonTypeGenerator(IPythonPropertyGenerator propertyGenerator,
+ IPythonMethodGenerator methodGenerator,
+ IPythonEnumValueGenerator enumValueGenerator,
+ IPythonDocstringGenerator docGenerator)
+ {
+ _propertyGenerator = propertyGenerator;
+ _methodGenerator = methodGenerator;
+ _enumValueGenerator = enumValueGenerator;
+ _docGenerator = docGenerator;
+ }
+
+ private bool HasInterface(INamedTypeSymbol type, INamedTypeSymbol test)
+ {
+ foreach (var childInterface in type.AllInterfaces)
+ {
+ if(string.Equals(childInterface.ToDisplayString(), test.ToDisplayString(), System.StringComparison.Ordinal))
+ {
+ return true;
+ }
+
+ if (HasInterface(childInterface, test))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private (ImmutableArray InheritedTypes, ImmutableArray ExcludedInterfaces)
+ GenerateInheritedTypes(ImmutableHashSet dotNetTypeNames, INamedTypeSymbol dotNetType)
+ {
+ var inheritedTypes = ImmutableArray.CreateBuilder();
+
+ if(dotNetType.TypeArguments.Any())
+ {
+ var genericTypes = dotNetType.TypeArguments
+ .Select(PythonTypeReference.ForGenericType)
+ .ToImmutableArray();
+
+ var extraImports = ImmutableArray.Create(
+ new PythonTypeReference("TypeVar", "typing", ConversionMode.Direct),
+ new PythonTypeReference("_generic_wrapper", "tableau_migration.migration", ConversionMode.Direct)
+ );
+
+ inheritedTypes.Add(new("Generic", "typing", ConversionMode.Direct, genericTypes, ExtraImports: extraImports));
+ }
+
+ if(dotNetType.IsOrdinalEnum())
+ {
+ inheritedTypes.Add(new("IntEnum", "enum", ConversionMode.Direct));
+ }
+ else if(dotNetType.IsStringEnum())
+ {
+ inheritedTypes.Add(new("StrEnum", "migration_enum", ConversionMode.Direct));
+ }
+
+ var interfaces = new List();
+ var excludedInterfaces = ImmutableArray.CreateBuilder();
+
+ foreach (var interfaceType in dotNetType.AllInterfaces)
+ {
+ if(dotNetTypeNames.Contains(interfaceType.ToDisplayString()))
+ {
+ interfaces.Add(interfaceType);
+ }
+ else
+ {
+ excludedInterfaces.Add(interfaceType);
+ }
+ }
+
+ // Remove interfaces implemented by other inherited interfaces,
+ // to reduce multi-inheritance complexity.
+ foreach(var interfaceType in interfaces.ToImmutableArray())
+ {
+ if(interfaces.Any(i => HasInterface(i, interfaceType)))
+ {
+ interfaces.Remove(interfaceType);
+ }
+ }
+
+ inheritedTypes.AddRange(interfaces.Select(PythonTypeReference.ForDotNetType));
+
+ return (inheritedTypes.ToImmutable(), excludedInterfaces.ToImmutable());
+ }
+
+ public PythonType Generate(ImmutableHashSet dotNetTypeNames, INamedTypeSymbol dotNetType)
+ {
+ (var inheritedTypes, var excludedInterfaces) = GenerateInheritedTypes(dotNetTypeNames, dotNetType);
+
+ var properties = _propertyGenerator.GenerateProperties(dotNetType);
+ var methods = _methodGenerator.GenerateMethods(dotNetType);
+ var enumValues = _enumValueGenerator.GenerateEnumValues(dotNetType);
+
+ // arg docs at the type level would be for record properties.
+ var docs = _docGenerator.Generate(dotNetType, ignoreArgs: true);
+
+ var typeRef = PythonTypeReference.ForDotNetType(dotNetType);
+
+ return new(typeRef.Name, typeRef.ImportModule!, inheritedTypes,
+ properties, methods, enumValues,
+ docs, dotNetType, excludedInterfaces);
+ }
+ }
+}
diff --git a/src/Tableau.Migration.PythonGenerator/INamedTypeSymbolExtensions.cs b/src/Tableau.Migration.PythonGenerator/INamedTypeSymbolExtensions.cs
new file mode 100644
index 0000000..900bf85
--- /dev/null
+++ b/src/Tableau.Migration.PythonGenerator/INamedTypeSymbolExtensions.cs
@@ -0,0 +1,34 @@
+//
+// Copyright (c) 2024, Salesforce, Inc.
+// SPDX-License-Identifier: Apache-2
+//
+// Licensed under the Apache License, Version 2.0 (the "License")
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+using System;
+using Microsoft.CodeAnalysis;
+
+namespace Tableau.Migration.PythonGenerator
+{
+ public static class INamedTypeSymbolExtensions
+ {
+ public static bool IsOrdinalEnum(this INamedTypeSymbol t)
+ => t.EnumUnderlyingType is not null || string.Equals(t.BaseType?.Name, nameof(Enum), StringComparison.Ordinal);
+
+ public static bool IsStringEnum(this INamedTypeSymbol t)
+ => string.Equals(t.BaseType?.Name, nameof(StringEnum