Skip to content

Commit 584eb76

Browse files
Add Azure Samples Developer CLI
1 parent d2bf13e commit 584eb76

File tree

7 files changed

+243
-14
lines changed

7 files changed

+243
-14
lines changed

README.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,38 @@ These prerequisites apply broadly across all infrastructure and samples. If ther
7676

7777
### 🛠️ Initialization
7878

79+
#### 🚀 Developer CLI
80+
81+
Use the interactive Developer CLI to verify setup, run tests, and manage your development workflow:
82+
83+
**Windows:**
84+
```powershell
85+
.\start.ps1
86+
```
87+
88+
**macOS / Linux:**
89+
```bash
90+
./start.sh
91+
```
92+
93+
This menu-driven interface provides quick access to:
94+
- **Setup**: Complete environment setup, verify local setup, show Azure account info
95+
- **Tests**: Run pylint, run pytest suite, run full Python checks
96+
- **Misc**: Exit the CLI
97+
98+
Example output from option 2 (Verify local setup):
99+
```
100+
🔍 APIM Samples Local Environment Verification
101+
✅ Virtual environment is active
102+
✅ All required packages installed
103+
✅ Shared modules can be imported
104+
✅ .env file configured
105+
⚠️ Jupyter kernel not found
106+
✅ VS Code settings configured
107+
```
108+
109+
---
110+
79111
#### Using Dev Container (Recommended)
80112

81113
If you're using the dev container (GitHub Codespaces or VS Code Dev Containers):

setup/setup_python_path.py

Lines changed: 92 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,94 @@
1616

1717
import sys
1818
import subprocess
19+
import shutil
1920
import os
2021
import json
2122
from pathlib import Path # Cross-platform path handling (Windows: \, Unix: /)
23+
import subprocess # Ensure subprocess is imported for the new functions
2224

2325

2426
DEFAULT_VSCODE_SEARCH_EXCLUDE = {
2527
"**/.venv": True,
2628
"**/.venv/**": True,
2729
}
2830

31+
def check_azure_cli_installed():
32+
"""Check if Azure CLI is installed."""
33+
az_path = shutil.which('az') or shutil.which('az.cmd') or shutil.which('az.bat')
34+
if not az_path:
35+
print(" ❌ Azure CLI is not installed. Please install from: https://learn.microsoft.com/cli/azure/install-azure-cli")
36+
return False
37+
try:
38+
subprocess.run([az_path, '--version'], capture_output=True, text=True, check=True)
39+
print(" ✅ Azure CLI is installed")
40+
return True
41+
except subprocess.CalledProcessError:
42+
print(" ❌ Azure CLI is not installed. Please install from: https://learn.microsoft.com/cli/azure/install-azure-cli")
43+
return False
44+
45+
def check_bicep_cli_installed():
46+
"""Check if Azure Bicep CLI is installed."""
47+
az_path = shutil.which('az') or shutil.which('az.cmd') or shutil.which('az.bat')
48+
if not az_path:
49+
print(" ❌ Azure CLI is not installed. Please install from: https://learn.microsoft.com/cli/azure/install-azure-cli")
50+
return False
51+
52+
try:
53+
subprocess.run([az_path, 'bicep', 'version'], capture_output=True, text=True, check=True)
54+
print(" ✅ Azure Bicep CLI is installed (via az bicep)")
55+
return True
56+
except subprocess.CalledProcessError:
57+
print(" ❌ Azure Bicep CLI is not installed. Install with: az bicep install")
58+
return False
59+
60+
def check_azure_providers_registered():
61+
"""Check if required Azure resource providers are registered in the current subscription."""
62+
az_path = shutil.which('az') or shutil.which('az.cmd') or shutil.which('az.bat')
63+
if not az_path:
64+
print(" ❌ Azure CLI is not installed. Please install from: https://learn.microsoft.com/cli/azure/install-azure-cli")
65+
return False
66+
required_providers = [
67+
'Microsoft.ApiManagement',
68+
'Microsoft.App',
69+
'Microsoft.Authorization',
70+
'Microsoft.CognitiveServices',
71+
'Microsoft.ContainerRegistry',
72+
'Microsoft.KeyVault',
73+
'Microsoft.Maps',
74+
'Microsoft.ManagedIdentity',
75+
'Microsoft.Network',
76+
'Microsoft.OperationalInsights',
77+
'Microsoft.Resources',
78+
'Microsoft.Storage'
79+
]
80+
81+
try:
82+
# Get list of registered providers
83+
result = subprocess.run(
84+
[az_path, 'provider', 'list', '--query', '[].namespace', '-o', 'json'],
85+
capture_output=True,
86+
text=True,
87+
check=True
88+
)
89+
registered_providers = json.loads(result.stdout)
90+
91+
missing_providers = [p for p in required_providers if p not in registered_providers]
92+
93+
if not missing_providers:
94+
print(" ✅ All required Azure resource providers are registered")
95+
return True
96+
97+
print(f" ❌ Missing {len(missing_providers)} Azure provider(s):")
98+
for provider in missing_providers:
99+
print(f" • {provider}")
100+
print(" Register with: az provider register -n <provider-namespace>")
101+
return False
102+
103+
except (subprocess.CalledProcessError, json.JSONDecodeError, FileNotFoundError):
104+
print(" ⚠️ Could not verify Azure provider registrations (Azure CLI may not be configured)")
105+
return False
106+
29107
DEFAULT_VSCODE_FILES_WATCHER_EXCLUDE = {
30108
"**/.venv/**": True,
31109
}
@@ -549,16 +627,26 @@ def force_kernel_consistency():
549627

550628
def setup_complete_environment():
551629
"""
552-
Complete setup: generate .env file, register kernel, and configure VS Code.
630+
Complete setup: check Azure prerequisites, generate .env file, register kernel, and configure VS Code.
553631
554632
This provides a one-command setup that makes the local environment
555633
as easy to use as the dev container.
556634
"""
557635

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

638+
# Step 0: Check Azure prerequisites
639+
print("0. Checking Azure prerequisites...")
640+
azure_cli_ok = check_azure_cli_installed()
641+
bicep_ok = check_bicep_cli_installed()
642+
providers_ok = check_azure_providers_registered()
643+
644+
if not (azure_cli_ok and bicep_ok and providers_ok):
645+
print("\n⚠️ Some Azure prerequisites are missing. Please address the issues above and re-run this script.")
646+
return
647+
560648
# Step 1: Generate .env file
561-
print("1. Generating .env file for Python path configuration...")
649+
print("\n1. Generating .env file for Python path configuration...")
562650
generate_env_file()
563651

564652
# Step 2: Register Jupyter kernel
@@ -576,6 +664,8 @@ def setup_complete_environment():
576664
# Summary
577665
print("\n" + "="*50)
578666
print("📋 Setup Summary:")
667+
print(" ✅ Azure CLI and Bicep: Available")
668+
print(" ✅ Azure resource providers: Registered")
579669
print(" ✅ Python path configuration: Complete")
580670
print(f" {'✅' if kernel_success else '❌'} Jupyter kernel registration: {'Complete' if kernel_success else 'Failed'}")
581671
print(f" {'✅' if vscode_success else '❌'} VS Code settings: {'Complete' if vscode_success else 'Failed'}")

setup/verify_local_setup.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
- Virtual environment is active
77
- Required packages are installed
88
- Shared modules can be imported
9+
- Azure CLI and Bicep CLI are installed
10+
- Required Azure resource providers are registered
911
- Jupyter kernel is registered
1012
- VS Code settings are configured
1113
@@ -16,6 +18,8 @@
1618
import subprocess
1719
import os
1820
import io
21+
import json
22+
import shutil
1923
from pathlib import Path
2024

2125
# Configure UTF-8 encoding for console output
@@ -184,6 +188,92 @@ def check_env_file():
184188
return False
185189

186190

191+
def check_azure_cli():
192+
"""Check if Azure CLI is installed."""
193+
az_path = shutil.which('az') or shutil.which('az.cmd') or shutil.which('az.bat')
194+
if not az_path:
195+
print_status("Azure CLI is not installed or not in PATH", False)
196+
return False
197+
try:
198+
result = subprocess.run([az_path, '--version'], capture_output=True, text=True, check=True)
199+
version_line = (result.stdout.splitlines() or ["unknown version"])[0].strip()
200+
print_status(f"Azure CLI is installed ({version_line})")
201+
return True
202+
except subprocess.CalledProcessError:
203+
print_status("Azure CLI is not installed or not in PATH", False)
204+
return False
205+
206+
207+
def check_bicep_cli():
208+
"""Check if Azure Bicep CLI is installed."""
209+
az_path = shutil.which('az') or shutil.which('az.cmd') or shutil.which('az.bat')
210+
if not az_path:
211+
print_status("Azure CLI is not installed or not in PATH", False)
212+
return False
213+
214+
try:
215+
result = subprocess.run([az_path, 'bicep', 'version'], capture_output=True, text=True, check=True)
216+
version_line = (result.stdout.splitlines() or ["unknown version"])[0].strip()
217+
print_status(f"Azure Bicep CLI is installed (az bicep version: {version_line})")
218+
return True
219+
except subprocess.CalledProcessError:
220+
print_status("Azure Bicep CLI is not installed. Install with: az bicep install", False)
221+
return False
222+
223+
224+
def check_azure_providers():
225+
"""Check if required Azure resource providers are registered in the current subscription."""
226+
az_path = shutil.which('az') or shutil.which('az.cmd') or shutil.which('az.bat')
227+
if not az_path:
228+
print_status("Azure CLI is not installed or not in PATH", False)
229+
return False
230+
required_providers = [
231+
'Microsoft.ApiManagement',
232+
'Microsoft.App',
233+
'Microsoft.Authorization',
234+
'Microsoft.CognitiveServices',
235+
'Microsoft.ContainerRegistry',
236+
'Microsoft.KeyVault',
237+
'Microsoft.Maps',
238+
'Microsoft.ManagedIdentity',
239+
'Microsoft.Network',
240+
'Microsoft.OperationalInsights',
241+
'Microsoft.Resources',
242+
'Microsoft.Storage'
243+
]
244+
245+
try:
246+
# Get list of registered providers
247+
result = subprocess.run(
248+
[az_path, 'provider', 'list', '--query', '[].namespace', '-o', 'json'],
249+
capture_output=True,
250+
text=True,
251+
check=True
252+
)
253+
registered_providers = sorted(set(json.loads(result.stdout)))
254+
255+
missing_providers = [p for p in required_providers if p not in registered_providers]
256+
found_providers = [p for p in required_providers if p in registered_providers]
257+
258+
print(" Registered providers:")
259+
for provider in found_providers:
260+
print(f" - {provider}")
261+
262+
if not missing_providers:
263+
print_status("All required Azure providers are registered")
264+
return True
265+
266+
print_status(f"Missing {len(missing_providers)} provider(s): {', '.join(missing_providers)}", False)
267+
print(" Register missing providers with:")
268+
for provider in missing_providers:
269+
print(f" az provider register -n {provider}")
270+
return False
271+
272+
except (subprocess.CalledProcessError, json.JSONDecodeError, FileNotFoundError):
273+
print_status("Could not verify Azure provider registrations", False)
274+
return False
275+
276+
187277
def main():
188278
"""Run all verification checks."""
189279
print("🔍 APIM Samples Local Environment Verification")
@@ -194,6 +284,9 @@ def main():
194284
("Required Packages", check_required_packages),
195285
("Shared Modules", check_shared_modules),
196286
("Environment File", check_env_file),
287+
("Azure CLI", check_azure_cli),
288+
("Azure Bicep CLI", check_bicep_cli),
289+
("Azure Providers", check_azure_providers),
197290
("Jupyter Kernel", check_jupyter_kernel),
198291
("VS Code Settings", check_vscode_settings)
199292
]

shared/python/console.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,8 @@ def _print_log(message: str, prefix: str = '', color: str = '', output: str = ''
156156

157157
def print_command(cmd: str = '') -> None:
158158
"""Print a command message."""
159-
_print_log(cmd, '⚙️ ', BOLD_B, blank_above = True, blank_below = True, level = logging.INFO)
159+
# Two spaces after the cog as opposed to one elsewhere are intentional for formatting. Please don't change this.
160+
_print_log(cmd, '⚙️ ', BOLD_B, blank_above = True, blank_below = True, level = logging.INFO)
160161

161162
def print_error(msg: str, output: str = '', duration: str = '') -> None:
162163
"""Print an error message."""

start.ps1

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,15 @@
33
Set-StrictMode -Version Latest
44
$ErrorActionPreference = 'Stop'
55

6+
# Set UTF-8 encoding for console output to properly display Unicode characters
7+
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
8+
9+
# Enable ANSI escape sequence support in PowerShell 7+
10+
$PSVersionTable.PSVersion.Major -ge 7 | Out-Null
11+
if ($PSVersionTable.PSVersion.Major -ge 7) {
12+
$env:TERM = "xterm-256color"
13+
}
14+
615
$ScriptDir = Split-Path -Path $MyInvocation.MyCommand.Path -Parent
716
$RepoRoot = $ScriptDir
817

@@ -34,11 +43,12 @@ function Invoke-Cmd {
3443

3544
$exe = $flatArgs[0]
3645
$cmdArgs = @()
46+
3747
if ($flatArgs.Count -gt 1) {
3848
$cmdArgs = @($flatArgs[1..($flatArgs.Count - 1)])
3949
}
4050

41-
$output = & $exe @cmdArgs 2>&1
51+
& $exe @cmdArgs 2>&1 | Write-Host
4252
$exitCode = $LASTEXITCODE
4353
}
4454
catch {
@@ -54,9 +64,6 @@ function Invoke-Cmd {
5464

5565
if ($exitCode -ne 0) {
5666
Write-Host ""
57-
if (-not $output) {
58-
Write-Host "No output was returned from the command." -ForegroundColor Yellow
59-
}
6067
Write-Host "Command exited with code $exitCode" -ForegroundColor Yellow
6168
Write-Host ""
6269
Invoke-Pause-Menu
@@ -97,7 +104,7 @@ except Exception as exc: # pylint: disable=broad-except
97104
function Invoke-Pause-Menu {
98105
Write-Host ""
99106
Write-Host "=========================="
100-
Read-Host "Press Enter to return to the APIM Samples Developer CLI menu" | Out-Null
107+
Read-Host "Press ENTER to return to the menu" | Out-Null
101108
}
102109

103110
while ($true) {
@@ -113,7 +120,7 @@ while ($true) {
113120
Write-Host "Tests" -ForegroundColor Yellow
114121
Write-Host " 4) Run pylint"
115122
Write-Host " 5) Run tests"
116-
Write-Host " 6) Run full python checks"
123+
Write-Host " 6) Run full Python checks"
117124
Write-Host ""
118125
Write-Host "Misc" -ForegroundColor Yellow
119126
Write-Host " 0) Exit"

start.sh

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,8 @@ shared = root / "shared" / "python"
5656
if str(shared) not in sys.path:
5757
sys.path.insert(0, str(shared))
5858
try:
59-
import utils
60-
info = utils.get_account_info()
59+
import azure_resources as az
60+
info = az.get_account_info()
6161
print(json.dumps(info, indent=2))
6262
except Exception as exc: # pylint: disable=broad-except
6363
print(f"Failed to read Azure account info: {exc}")
@@ -67,7 +67,7 @@ PY
6767

6868
pause_prompt() {
6969
echo
70-
read -rp "Press Enter to return to the menu..." _
70+
read -rp "Press ENTER to return to the menu..." _
7171
}
7272

7373
while true; do
@@ -83,7 +83,7 @@ while true; do
8383
echo "Tests"
8484
echo " 4) Run pylint"
8585
echo " 5) Run tests"
86-
echo " 6) Run full python checks"
86+
echo " 6) Run full Python checks"
8787
echo ""
8888
echo "Misc"
8989
echo " 0) Exit"

0 commit comments

Comments
 (0)