Skip to content

Commit 039c497

Browse files
Add Developer CLI (#111)
1 parent eb93b77 commit 039c497

File tree

10 files changed

+534
-13
lines changed

10 files changed

+534
-13
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: 98 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
@@ -15,8 +17,15 @@
1517
import sys
1618
import subprocess
1719
import os
20+
import io
21+
import json
22+
import shutil
1823
from pathlib import Path
1924

25+
# Configure UTF-8 encoding for console output
26+
if sys.stdout.encoding != 'utf-8':
27+
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
28+
2029

2130
def print_status(message, success=True):
2231
"""Print status message with colored output."""
@@ -179,6 +188,92 @@ def check_env_file():
179188
return False
180189

181190

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+
182277
def main():
183278
"""Run all verification checks."""
184279
print("🔍 APIM Samples Local Environment Verification")
@@ -189,6 +284,9 @@ def main():
189284
("Required Packages", check_required_packages),
190285
("Shared Modules", check_shared_modules),
191286
("Environment File", check_env_file),
287+
("Azure CLI", check_azure_cli),
288+
("Azure Bicep CLI", check_bicep_cli),
289+
("Azure Providers", check_azure_providers),
192290
("Jupyter Kernel", check_jupyter_kernel),
193291
("VS Code Settings", check_vscode_settings)
194292
]

shared/python/console.py

Lines changed: 3 additions & 2 deletions
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."""
@@ -180,7 +181,7 @@ def print_warning(msg: str, output: str = '', duration: str = '') -> None:
180181

181182
def print_val(name: str, value: str, val_below: bool = False) -> None:
182183
"""Print a key-value pair."""
183-
_print_log(f"{name:<25}:{'\n' if val_below else ' '}{value}", '👉🏽 ', BOLD_B, wrap_lines = True, level = logging.INFO)
184+
_print_log(f"{name:<25}:{'\n' if val_below else ' '}{value}", '👉 ', BOLD_B, wrap_lines = True, level = logging.INFO)
184185

185186
def print_plain(msg: str = '', *, level: int | None = None, wrap_lines: bool = True, blank_above: bool = False, blank_below: bool = False) -> None:
186187
"""Log a message without any icon/prefix.

shared/python/infrastructures.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1248,7 +1248,7 @@ def _cleanup_resources_parallel(resources: list[dict], thread_prefix: str = '',
12481248
def log_info(msg):
12491249
if use_thread_safe_printing:
12501250
with _print_lock:
1251-
_print_log(f"{thread_prefix}{msg}", '👉🏽 ', thread_color)
1251+
_print_log(f"{thread_prefix}{msg}", '👉 ', thread_color)
12521252
else:
12531253
print_info(msg)
12541254

@@ -1494,13 +1494,13 @@ def _cleanup_resources_thread_safe(deployment_name: str, rg_name: str, thread_pr
14941494
"""
14951495
try:
14961496
with _print_lock:
1497-
_print_log(f"{thread_prefix}Starting cleanup for resource group: {rg_name}", '👉🏽 ', thread_color)
1497+
_print_log(f"{thread_prefix}Starting cleanup for resource group: {rg_name}", '👉 ', thread_color)
14981498

14991499
# Create a modified version of _cleanup_resources that uses thread-safe printing
15001500
_cleanup_resources_with_thread_safe_printing(deployment_name, rg_name, thread_prefix, thread_color)
15011501

15021502
with _print_lock:
1503-
_print_log(f"{thread_prefix}Completed cleanup for resource group: {rg_name}", '👉🏽 ', thread_color)
1503+
_print_log(f"{thread_prefix}Completed cleanup for resource group: {rg_name}", '👉 ', thread_color)
15041504

15051505
return True, ""
15061506

@@ -1531,7 +1531,7 @@ def _cleanup_resources_with_thread_safe_printing(deployment_name: str, rg_name:
15311531

15321532
try:
15331533
with _print_lock:
1534-
_print_log(f"{thread_prefix}Resource group : {rg_name}", '👉🏽 ', thread_color)
1534+
_print_log(f"{thread_prefix}Resource group : {rg_name}", '👉 ', thread_color)
15351535

15361536
# Show the deployment details
15371537
output = az.run(
@@ -1596,13 +1596,13 @@ def _cleanup_resources_with_thread_safe_printing(deployment_name: str, rg_name:
15961596
with _print_lock:
15971597
_print_log(
15981598
f"{thread_prefix}Found {len(resources_to_cleanup)} resource(s) to clean up. Processing in parallel...",
1599-
'👉🏽 ',
1599+
'👉 ',
16001600
thread_color
16011601
)
16021602
_cleanup_resources_parallel_thread_safe(resources_to_cleanup, thread_prefix, thread_color)
16031603
else:
16041604
with _print_lock:
1605-
_print_log(f"{thread_prefix}No resources found to clean up.", '👉🏽 ', thread_color)
1605+
_print_log(f"{thread_prefix}No resources found to clean up.", '👉 ', thread_color)
16061606

16071607
# Delete the resource group last
16081608
rg_delete_attempted = True

0 commit comments

Comments
 (0)