Skip to content

Commit 77bd0bb

Browse files
Minor fixes (#121)
1 parent eef5d2f commit 77bd0bb

File tree

10 files changed

+128
-28
lines changed

10 files changed

+128
-28
lines changed

.devcontainer/python312/devcontainer.json

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,7 @@
4545
"files.associations": {
4646
"*.bicep": "bicep"
4747
}
48-
},
49-
"openFiles": [
50-
".devcontainer/CODESPACES-QUICKSTART.md"
51-
]
48+
}
5249
},
5350
"containerEnv": {
5451
"PATH": "/workspaces/Apim-Samples/.venv/bin:${PATH}",
@@ -69,7 +66,7 @@
6966
"postStartCommand": [
7067
"bash",
7168
"-c",
72-
"echo 'APIM Samples Codespace Starting - Keep this terminal open to see progress!' && bash .devcontainer/post-start-setup.sh"
69+
"echo 'APIM Samples Codespace Starting - Keep this terminal open to see progress!' && bash .devcontainer/post-start-setup.sh && sleep 2 && code --command markdown.showPreviewToSide .devcontainer/CODESPACES-QUICKSTART.md"
7370
],
7471
"forwardPorts": [
7572
8000,

.devcontainer/python313/devcontainer.json

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,7 @@
4545
"files.associations": {
4646
"*.bicep": "bicep"
4747
}
48-
},
49-
"openFiles": [
50-
".devcontainer/CODESPACES-QUICKSTART.md"
51-
]
48+
}
5249
},
5350
"containerEnv": {
5451
"PATH": "/workspaces/Apim-Samples/.venv/bin:${PATH}",
@@ -69,7 +66,7 @@
6966
"postStartCommand": [
7067
"bash",
7168
"-c",
72-
"echo 'APIM Samples Codespace Starting - Keep this terminal open to see progress!' && bash .devcontainer/post-start-setup.sh"
69+
"echo 'APIM Samples Codespace Starting - Keep this terminal open to see progress!' && bash .devcontainer/post-start-setup.sh && sleep 2 && code --command markdown.showPreviewToSide .devcontainer/CODESPACES-QUICKSTART.md"
7370
],
7471
"forwardPorts": [
7572
8000,

.devcontainer/python314/devcontainer.json

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,7 @@
4545
"files.associations": {
4646
"*.bicep": "bicep"
4747
}
48-
},
49-
"openFiles": [
50-
".devcontainer/CODESPACES-QUICKSTART.md"
51-
]
48+
}
5249
},
5350
"containerEnv": {
5451
"PATH": "/workspaces/Apim-Samples/.venv/bin:${PATH}",
@@ -69,7 +66,7 @@
6966
"postStartCommand": [
7067
"bash",
7168
"-c",
72-
"echo 'APIM Samples Codespace Starting - Keep this terminal open to see progress!' && bash .devcontainer/post-start-setup.sh"
69+
"echo 'APIM Samples Codespace Starting - Keep this terminal open to see progress!' && bash .devcontainer/post-start-setup.sh && sleep 2 && code --command markdown.showPreviewToSide .devcontainer/CODESPACES-QUICKSTART.md"
7370
],
7471
"forwardPorts": [
7572
8000,

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
[![OpenSSF Best Practices](https://www.bestpractices.dev/projects/11057/badge)][openssf]
44
[![Python Tests][badge-python-tests]][workflow-python-tests]
55

6+
[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/Azure-Samples/Apim-Samples?devcontainer_path=.devcontainer%2Fpython314%2Fdevcontainer.json)
7+
68
**This repository provides resources to quickly deploy high-fidelity Azure API Management (APIM) infrastructures and experiment with common APIM.**
79

810
Historically, there were two general paths to experimenting with APIM. Standing up an entire landing zone with the [APIM Landing Zone Accelerator][apim-lza] can feel overwhelming and _more than needed_. Similarly, using [APIM policy snippets][apim-snippets] is only helpful when an APIM instance and infrastructure already exists.
@@ -89,6 +91,8 @@ This menu-driven interface provides quick access to:
8991
- **Setup**: Complete environment setup, verify local setup, and show Azure account info
9092
- **Tests**: Run pylint, pytest, and full Python checks
9193

94+
<img src="./assets/dev-cli-lint-test-results.png" alt="APIM Samples Developer CLI showing final linting, test, and code coverage results" title="APIM Samples Developer CLI Final Results" />
95+
9296
</details>
9397

9498

@@ -98,6 +102,9 @@ APIM Samples supports two setup options:
98102

99103
### Option 1: GitHub Codespaces / Dev Container (Recommended for First-Time Users)
100104
<details>
105+
<br/>
106+
107+
[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/Azure-Samples/Apim-Samples?devcontainer_path=.devcontainer%2Fpython314%2Fdevcontainer.json)
101108

102109
**The fastest way to get started is using our pre-configured development environment.** Everything is pre-installed and configured—just sign in to Azure and you're ready to go.
103110

18.4 KB
Loading

shared/python/charts.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -132,16 +132,16 @@ def _plot_barchart(self, api_results: list[dict]) -> None:
132132

133133
# Exclude high outliers for average calculation
134134
valid_200 = df[(df['Status Code'] == 200)].copy()
135+
136+
# Exclude high outliers (e.g., above 95th percentile)
135137
if not valid_200.empty:
136-
# Exclude high outliers (e.g., above 95th percentile)
137-
if not valid_200.empty:
138-
upper = valid_200['Response Time (ms)'].quantile(0.95)
139-
filtered = valid_200[valid_200['Response Time (ms)'] <= upper]
140-
if not filtered.empty:
141-
avg = filtered['Response Time (ms)'].mean()
142-
avg_label = f'Mean APIM response time: {avg:.1f} ms'
143-
plt.axhline(y = avg, color = 'b', linestyle = '--')
144-
plt.text(len(df) - 1, avg, avg_label, color = 'b', va = 'bottom', ha = 'right', fontsize = 10)
138+
upper = valid_200['Response Time (ms)'].quantile(0.95)
139+
filtered = valid_200[valid_200['Response Time (ms)'] <= upper]
140+
if not filtered.empty:
141+
avg = filtered['Response Time (ms)'].mean()
142+
avg_label = f'Mean APIM response time: {avg:.1f} ms'
143+
plt.axhline(y = avg, color = 'b', linestyle = '--')
144+
plt.text(len(df) - 1, avg, avg_label, color = 'b', va = 'bottom', ha = 'right', fontsize = 10)
145145

146146
# Add figtext under the chart
147147
plt.figtext(0.13, -0.1, wrap = True, ha = 'left', fontsize = 11, s = self.fig_text)

shared/python/utils.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -148,10 +148,10 @@ def create_infrastructure(self, bypass_infrastructure_check: bool = False, allow
148148
self.index = new_index
149149
# Recursively call create_infrastructure with the new index
150150
return self.create_infrastructure(bypass_infrastructure_check, allow_update)
151-
elif not should_proceed:
151+
elif not should_proceed: # pragma: no cover
152152
print_error('Infrastructure deployment cancelled by user.')
153153
raise SystemExit("User cancelled deployment")
154-
except (KeyboardInterrupt, EOFError) as exc:
154+
except (KeyboardInterrupt, EOFError) as exc: # pragma: no cover
155155
raise SystemExit("User cancelled deployment") from exc
156156

157157
# Check infrastructure existence for the normal flow
@@ -766,7 +766,7 @@ def _prompt_for_infrastructure_update(rg_name: str) -> tuple[bool, int | None]:
766766
print_plain('❌ Please enter a valid integer for the index.')
767767
elif choice == '3':
768768
return False, None
769-
elif not choice:
769+
elif not choice: # pragma: no cover
770770
# Empty input (ESC pressed in Jupyter) - cancel
771771
raise EOFError()
772772
else:
@@ -812,7 +812,7 @@ def does_infrastructure_exist(infrastructure: INFRASTRUCTURE, index: int, allow_
812812
return False # Allow deployment to proceed
813813
elif choice in ('2', '3'):
814814
return True # Block deployment
815-
elif not choice:
815+
elif not choice: # pragma: no cover
816816
# Empty input (ESC pressed in Jupyter) - cancel
817817
raise EOFError()
818818
else:

tests/python/test_apimtypes.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"""
44

55
import importlib
6+
import json
67
from pathlib import Path
78
from unittest.mock import MagicMock, patch
89
import pytest
@@ -1166,3 +1167,59 @@ def test_api_subscription_required(self):
11661167

11671168
assert api.subscriptionRequired is False
11681169
assert api.to_dict()['subscriptionRequired'] is False
1170+
1171+
1172+
class TestProjectRootDetection:
1173+
"""Test project root detection logic."""
1174+
1175+
def test_get_project_root_from_env_var(self, monkeypatch):
1176+
"""Test get_project_root when PROJECT_ROOT env var is set."""
1177+
expected_root = Path('/custom/project/root')
1178+
monkeypatch.setenv('PROJECT_ROOT', str(expected_root))
1179+
1180+
result = get_project_root()
1181+
assert result == expected_root
1182+
1183+
def test_get_project_root_without_env_var(self, monkeypatch):
1184+
"""Test get_project_root when PROJECT_ROOT env var is not set."""
1185+
monkeypatch.delenv('PROJECT_ROOT', raising=False)
1186+
1187+
# Should return a Path object (doesn't have to exist in tests)
1188+
result = get_project_root()
1189+
assert isinstance(result, Path)
1190+
1191+
1192+
class TestOutputGetEdgeCases:
1193+
"""Additional edge case tests for Output.get() method."""
1194+
1195+
def test_output_get_with_missing_label_returns_none(self):
1196+
"""Test Output.get() without label returns None on error."""
1197+
output = apimtypes.Output(success=True, text='{}')
1198+
1199+
# Should return None when key is missing and no label is provided
1200+
result = output.get('nonexistent_key')
1201+
assert result is None
1202+
1203+
def test_output_get_json_with_syntax_error(self):
1204+
"""Test Output.getJson() returns original value when parsing fails."""
1205+
json_text = json.dumps({
1206+
'output1': {
1207+
'value': 'invalid syntax {bracket'
1208+
}
1209+
})
1210+
output = apimtypes.Output(success=True, text=json_text)
1211+
1212+
# Should return the original value when parsing fails
1213+
result = output.getJson('output1')
1214+
assert result == 'invalid syntax {bracket'
1215+
1216+
def test_output_get_json_with_non_dict_properties(self):
1217+
"""Test Output.getJson() when properties is not a dict."""
1218+
json_text = json.dumps({
1219+
'properties': 'not a dict'
1220+
})
1221+
output = apimtypes.Output(success=True, text=json_text)
1222+
1223+
# Should handle gracefully and return None
1224+
result = output.getJson('key')
1225+
assert result is None

tests/python/test_local_setup.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,22 @@ def test_merge_string_list_no_duplicates():
144144
assert result == ["a", "b", "c"]
145145

146146

147+
def test_merge_string_list_empty_existing():
148+
"""Test _merge_string_list with empty existing list."""
149+
existing = []
150+
required = ["a", "b", "c"]
151+
result = sps._merge_string_list(existing, required)
152+
assert result == ["a", "b", "c"]
153+
154+
155+
def test_merge_string_list_with_none_and_empty_string():
156+
"""Test _merge_string_list handles None existing and normalizes it to empty list."""
157+
existing = None
158+
required = ["a", "b"]
159+
result = sps._merge_string_list(existing, required)
160+
assert result == ["a", "b"]
161+
162+
147163
def test_get_project_root_finds_indicators(tmp_path: Path):
148164
"""Test get_project_root locates project root by indicators."""
149165
# Create indicators at root

tests/python/test_show_soft_deleted_resources.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,12 @@ def test_purge_key_vaults_all_protected():
181181
assert skipped_count == 2
182182

183183

184+
def test_purge_key_vaults_empty_list():
185+
"""Test purge_key_vaults with empty list returns early."""
186+
result = sdr.purge_key_vaults([])
187+
assert result == (0, 0)
188+
189+
184190
# ------------------------------
185191
# CONFIRMATION TESTS
186192
# ------------------------------
@@ -701,6 +707,29 @@ def mock_az_run(cmd, *a, **k):
701707
assert not result
702708

703709

710+
def test_main_purge_all_resources_successfully(monkeypatch):
711+
"""Test main when all purgeable resources are successfully purged."""
712+
services = [{'name': 'apim-1', 'location': 'eastus', 'deletionDate': '2025-12-13T10:00:00Z', 'scheduledPurgeDate': '2026-01-13T10:00:00Z', 'serviceId': 'id-1'}]
713+
vaults = [{'name': 'vault-1', 'properties': {'location': 'eastus', 'purgeProtectionEnabled': False}}]
714+
715+
def mock_purge_apim(s):
716+
return len(s) # Successfully purge all
717+
718+
def mock_purge_kv(v):
719+
return (1, 0) # Successfully purge all, 0 skipped
720+
721+
monkeypatch.setattr('show_soft_deleted_resources.get_deleted_apim_services', lambda: services)
722+
monkeypatch.setattr('show_soft_deleted_resources.get_deleted_key_vaults', lambda: vaults)
723+
monkeypatch.setattr('show_soft_deleted_resources.purge_apim_services', mock_purge_apim)
724+
monkeypatch.setattr('show_soft_deleted_resources.purge_key_vaults', mock_purge_kv)
725+
monkeypatch.setattr('show_soft_deleted_resources.az.run', lambda cmd, *a, **k: MagicMock(success=True, json_data={'name': 'test-sub', 'id': 'sub-id'}))
726+
mock_module_functions(monkeypatch, builtins, ['print'])
727+
monkeypatch.setattr('sys.argv', ['script.py', '--purge', '--yes'])
728+
729+
result = sdr.main()
730+
assert not result
731+
732+
704733
def test_show_deleted_key_vaults_empty():
705734
"""Test show_deleted_key_vaults with no data."""
706735
with patch('builtins.print') as mock_print:

0 commit comments

Comments
 (0)