Repository: Azure-Samples/Apim-Samples
Pull request #12: Feature/add supported infra to samples
Author: login: simonkurtz-MSFT
Created: 2025-05-28T17:27:51Z | Merged: 2025-05-28T17:36:18Z

Resolves #11 

Linked issue #11: [Feature]: Add Supported Infrastructure Architecture Array to each Sample
### Describe the feature

Add an array to each sample's create.ipynb file that defines the infrastructures which the sample can run on. Ensure the sample build process checks the user-defined infrastructure against that array and only proceeds if it's supported.

### Improvement to Project

By defining the set of infrastructures, we stop creating samples from being created for unsupported scenarios. Having a clear, actionable error message will help users make appropriate adjustments for success.

### Are you able to collaborate and/or submit a pull request?

Yes

Diff:
diff --git a/samples/_TEMPLATE/create.ipynb b/samples/_TEMPLATE/create.ipynb
index e12c316d..1d98bf6d 100644
--- a/samples/_TEMPLATE/create.ipynb
+++ b/samples/_TEMPLATE/create.ipynb
@@ -60,6 +60,8 @@
     "\n",
     "# 2) Service-defined parameters (please do not change these)\n",
     "rg_name = utils.get_infra_rg_name(deployment, index)\n",
+    "supported_infrastructures = []              # ENTER SUPPORTED INFRASTRUCTURES HERE, e.g., [INFRASTRUCTURE.AFD_APIM_PE, INFRASTRUCTURE.AFD_APIM_FE]\n",
+    "utils.validate_infrastructure(deployment, supported_infrastructures)\n",
     "\n",
     "# 3) Define the APIs and their operations and policies\n",
     "\n",
diff --git a/samples/general/create.ipynb b/samples/general/create.ipynb
index f3a33411..509df347 100644
--- a/samples/general/create.ipynb
+++ b/samples/general/create.ipynb
@@ -51,6 +51,8 @@
     "\n",
     "# 2) Service-defined parameters (please do not change these)\n",
     "rg_name = utils.get_infra_rg_name(deployment, index)\n",
+    "supported_infrastructures = [INFRASTRUCTURE.AFD_APIM_PE, INFRASTRUCTURE.APIM_ACA, INFRASTRUCTURE.SIMPLE_APIM]\n",
+    "utils.validate_infrastructure(deployment, supported_infrastructures)\n",
     "\n",
     "# 3) Define the APIs and their operations and policies\n",
     "\n",
diff --git a/samples/load-balancing/create.ipynb b/samples/load-balancing/create.ipynb
index 62785a29..e92b7fd4 100644
--- a/samples/load-balancing/create.ipynb
+++ b/samples/load-balancing/create.ipynb
@@ -64,6 +64,8 @@
     "\n",
     "# 2) Service-defined parameters (please do not change these)\n",
     "rg_name = utils.get_infra_rg_name(deployment, index)\n",
+    "supported_infrastructures = [INFRASTRUCTURE.AFD_APIM_PE, INFRASTRUCTURE.APIM_ACA]\n",
+    "utils.validate_infrastructure(deployment, supported_infrastructures)\n",
     "\n",
     "# 3) Define the APIs and their operations and policies\n",
     "\n",
diff --git a/shared/python/apimtypes.py b/shared/python/apimtypes.py
index c0302556..3ac92d0d 100644
--- a/shared/python/apimtypes.py
+++ b/shared/python/apimtypes.py
@@ -2,7 +2,7 @@
 Types and constants for Azure API Management automation and deployment.
 """
 
-from enum import Enum
+from enum import StrEnum
 from dataclasses import dataclass
 from typing import List, Optional
 
@@ -51,7 +51,7 @@ def _read_policy_xml(policy_xml_filepath: str) -> str:
 #    CLASSES
 # ------------------------------
 
-class APIMNetworkMode(str, Enum):
+class APIMNetworkMode(StrEnum):
     """
     Networking configuration modes for Azure API Management (APIM).
     """
@@ -62,7 +62,7 @@ class APIMNetworkMode(str, Enum):
     NONE          = "None"      # No explicit network configuration (legacy or default)
 
 
-class APIM_SKU(str, Enum):
+class APIM_SKU(StrEnum):
     """
     APIM SKU types.
     """
@@ -76,7 +76,7 @@ class APIM_SKU(str, Enum):
     PREMIUMV2  = "Premiumv2"
 
 
-class HTTP_VERB(str, Enum):
+class HTTP_VERB(StrEnum):
     """
     HTTP verbs that can be used for API operations.
     """
@@ -90,7 +90,7 @@ class HTTP_VERB(str, Enum):
     HEAD    = "HEAD"
 
 
-class INFRASTRUCTURE(str, Enum):
+class INFRASTRUCTURE(StrEnum):
     """
     Infrastructure types for APIM automation scenarios.
     """
diff --git a/shared/python/utils.py b/shared/python/utils.py
index c819e317..a8ddf5f6 100644
--- a/shared/python/utils.py
+++ b/shared/python/utils.py
@@ -117,6 +117,73 @@ def get(self, key: str, label: str = '', secure: bool = False) -> str | None:
 #    PRIVATE METHODS
 # ------------------------------
 
+def _cleanup_resources(deployment_name: str, rg_name: str) -> None:
+    """
+    Clean up resources associated with a deployment in a resource group.
+    Deletes and purges Cognitive Services, API Management, and Key Vault resources, then deletes the resource group itself.
+
+    Args:
+        deployment_name (str): The deployment name (string).
+        rg_name (str): The resource group name.
+
+    Returns:
+        None
+
+    Raises:
+        Exception: If an error occurs during cleanup.
+    """
+    if not deployment_name:
+        print_error("Missing deployment name parameter.")
+        return
+
+    if not rg_name:
+        print_error("Missing resource group name parameter.")
+        return
+
+    try:
+        print_info(f"🧹 Cleaning up resource group '{rg_name}'...")
+
+        # Show the deployment details
+        output = run(f"az deployment group show --name {deployment_name} -g {rg_name} -o json", "Deployment retrieved", "Failed to retrieve the deployment")
+
+        if output.success and output.json_data:
+            provisioning_state = output.json_data.get("properties").get("provisioningState")
+            print_info(f"Deployment provisioning state: {provisioning_state}")
+
+            # Delete and purge CognitiveService accounts
+            output = run(f" az cognitiveservices account list -g {rg_name}", f"Listed CognitiveService accounts", f"Failed to list CognitiveService accounts")
+            if output.success and output.json_data:
+                for resource in output.json_data:
+                    print_info(f"Deleting and purging Cognitive Service Account '{resource['name']}' in resource group '{rg_name}'...")
+                    output = run(f"az cognitiveservices account delete -g {rg_name} -n {resource['name']}", f"Cognitive Services '{resource['name']}' deleted", f"Failed to delete Cognitive Services '{resource['name']}'")
+                    output = run(f"az cognitiveservices account purge -g {rg_name} -n {resource['name']} -l \"{resource['location']}\"", f"Cognitive Services '{resource['name']}' purged", f"Failed to purge Cognitive Services '{resource['name']}'")
+
+            # Delete and purge APIM resources
+            output = run(f" az apim list -g {rg_name}", f"Listed APIM resources", f"Failed to list APIM resources")
+            if output.success and output.json_data:
+                for resource in output.json_data:
+                    print_info(f"Deleting and purging API Management '{resource['name']}' in resource group '{rg_name}'...")
+                    output = run(f"az apim delete -n {resource['name']} -g {rg_name} -y", f"API Management '{resource['name']}' deleted", f"Failed to delete API Management '{resource['name']}'")
+                    output = run(f"az apim deletedservice purge --service-name {resource['name']} --location \"{resource['location']}\"", f"API Management '{resource['name']}' purged", f"Failed to purge API Management '{resource['name']}'")
+
+            # Delete and purge Key Vault resources
+            output = run(f" az keyvault list -g {rg_name}", f"Listed Key Vault resources", f"Failed to list Key Vault resources")
+            if output.success and output.json_data:
+                for resource in output.json_data:
+                    print_info(f"Deleting and purging Key Vault '{resource['name']}' in resource group '{rg_name}'...")
+                    output = run(f"az keyvault delete -n {resource['name']} -g {rg_name}", f"Key Vault '{resource['name']}' deleted", f"Failed to delete Key Vault '{resource['name']}'")
+                    output = run(f"az keyvault purge -n {resource['name']} --location \"{resource['location']}\"", f"Key Vault '{resource['name']}' purged", f"Failed to purge Key Vault '{resource['name']}'")
+
+            # Delete the resource group last
+            print_message(f"🧹 Deleting resource group '{rg_name}'...")
+            output = run(f"az group delete --name {rg_name} -y", f"Resource group '{rg_name}' deleted", f"Failed to delete resource group '{rg_name}'")
+
+            print_message("🧹 Cleanup completed.")
+
+    except Exception as e:
+        print(f"An error occurred during cleanup: {e}")
+        traceback.print_exc()
+
 def _print_log(message: str, prefix: str = '', color: str = '', output: str = '', duration: str = '', show_time: bool = False, blank_above: bool = False, blank_below: bool = False) -> None:
     """
     Print a formatted log message with optional prefix, color, output, duration, and time.
@@ -163,12 +230,6 @@ def _print_log(message: str, prefix: str = '', color: str = '', output: str = ''
 print_warning   = lambda msg, output = '', duration = ''                        : _print_log(msg, '⚠️ ', BOLD_Y, output, duration, True)
 print_val       = lambda name, value, val_below = False                         : _print_log(f"{name:<25}:{'\n' if val_below else ' '}{value}", '👉🏽 ', BOLD_B)
 
-# Validation functions will raise ValueError if the value is not valid
-
-validate_http_verb      = lambda val: HTTP_VERB(val)
-validate_infrastructure = lambda val: INFRASTRUCTURE(val)
-validate_sku            = lambda val: APIM_SKU(val)
-
 def create_bicep_deployment_group(rg_name: str, rg_location: str, deployment: str | INFRASTRUCTURE, bicep_parameters: dict, bicep_parameters_file: str = 'params.json') -> Output:
     """
     Create a Bicep deployment in a resource group, writing parameters to a file and running the deployment.
@@ -279,78 +340,6 @@ def read_policy_xml(policy_xml_filepath: str) -> str:
 
     return policy_template_xml
 
-def _cleanup_resources(deployment_name: str, rg_name: str) -> None:
-    """
-    Clean up resources associated with a deployment in a resource group.
-    Deletes and purges Cognitive Services, API Management, and Key Vault resources, then deletes the resource group itself.
-
-    Args:
-        deployment_name (str): The deployment name (string).
-        rg_name (str): The resource group name.
-
-    Returns:
-        None
-
-    Raises:
-        Exception: If an error occurs during cleanup.
-    """
-    if not deployment_name:
-        print_error("Missing deployment name parameter.")
-        return
-
-    if not rg_name:
-        print_error("Missing resource group name parameter.")
-        return
-
-    try:
-        print_info(f"🧹 Cleaning up resource group '{rg_name}'...")
-
-        # Show the deployment details
-        output = run(f"az deployment group show --name {deployment_name} -g {rg_name} -o json", "Deployment retrieved", "Failed to retrieve the deployment")
-
-        if output.success and output.json_data:
-            provisioning_state = output.json_data.get("properties").get("provisioningState")
-            print_info(f"Deployment provisioning state: {provisioning_state}")
-
-            # Delete and purge CognitiveService accounts
-            output = run(f" az cognitiveservices account list -g {rg_name}", f"Listed CognitiveService accounts", f"Failed to list CognitiveService accounts")
-            if output.success and output.json_data:
-                for resource in output.json_data:
-                    print_info(f"Deleting and purging Cognitive Service Account '{resource['name']}' in resource group '{rg_name}'...")
-                    output = run(f"az cognitiveservices account delete -g {rg_name} -n {resource['name']}", f"Cognitive Services '{resource['name']}' deleted", f"Failed to delete Cognitive Services '{resource['name']}'")
-                    output = run(f"az cognitiveservices account purge -g {rg_name} -n {resource['name']} -l \"{resource['location']}\"", f"Cognitive Services '{resource['name']}' purged", f"Failed to purge Cognitive Services '{resource['name']}'")
-
-            # Delete and purge APIM resources
-            output = run(f" az apim list -g {rg_name}", f"Listed APIM resources", f"Failed to list APIM resources")
-            if output.success and output.json_data:
-                for resource in output.json_data:
-                    print_info(f"Deleting and purging API Management '{resource['name']}' in resource group '{rg_name}'...")
-                    output = run(f"az apim delete -n {resource['name']} -g {rg_name} -y", f"API Management '{resource['name']}' deleted", f"Failed to delete API Management '{resource['name']}'")
-                    output = run(f"az apim deletedservice purge --service-name {resource['name']} --location \"{resource['location']}\"", f"API Management '{resource['name']}' purged", f"Failed to purge API Management '{resource['name']}'")
-
-            # Delete and purge Key Vault resources
-            output = run(f" az keyvault list -g {rg_name}", f"Listed Key Vault resources", f"Failed to list Key Vault resources")
-            if output.success and output.json_data:
-                for resource in output.json_data:
-                    print_info(f"Deleting and purging Key Vault '{resource['name']}' in resource group '{rg_name}'...")
-                    output = run(f"az keyvault delete -n {resource['name']} -g {rg_name}", f"Key Vault '{resource['name']}' deleted", f"Failed to delete Key Vault '{resource['name']}'")
-                    output = run(f"az keyvault purge -n {resource['name']} --location \"{resource['location']}\"", f"Key Vault '{resource['name']}' purged", f"Failed to purge Key Vault '{resource['name']}'")
-
-            # Delete the resource group last
-            print_message(f"🧹 Deleting resource group '{rg_name}'...")
-            output = run(f"az group delete --name {rg_name} -y", f"Resource group '{rg_name}' deleted", f"Failed to delete resource group '{rg_name}'")
-
-            print_message("🧹 Cleanup completed.")
-
-    except Exception as e:
-        print(f"An error occurred during cleanup: {e}")
-        traceback.print_exc()
-
-
-# ------------------------------
-#    PUBLIC METHODS
-# ------------------------------
-
 def cleanup_infra_deployments(deployment: INFRASTRUCTURE, indexes: int | list[int] | None = None) -> None:
     """
     Clean up infrastructure deployments by deployment enum and index/indexes.
@@ -360,7 +349,6 @@ def cleanup_infra_deployments(deployment: INFRASTRUCTURE, indexes: int | list[in
         deployment (INFRASTRUCTURE): The infrastructure deployment enum value.
         indexes (int | list[int] | None): A single index, a list of indexes, or None for no index.
     """
-    validate_infrastructure(deployment)
 
     if indexes is None:
         indexes_list = [None]
@@ -545,8 +533,6 @@ def get_infra_rg_name(deployment_name: INFRASTRUCTURE, index: int | None = None)
         str: The generated resource group name.
     """
 
-    validate_infrastructure(deployment_name)
-
     rg_name = f"apim-infra-{deployment_name.value}"
 
     if index is not None:
@@ -638,3 +624,23 @@ def run(command: str, ok_message: str = '', error_message: str = '', print_outpu
             print_message(ok_message if success else error_message, output_text if not success or print_output else "", f"[{int(minutes)}m:{int(seconds)}s]")
 
     return Output(success, output_text)
+
+# Validation functions will raise ValueError if the value is not valid
+
+validate_http_verb      = lambda val: HTTP_VERB(val)
+validate_sku            = lambda val: APIM_SKU(val)
+
+def validate_infrastructure(infra: INFRASTRUCTURE, supported_infras: list[INFRASTRUCTURE]) -> None:
+    """
+    Validate that the provided infrastructure is a supported infrastructure.
+
+    Args:
+        infra (INFRASTRUCTURE): The infrastructure deployment enum value.
+        supported_infras (list[INFRASTRUCTURE]): List of supported infrastructures.
+
+    Raises:
+        ValueError: If the infrastructure is not supported.
+    """
+
+    if infra not in supported_infras:
+        raise ValueError(f"Unsupported infrastructure: {infra}. Supported infrastructures are: {', '.join([i.value for i in supported_infras])}")
\ No newline at end of file
diff --git a/tests/python/test_utils.py b/tests/python/test_utils.py
index b4bfb3ed..d4b9fce7 100644
--- a/tests/python/test_utils.py
+++ b/tests/python/test_utils.py
@@ -1,6 +1,7 @@
+import pytest
+from apimtypes import INFRASTRUCTURE
 import os
 import builtins
-import pytest
 from io import StringIO
 from unittest.mock import patch, MagicMock, mock_open
 from shared.python import utils
@@ -252,3 +253,22 @@ def test_extract_json_multiple_json_types():
     assert utils.extract_json(s) == [1, 2, 3]
     s2 = '{"a": 1}[1,2,3]'
     assert utils.extract_json(s2) == {"a": 1}
+
+# ------------------------------
+#    validate_infrastructure
+# ------------------------------
+
+def test_validate_infrastructure_supported():
+    # Should return None for supported infra
+    assert utils.validate_infrastructure(INFRASTRUCTURE.SIMPLE_APIM, [INFRASTRUCTURE.SIMPLE_APIM]) is None
+
+def test_validate_infrastructure_unsupported():
+    # Should raise ValueError for unsupported infra
+    with pytest.raises(ValueError) as exc:
+        utils.validate_infrastructure(INFRASTRUCTURE.SIMPLE_APIM, [INFRASTRUCTURE.APIM_ACA])
+    assert "Unsupported infrastructure" in str(exc.value)
+
+def test_validate_infrastructure_multiple_supported():
+    # Should return True if infra is in the supported list
+    supported = [INFRASTRUCTURE.SIMPLE_APIM, INFRASTRUCTURE.APIM_ACA]
+    assert utils.validate_infrastructure(INFRASTRUCTURE.APIM_ACA, supported) is None
\ No newline at end of file


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR adds infrastructure validation support, refactors resource cleanup logic, and updates enum usage across the codebase.

- Introduced a `validate_infrastructure` function with accompanying tests
- Refactored and de-duplicated `_cleanup_resources` implementation
- Migrated `Enum` subclasses to `StrEnum`
- Injected infrastructure validation into sample notebooks

### Reviewed Changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.

<details>
<summary>Show a summary per file</summary>

| File                          | Description                                                                            |
|-------------------------------|----------------------------------------------------------------------------------------|
| tests/python/test_utils.py    | Added imports and tests for `validate_infrastructure`                                  |
| shared/python/utils.py        | Introduced `_cleanup_resources`, removed duplicate, and implemented `validate_infrastructure` |
| shared/python/apimtypes.py    | Converted `Enum` subclasses to `StrEnum`                                               |
| samples/load-balancing/create.ipynb | Added `supported_infrastructures` list and validation call                       |
| samples/general/create.ipynb  | Added `supported_infrastructures` list and validation call                             |
| samples/_TEMPLATE/create.ipynb| Provided template slot for `supported_infrastructures` and validation call             |
</details>



<details>
<summary>Comments suppressed due to low confidence (1)</summary>

**shared/python/utils.py:185**
* The code calls `traceback.print_exc()` but there is no `import traceback` in this module, which will raise a `NameError`. Add `import traceback` at the top.
```
        traceback.print_exc()
```
</details>


state: COMMENTED
author_association: NONE
submitted_at: 2025-05-28T17:30:05Z
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR adds infrastructure support to the samples by introducing new tests and functions for validating supported infrastructures. Key changes include:
- Adding tests in tests/python/test_utils.py to verify the new validate_infrastructure functionality.
- Introducing a new _cleanup_resources helper and a proper validate_infrastructure function in shared/python/utils.py.
- Updating enums in shared/python/apimtypes.py to use StrEnum.
- Updating sample notebooks to incorporate and demonstrate the new supported infrastructures pattern.

### Reviewed Changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

<details>
<summary>Show a summary per file</summary>

| File                              | Description                                                               |
| --------------------------------- | ------------------------------------------------------------------------- |
| tests/python/test_utils.py        | Added tests for the new validate_infrastructure function                  |
| shared/python/utils.py            | Added _cleanup_resources and validate_infrastructure, and removed duplicate validation function usage |
| shared/python/apimtypes.py        | Updated enum definitions to use StrEnum                                    |
| samples/load-balancing/create.ipynb | Added supported_infrastructures list and validate_infrastructure usage      |
| samples/general/create.ipynb      | Added supported_infrastructures list and validate_infrastructure usage      |
| samples/_TEMPLATE/create.ipynb    | Added placeholder for supported_infrastructures and validation call        |
</details>



<details>
<summary>Comments suppressed due to low confidence (1)</summary>

**shared/python/utils.py:183**
* The _cleanup_resources function catches all exceptions and then calls traceback.print_exc(), but there is no import statement for the traceback module. Consider importing traceback or removing its usage.
```
except Exception as e:
```
</details>


state: COMMENTED
author_association: NONE
submitted_at: 2025-05-28T17:35:11Z

Review comments:
diff_hunk: @@ -252,3 +253,22 @@ def test_extract_json_multiple_json_types():
     assert utils.extract_json(s) == [1, 2, 3]
     s2 = '{"a": 1}[1,2,3]'
     assert utils.extract_json(s2) == {"a": 1}
+
+# ------------------------------
+#    validate_infrastructure
+# ------------------------------
+
+def test_validate_infrastructure_supported():
+    # Should return True for supported infra
path: tests/python/test_utils.py
user: login: Copilot
body: The test comment says 'Should return True' but the assertion checks for `None`. Either update the comment to reflect that the function returns `None` on success or modify the function to return `True` explicitly.
```suggestion
    # Should return None for supported infra
```
created_at: 2025-05-28T17:30:04Z
author_association: NONE
diff_hunk: @@ -638,3 +624,23 @@ def run(command: str, ok_message: str = '', error_message: str = '', print_outpu
             print_message(ok_message if success else error_message, output_text if not success or print_output else "", f"[{int(minutes)}m:{int(seconds)}s]")
 
     return Output(success, output_text)
+
+# Validation functions will raise ValueError if the value is not valid
+
+validate_http_verb      = lambda val: HTTP_VERB(val)
+validate_sku            = lambda val: APIM_SKU(val)
+
+def validate_infrastructure(infra: INFRASTRUCTURE, supported_infras: list[INFRASTRUCTURE]) -> bool:
path: shared/python/utils.py
user: login: Copilot
body: The function is annotated to return `bool` but implicitly returns `None` on success. Update the return type to `None` or have the function return a boolean for consistency.
```suggestion
def validate_infrastructure(infra: INFRASTRUCTURE, supported_infras: list[INFRASTRUCTURE]) -> None:
```
created_at: 2025-05-28T17:30:04Z
author_association: NONE
diff_hunk: @@ -117,6 +117,73 @@ def get(self, key: str, label: str = '', secure: bool = False) -> str | None:
 #    PRIVATE METHODS
 # ------------------------------
 
+def _cleanup_resources(deployment_name: str, rg_name: str) -> None:
path: shared/python/utils.py
user: login: Copilot
body: [nitpick] The `_cleanup_resources` function contains repeated loops for each resource type. Consider abstracting the common purge/delete logic into a helper to reduce duplication.
created_at: 2025-05-28T17:30:04Z
author_association: NONE
diff_hunk: @@ -117,6 +117,73 @@ def get(self, key: str, label: str = '', secure: bool = False) -> str | None:
 #    PRIVATE METHODS
 # ------------------------------
 
+def _cleanup_resources(deployment_name: str, rg_name: str) -> None:
path: shared/python/utils.py
user: login: Copilot
body: The function docstring for _cleanup_resources indicates that it raises an Exception on error, but the implementation catches and logs exceptions instead. Update the docstring to reflect the actual behavior.
created_at: 2025-05-28T17:35:11Z
author_association: NONE

PR comments:
user: login: github-actions[bot]
created_at: 2025-05-28T17:28:42Z
body: ## Python 3.12 Test Results
87 tests  +3   87 ✅ +3   1s ⏱️ ±0s
 1 suites ±0    0 💤 ±0 
 1 files   ±0    0 ❌ ±0 

Results for commit d13293e6. ± Comparison against base commit 4063e697.

[test-results]:data:application/gzip;base64,H4sIABRJN2gC/12Myw6DIBBFf8Ww7kLAZ3+mGYFJJlVpeKya/nvRKmp395ybnDdDGo1n94LfCuYjhQw6Oghk5w3TEZara3d4+KjUn3nSK5kyCwQaL8I4Z91mXJxzcdmX4E8cvZVPuZXPNWWniUICprkUvTRNC00vQKlKIhissQM9lHyoQYjKpMk+X6N8Smb/AAAA

:recycle: This comment has been updated with latest results.
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-28T17:28:42Z
body: ## Python 3.13 Test Results
87 tests  +3   87 ✅ +3   0s ⏱️ -1s
 1 suites ±0    0 💤 ±0 
 1 files   ±0    0 ❌ ±0 

Results for commit d13293e6. ± Comparison against base commit 4063e697.

[test-results]:data:application/gzip;base64,H4sIABNJN2gC/12MSQ6DMAxFr4K87oIkjL0MMhkkq0CqDKuqd2+gQEN3fu9b7wWGJu3hXrBbAT5SOEFFh4HskrBMmIawTl17wOCjlH/mQc/8fzBI00Vo56zbjYvLWVzvS/Arfr2Ns9zGeU3aeaaQABQTvBe6abHpOUpZCYPa1KZDNZZsrJHzSqcT3h/pM2TT/wAAAA==

:recycle: This comment has been updated with latest results.
author_association: NONE