netbox-toolkit-plugin 0.1.1__py3-none-any.whl → 0.1.3__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.
Files changed (43) hide show
  1. netbox_toolkit_plugin/__init__.py +1 -1
  2. netbox_toolkit_plugin/admin.py +11 -7
  3. netbox_toolkit_plugin/api/mixins.py +20 -16
  4. netbox_toolkit_plugin/api/schemas.py +53 -74
  5. netbox_toolkit_plugin/api/serializers.py +10 -11
  6. netbox_toolkit_plugin/api/urls.py +2 -1
  7. netbox_toolkit_plugin/api/views/__init__.py +4 -3
  8. netbox_toolkit_plugin/api/views/command_logs.py +80 -73
  9. netbox_toolkit_plugin/api/views/commands.py +140 -134
  10. netbox_toolkit_plugin/connectors/__init__.py +9 -9
  11. netbox_toolkit_plugin/connectors/base.py +30 -31
  12. netbox_toolkit_plugin/connectors/factory.py +22 -26
  13. netbox_toolkit_plugin/connectors/netmiko_connector.py +18 -28
  14. netbox_toolkit_plugin/connectors/scrapli_connector.py +17 -16
  15. netbox_toolkit_plugin/exceptions.py +0 -7
  16. netbox_toolkit_plugin/filtersets.py +26 -42
  17. netbox_toolkit_plugin/forms.py +13 -11
  18. netbox_toolkit_plugin/migrations/0008_remove_parsed_data_storage.py +26 -0
  19. netbox_toolkit_plugin/models.py +2 -17
  20. netbox_toolkit_plugin/navigation.py +3 -0
  21. netbox_toolkit_plugin/search.py +12 -9
  22. netbox_toolkit_plugin/services/__init__.py +1 -1
  23. netbox_toolkit_plugin/services/command_service.py +7 -10
  24. netbox_toolkit_plugin/services/device_service.py +40 -32
  25. netbox_toolkit_plugin/services/rate_limiting_service.py +4 -3
  26. netbox_toolkit_plugin/{config.py → settings.py} +17 -7
  27. netbox_toolkit_plugin/static/netbox_toolkit_plugin/js/toolkit.js +245 -119
  28. netbox_toolkit_plugin/tables.py +10 -1
  29. netbox_toolkit_plugin/templates/netbox_toolkit_plugin/commandlog.html +16 -84
  30. netbox_toolkit_plugin/templates/netbox_toolkit_plugin/device_toolkit.html +37 -33
  31. netbox_toolkit_plugin/urls.py +10 -3
  32. netbox_toolkit_plugin/utils/connection.py +54 -54
  33. netbox_toolkit_plugin/utils/error_parser.py +128 -109
  34. netbox_toolkit_plugin/utils/logging.py +1 -0
  35. netbox_toolkit_plugin/utils/network.py +74 -47
  36. netbox_toolkit_plugin/views.py +51 -22
  37. {netbox_toolkit_plugin-0.1.1.dist-info → netbox_toolkit_plugin-0.1.3.dist-info}/METADATA +2 -2
  38. netbox_toolkit_plugin-0.1.3.dist-info/RECORD +61 -0
  39. netbox_toolkit_plugin-0.1.1.dist-info/RECORD +0 -60
  40. {netbox_toolkit_plugin-0.1.1.dist-info → netbox_toolkit_plugin-0.1.3.dist-info}/WHEEL +0 -0
  41. {netbox_toolkit_plugin-0.1.1.dist-info → netbox_toolkit_plugin-0.1.3.dist-info}/entry_points.txt +0 -0
  42. {netbox_toolkit_plugin-0.1.1.dist-info → netbox_toolkit_plugin-0.1.3.dist-info}/licenses/LICENSE +0 -0
  43. {netbox_toolkit_plugin-0.1.1.dist-info → netbox_toolkit_plugin-0.1.3.dist-info}/top_level.txt +0 -0
@@ -1,5 +1,7 @@
1
+ from netbox.tables import NetBoxTable
2
+
1
3
  import django_tables2 as tables
2
- from netbox.tables import NetBoxTable, columns
4
+
3
5
  from .models import Command, CommandLog
4
6
 
5
7
 
@@ -17,6 +19,13 @@ class CommandTable(NetBoxTable):
17
19
 
18
20
 
19
21
  class CommandLogTable(NetBoxTable):
22
+ pk = tables.Column(
23
+ linkify=(
24
+ "plugins:netbox_toolkit_plugin:commandlog_view",
25
+ [tables.A("pk")],
26
+ ),
27
+ verbose_name="ID",
28
+ )
20
29
  command = tables.Column(
21
30
  linkify=(
22
31
  "plugins:netbox_toolkit_plugin:command_detail",
@@ -35,7 +35,7 @@
35
35
  <th scope="row">Status</th>
36
36
  <td>
37
37
  {% if object.success %}
38
- <span class="badge bg-success">Success</span>
38
+ <span class="badge bg-success-lt text-success-lt-fg">Success</span>
39
39
  {% else %}
40
40
  <span class="badge bg-danger">Failed</span>
41
41
  {% endif %}
@@ -47,7 +47,9 @@
47
47
  <td>{{ object.execution_duration|floatformat:3 }}s</td>
48
48
  </tr>
49
49
  {% endif %}
50
- {% if object.parsing_success %}
50
+ {% comment %}
51
+ <!-- Parsing status removed - focus on execution history -->
52
+ {% if object.get_fresh_parsed_data %}
51
53
  <tr>
52
54
  <th scope="row">Parsing Status</th>
53
55
  <td>
@@ -55,22 +57,11 @@
55
57
  <i class="mdi mdi-check-circle me-1"></i>
56
58
  Successfully Parsed
57
59
  </span>
58
- {% if object.parsing_template %}
59
- <br><small class="text-muted">Template: {{ object.parsing_template }}</small>
60
- {% endif %}
61
- </td>
62
- </tr>
63
- {% elif object.parsed_data is not None %}
64
- <tr>
65
- <th scope="row">Parsing Status</th>
66
- <td>
67
- <span class="badge bg-warning">
68
- <i class="mdi mdi-alert-circle me-1"></i>
69
- Parsing Attempted
70
- </span>
60
+ <br><small class="text-muted">Parsed from raw output</small>
71
61
  </td>
72
62
  </tr>
73
63
  {% endif %}
64
+ {% endcomment %}
74
65
  {% if not object.success and object.error_message %}
75
66
  <tr>
76
67
  <th scope="row">Error</th>
@@ -86,82 +77,23 @@
86
77
  <div class="col col-md-12">
87
78
  <div class="card">
88
79
  <div class="card-header">
89
- <h3 class="card-title">Command Output</h3>
90
- </div>
91
- <div class="card-body">
92
- <pre>{{ object.output }}</pre>
93
- </div>
94
- </div>
95
- </div>
96
- </div>
97
-
98
- {% if object.parsed_data %}
99
- <div class="row mt-3">
100
- <div class="col col-md-12">
101
- <div class="card">
102
- <div class="card-header">
103
- <h3 class="card-title">
104
- <i class="mdi mdi-table me-1"></i>
105
- Parsed Data
106
- </h3>
107
- {% if object.parsing_template %}
108
- <div class="card-subtitle">
109
- <small class="text-muted">Template: {{ object.parsing_template }}</small>
80
+ <div class="d-flex justify-content-between align-items-start">
81
+ <h3 class="card-title mb-0 mt-1 me-3">Command Output</h3>
82
+ <div class="btn-list">
83
+ <button type="button" class="btn btn-sm btn-outline-primary copy-output-btn"
84
+ title="Copy output to clipboard">
85
+ <i class="mdi mdi-content-copy me-1"></i>Copy
86
+ </button>
110
87
  </div>
111
- {% endif %}
88
+ </div>
112
89
  </div>
113
90
  <div class="card-body">
114
- {% if object.parsed_data|length > 0 %}
115
- {% if object.parsed_data.0 %}
116
- <!-- Table format for structured data -->
117
- <div class="table-responsive">
118
- <table class="table table-sm table-striped">
119
- <thead>
120
- <tr>
121
- {% for key in object.parsed_data.0.keys %}
122
- <th>{{ key|title }}</th>
123
- {% endfor %}
124
- </tr>
125
- </thead>
126
- <tbody>
127
- {% for row in object.parsed_data %}
128
- <tr>
129
- {% for value in row.values %}
130
- <td>{{ value }}</td>
131
- {% endfor %}
132
- </tr>
133
- {% endfor %}
134
- </tbody>
135
- </table>
136
- </div>
137
- {% else %}
138
- <!-- JSON format for other data types -->
139
- <pre class="bg-light p-3 rounded">{{ object.parsed_data|pprint }}</pre>
140
- {% endif %}
141
- {% else %}
142
- <div class="alert alert-info mb-0">
143
- <i class="mdi mdi-information-outline me-1"></i>
144
- No structured data found in the output.
145
- </div>
146
- {% endif %}
147
-
148
- <!-- Copy parsed data button -->
149
- {% if object.parsed_data|length > 0 %}
150
- {{ object.parsed_data|json_script:"commandlog-parsed-data-json" }}
151
- <div class="mt-3">
152
- <div class="btn-list">
153
- <button type="button" class="btn btn-sm btn-outline-primary copy-parsed-btn"
154
- title="Copy parsed data as JSON">
155
- <i class="mdi mdi-content-copy me-1"></i>Copy Parsed Data
156
- </button>
157
- </div>
158
- </div>
159
- {% endif %}
91
+ <pre class="command-output bg-surface p-3 rounded font-monospace">{{ object.output }}</pre>
160
92
  </div>
161
93
  </div>
162
94
  </div>
163
95
  </div>
164
- {% endif %}
96
+
165
97
  {% endblock %}
166
98
 
167
99
  {% block javascript %}
@@ -15,8 +15,8 @@
15
15
  <div class="card-header">
16
16
  <h3 class="card-title">Connection Info</h3>
17
17
  <div class="card-actions">
18
- <button class="btn btn-icon" type="button" data-bs-toggle="collapse"
19
- data-bs-target="#connectionInfoCollapse" aria-expanded="true"
18
+ <button class="btn btn-icon" type="button" data-bs-toggle="collapse"
19
+ data-bs-target="#connectionInfoCollapse" aria-expanded="true"
20
20
  aria-controls="connectionInfoCollapse">
21
21
  <i class="mdi mdi-chevron-up collapse-icon"></i>
22
22
  </button>
@@ -77,15 +77,15 @@
77
77
  </div>
78
78
  </div>
79
79
  </div>
80
-
80
+
81
81
  <!-- Rate Limiting Status -->
82
82
  {% if rate_limit_status.enabled %}
83
83
  <div class="card mt-3">
84
84
  <div class="card-header">
85
85
  <h3 class="card-title">Rate Limiting</h3>
86
86
  <div class="card-actions">
87
- <button class="btn btn-icon" type="button" data-bs-toggle="collapse"
88
- data-bs-target="#rateLimitCollapse" aria-expanded="true"
87
+ <button class="btn btn-icon" type="button" data-bs-toggle="collapse"
88
+ data-bs-target="#rateLimitCollapse" aria-expanded="true"
89
89
  aria-controls="rateLimitCollapse">
90
90
  <i class="mdi mdi-chevron-up collapse-icon"></i>
91
91
  </button>
@@ -132,7 +132,7 @@
132
132
  {{ rate_limit_status.message }}
133
133
  </small>
134
134
  </div>
135
-
135
+
136
136
  {% if rate_limit_status.is_exceeded %}
137
137
  <div class="alert alert-danger mt-3 mb-0">
138
138
  <i class="mdi mdi-block-helper me-2"></i>
@@ -151,7 +151,7 @@
151
151
  </div>
152
152
  </div>
153
153
  {% endif %}
154
-
154
+
155
155
  <!-- Commands List -->
156
156
  <div class="card flex-grow-1">
157
157
  <div class="card-header">
@@ -169,27 +169,27 @@
169
169
  {% if commands %}
170
170
  <div class="list-group list-group-flush">
171
171
  {% for command in commands %}
172
- <div class="list-group-item list-group-item-action command-item"
173
- data-command-id="{{ command.id }}" data-command-name="{{ command.name }}"
172
+ <div class="list-group-item list-group-item-action command-item"
173
+ data-command-id="{{ command.id }}" data-command-name="{{ command.name }}"
174
174
  title="{{ command.description }}">
175
175
  <div class="d-flex justify-content-between align-items-center">
176
176
  <div class="d-flex align-items-center">
177
177
  {% if command.command_type == 'config' %}
178
- <i class="mdi mdi-alert-outline text-danger me-2 opacity-75"
178
+ <i class="mdi mdi-alert-outline text-danger me-2 opacity-75"
179
179
  style="font-size: 1.25rem;"
180
180
  title="Configuration command - use with caution"></i>
181
181
  {% endif %}
182
- <a href="{% url 'plugins:netbox_toolkit_plugin:command_detail' pk=command.pk %}"
182
+ <a href="{% url 'plugins:netbox_toolkit_plugin:command_detail' pk=command.pk %}"
183
183
  class="text-decoration-none text-body">
184
184
  {{ command.name }}
185
185
  </a>
186
186
  </div>
187
187
  <div class="d-flex align-items-center">
188
188
  <!-- Run button (hidden by default, shown on hover) -->
189
- <button type="button"
190
- class="btn btn-sm btn-success command-run-btn"
189
+ <button type="button"
190
+ class="btn btn-sm btn-success command-run-btn"
191
191
  title="Execute command"
192
- data-command-id="{{ command.id }}"
192
+ data-command-id="{{ command.id }}"
193
193
  data-command-name="{{ command.name }}">
194
194
  <i class="mdi mdi-play me-1"></i>Run
195
195
  </button>
@@ -218,7 +218,7 @@
218
218
  {% endif %}
219
219
  </div>
220
220
  </div>
221
-
221
+
222
222
  <!-- Recent Command History -->
223
223
  <div class="card mt-3">
224
224
  <div class="card-header">
@@ -252,7 +252,7 @@
252
252
  </div>
253
253
  </div>
254
254
  </div>
255
-
255
+
256
256
  <!-- Right Column with Output -->
257
257
  <div class="col-md-9">
258
258
  <div class="card h-100">
@@ -260,8 +260,8 @@
260
260
  <h3 class="card-title">Command Output</h3>
261
261
  {% if executed_command %}
262
262
  <div class="card-actions">
263
- <a href="{% url 'plugins:netbox_toolkit_plugin:command_detail' pk=executed_command.pk %}"
264
- class="text-decoration-none text-muted"
263
+ <a href="{% url 'plugins:netbox_toolkit_plugin:command_detail' pk=executed_command.pk %}"
264
+ class="text-decoration-none text-muted"
265
265
  title="View command details">
266
266
  {{ executed_command.name }}
267
267
  <i class="mdi mdi-open-in-new ms-1" style="font-size: 0.8rem;"></i>
@@ -283,14 +283,14 @@
283
283
  {% endif %}
284
284
  </div>
285
285
  </div>
286
-
286
+
287
287
  <!-- Tabbed output interface -->
288
288
  <div class="card">
289
289
  <div class="card-header">
290
290
  <ul class="nav nav-tabs nav-fill" id="outputTabs" role="tablist">
291
291
  <li class="nav-item" role="presentation">
292
- <button class="nav-link active" id="raw-output-tab" data-bs-toggle="tab"
293
- data-bs-target="#raw-output" type="button" role="tab"
292
+ <button class="nav-link active" id="raw-output-tab" data-bs-toggle="tab"
293
+ data-bs-target="#raw-output" type="button" role="tab"
294
294
  aria-controls="raw-output" aria-selected="true">
295
295
  <i class="mdi mdi-console me-1"></i>
296
296
  Raw Output
@@ -298,8 +298,8 @@
298
298
  </li>
299
299
  {% if parsed_data %}
300
300
  <li class="nav-item" role="presentation">
301
- <button class="nav-link" id="parsed-data-tab" data-bs-toggle="tab"
302
- data-bs-target="#parsed-data" type="button" role="tab"
301
+ <button class="nav-link" id="parsed-data-tab" data-bs-toggle="tab"
302
+ data-bs-target="#parsed-data" type="button" role="tab"
303
303
  aria-controls="parsed-data" aria-selected="false">
304
304
  <i class="mdi mdi-table me-1"></i>
305
305
  Parsed Data
@@ -311,12 +311,12 @@
311
311
  <div class="card-body p-0">
312
312
  <div class="tab-content" id="outputTabContent">
313
313
  <!-- Raw Output Tab -->
314
- <div class="tab-pane fade show active" id="raw-output" role="tabpanel"
314
+ <div class="tab-pane fade show active" id="raw-output" role="tabpanel"
315
315
  aria-labelledby="raw-output-tab">
316
316
  <div class="d-flex justify-content-between align-items-start py-3 px-3 border-bottom">
317
317
  <h6 class="mb-0 mt-1">Command Output</h6>
318
318
  <div class="btn-list">
319
- <button type="button" class="btn btn-sm btn-outline-primary copy-output-btn"
319
+ <button type="button" class="btn btn-sm btn-outline-primary copy-output-btn"
320
320
  title="Copy raw output to clipboard">
321
321
  <i class="mdi mdi-content-copy me-1"></i>Copy
322
322
  </button>
@@ -325,10 +325,10 @@
325
325
  <pre class="command-output bg-surface p-3 rounded font-monospace">{{ command_output }}</pre>
326
326
  </div>
327
327
  </div>
328
-
328
+
329
329
  <!-- Parsed Data Tab -->
330
330
  {% if parsed_data %}
331
- <div class="tab-pane fade" id="parsed-data" role="tabpanel"
331
+ <div class="tab-pane fade" id="parsed-data" role="tabpanel"
332
332
  aria-labelledby="parsed-data-tab">
333
333
  <div class="d-flex justify-content-between align-items-start py-3 px-3 border-bottom">
334
334
  <div class="mt-1">
@@ -340,10 +340,14 @@
340
340
  {% if parsed_data|length > 0 %}
341
341
  {{ parsed_data|json_script:"parsed-data-json" }}
342
342
  <div class="btn-list">
343
- <button type="button" class="btn btn-sm btn-outline-primary copy-parsed-btn"
343
+ <button type="button" class="btn btn-sm btn-outline-primary copy-parsed-btn"
344
344
  title="Copy parsed data as JSON">
345
345
  <i class="mdi mdi-content-copy me-1"></i>Copy JSON
346
346
  </button>
347
+ <button type="button" class="btn btn-sm btn-outline-info download-csv-btn"
348
+ title="Download parsed data as CSV">
349
+ <i class="mdi mdi-file-table me-1"></i>Download CSV
350
+ </button>
347
351
  </div>
348
352
  {% endif %}
349
353
  </div>
@@ -355,7 +359,7 @@
355
359
  <table class="table table-sm table-striped mb-0">
356
360
  <thead>
357
361
  <tr>
358
- {% for key in parsed_data.0.keys %}
362
+ {% for key, value in parsed_data.0.items %}
359
363
  <th>{{ key|title }}</th>
360
364
  {% endfor %}
361
365
  </tr>
@@ -363,7 +367,7 @@
363
367
  <tbody>
364
368
  {% for row in parsed_data %}
365
369
  <tr>
366
- {% for value in row.values %}
370
+ {% for key, value in row.items %}
367
371
  <td>{{ value }}</td>
368
372
  {% endfor %}
369
373
  </tr>
@@ -468,7 +472,7 @@
468
472
  Enter your credentials to execute: <strong id="commandToExecute"></strong>
469
473
  </p>
470
474
  </div>
471
-
475
+
472
476
  <!-- Rate Limiting Information in Modal -->
473
477
  {% if rate_limit_status.enabled and not rate_limit_status.bypassed %}
474
478
  <div class="alert {% if rate_limit_status.is_exceeded %}alert-danger{% elif rate_limit_status.is_warning %}alert-warning{% else %}alert-info{% endif %} mb-3">
@@ -476,7 +480,7 @@
476
480
  <i class="mdi {% if rate_limit_status.is_exceeded %}mdi-block-helper{% elif rate_limit_status.is_warning %}mdi-alert-outline{% else %}mdi-information-outline{% endif %} me-2 mt-1"></i>
477
481
  <div>
478
482
  <small>
479
- <strong>Rate Limiting:</strong> {{ rate_limit_status.remaining }} commands remaining
483
+ <strong>Rate Limiting:</strong> {{ rate_limit_status.remaining }} commands remaining
480
484
  ({{ rate_limit_status.current_count }}/{{ rate_limit_status.limit }} successful commands used)
481
485
  {% if rate_limit_status.is_exceeded %}
482
486
  <br><span class="text-danger">Error: Rate limit exceeded! Commands are blocked.</span>
@@ -488,7 +492,7 @@
488
492
  </div>
489
493
  </div>
490
494
  {% endif %}
491
-
495
+
492
496
  <form id="modalCredentialsForm">
493
497
  <div class="mb-3">
494
498
  <label for="modalUsername" class="form-label">Username</label>
@@ -1,4 +1,5 @@
1
1
  from django.urls import path
2
+
2
3
  from . import views
3
4
 
4
5
  app_name = "netbox_toolkit_plugin"
@@ -23,11 +24,17 @@ urlpatterns = [
23
24
  ),
24
25
  # Command Log views
25
26
  path("logs/", views.CommandLogListView.as_view(), name="commandlog_list"),
27
+ path("logs/add/", views.CommandLogEditView.as_view(), name="commandlog_add"),
26
28
  path("logs/<int:pk>/", views.CommandLogView.as_view(), name="commandlog_view"),
27
29
  path(
28
- "logs/<int:pk>/changelog/",
29
- views.CommandLogChangeLogView.as_view(),
30
- name="commandlog_changelog",
30
+ "logs/<int:pk>/edit/",
31
+ views.CommandLogEditView.as_view(),
32
+ name="commandlog_edit",
33
+ ),
34
+ path(
35
+ "logs/<int:pk>/delete/",
36
+ views.CommandLogDeleteView.as_view(),
37
+ name="commandlog_delete",
31
38
  ),
32
39
  # Device toolkit view
33
40
  path(
@@ -1,34 +1,34 @@
1
1
  """Connection management utilities for robust socket handling."""
2
+
3
+ import contextlib
2
4
  import socket
3
5
  import time
4
- from typing import Any, Optional
6
+ from typing import Any
5
7
 
6
8
 
7
- def safe_socket_close(sock: Optional[socket.socket]) -> None:
9
+ def safe_socket_close(sock: socket.socket | None) -> None:
8
10
  """Safely close a socket with proper error handling."""
9
11
  if sock is None:
10
12
  return
11
-
13
+
12
14
  try:
13
15
  # Try graceful shutdown first
14
- try:
15
- sock.shutdown(socket.SHUT_RDWR)
16
- except (OSError, AttributeError):
16
+ with contextlib.suppress(OSError, AttributeError):
17
17
  # Socket might already be closed or not connected
18
- pass
19
-
18
+ sock.shutdown(socket.SHUT_RDWR)
19
+
20
20
  # Then close the socket
21
21
  sock.close()
22
-
23
- except Exception as e:
22
+
23
+ except Exception:
24
24
  pass # Socket cleanup error ignored
25
25
 
26
26
 
27
- def is_socket_valid(sock: Optional[socket.socket]) -> bool:
27
+ def is_socket_valid(sock: socket.socket | None) -> bool:
28
28
  """Check if a socket is valid and usable."""
29
29
  if sock is None:
30
30
  return False
31
-
31
+
32
32
  try:
33
33
  # Try to get socket peer name - this will fail if socket is closed
34
34
  sock.getpeername()
@@ -41,47 +41,46 @@ def cleanup_connection_resources(connection: Any) -> None:
41
41
  """Clean up all resources associated with a connection object."""
42
42
  if not connection:
43
43
  return
44
-
44
+
45
45
  try:
46
46
  # Handle Scrapli-specific cleanup
47
- if hasattr(connection, 'channel') and connection.channel:
47
+ if hasattr(connection, "channel") and connection.channel:
48
48
  try:
49
49
  # Close the channel
50
- if hasattr(connection.channel, 'close'):
50
+ if hasattr(connection.channel, "close"):
51
51
  connection.channel.close()
52
-
52
+
53
53
  # Clean up the underlying socket if accessible
54
- if hasattr(connection.channel, 'socket'):
54
+ if hasattr(connection.channel, "socket"):
55
55
  safe_socket_close(connection.channel.socket)
56
- elif hasattr(connection.channel, '_socket'):
56
+ elif hasattr(connection.channel, "_socket"):
57
57
  safe_socket_close(connection.channel._socket)
58
-
59
- except Exception as e:
58
+
59
+ except Exception:
60
60
  pass # Channel cleanup error ignored
61
-
61
+
62
62
  # Handle transport cleanup
63
- if hasattr(connection, 'transport') and connection.transport:
63
+ if hasattr(connection, "transport") and connection.transport:
64
64
  try:
65
- if hasattr(connection.transport, 'close'):
65
+ if hasattr(connection.transport, "close"):
66
66
  connection.transport.close()
67
-
67
+
68
68
  # Clean up transport socket if accessible
69
- if hasattr(connection.transport, 'sock'):
69
+ if hasattr(connection.transport, "sock"):
70
70
  safe_socket_close(connection.transport.sock)
71
- elif hasattr(connection.transport, '_socket'):
71
+ elif hasattr(connection.transport, "_socket"):
72
72
  safe_socket_close(connection.transport._socket)
73
-
74
- except Exception as e:
73
+
74
+ except Exception:
75
75
  pass # Transport cleanup error ignored
76
-
76
+
77
77
  # Try the main close method
78
- if hasattr(connection, 'close'):
79
- try:
78
+ if hasattr(connection, "close"):
79
+ with contextlib.suppress(Exception):
80
+ # Connection close error ignored
80
81
  connection.close()
81
- except Exception as e:
82
- pass # Connection close error ignored
83
-
84
- except Exception as e:
82
+
83
+ except Exception:
85
84
  pass # Connection cleanup error ignored
86
85
 
87
86
 
@@ -89,34 +88,35 @@ def validate_connection_health(connection: Any) -> bool:
89
88
  """Validate that a connection is healthy and usable."""
90
89
  if not connection:
91
90
  return False
92
-
91
+
93
92
  try:
94
93
  # Check if connection reports as alive
95
- if hasattr(connection, 'isalive'):
96
- if not connection.isalive():
97
- return False
98
-
94
+ if hasattr(connection, "isalive") and not connection.isalive():
95
+ return False
96
+
99
97
  # Check underlying socket health if available
100
- if hasattr(connection, 'channel') and connection.channel:
101
- if hasattr(connection.channel, 'socket'):
98
+ if hasattr(connection, "channel") and connection.channel:
99
+ if hasattr(connection.channel, "socket"):
102
100
  if not is_socket_valid(connection.channel.socket):
103
101
  return False
104
- elif hasattr(connection.channel, '_socket'):
105
- if not is_socket_valid(connection.channel._socket):
106
- return False
107
-
102
+ elif hasattr(connection.channel, "_socket") and not is_socket_valid(
103
+ connection.channel._socket
104
+ ):
105
+ return False
106
+
108
107
  # Check transport socket health if available
109
- if hasattr(connection, 'transport') and connection.transport:
110
- if hasattr(connection.transport, 'sock'):
108
+ if hasattr(connection, "transport") and connection.transport:
109
+ if hasattr(connection.transport, "sock"):
111
110
  if not is_socket_valid(connection.transport.sock):
112
111
  return False
113
- elif hasattr(connection.transport, '_socket'):
114
- if not is_socket_valid(connection.transport._socket):
115
- return False
116
-
112
+ elif hasattr(connection.transport, "_socket") and not is_socket_valid(
113
+ connection.transport._socket
114
+ ):
115
+ return False
116
+
117
117
  return True
118
-
119
- except Exception as e:
118
+
119
+ except Exception:
120
120
  return False
121
121
 
122
122