Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/python.instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ Before completing any Python code changes, verify:
## Testing

- Aim for 90+% code coverage for each file.
- Slow tests (> 0.1s runtime) should be identified and fixed, if possible.
- Add or update pytest unit tests when changing behavior.
- Prefer focused tests for the code being changed.
- Avoid tests that require live Azure access; mock Azure CLI interactions and `azure_resources` helpers.
Expand Down
85 changes: 43 additions & 42 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,44 +1,45 @@
{
"python.analysis.exclude": [
"**/node_modules",
"**/__pycache__",
".git",
"**/build",
"env/**",
"**/.*",
"**/.venv",
"**/venv",
"**/env"
],
"files.trimTrailingWhitespace": true,
"files.insertFinalNewline": true,
"files.trimFinalNewlines": true,
"files.eol": "\n",
"editor.renderWhitespace": "trailing",
"python.defaultInterpreterPath": "./.venv/Scripts/python.exe",
"python.envFile": "${workspaceFolder}/.env",
"[python]": {
"editor.codeActionsOnSave": {
"source.organizeImports": "explicit",
"source.unusedImports": "explicit"
},
"editor.formatOnSave": true
"[markdown]": {
"files.trimTrailingWhitespace": false
},
"[python]": {
"editor.codeActionsOnSave": {
"source.organizeImports": "explicit",
"source.unusedImports": "explicit"
},
"[markdown]": {
"files.trimTrailingWhitespace": false
},
"terminal.integrated.defaultProfile.windows": "PowerShell",
"plantuml.render": "Local",
"plantuml.exportFormat": "svg",
"plantuml.java": "C:\\Program Files\\OpenJDK\\jdk-22.0.2\\bin\\java.exe",
"plantuml.diagramsRoot": "assets/diagrams/src",
"plantuml.exportOutDir": "assets/diagrams/out",
"python.terminal.activateEnvironment": true,
"python.terminal.activateEnvInCurrentTerminal": true,
"python.testing.pytestEnabled": true,
"python.linting.enabled": true,
"python.linting.pylintEnabled": true,
"jupyter.kernels.trusted": [
"./.venv/Scripts/python.exe"
]
}
"editor.formatOnSave": true
},
"editor.renderWhitespace": "trailing",
"files.eol": "\n",
"files.insertFinalNewline": true,
"files.trimFinalNewlines": true,
"files.trimTrailingWhitespace": true,
"jupyter.kernels.trusted": [
"./.venv/Scripts/python.exe"
],
"plantuml.diagramsRoot": "assets/diagrams/src",
"plantuml.exportFormat": "svg",
"plantuml.exportOutDir": "assets/diagrams/out",
"plantuml.java": "C:\\Program Files\\OpenJDK\\jdk-22.0.2\\bin\\java.exe",
"plantuml.render": "Local",
"python.analysis.exclude": [
"**/node_modules",
"**/__pycache__",
".git",
"**/build",
"env/**",
"**/.*",
"**/.venv",
"**/venv",
"**/env"
],
"python.defaultInterpreterPath": "./.venv/Scripts/python.exe",
"python.envFile": "${workspaceFolder}/.env",
"python.linting.enabled": true,
"python.linting.pylintEnabled": true,
"python.terminal.activateEnvInCurrentTerminal": true,
"python.terminal.activateEnvironment": true,
"python.terminal.useEnvFile": true,
"python.testing.pytestEnabled": true,
"terminal.integrated.defaultProfile.windows": "PowerShell"
}
49 changes: 25 additions & 24 deletions setup/local_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,29 +74,29 @@ def check_azure_cli_installed():
"""Check if Azure CLI is installed."""
az_path = shutil.which('az') or shutil.which('az.cmd') or shutil.which('az.bat')
if not az_path:
print(" ❌ Azure CLI is not installed. Please install from: https://learn.microsoft.com/cli/azure/install-azure-cli")
print("❌ Azure CLI is not installed. Please install from: https://learn.microsoft.com/cli/azure/install-azure-cli")
return False
try:
subprocess.run([az_path, '--version'], capture_output=True, text=True, check=True)
print(" ✅ Azure CLI is installed")
print("✅ Azure CLI is installed")
return True
except subprocess.CalledProcessError:
print(" ❌ Azure CLI is not installed. Please install from: https://learn.microsoft.com/cli/azure/install-azure-cli")
print("❌ Azure CLI is not installed. Please install from: https://learn.microsoft.com/cli/azure/install-azure-cli")
return False

def check_bicep_cli_installed():
"""Check if Azure Bicep CLI is installed."""
az_path = shutil.which('az') or shutil.which('az.cmd') or shutil.which('az.bat')
if not az_path:
print(" ❌ Azure CLI is not installed. Please install from: https://learn.microsoft.com/cli/azure/install-azure-cli")
print("❌ Azure CLI is not installed. Please install from: https://learn.microsoft.com/cli/azure/install-azure-cli")
return False

try:
subprocess.run([az_path, 'bicep', 'version'], capture_output=True, text=True, check=True)
print(" ✅ Azure Bicep CLI is installed (via az bicep)")
print("✅ Azure Bicep CLI is installed (via az bicep)")
return True
except subprocess.CalledProcessError:
print(" ❌ Azure Bicep CLI is not installed. Install with: az bicep install")
print("❌ Azure Bicep CLI is not installed. Install with: az bicep install")
return False

def check_azure_providers_registered():
Expand Down Expand Up @@ -133,10 +133,10 @@ def check_azure_providers_registered():
missing_providers = [p for p in required_providers if p not in registered_providers]

if not missing_providers:
print(" ✅ All required Azure resource providers are registered")
print("✅ All required Azure resource providers are registered")
return True

print(f" ❌ Missing {len(missing_providers)} Azure provider(s):")
print(f"❌ Missing {len(missing_providers)} Azure provider(s):")
for provider in missing_providers:
print(f" • {provider}")
print(" Register with: az provider register -n <provider-namespace>")
Expand Down Expand Up @@ -306,7 +306,7 @@ def generate_env_file() -> None:
with open(env_file_path, 'w', encoding='utf-8') as f:
f.write(env_content)

print(f"\nSuccessfully generated .env file: {env_file_path}\n")
print(f"\n✅ Successfully generated .env file: {env_file_path}\n")


def install_jupyter_kernel():
Expand Down Expand Up @@ -380,8 +380,6 @@ def create_vscode_settings():
"python.terminal.activateEnvironment": True,
"python.terminal.activateEnvInCurrentTerminal": True,
"python.testing.pytestEnabled": True,
"python.linting.enabled": True,
"python.linting.pylintEnabled": True,
"jupyter.kernels.trusted": [venv_python],
}

Expand All @@ -408,7 +406,8 @@ def create_vscode_settings():
)

with open(settings_file, 'w', encoding='utf-8') as f:
json.dump(merged_settings, f, indent=4)
json.dump(merged_settings, f, indent=2, sort_keys=True)
f.write('\n')

print(f"✅ VS Code settings updated: {settings_file}")
print(" - Existing settings preserved")
Expand All @@ -419,7 +418,8 @@ def create_vscode_settings():
required_settings["python.analysis.exclude"] = DEFAULT_PYTHON_ANALYSIS_EXCLUDE

with open(settings_file, 'w', encoding='utf-8') as f:
json.dump(required_settings, f, indent=4)
json.dump(required_settings, f, indent=2, sort_keys=True)
f.write('\n')

print(f"✅ VS Code settings created: {settings_file}")
print(" - Python interpreter configured for .venv")
Expand Down Expand Up @@ -502,7 +502,8 @@ def force_kernel_consistency():
)

with open(settings_file, 'w', encoding='utf-8') as f:
json.dump(merged_settings, f, indent=4)
json.dump(merged_settings, f, indent=2)
f.write('\n')

print("✅ Kernel trust refreshed without overriding user settings")
return True
Expand All @@ -522,8 +523,8 @@ def setup_complete_environment():

print("🚀 Setting up complete APIM Samples environment...\n")

# Step 0: Check Azure prerequisites
print("0. Checking Azure prerequisites...")
# Step 1: Check Azure prerequisites
print("1/5) Checking Azure prerequisites...\n")
azure_cli_ok = check_azure_cli_installed()
bicep_ok = check_bicep_cli_installed()
providers_ok = check_azure_providers_registered()
Expand All @@ -532,20 +533,20 @@ def setup_complete_environment():
print("\n⚠️ Some Azure prerequisites are missing. Please address the issues above and re-run this script.")
return

# Step 1: Generate .env file
print("\n1. Generating .env file for Python path configuration...")
# Step 2: Generate .env file
print("\n2/5) Generating .env file for Python path configuration...")
generate_env_file()

# Step 2: Register Jupyter kernel
print("2. Registering standardized Jupyter kernel...")
# Step 3: Register Jupyter kernel
print("3/5) Registering standardized Jupyter kernel...\n")
kernel_success = install_jupyter_kernel()

# Step 3: Configure VS Code settings with minimal, merged defaults
print("\n3. Configuring VS Code workspace settings...")
# Step 4: Configure VS Code settings with minimal, merged defaults
print("\n4/5) Configuring VS Code workspace settings...\n")
vscode_success = create_vscode_settings()

# Step 4: Enforce kernel consistency
print("\n4. Enforcing kernel consistency for future reliability...")
# Step 5: Enforce kernel consistency
print("\n5/5) Enforcing kernel consistency for future reliability...\n")
consistency_success = force_kernel_consistency()

# Summary
Expand Down
19 changes: 15 additions & 4 deletions setup/verify_local_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,9 @@ def check_azure_cli():
try:
result = subprocess.run([az_path, '--version'], capture_output=True, text=True, check=True)
version_line = (result.stdout.splitlines() or ["unknown version"])[0].strip()
print_status(f"Azure CLI is installed ({version_line})")
# Extract just the version number from "azure-cli 2.81.0"
version = version_line.split()[-1] if version_line else "unknown"
print_status(f"Azure CLI is installed ({version})")
return True
except subprocess.CalledProcessError:
print_status("Azure CLI is not installed or not in PATH", False)
Expand All @@ -216,7 +218,16 @@ def check_bicep_cli():
try:
result = subprocess.run([az_path, 'bicep', 'version'], capture_output=True, text=True, check=True)
version_line = (result.stdout.splitlines() or ["unknown version"])[0].strip()
print_status(f"Azure Bicep CLI is installed (az bicep version: {version_line})")
# Extract version number from "Bicep CLI version 0.39.26 (1e90b06e40)"
version = "unknown"
if "version" in version_line.lower():
parts = version_line.split()
# Find index of "version" and get the next part
for i, part in enumerate(parts):
if part.lower() == "version" and i + 1 < len(parts):
version = parts[i + 1]
break
print_status(f"Azure Bicep CLI is installed ({version})")
return True
except subprocess.CalledProcessError:
print_status("Azure Bicep CLI is not installed. Install with: az bicep install", False)
Expand Down Expand Up @@ -262,10 +273,10 @@ def check_azure_providers():
print(f" - {provider}")

if not missing_providers:
print_status("All required Azure providers are registered")
print_status("\nAll required Azure providers are registered")
return True

print_status(f"Missing {len(missing_providers)} provider(s): {', '.join(missing_providers)}", False)
print_status(f"\nMissing {len(missing_providers)} provider(s): {', '.join(missing_providers)}", False)
print(" Register missing providers with:")
for provider in missing_providers:
print(f" az provider register -n {provider}")
Expand Down
78 changes: 71 additions & 7 deletions tests/python/check_python.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,46 @@ foreach ($Line in $TestOutput) {

$TotalTests = $PassedTests + $FailedTests

# Parse coverage from coverage.json
$CoveragePercent = $null
$CoverageJsonPath = Join-Path $ScriptDir "..\..\coverage.json"
if (Test-Path $CoverageJsonPath) {
try {
$CoverageData = Get-Content $CoverageJsonPath -Raw | ConvertFrom-Json
if ($CoverageData.totals -and $CoverageData.totals.percent_covered) {
$CoveragePercent = $CoverageData.totals.percent_covered
}
}
catch {
# Silently continue if coverage parsing fails
}
}

# Fallback: Parse coverage from pytest output (e.g., "TOTAL ... 95%")
if ($CoveragePercent -eq $null) {
foreach ($Line in $TestOutput) {
$LineStr = $Line.ToString()
if ($LineStr -match 'TOTAL\s+.*\s+(\d+)%') {
$CoveragePercent = [int]::Parse($matches[1])
break
}
}
}

# Detect slow tests (>0.1s execution time)
$SlowTestsFound = $false
foreach ($Line in $TestOutput) {
$LineStr = $Line.ToString()
# Match lines like "1.23s call test_file.py::test_name"
if ($LineStr -match '(\d+\.\d+)s\s+call\s+') {
$time = [double]::Parse($matches[1])
if ($time -gt 0.1) {
$SlowTestsFound = $true
break
}
}
}

Write-Host ""


Expand Down Expand Up @@ -131,11 +171,11 @@ $LintColor = if ($LintExitCode -eq 0) { "Green" } else { "Yellow" }
$TestColor = if ($TestExitCode -eq 0) { "Green" } else { "Red" }

# Calculate column widths for alignment
$LabelWidth = "Pylint :".Length # 7
$LabelWidth = "Pylint :".Length # 7
$Padding = " " * ($LabelWidth - 1)

# Display Pylint status with score
Write-Host "Pylint : " -NoNewline
Write-Host "Pylint : " -NoNewline
Write-Host $LintStatus -ForegroundColor $LintColor -NoNewline
if ($PylintScore) {
Write-Host " ($PylintScore)" -ForegroundColor Gray
Expand All @@ -144,19 +184,43 @@ if ($PylintScore) {
}

# Display Test status with counts
Write-Host "Tests : " -NoNewline
Write-Host "Tests : " -NoNewline
Write-Host $TestStatus -ForegroundColor $TestColor

# Display test counts with right-aligned numbers
# Display test counts with right-aligned numbers and percentages
if ($TotalTests -gt 0) {
# Calculate padding for right-alignment (max 5 digits)
$TotalPadded = "{0,5}" -f $TotalTests
$PassedPadded = "{0,5}" -f $PassedTests
$FailedPadded = "{0,5}" -f $FailedTests

Write-Host " • Total : $TotalPadded" -ForegroundColor Gray
Write-Host " • Passed : $PassedPadded" -ForegroundColor Gray
Write-Host " • Failed : $FailedPadded" -ForegroundColor Gray
# Calculate percentages
$PassedPercent = ($PassedTests / $TotalTests * 100)
$FailedPercent = ($FailedTests / $TotalTests * 100)
$PassedPercentStr = "{0,6:F2}" -f $PassedPercent
$FailedPercentStr = "{0,6:F2}" -f $FailedPercent

Write-Host " • Total : $TotalPadded" -ForegroundColor Gray
Write-Host " • Passed : $PassedPadded (" -ForegroundColor Gray -NoNewline
Write-Host $PassedPercentStr -ForegroundColor Gray -NoNewline
Write-Host "%)" -ForegroundColor Gray
Write-Host " • Failed : $FailedPadded (" -ForegroundColor Gray -NoNewline
Write-Host $FailedPercentStr -ForegroundColor Gray -NoNewline
Write-Host "%)" -ForegroundColor Gray
}

# Display code coverage
if ($CoveragePercent -ne $null) {
Write-Host "Coverage : " -NoNewline
Write-Host "📊 " -NoNewline
Write-Host ("{0:F2}" -f $CoveragePercent) -ForegroundColor Cyan -NoNewline
Write-Host "%" -ForegroundColor Cyan
}

# Display slow tests warning if detected
if ($SlowTestsFound) {
Write-Host ""
Write-Host "⚠️ SLOW TESTS DETECTED (> 0.1s). Please review slowest durations in test summary." -ForegroundColor Yellow
}

Write-Host ""
Expand Down
Loading
Loading