Skip to content

Commit 657edc0

Browse files
committed
Added Permissions verbose & testing
1 parent ba85aae commit 657edc0

File tree

2 files changed

+211
-21
lines changed

2 files changed

+211
-21
lines changed

src/brightdata/core/zone_manager.py

Lines changed: 90 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ async def ensure_required_zones(
4242
self,
4343
web_unlocker_zone: str,
4444
serp_zone: Optional[str] = None,
45-
browser_zone: Optional[str] = None
45+
browser_zone: Optional[str] = None,
46+
skip_verification: bool = False
4647
) -> None:
4748
"""
4849
Check if required zones exist and create them if they don't.
@@ -90,13 +91,42 @@ async def ensure_required_zones(
9091
# Create zones
9192
for zone_name, zone_type in zones_to_create:
9293
logger.info(f"Creating zone: {zone_name} (type: {zone_type})")
93-
await self._create_zone(zone_name, zone_type)
94-
logger.info(f"Successfully created zone: {zone_name}")
95-
96-
# Verify zones were created
97-
await self._verify_zones_created([zone[0] for zone in zones_to_create])
94+
try:
95+
await self._create_zone(zone_name, zone_type)
96+
logger.info(f"Successfully created zone: {zone_name}")
97+
except AuthenticationError as e:
98+
# Re-raise with clear message - this is a permission issue
99+
logger.error(f"Failed to create zone '{zone_name}' due to insufficient permissions")
100+
raise
101+
except ZoneError as e:
102+
# Log and re-raise zone errors
103+
logger.error(f"Failed to create zone '{zone_name}': {e}")
104+
raise
98105

99-
except (ZoneError, AuthenticationError, APIError):
106+
# Verify zones were created (unless skipped)
107+
if not skip_verification:
108+
try:
109+
await self._verify_zones_created([zone[0] for zone in zones_to_create])
110+
except ZoneError as e:
111+
# Log verification failure but don't fail the entire operation
112+
logger.warning(
113+
f"Zone verification failed: {e}. "
114+
f"Zones may have been created but aren't yet visible in the API. "
115+
f"Check your dashboard at https://brightdata.com/cp/zones"
116+
)
117+
# Don't re-raise - zones were likely created successfully
118+
else:
119+
logger.info("Skipping zone verification (skip_verification=True)")
120+
121+
except AuthenticationError as e:
122+
# Permission errors are critical - show clear message
123+
logger.error(
124+
"\n❌ ZONE CREATION BLOCKED: API token lacks required permissions\n"
125+
f" Error: {e}\n"
126+
" Fix: Update your token permissions at https://brightdata.com/cp/setting/users"
127+
)
128+
raise
129+
except (ZoneError, APIError):
100130
raise
101131
except Exception as e:
102132
logger.error(f"Unexpected error while ensuring zones exist: {e}")
@@ -204,11 +234,36 @@ async def _create_zone(self, zone_name: str, zone_type: str) -> None:
204234
logger.info(f"Zone {zone_name} already exists - this is expected")
205235
return
206236

207-
# Handle authentication errors
237+
# Handle authentication/permission errors
208238
if response.status in (HTTP_UNAUTHORIZED, HTTP_FORBIDDEN):
209-
raise AuthenticationError(
210-
f"Authentication failed ({response.status}) creating zone '{zone_name}': {error_text}"
211-
)
239+
# Check for specific permission error
240+
if "permission" in error_text.lower() or "lacks the required" in error_text.lower():
241+
error_msg = (
242+
f"\n{'='*70}\n"
243+
f"❌ PERMISSION ERROR: Cannot create zone '{zone_name}'\n"
244+
f"{'='*70}\n"
245+
f"Your API key lacks the required permissions for zone creation.\n\n"
246+
f"To fix this:\n"
247+
f" 1. Go to: https://brightdata.com/cp/setting/users\n"
248+
f" 2. Find your API token\n"
249+
f" 3. Enable 'Zone Management' or 'Create Zones' permission\n"
250+
f" 4. Save changes and try again\n\n"
251+
f"API Response: {error_text}\n"
252+
f"{'='*70}\n"
253+
)
254+
logger.error(error_msg)
255+
raise AuthenticationError(
256+
f"API key lacks permission to create zones. "
257+
f"Update permissions at https://brightdata.com/cp/setting/users"
258+
)
259+
else:
260+
# Generic auth error
261+
logger.error(
262+
f"Authentication failed ({response.status}) creating zone '{zone_name}': {error_text}"
263+
)
264+
raise AuthenticationError(
265+
f"Authentication failed ({response.status}) creating zone '{zone_name}': {error_text}"
266+
)
212267

213268
# Handle bad request
214269
if response.status == HTTP_BAD_REQUEST:
@@ -245,41 +300,55 @@ async def _verify_zones_created(self, zone_names: List[str]) -> None:
245300
"""
246301
Verify that zones were successfully created by checking the zones list.
247302
303+
Note: Zones may take several seconds to appear in the API after creation.
304+
This method retries multiple times with exponential backoff.
305+
248306
Args:
249307
zone_names: List of zone names to verify
250308
251309
Raises:
252-
ZoneError: If zone verification fails
310+
ZoneError: If zone verification fails after all retries
253311
"""
254-
max_attempts = 3
255-
retry_delay = 1.0
312+
max_attempts = 5 # Increased from 3 to handle slower propagation
313+
base_delay = 2.0 # Increased from 1.0 for better reliability
256314

257315
for attempt in range(max_attempts):
258316
try:
259-
logger.info(f"Verifying zone creation (attempt {attempt + 1}/{max_attempts})")
260-
await asyncio.sleep(retry_delay)
317+
# Calculate delay with exponential backoff
318+
wait_time = base_delay * (1.5 ** attempt) if attempt > 0 else base_delay
319+
logger.info(f"Verifying zone creation (attempt {attempt + 1}/{max_attempts}) after {wait_time:.1f}s...")
320+
await asyncio.sleep(wait_time)
261321

262322
zones = await self._get_zones()
263323
existing_zone_names = {zone.get('name') for zone in zones}
264324

265325
missing_zones = [name for name in zone_names if name not in existing_zone_names]
266326

267327
if not missing_zones:
268-
logger.info("All zones verified successfully")
328+
logger.info(f"All {len(zone_names)} zone(s) verified successfully")
269329
return
270330

271331
if attempt == max_attempts - 1:
272-
raise ZoneError(
273-
f"Zone verification failed: zones {missing_zones} not found after creation"
332+
# Final attempt failed - provide helpful error message
333+
error_msg = (
334+
f"Zone verification failed after {max_attempts} attempts: "
335+
f"zones {missing_zones} not found after creation. "
336+
f"The zones may have been created but are not yet visible in the API. "
337+
f"Please check your dashboard at https://brightdata.com/cp/zones"
274338
)
339+
logger.error(error_msg)
340+
raise ZoneError(error_msg)
275341

276-
logger.warning(f"Zones not yet visible: {missing_zones}. Retrying verification...")
342+
logger.warning(
343+
f"Zones not yet visible: {missing_zones}. "
344+
f"Retrying in {base_delay * (1.5 ** attempt):.1f}s..."
345+
)
277346

278347
except ZoneError:
279348
if attempt == max_attempts - 1:
280349
raise
281350
logger.warning(f"Zone verification attempt {attempt + 1} failed, retrying...")
282-
await asyncio.sleep(retry_delay * (2 ** attempt))
351+
await asyncio.sleep(base_delay * (1.5 ** attempt))
283352

284353
async def list_zones(self) -> List[Dict[str, Any]]:
285354
"""

tests/enes/zones/permission.py

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Test to demonstrate improved permission error handling.
4+
5+
This test shows how the SDK now provides clear, helpful error messages
6+
when API tokens lack zone creation permissions.
7+
"""
8+
9+
import os
10+
import sys
11+
import asyncio
12+
from pathlib import Path
13+
14+
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent / "src"))
15+
16+
from brightdata import BrightDataClient
17+
from brightdata.exceptions import AuthenticationError
18+
19+
20+
async def test_permission_error_handling():
21+
"""Test that permission errors are caught and displayed clearly."""
22+
23+
print("\n" + "="*70)
24+
print("🧪 TESTING PERMISSION ERROR HANDLING")
25+
print("="*70)
26+
27+
print("""
28+
This test demonstrates the improved error handling when your API token
29+
lacks zone creation permissions.
30+
31+
Expected behavior:
32+
✅ Clear error message explaining the issue
33+
✅ Direct link to fix the problem
34+
✅ No silent failures
35+
✅ Helpful instructions for users
36+
""")
37+
38+
if not os.environ.get("BRIGHTDATA_API_TOKEN"):
39+
print("\n❌ ERROR: No API token found")
40+
return False
41+
42+
client = BrightDataClient(
43+
auto_create_zones=True,
44+
web_unlocker_zone="test_permission_zone",
45+
validate_token=False
46+
)
47+
48+
print("🔧 Attempting to create a zone with auto_create_zones=True...")
49+
print("-" * 70)
50+
51+
try:
52+
async with client:
53+
# This will trigger zone creation
54+
print("\n⏳ Initializing client (will attempt zone creation)...")
55+
print(" If your token lacks permissions, you'll see a clear error message.\n")
56+
57+
# If we get here, zones were created successfully or already exist
58+
zones = await client.list_zones()
59+
print(f"✅ SUCCESS: Client initialized, {len(zones)} zones available")
60+
61+
# Check if our test zone exists
62+
zone_names = {z.get('name') for z in zones}
63+
if "test_permission_zone" in zone_names:
64+
print(" ✓ Test zone was created successfully")
65+
print(" ✓ Your API token HAS zone creation permissions")
66+
else:
67+
print(" ℹ️ Test zone not created (may already exist with different name)")
68+
69+
return True
70+
71+
except AuthenticationError as e:
72+
print("\n" + "="*70)
73+
print("✅ PERMISSION ERROR CAUGHT (Expected if you lack permissions)")
74+
print("="*70)
75+
print(f"\nError Message:\n{e}")
76+
print("\n" + "="*70)
77+
print("📝 This is the IMPROVED error handling!")
78+
print("="*70)
79+
print("""
80+
Before: Error was unclear and could fail silently
81+
After: Clear message with actionable steps to fix the issue
82+
83+
The error message should have told you:
84+
1. ❌ What went wrong (permission denied)
85+
2. 🔗 Where to fix it (https://brightdata.com/cp/setting/users)
86+
3. 📋 What to do (enable zone creation permission)
87+
""")
88+
return True # This is expected behavior
89+
90+
except Exception as e:
91+
print(f"\n❌ UNEXPECTED ERROR: {e}")
92+
import traceback
93+
traceback.print_exc()
94+
return False
95+
96+
97+
if __name__ == "__main__":
98+
try:
99+
success = asyncio.run(test_permission_error_handling())
100+
101+
print("\n" + "="*70)
102+
if success:
103+
print("✅ TEST PASSED")
104+
print("="*70)
105+
print("""
106+
Summary:
107+
• Permission errors are now caught and displayed clearly
108+
• Users get actionable instructions to fix the problem
109+
• No more silent failures
110+
• SDK provides helpful guidance
111+
""")
112+
else:
113+
print("❌ TEST FAILED")
114+
print("="*70)
115+
116+
sys.exit(0 if success else 1)
117+
118+
except KeyboardInterrupt:
119+
print("\n⚠️ Test interrupted")
120+
sys.exit(2)
121+

0 commit comments

Comments
 (0)