django-cfg 1.2.16__py3-none-any.whl → 1.2.18__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- django_cfg/__init__.py +1 -1
- django_cfg/apps/accounts/models/__init__.py +68 -0
- django_cfg/apps/accounts/models/activity.py +34 -0
- django_cfg/apps/accounts/models/auth.py +50 -0
- django_cfg/apps/accounts/models/base.py +8 -0
- django_cfg/apps/accounts/models/choices.py +32 -0
- django_cfg/apps/accounts/models/integrations.py +75 -0
- django_cfg/apps/accounts/models/registration.py +52 -0
- django_cfg/apps/accounts/models/user.py +80 -0
- django_cfg/apps/maintenance/__init__.py +53 -24
- django_cfg/apps/maintenance/admin/__init__.py +7 -18
- django_cfg/apps/maintenance/admin/api_key_admin.py +185 -0
- django_cfg/apps/maintenance/admin/log_admin.py +156 -0
- django_cfg/apps/maintenance/admin/scheduled_admin.py +390 -0
- django_cfg/apps/maintenance/admin/site_admin.py +448 -0
- django_cfg/apps/maintenance/apps.py +9 -96
- django_cfg/apps/maintenance/management/commands/maintenance.py +193 -307
- django_cfg/apps/maintenance/management/commands/process_scheduled_maintenance.py +241 -0
- django_cfg/apps/maintenance/management/commands/sync_cloudflare.py +152 -111
- django_cfg/apps/maintenance/managers/__init__.py +7 -12
- django_cfg/apps/maintenance/managers/cloudflare_site_manager.py +192 -0
- django_cfg/apps/maintenance/managers/maintenance_log_manager.py +151 -0
- django_cfg/apps/maintenance/migrations/0001_initial.py +145 -705
- django_cfg/apps/maintenance/migrations/0002_cloudflaresite_maintenance_url.py +21 -0
- django_cfg/apps/maintenance/models/__init__.py +23 -21
- django_cfg/apps/maintenance/models/cloudflare_api_key.py +109 -0
- django_cfg/apps/maintenance/models/cloudflare_site.py +125 -0
- django_cfg/apps/maintenance/models/maintenance_log.py +131 -0
- django_cfg/apps/maintenance/models/scheduled_maintenance.py +307 -0
- django_cfg/apps/maintenance/services/__init__.py +37 -16
- django_cfg/apps/maintenance/services/bulk_operations_service.py +400 -0
- django_cfg/apps/maintenance/services/maintenance_service.py +230 -0
- django_cfg/apps/maintenance/services/scheduled_maintenance_service.py +381 -0
- django_cfg/apps/maintenance/services/site_sync_service.py +390 -0
- django_cfg/apps/maintenance/utils/__init__.py +12 -0
- django_cfg/apps/maintenance/utils/retry_utils.py +109 -0
- django_cfg/config.py +3 -0
- django_cfg/core/config.py +4 -6
- django_cfg/modules/django_unfold/dashboard.py +4 -5
- {django_cfg-1.2.16.dist-info → django_cfg-1.2.18.dist-info}/METADATA +52 -1
- {django_cfg-1.2.16.dist-info → django_cfg-1.2.18.dist-info}/RECORD +45 -55
- django_cfg/apps/maintenance/README.md +0 -305
- django_cfg/apps/maintenance/admin/deployments_admin.py +0 -251
- django_cfg/apps/maintenance/admin/events_admin.py +0 -374
- django_cfg/apps/maintenance/admin/monitoring_admin.py +0 -215
- django_cfg/apps/maintenance/admin/sites_admin.py +0 -464
- django_cfg/apps/maintenance/managers/deployments.py +0 -287
- django_cfg/apps/maintenance/managers/events.py +0 -374
- django_cfg/apps/maintenance/managers/monitoring.py +0 -301
- django_cfg/apps/maintenance/managers/sites.py +0 -335
- django_cfg/apps/maintenance/models/cloudflare.py +0 -316
- django_cfg/apps/maintenance/models/maintenance.py +0 -334
- django_cfg/apps/maintenance/models/monitoring.py +0 -393
- django_cfg/apps/maintenance/models/sites.py +0 -419
- django_cfg/apps/maintenance/serializers/__init__.py +0 -60
- django_cfg/apps/maintenance/serializers/actions.py +0 -310
- django_cfg/apps/maintenance/serializers/base.py +0 -44
- django_cfg/apps/maintenance/serializers/deployments.py +0 -209
- django_cfg/apps/maintenance/serializers/events.py +0 -210
- django_cfg/apps/maintenance/serializers/monitoring.py +0 -278
- django_cfg/apps/maintenance/serializers/sites.py +0 -213
- django_cfg/apps/maintenance/services/README.md +0 -168
- django_cfg/apps/maintenance/services/cloudflare_client.py +0 -441
- django_cfg/apps/maintenance/services/dns_manager.py +0 -497
- django_cfg/apps/maintenance/services/maintenance_manager.py +0 -504
- django_cfg/apps/maintenance/services/site_sync.py +0 -448
- django_cfg/apps/maintenance/services/sync_command_service.py +0 -330
- django_cfg/apps/maintenance/services/worker_manager.py +0 -264
- django_cfg/apps/maintenance/signals.py +0 -38
- django_cfg/apps/maintenance/urls.py +0 -36
- django_cfg/apps/maintenance/views/__init__.py +0 -18
- django_cfg/apps/maintenance/views/base.py +0 -61
- django_cfg/apps/maintenance/views/deployments.py +0 -175
- django_cfg/apps/maintenance/views/events.py +0 -204
- django_cfg/apps/maintenance/views/monitoring.py +0 -213
- django_cfg/apps/maintenance/views/sites.py +0 -338
- django_cfg/models/cloudflare.py +0 -316
- /django_cfg/apps/accounts/{models.py → __models.py} +0 -0
- {django_cfg-1.2.16.dist-info → django_cfg-1.2.18.dist-info}/WHEEL +0 -0
- {django_cfg-1.2.16.dist-info → django_cfg-1.2.18.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.2.16.dist-info → django_cfg-1.2.18.dist-info}/licenses/LICENSE +0 -0
@@ -1,441 +0,0 @@
|
|
1
|
-
"""
|
2
|
-
Modern Cloudflare client using cloudflare v4.x library.
|
3
|
-
|
4
|
-
Provides a clean, type-safe wrapper around the latest Cloudflare Python SDK.
|
5
|
-
Supports all major Cloudflare API operations with proper error handling and retry logic.
|
6
|
-
"""
|
7
|
-
|
8
|
-
import logging
|
9
|
-
from typing import Dict, List, Optional, Any, Iterator
|
10
|
-
from dataclasses import dataclass
|
11
|
-
from datetime import datetime
|
12
|
-
import time
|
13
|
-
import random
|
14
|
-
|
15
|
-
try:
|
16
|
-
from cloudflare import Cloudflare
|
17
|
-
# Note: Using Any for types as cloudflare 4.x has different type structure
|
18
|
-
# We'll handle typing at runtime
|
19
|
-
except ImportError:
|
20
|
-
raise ImportError(
|
21
|
-
"cloudflare library is required. Install with: pip install cloudflare>=4.3.0"
|
22
|
-
)
|
23
|
-
|
24
|
-
from django_cfg.models.cloudflare import CloudflareConfig
|
25
|
-
|
26
|
-
logger = logging.getLogger(__name__)
|
27
|
-
|
28
|
-
|
29
|
-
@dataclass
|
30
|
-
class RetryConfig:
|
31
|
-
"""Configuration for retry behavior."""
|
32
|
-
max_retries: int = 5
|
33
|
-
base_delay: float = 1.0
|
34
|
-
max_delay: float = 60.0
|
35
|
-
backoff_factor: float = 2.0
|
36
|
-
jitter: bool = True
|
37
|
-
retry_on_status: List[int] = None
|
38
|
-
|
39
|
-
def __post_init__(self):
|
40
|
-
if self.retry_on_status is None:
|
41
|
-
self.retry_on_status = [429, 502, 503, 504]
|
42
|
-
|
43
|
-
|
44
|
-
class CloudflareClient:
|
45
|
-
"""
|
46
|
-
Modern Cloudflare client using v4.x library.
|
47
|
-
|
48
|
-
Provides rate limiting, retry logic, and clean API access with proper typing.
|
49
|
-
"""
|
50
|
-
|
51
|
-
def __init__(
|
52
|
-
self,
|
53
|
-
config: CloudflareConfig,
|
54
|
-
retry_config: Optional[RetryConfig] = None
|
55
|
-
):
|
56
|
-
"""Initialize Cloudflare client."""
|
57
|
-
self.config = config
|
58
|
-
self.retry_config = retry_config or RetryConfig()
|
59
|
-
|
60
|
-
# Initialize official Cloudflare client (v4.x API)
|
61
|
-
self.client = Cloudflare(
|
62
|
-
api_token=config.get_api_token(),
|
63
|
-
max_retries=self.retry_config.max_retries
|
64
|
-
)
|
65
|
-
|
66
|
-
# Rate limiting state
|
67
|
-
self._last_request_time = 0
|
68
|
-
self._request_count = 0
|
69
|
-
|
70
|
-
logger.info("Cloudflare client v4.x initialized")
|
71
|
-
|
72
|
-
# === Zone Management ===
|
73
|
-
|
74
|
-
def list_zones(self, **kwargs) -> List[Any]:
|
75
|
-
"""List all zones in the account."""
|
76
|
-
zones = []
|
77
|
-
try:
|
78
|
-
# Use the paginated iterator
|
79
|
-
for zone in self.client.zones.list(**kwargs):
|
80
|
-
zones.append(zone)
|
81
|
-
return zones
|
82
|
-
except Exception as e:
|
83
|
-
logger.error(f"Failed to list zones: {e}")
|
84
|
-
return []
|
85
|
-
|
86
|
-
def get_zone(self, zone_id: str) -> Optional[Any]:
|
87
|
-
"""Get zone by ID."""
|
88
|
-
try:
|
89
|
-
return self._execute_with_retry(
|
90
|
-
lambda: self.client.zones.get(zone_id=zone_id)
|
91
|
-
)
|
92
|
-
except Exception as e:
|
93
|
-
logger.warning(f"Zone {zone_id} not found: {e}")
|
94
|
-
return None
|
95
|
-
|
96
|
-
def get_zone_by_name(self, domain: str) -> Optional[Any]:
|
97
|
-
"""Get zone by domain name."""
|
98
|
-
try:
|
99
|
-
zones = list(self.client.zones.list(name=domain))
|
100
|
-
return zones[0] if zones else None
|
101
|
-
except Exception as e:
|
102
|
-
logger.warning(f"Zone for domain {domain} not found: {e}")
|
103
|
-
return None
|
104
|
-
|
105
|
-
# === DNS Management ===
|
106
|
-
|
107
|
-
def list_dns_records(
|
108
|
-
self,
|
109
|
-
zone_id: str,
|
110
|
-
record_type: Optional[str] = None,
|
111
|
-
name: Optional[str] = None,
|
112
|
-
**kwargs
|
113
|
-
) -> List[Any]:
|
114
|
-
"""List DNS records for a zone."""
|
115
|
-
params = kwargs.copy()
|
116
|
-
if record_type:
|
117
|
-
params['type'] = record_type
|
118
|
-
if name:
|
119
|
-
params['name'] = name
|
120
|
-
|
121
|
-
records = []
|
122
|
-
try:
|
123
|
-
for record in self.client.dns.records.list(zone_id=zone_id, **params):
|
124
|
-
records.append(record)
|
125
|
-
return records
|
126
|
-
except Exception as e:
|
127
|
-
logger.error(f"Failed to list DNS records for zone {zone_id}: {e}")
|
128
|
-
return []
|
129
|
-
|
130
|
-
def create_dns_record(
|
131
|
-
self,
|
132
|
-
zone_id: str,
|
133
|
-
record_type: str,
|
134
|
-
name: str,
|
135
|
-
content: str,
|
136
|
-
ttl: int = 300,
|
137
|
-
proxied: Optional[bool] = None,
|
138
|
-
**kwargs
|
139
|
-
) -> Optional[Any]:
|
140
|
-
"""Create a DNS record."""
|
141
|
-
record_data = {
|
142
|
-
'type': record_type,
|
143
|
-
'name': name,
|
144
|
-
'content': content,
|
145
|
-
'ttl': ttl,
|
146
|
-
**kwargs
|
147
|
-
}
|
148
|
-
|
149
|
-
# Only set proxied for supported record types
|
150
|
-
if proxied is not None and record_type.upper() in ['A', 'AAAA', 'CNAME']:
|
151
|
-
record_data['proxied'] = proxied
|
152
|
-
|
153
|
-
try:
|
154
|
-
return self._execute_with_retry(
|
155
|
-
lambda: self.client.dns.records.create(zone_id=zone_id, **record_data)
|
156
|
-
)
|
157
|
-
except Exception as e:
|
158
|
-
logger.error(f"Failed to create DNS record: {e}")
|
159
|
-
return None
|
160
|
-
|
161
|
-
def update_dns_record(
|
162
|
-
self,
|
163
|
-
zone_id: str,
|
164
|
-
record_id: str,
|
165
|
-
**kwargs
|
166
|
-
) -> Optional[Any]:
|
167
|
-
"""Update a DNS record."""
|
168
|
-
try:
|
169
|
-
return self._execute_with_retry(
|
170
|
-
lambda: self.client.dns.records.update(
|
171
|
-
dns_record_id=record_id, zone_id=zone_id, **kwargs
|
172
|
-
)
|
173
|
-
)
|
174
|
-
except Exception as e:
|
175
|
-
logger.error(f"Failed to update DNS record {record_id}: {e}")
|
176
|
-
return None
|
177
|
-
|
178
|
-
def delete_dns_record(self, zone_id: str, record_id: str) -> bool:
|
179
|
-
"""Delete a DNS record."""
|
180
|
-
try:
|
181
|
-
self._execute_with_retry(
|
182
|
-
lambda: self.client.dns.records.delete(
|
183
|
-
dns_record_id=record_id, zone_id=zone_id
|
184
|
-
)
|
185
|
-
)
|
186
|
-
return True
|
187
|
-
except Exception as e:
|
188
|
-
logger.error(f"Failed to delete DNS record {record_id}: {e}")
|
189
|
-
return False
|
190
|
-
|
191
|
-
# === Workers Management ===
|
192
|
-
|
193
|
-
def list_workers(self, account_id: str) -> List[Any]:
|
194
|
-
"""List all Workers scripts."""
|
195
|
-
scripts = []
|
196
|
-
try:
|
197
|
-
for script in self.client.workers.scripts.list(account_id=account_id):
|
198
|
-
scripts.append(script)
|
199
|
-
return scripts
|
200
|
-
except Exception as e:
|
201
|
-
logger.error(f"Failed to list workers for account {account_id}: {e}")
|
202
|
-
return []
|
203
|
-
|
204
|
-
def create_worker(
|
205
|
-
self,
|
206
|
-
account_id: str,
|
207
|
-
script_name: str,
|
208
|
-
script_content: str,
|
209
|
-
**kwargs
|
210
|
-
) -> Optional[Any]:
|
211
|
-
"""Create or update a Worker script."""
|
212
|
-
try:
|
213
|
-
return self._execute_with_retry(
|
214
|
-
lambda: self.client.workers.scripts.update(
|
215
|
-
script_name=script_name,
|
216
|
-
account_id=account_id,
|
217
|
-
body=script_content,
|
218
|
-
**kwargs
|
219
|
-
)
|
220
|
-
)
|
221
|
-
except Exception as e:
|
222
|
-
logger.error(f"Failed to create/update worker {script_name}: {e}")
|
223
|
-
return None
|
224
|
-
|
225
|
-
def delete_worker(self, account_id: str, script_name: str) -> bool:
|
226
|
-
"""Delete a Worker script."""
|
227
|
-
try:
|
228
|
-
self._execute_with_retry(
|
229
|
-
lambda: self.client.workers.scripts.delete(
|
230
|
-
script_name=script_name, account_id=account_id
|
231
|
-
)
|
232
|
-
)
|
233
|
-
return True
|
234
|
-
except Exception as e:
|
235
|
-
logger.error(f"Failed to delete worker {script_name}: {e}")
|
236
|
-
return False
|
237
|
-
|
238
|
-
def create_worker_route(
|
239
|
-
self,
|
240
|
-
zone_id: str,
|
241
|
-
pattern: str,
|
242
|
-
script_name: str
|
243
|
-
) -> Optional[Dict[str, Any]]:
|
244
|
-
"""Create a Worker route."""
|
245
|
-
try:
|
246
|
-
return self._execute_with_retry(
|
247
|
-
lambda: self.client.workers.routes.create(
|
248
|
-
zone_id=zone_id,
|
249
|
-
pattern=pattern,
|
250
|
-
script=script_name
|
251
|
-
)
|
252
|
-
)
|
253
|
-
except Exception as e:
|
254
|
-
logger.error(f"Failed to create worker route: {e}")
|
255
|
-
return None
|
256
|
-
|
257
|
-
def delete_worker_route(self, zone_id: str, route_id: str) -> bool:
|
258
|
-
"""Delete a Worker route."""
|
259
|
-
try:
|
260
|
-
self._execute_with_retry(
|
261
|
-
lambda: self.client.workers.routes.delete(
|
262
|
-
route_id=route_id, zone_id=zone_id
|
263
|
-
)
|
264
|
-
)
|
265
|
-
return True
|
266
|
-
except Exception as e:
|
267
|
-
logger.error(f"Failed to delete worker route {route_id}: {e}")
|
268
|
-
return False
|
269
|
-
|
270
|
-
# === Account Information ===
|
271
|
-
|
272
|
-
def get_account_info(self) -> List[Dict[str, Any]]:
|
273
|
-
"""Get account information."""
|
274
|
-
accounts = []
|
275
|
-
try:
|
276
|
-
for account in self.client.accounts.list():
|
277
|
-
accounts.append(account.model_dump())
|
278
|
-
return accounts
|
279
|
-
except Exception as e:
|
280
|
-
logger.error(f"Failed to get account info: {e}")
|
281
|
-
return []
|
282
|
-
|
283
|
-
def get_account_id(self) -> Optional[str]:
|
284
|
-
"""Get the first account ID."""
|
285
|
-
try:
|
286
|
-
accounts = list(self.client.accounts.list())
|
287
|
-
return accounts[0].id if accounts else None
|
288
|
-
except Exception as e:
|
289
|
-
logger.error(f"Failed to get account ID: {e}")
|
290
|
-
return None
|
291
|
-
|
292
|
-
# === Zone Serialization ===
|
293
|
-
|
294
|
-
def serialize_zone(self, zone: Any) -> Dict[str, Any]:
|
295
|
-
"""Serialize Zone object to dict."""
|
296
|
-
try:
|
297
|
-
zone_dict = zone.model_dump()
|
298
|
-
|
299
|
-
# Ensure account info is properly serialized
|
300
|
-
if hasattr(zone, 'account') and zone.account:
|
301
|
-
zone_dict['account'] = zone.account.model_dump()
|
302
|
-
|
303
|
-
return zone_dict
|
304
|
-
except Exception as e:
|
305
|
-
logger.warning(f"Failed to serialize zone {zone.name}: {e}")
|
306
|
-
return {
|
307
|
-
'id': zone.id,
|
308
|
-
'name': zone.name,
|
309
|
-
'status': zone.status,
|
310
|
-
'error': f"Serialization failed: {e}"
|
311
|
-
}
|
312
|
-
|
313
|
-
# === Rate Limiting & Retry Logic ===
|
314
|
-
|
315
|
-
def _execute_with_retry(self, func, *args, **kwargs):
|
316
|
-
"""Execute function with retry logic and rate limiting."""
|
317
|
-
last_exception = None
|
318
|
-
|
319
|
-
for attempt in range(self.retry_config.max_retries + 1):
|
320
|
-
try:
|
321
|
-
# Rate limiting check
|
322
|
-
self._check_rate_limit()
|
323
|
-
|
324
|
-
# Execute function
|
325
|
-
result = func(*args, **kwargs)
|
326
|
-
|
327
|
-
# Update success stats
|
328
|
-
self._update_request_stats(success=True)
|
329
|
-
|
330
|
-
return result
|
331
|
-
|
332
|
-
except Exception as e:
|
333
|
-
self._update_request_stats(success=False)
|
334
|
-
last_exception = e
|
335
|
-
|
336
|
-
# Check if we should retry
|
337
|
-
if not self._should_retry(e, attempt):
|
338
|
-
break
|
339
|
-
|
340
|
-
# Calculate delay and wait
|
341
|
-
if attempt < self.retry_config.max_retries:
|
342
|
-
delay = self._calculate_retry_delay(attempt, e)
|
343
|
-
logger.warning(
|
344
|
-
f"Request failed (attempt {attempt + 1}), "
|
345
|
-
f"retrying in {delay:.2f}s: {e}"
|
346
|
-
)
|
347
|
-
time.sleep(delay)
|
348
|
-
|
349
|
-
# All retries exhausted
|
350
|
-
logger.error(f"Request failed after {self.retry_config.max_retries} retries: {last_exception}")
|
351
|
-
raise last_exception
|
352
|
-
|
353
|
-
def _should_retry(self, exception: Exception, attempt: int) -> bool:
|
354
|
-
"""Determine if we should retry the request."""
|
355
|
-
if attempt >= self.retry_config.max_retries:
|
356
|
-
return False
|
357
|
-
|
358
|
-
# Check for Cloudflare API exceptions
|
359
|
-
if hasattr(exception, 'status_code'):
|
360
|
-
return exception.status_code in self.retry_config.retry_on_status
|
361
|
-
|
362
|
-
# Retry on network errors
|
363
|
-
error_str = str(exception).lower()
|
364
|
-
if any(keyword in error_str for keyword in ['network', 'timeout', 'connection']):
|
365
|
-
return True
|
366
|
-
|
367
|
-
return False
|
368
|
-
|
369
|
-
def _calculate_retry_delay(self, attempt: int, exception: Exception = None) -> float:
|
370
|
-
"""Calculate delay for retry with exponential backoff."""
|
371
|
-
# Exponential backoff with jitter
|
372
|
-
delay = self.retry_config.base_delay * (
|
373
|
-
self.retry_config.backoff_factor ** attempt
|
374
|
-
)
|
375
|
-
|
376
|
-
if self.retry_config.jitter:
|
377
|
-
jitter_factor = random.uniform(0.5, 1.5)
|
378
|
-
delay *= jitter_factor
|
379
|
-
|
380
|
-
return min(delay, self.retry_config.max_delay)
|
381
|
-
|
382
|
-
def _check_rate_limit(self):
|
383
|
-
"""Check and enforce rate limiting."""
|
384
|
-
current_time = time.time()
|
385
|
-
|
386
|
-
# Simple rate limiting: max 4 requests per second
|
387
|
-
min_interval = 0.25
|
388
|
-
time_since_last = current_time - self._last_request_time
|
389
|
-
|
390
|
-
if time_since_last < min_interval:
|
391
|
-
sleep_time = min_interval - time_since_last
|
392
|
-
time.sleep(sleep_time)
|
393
|
-
|
394
|
-
self._last_request_time = time.time()
|
395
|
-
|
396
|
-
def _update_request_stats(self, success: bool):
|
397
|
-
"""Update request statistics."""
|
398
|
-
self._request_count += 1
|
399
|
-
|
400
|
-
if success:
|
401
|
-
logger.debug(f"Request #{self._request_count} successful")
|
402
|
-
else:
|
403
|
-
logger.debug(f"Request #{self._request_count} failed")
|
404
|
-
|
405
|
-
# === Health Check ===
|
406
|
-
|
407
|
-
def health_check(self) -> Dict[str, Any]:
|
408
|
-
"""Perform a health check of the Cloudflare API."""
|
409
|
-
try:
|
410
|
-
start_time = time.time()
|
411
|
-
|
412
|
-
# Simple API call to check connectivity
|
413
|
-
zones = list(self.client.zones.list())
|
414
|
-
|
415
|
-
end_time = time.time()
|
416
|
-
response_time = end_time - start_time
|
417
|
-
|
418
|
-
return {
|
419
|
-
'status': 'healthy',
|
420
|
-
'response_time': response_time,
|
421
|
-
'zones_count': len(zones),
|
422
|
-
'timestamp': datetime.now().isoformat()
|
423
|
-
}
|
424
|
-
|
425
|
-
except Exception as e:
|
426
|
-
return {
|
427
|
-
'status': 'unhealthy',
|
428
|
-
'error': str(e),
|
429
|
-
'timestamp': datetime.now().isoformat()
|
430
|
-
}
|
431
|
-
|
432
|
-
# === Context Manager Support ===
|
433
|
-
|
434
|
-
def __enter__(self):
|
435
|
-
"""Context manager entry."""
|
436
|
-
return self
|
437
|
-
|
438
|
-
def __exit__(self, exc_type, exc_val, exc_tb):
|
439
|
-
"""Context manager exit."""
|
440
|
-
if hasattr(self.client, 'close'):
|
441
|
-
self.client.close()
|