Skip to content

Commit dac165f

Browse files
committed
Added zone deletion endpoint to zone manager
1 parent df399d7 commit dac165f

File tree

5 files changed

+436
-1
lines changed

5 files changed

+436
-1
lines changed

src/brightdata/client.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -480,9 +480,41 @@ async def list_zones(self) -> List[Dict[str, Any]]:
480480
self._zone_manager = ZoneManager(self.engine)
481481
return await self._zone_manager.list_zones()
482482

483+
async def delete_zone(self, zone_name: str) -> None:
484+
"""
485+
Delete a zone from your Bright Data account.
486+
487+
Args:
488+
zone_name: Name of the zone to delete
489+
490+
Raises:
491+
ZoneError: If zone deletion fails or zone doesn't exist
492+
AuthenticationError: If authentication fails
493+
APIError: If API request fails
494+
495+
Example:
496+
>>> # Delete a test zone
497+
>>> await client.delete_zone("test_zone_123")
498+
>>> print("Zone deleted successfully")
499+
500+
>>> # With error handling
501+
>>> try:
502+
... await client.delete_zone("my_zone")
503+
... except ZoneError as e:
504+
... print(f"Failed to delete zone: {e}")
505+
"""
506+
async with self.engine:
507+
if self._zone_manager is None:
508+
self._zone_manager = ZoneManager(self.engine)
509+
await self._zone_manager.delete_zone(zone_name)
510+
483511
def list_zones_sync(self) -> List[Dict[str, Any]]:
484512
"""Synchronous version of list_zones()."""
485-
return asyncio.run(self.list_zones())
513+
return self._run_async_with_cleanup(self.list_zones())
514+
515+
def delete_zone_sync(self, zone_name: str) -> None:
516+
"""Synchronous version of delete_zone()."""
517+
return self._run_async_with_cleanup(self.delete_zone(zone_name))
486518

487519

488520
async def scrape_url_async(

src/brightdata/core/engine.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,16 @@ def get(
202202
"""Make GET request. Returns context manager."""
203203
return self.request("GET", endpoint, params=params, headers=headers)
204204

205+
def delete(
206+
self,
207+
endpoint: str,
208+
json_data: Optional[Dict[str, Any]] = None,
209+
params: Optional[Dict[str, Any]] = None,
210+
headers: Optional[Dict[str, str]] = None,
211+
):
212+
"""Make DELETE request. Returns context manager."""
213+
return self.request("DELETE", endpoint, json_data=json_data, params=params, headers=headers)
214+
205215
def post_to_url(
206216
self,
207217
url: str,

src/brightdata/core/zone_manager.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,3 +310,82 @@ async def list_zones(self) -> List[Dict[str, Any]]:
310310
except Exception as e:
311311
logger.error(f"Unexpected error listing zones: {e}")
312312
raise ZoneError(f"Unexpected error while listing zones: {str(e)}")
313+
314+
async def delete_zone(self, zone_name: str) -> None:
315+
"""
316+
Delete a zone from your Bright Data account.
317+
318+
Args:
319+
zone_name: Name of the zone to delete
320+
321+
Raises:
322+
ZoneError: If zone deletion fails
323+
AuthenticationError: If authentication fails
324+
APIError: If API request fails
325+
326+
Example:
327+
>>> zone_manager = ZoneManager(engine)
328+
>>> await zone_manager.delete_zone("my_test_zone")
329+
>>> print(f"Zone 'my_test_zone' deleted successfully")
330+
"""
331+
if not zone_name or not isinstance(zone_name, str):
332+
raise ZoneError("Zone name must be a non-empty string")
333+
334+
max_retries = 3
335+
retry_delay = 1.0
336+
337+
for attempt in range(max_retries):
338+
try:
339+
logger.info(f"Attempting to delete zone: {zone_name}")
340+
341+
# Prepare the payload for zone deletion
342+
payload = {
343+
"zone": zone_name
344+
}
345+
346+
async with self.engine.delete('/zone', json_data=payload) as response:
347+
if response.status == HTTP_OK:
348+
logger.info(f"Zone '{zone_name}' successfully deleted")
349+
return
350+
elif response.status in (HTTP_UNAUTHORIZED, HTTP_FORBIDDEN):
351+
error_text = await response.text()
352+
raise AuthenticationError(
353+
f"Authentication failed ({response.status}) deleting zone '{zone_name}': {error_text}"
354+
)
355+
elif response.status == HTTP_BAD_REQUEST:
356+
error_text = await response.text()
357+
# Check if zone doesn't exist
358+
if "not found" in error_text.lower() or "does not exist" in error_text.lower():
359+
raise ZoneError(
360+
f"Zone '{zone_name}' does not exist or has already been deleted"
361+
)
362+
raise ZoneError(
363+
f"Bad request ({HTTP_BAD_REQUEST}) deleting zone '{zone_name}': {error_text}"
364+
)
365+
else:
366+
error_text = await response.text()
367+
368+
# Retry on server errors
369+
if attempt < max_retries - 1 and response.status >= HTTP_INTERNAL_SERVER_ERROR:
370+
logger.warning(
371+
f"Zone deletion failed (attempt {attempt + 1}/{max_retries}): "
372+
f"{response.status} - {error_text}"
373+
)
374+
await asyncio.sleep(retry_delay * (1.5 ** attempt))
375+
continue
376+
377+
raise ZoneError(
378+
f"Failed to delete zone '{zone_name}' ({response.status}): {error_text}"
379+
)
380+
except (AuthenticationError, ZoneError):
381+
raise
382+
except (aiohttp.ClientError, asyncio.TimeoutError, OSError) as e:
383+
if attempt < max_retries - 1:
384+
logger.warning(
385+
f"Error deleting zone (attempt {attempt + 1}/{max_retries}): {e}"
386+
)
387+
await asyncio.sleep(retry_delay * (1.5 ** attempt))
388+
continue
389+
raise ZoneError(f"Failed to delete zone '{zone_name}': {str(e)}")
390+
391+
raise ZoneError(f"Failed to delete zone '{zone_name}' after all retry attempts")

tests/enes/delete_zone.py

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Demo script for zone deletion functionality.
4+
5+
This script demonstrates:
6+
1. Listing all zones
7+
2. Creating a test zone
8+
3. Verifying it exists
9+
4. Deleting the test zone
10+
5. Verifying it's gone
11+
"""
12+
13+
import os
14+
import sys
15+
import asyncio
16+
import time
17+
from pathlib import Path
18+
19+
# Add parent directory to path
20+
sys.path.insert(0, str(Path(__file__).parent.parent.parent / "src"))
21+
22+
from brightdata import BrightDataClient
23+
from brightdata.exceptions import ZoneError, AuthenticationError
24+
25+
26+
async def demo_delete_zone():
27+
"""Demonstrate zone deletion functionality."""
28+
29+
print("\n" + "="*60)
30+
print("ZONE DELETION DEMO")
31+
print("="*60)
32+
33+
# Check for API token
34+
if not os.environ.get("BRIGHTDATA_API_TOKEN"):
35+
print("\n❌ ERROR: No API token found")
36+
print("Please set BRIGHTDATA_API_TOKEN environment variable")
37+
return False
38+
39+
# Create client
40+
client = BrightDataClient(validate_token=False)
41+
42+
# Create a unique test zone name
43+
timestamp = str(int(time.time()))[-6:]
44+
test_zone_name = f"test_delete_zone_{timestamp}"
45+
46+
try:
47+
async with client:
48+
# Step 1: List initial zones
49+
print("\n📊 Step 1: Listing current zones...")
50+
initial_zones = await client.list_zones()
51+
initial_zone_names = {z.get('name') for z in initial_zones}
52+
print(f"✅ Found {len(initial_zones)} zones")
53+
54+
# Step 2: Create a test zone
55+
print(f"\n🔧 Step 2: Creating test zone '{test_zone_name}'...")
56+
test_client = BrightDataClient(
57+
auto_create_zones=True,
58+
web_unlocker_zone=test_zone_name,
59+
validate_token=False
60+
)
61+
62+
try:
63+
async with test_client:
64+
# Trigger zone creation
65+
try:
66+
await test_client.scrape_url_async(
67+
url="https://example.com",
68+
zone=test_zone_name
69+
)
70+
except Exception as e:
71+
# Zone might be created even if scrape fails
72+
print(f" ℹ️ Scrape error (expected): {e}")
73+
74+
print(f"✅ Test zone '{test_zone_name}' created")
75+
except Exception as e:
76+
print(f"❌ Failed to create test zone: {e}")
77+
return False
78+
79+
# Wait a bit for zone to be fully registered
80+
await asyncio.sleep(2)
81+
82+
# Step 3: Verify zone exists
83+
print(f"\n🔍 Step 3: Verifying zone '{test_zone_name}' exists...")
84+
zones_after_create = await client.list_zones()
85+
zone_names_after_create = {z.get('name') for z in zones_after_create}
86+
87+
if test_zone_name in zone_names_after_create:
88+
print(f"✅ Zone '{test_zone_name}' found in zone list")
89+
# Print zone details
90+
test_zone = next(z for z in zones_after_create if z.get('name') == test_zone_name)
91+
print(f" Type: {test_zone.get('type', 'unknown')}")
92+
print(f" Status: {test_zone.get('status', 'unknown')}")
93+
else:
94+
print(f"⚠️ Zone '{test_zone_name}' not found (might still be creating)")
95+
96+
# Step 4: Delete the test zone
97+
print(f"\n🗑️ Step 4: Deleting zone '{test_zone_name}'...")
98+
try:
99+
await client.delete_zone(test_zone_name)
100+
print(f"✅ Zone '{test_zone_name}' deleted successfully")
101+
except ZoneError as e:
102+
print(f"❌ Failed to delete zone: {e}")
103+
return False
104+
except AuthenticationError as e:
105+
print(f"❌ Authentication error: {e}")
106+
return False
107+
108+
# Wait a bit for deletion to propagate
109+
await asyncio.sleep(2)
110+
111+
# Step 5: Verify zone is gone
112+
print(f"\n🔍 Step 5: Verifying zone '{test_zone_name}' is deleted...")
113+
final_zones = await client.list_zones()
114+
final_zone_names = {z.get('name') for z in final_zones}
115+
116+
if test_zone_name not in final_zone_names:
117+
print(f"✅ Confirmed: Zone '{test_zone_name}' no longer exists")
118+
else:
119+
print(f"⚠️ Zone '{test_zone_name}' still appears in list (deletion might be delayed)")
120+
121+
# Summary
122+
print("\n" + "="*60)
123+
print("📈 SUMMARY:")
124+
print(f" Initial zones: {len(initial_zones)}")
125+
print(f" After creation: {len(zones_after_create)}")
126+
print(f" After deletion: {len(final_zones)}")
127+
print(f" Net change: {len(final_zones) - len(initial_zones)}")
128+
129+
print("\n" + "="*60)
130+
print("✅ DEMO COMPLETED SUCCESSFULLY")
131+
print("="*60)
132+
133+
return True
134+
135+
except Exception as e:
136+
print(f"\n❌ Unexpected error: {e}")
137+
import traceback
138+
traceback.print_exc()
139+
return False
140+
141+
142+
def main():
143+
"""Main entry point."""
144+
try:
145+
success = asyncio.run(demo_delete_zone())
146+
sys.exit(0 if success else 1)
147+
except KeyboardInterrupt:
148+
print("\n\n⚠️ Demo interrupted by user")
149+
sys.exit(2)
150+
except Exception as e:
151+
print(f"\n❌ Fatal error: {e}")
152+
sys.exit(3)
153+
154+
155+
if __name__ == "__main__":
156+
main()
157+

0 commit comments

Comments
 (0)