netbox-sqlquery 0.1.2__tar.gz → 0.1.3__tar.gz

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 (42) hide show
  1. {netbox_sqlquery-0.1.2 → netbox_sqlquery-0.1.3}/PKG-INFO +3 -3
  2. {netbox_sqlquery-0.1.2 → netbox_sqlquery-0.1.3}/README.md +1 -1
  3. {netbox_sqlquery-0.1.2 → netbox_sqlquery-0.1.3}/netbox_sqlquery/__init__.py +2 -2
  4. {netbox_sqlquery-0.1.2 → netbox_sqlquery-0.1.3}/netbox_sqlquery/migrations/0001_initial.py +2 -2
  5. {netbox_sqlquery-0.1.2 → netbox_sqlquery-0.1.3}/netbox_sqlquery/static/netbox_sqlquery/editor.js +1 -27
  6. {netbox_sqlquery-0.1.2 → netbox_sqlquery-0.1.3}/netbox_sqlquery/templates/netbox_sqlquery/query.html +8 -4
  7. {netbox_sqlquery-0.1.2 → netbox_sqlquery-0.1.3}/netbox_sqlquery/urls.py +1 -0
  8. {netbox_sqlquery-0.1.2 → netbox_sqlquery-0.1.3}/netbox_sqlquery/views.py +68 -3
  9. {netbox_sqlquery-0.1.2 → netbox_sqlquery-0.1.3}/netbox_sqlquery.egg-info/PKG-INFO +3 -3
  10. {netbox_sqlquery-0.1.2 → netbox_sqlquery-0.1.3}/netbox_sqlquery.egg-info/SOURCES.txt +0 -1
  11. {netbox_sqlquery-0.1.2 → netbox_sqlquery-0.1.3}/pyproject.toml +2 -2
  12. netbox_sqlquery-0.1.2/netbox_sqlquery/migrations/0003_tablepermission_groups_to_users_group.py +0 -16
  13. {netbox_sqlquery-0.1.2 → netbox_sqlquery-0.1.3}/LICENSE +0 -0
  14. {netbox_sqlquery-0.1.2 → netbox_sqlquery-0.1.3}/netbox_sqlquery/abstract_schema.py +0 -0
  15. {netbox_sqlquery-0.1.2 → netbox_sqlquery-0.1.3}/netbox_sqlquery/access.py +0 -0
  16. {netbox_sqlquery-0.1.2 → netbox_sqlquery-0.1.3}/netbox_sqlquery/api/__init__.py +0 -0
  17. {netbox_sqlquery-0.1.2 → netbox_sqlquery-0.1.3}/netbox_sqlquery/api/serializers.py +0 -0
  18. {netbox_sqlquery-0.1.2 → netbox_sqlquery-0.1.3}/netbox_sqlquery/api/urls.py +0 -0
  19. {netbox_sqlquery-0.1.2 → netbox_sqlquery-0.1.3}/netbox_sqlquery/api/views.py +0 -0
  20. {netbox_sqlquery-0.1.2 → netbox_sqlquery-0.1.3}/netbox_sqlquery/filtersets.py +0 -0
  21. {netbox_sqlquery-0.1.2 → netbox_sqlquery-0.1.3}/netbox_sqlquery/forms.py +0 -0
  22. {netbox_sqlquery-0.1.2 → netbox_sqlquery-0.1.3}/netbox_sqlquery/management/__init__.py +0 -0
  23. {netbox_sqlquery-0.1.2 → netbox_sqlquery-0.1.3}/netbox_sqlquery/management/commands/__init__.py +0 -0
  24. {netbox_sqlquery-0.1.2 → netbox_sqlquery-0.1.3}/netbox_sqlquery/management/commands/sqlquery_create_views.py +0 -0
  25. {netbox_sqlquery-0.1.2 → netbox_sqlquery-0.1.3}/netbox_sqlquery/migrations/0002_query_permissions.py +0 -0
  26. {netbox_sqlquery-0.1.2 → netbox_sqlquery-0.1.3}/netbox_sqlquery/migrations/__init__.py +0 -0
  27. {netbox_sqlquery-0.1.2 → netbox_sqlquery-0.1.3}/netbox_sqlquery/models.py +0 -0
  28. {netbox_sqlquery-0.1.2 → netbox_sqlquery-0.1.3}/netbox_sqlquery/navigation.py +0 -0
  29. {netbox_sqlquery-0.1.2 → netbox_sqlquery-0.1.3}/netbox_sqlquery/preferences.py +0 -0
  30. {netbox_sqlquery-0.1.2 → netbox_sqlquery-0.1.3}/netbox_sqlquery/schema.py +0 -0
  31. {netbox_sqlquery-0.1.2 → netbox_sqlquery-0.1.3}/netbox_sqlquery/static/netbox_sqlquery/icon.LICENSE +0 -0
  32. {netbox_sqlquery-0.1.2 → netbox_sqlquery-0.1.3}/netbox_sqlquery/static/netbox_sqlquery/icon.svg +0 -0
  33. {netbox_sqlquery-0.1.2 → netbox_sqlquery-0.1.3}/netbox_sqlquery/tables.py +0 -0
  34. {netbox_sqlquery-0.1.2 → netbox_sqlquery-0.1.3}/netbox_sqlquery/templates/netbox_sqlquery/saved_query.html +0 -0
  35. {netbox_sqlquery-0.1.2 → netbox_sqlquery-0.1.3}/netbox_sqlquery/tests/__init__.py +0 -0
  36. {netbox_sqlquery-0.1.2 → netbox_sqlquery-0.1.3}/netbox_sqlquery/tests/test_access.py +0 -0
  37. {netbox_sqlquery-0.1.2 → netbox_sqlquery-0.1.3}/netbox_sqlquery/tests/test_api.py +0 -0
  38. {netbox_sqlquery-0.1.2 → netbox_sqlquery-0.1.3}/netbox_sqlquery/tests/test_models.py +0 -0
  39. {netbox_sqlquery-0.1.2 → netbox_sqlquery-0.1.3}/netbox_sqlquery/tests/test_views.py +0 -0
  40. {netbox_sqlquery-0.1.2 → netbox_sqlquery-0.1.3}/netbox_sqlquery.egg-info/dependency_links.txt +0 -0
  41. {netbox_sqlquery-0.1.2 → netbox_sqlquery-0.1.3}/netbox_sqlquery.egg-info/top_level.txt +0 -0
  42. {netbox_sqlquery-0.1.2 → netbox_sqlquery-0.1.3}/setup.cfg +0 -0
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: netbox-sqlquery
3
- Version: 0.1.2
3
+ Version: 0.1.3
4
4
  Summary: SQL query interface for NetBox with syntax highlighting, abstract views, and role-based access control
5
5
  Author-email: Ravi Pina <ravi@pina.org>
6
6
  License-Expression: Apache-2.0
7
7
  Project-URL: Homepage, https://github.com/ravinald/netbox-sqlquery
8
8
  Project-URL: Issues, https://github.com/ravinald/netbox-sqlquery/issues
9
- Requires-Python: >=3.10
9
+ Requires-Python: >=3.12
10
10
  Description-Content-Type: text/markdown
11
11
  License-File: LICENSE
12
12
  Dynamic: license-file
@@ -73,7 +73,7 @@ See [COMPATIBILITY.md](COMPATIBILITY.md) for the full version matrix.
73
73
 
74
74
  | NetBox version | Python versions |
75
75
  |----------------|------------------------------|
76
- | 4.0 - 4.5 | 3.10, 3.11, 3.12, 3.13, 3.14 |
76
+ | 4.5+ | 3.12, 3.13, 3.14 |
77
77
 
78
78
  ## Installation
79
79
 
@@ -60,7 +60,7 @@ See [COMPATIBILITY.md](COMPATIBILITY.md) for the full version matrix.
60
60
 
61
61
  | NetBox version | Python versions |
62
62
  |----------------|------------------------------|
63
- | 4.0 - 4.5 | 3.10, 3.11, 3.12, 3.13, 3.14 |
63
+ | 4.5+ | 3.12, 3.13, 3.14 |
64
64
 
65
65
  ## Installation
66
66
 
@@ -12,11 +12,11 @@ class NetBoxSQLQueryConfig(PluginConfig):
12
12
  "SQL query interface for NetBox with syntax highlighting,"
13
13
  " abstract views, and role-based access control"
14
14
  )
15
- version = "0.1.2"
15
+ version = "0.1.3"
16
16
  author = "Ravi Pina"
17
17
  author_email = "ravi@pina.org"
18
18
  base_url = "sqlquery"
19
- min_version = "4.0.0"
19
+ min_version = "4.5.0"
20
20
  max_version = None
21
21
  required_settings = []
22
22
  default_settings = {
@@ -8,8 +8,8 @@ class Migration(migrations.Migration):
8
8
 
9
9
  dependencies = [
10
10
  migrations.swappable_dependency(settings.AUTH_USER_MODEL),
11
- ("auth", "0012_alter_user_first_name_max_length"),
12
11
  ("extras", "0001_initial"),
12
+ ("users", "0001_squashed_0011"),
13
13
  ]
14
14
 
15
15
  operations = [
@@ -85,7 +85,7 @@ class Migration(migrations.Migration):
85
85
  ("require_staff", models.BooleanField(default=False)),
86
86
  ("require_superuser", models.BooleanField(default=False)),
87
87
  ("allow", models.BooleanField(default=True)),
88
- ("groups", models.ManyToManyField(blank=True, to="auth.group")),
88
+ ("groups", models.ManyToManyField(blank=True, to="users.group")),
89
89
  ],
90
90
  options={
91
91
  "ordering": ["-require_superuser", "-require_staff", "pattern"],
@@ -552,33 +552,7 @@
552
552
  });
553
553
 
554
554
 
555
- // CSV download
556
- var csvBtn = document.getElementById("csv-download");
557
- if (csvBtn) {
558
- csvBtn.addEventListener("click", function () {
559
- var table = document.querySelector(".results-pane table");
560
- if (!table) return;
561
- var rows = [];
562
- table.querySelectorAll("tr").forEach(function (tr) {
563
- var cells = [];
564
- tr.querySelectorAll("th, td").forEach(function (cell) {
565
- var text = cell.textContent;
566
- if (text.includes(",") || text.includes('"') || text.includes("\n")) {
567
- text = '"' + text.replace(/"/g, '""') + '"';
568
- }
569
- cells.push(text);
570
- });
571
- rows.push(cells.join(","));
572
- });
573
- var blob = new Blob([rows.join("\n")], { type: "text/csv" });
574
- var url = URL.createObjectURL(blob);
575
- var a = document.createElement("a");
576
- a.href = url;
577
- a.download = "query_results.csv";
578
- a.click();
579
- URL.revokeObjectURL(url);
580
- });
581
- }
555
+ // CSV download is now a server-side form POST (no JS needed)
582
556
 
583
557
  function insertAtCursor(text) {
584
558
  var start = editor.selectionStart;
@@ -299,10 +299,14 @@
299
299
  </tbody>
300
300
  </table>
301
301
  <div class="results-meta">
302
- <span>{{ row_count }} row{{ row_count|pluralize }}</span>
303
- <button type="button" class="btn btn-sm btn-outline-secondary" id="csv-download">
304
- Download CSV
305
- </button>
302
+ <span>{{ row_count }} row{{ row_count|pluralize }}{% if truncated %} <span class="text-warning">(limited to {{ max_rows }} -- add LIMIT/OFFSET to page through results)</span>{% endif %}</span>
303
+ <form method="post" action="{% url 'plugins:netbox_sqlquery:export_csv' %}" style="display:inline;">
304
+ {% csrf_token %}
305
+ <input type="hidden" name="sql" value="{{ sql }}">
306
+ <button type="submit" class="btn btn-sm btn-outline-secondary" title="Download all results as CSV (no row limit)">
307
+ Download CSV
308
+ </button>
309
+ </form>
306
310
  </div>
307
311
  </div>
308
312
  {% endif %}
@@ -19,4 +19,5 @@ urlpatterns = [
19
19
  ),
20
20
  path("ajax/save-query/", views.SavedQueryAjaxSave.as_view(), name="ajax_save_query"),
21
21
  path("ajax/list-queries/", views.SavedQueryAjaxList.as_view(), name="ajax_list_queries"),
22
+ path("export-csv/", views.CSVExportView.as_view(), name="export_csv"),
22
23
  ]
@@ -1,9 +1,11 @@
1
+ import csv
2
+ import io
1
3
  import json
2
4
  import logging
3
5
 
4
6
  from django.contrib.auth.mixins import UserPassesTestMixin
5
7
  from django.db import DatabaseError, connection, transaction
6
- from django.http import JsonResponse
8
+ from django.http import HttpResponse, JsonResponse
7
9
  from django.views import View
8
10
  from django.views.generic import TemplateView
9
11
  from netbox.plugins import get_plugin_config
@@ -185,10 +187,19 @@ class QueryView(UserPassesTestMixin, TemplateView):
185
187
  cursor.execute("SET TRANSACTION READ ONLY")
186
188
  cursor.execute(sql)
187
189
  columns = [col[0] for col in cursor.description]
188
- rows = cursor.fetchmany(max_rows)
190
+ rows = cursor.fetchmany(max_rows + 1)
189
191
  raise _ReadOnlyRollback()
190
192
  except _ReadOnlyRollback:
191
- ctx.update(columns=columns, rows=rows, row_count=len(rows))
193
+ truncated = len(rows) > max_rows
194
+ if truncated:
195
+ rows = rows[:max_rows]
196
+ ctx.update(
197
+ columns=columns,
198
+ rows=rows,
199
+ row_count=len(rows),
200
+ truncated=truncated,
201
+ max_rows=max_rows,
202
+ )
192
203
  except DatabaseError as exc:
193
204
  ctx["error"] = str(exc)
194
205
  return ctx
@@ -351,3 +362,57 @@ class SavedQueryAjaxList(UserPassesTestMixin, View):
351
362
  ]
352
363
  }
353
364
  )
365
+
366
+
367
+ class CSVExportView(UserPassesTestMixin, View):
368
+ """Export query results as CSV with no row limit."""
369
+
370
+ def test_func(self):
371
+ user = self.request.user
372
+ if not user.is_active:
373
+ return False
374
+ if user.is_superuser:
375
+ return True
376
+ if get_plugin_config("netbox_sqlquery", "require_superuser"):
377
+ return False
378
+ return user.has_perm("netbox_sqlquery.view_querypermission")
379
+
380
+ def post(self, request):
381
+ sql = request.POST.get("sql", "").strip()
382
+ if not sql:
383
+ return HttpResponse("No SQL provided.", status=400)
384
+
385
+ normalized = sql.lstrip().upper()
386
+ if not (normalized.startswith("SELECT") or normalized.startswith("WITH")):
387
+ return HttpResponse("Only SELECT queries can be exported.", status=400)
388
+
389
+ denied = check_access(request.user, extract_tables(sql))
390
+ if denied:
391
+ return HttpResponse(f"Access denied to: {', '.join(sorted(denied))}", status=403)
392
+
393
+ timeout_ms = get_plugin_config("netbox_sqlquery", "statement_timeout_ms")
394
+
395
+ try:
396
+ with transaction.atomic():
397
+ with connection.cursor() as cursor:
398
+ cursor.execute(f"SET LOCAL statement_timeout = '{timeout_ms}'")
399
+ cursor.execute("SET TRANSACTION READ ONLY")
400
+ cursor.execute(sql)
401
+ columns = [col[0] for col in cursor.description]
402
+ rows = cursor.fetchall()
403
+ raise _ReadOnlyRollback()
404
+ except _ReadOnlyRollback:
405
+ pass
406
+ except DatabaseError as exc:
407
+ return HttpResponse(f"Query error: {exc}", status=400)
408
+
409
+ output = io.StringIO()
410
+ writer = csv.writer(output)
411
+ writer.writerow(columns)
412
+ writer.writerows(rows)
413
+
414
+ response = HttpResponse(output.getvalue(), content_type="text/csv")
415
+ response["Content-Disposition"] = 'attachment; filename="query_results.csv"'
416
+
417
+ _record_query(request.user, sql)
418
+ return response
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: netbox-sqlquery
3
- Version: 0.1.2
3
+ Version: 0.1.3
4
4
  Summary: SQL query interface for NetBox with syntax highlighting, abstract views, and role-based access control
5
5
  Author-email: Ravi Pina <ravi@pina.org>
6
6
  License-Expression: Apache-2.0
7
7
  Project-URL: Homepage, https://github.com/ravinald/netbox-sqlquery
8
8
  Project-URL: Issues, https://github.com/ravinald/netbox-sqlquery/issues
9
- Requires-Python: >=3.10
9
+ Requires-Python: >=3.12
10
10
  Description-Content-Type: text/markdown
11
11
  License-File: LICENSE
12
12
  Dynamic: license-file
@@ -73,7 +73,7 @@ See [COMPATIBILITY.md](COMPATIBILITY.md) for the full version matrix.
73
73
 
74
74
  | NetBox version | Python versions |
75
75
  |----------------|------------------------------|
76
- | 4.0 - 4.5 | 3.10, 3.11, 3.12, 3.13, 3.14 |
76
+ | 4.5+ | 3.12, 3.13, 3.14 |
77
77
 
78
78
  ## Installation
79
79
 
@@ -26,7 +26,6 @@ netbox_sqlquery/management/commands/__init__.py
26
26
  netbox_sqlquery/management/commands/sqlquery_create_views.py
27
27
  netbox_sqlquery/migrations/0001_initial.py
28
28
  netbox_sqlquery/migrations/0002_query_permissions.py
29
- netbox_sqlquery/migrations/0003_tablepermission_groups_to_users_group.py
30
29
  netbox_sqlquery/migrations/__init__.py
31
30
  netbox_sqlquery/static/netbox_sqlquery/editor.js
32
31
  netbox_sqlquery/static/netbox_sqlquery/icon.LICENSE
@@ -4,11 +4,11 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "netbox-sqlquery"
7
- version = "0.1.2"
7
+ version = "0.1.3"
8
8
  description = "SQL query interface for NetBox with syntax highlighting, abstract views, and role-based access control"
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
11
- requires-python = ">=3.10"
11
+ requires-python = ">=3.12"
12
12
  dependencies = []
13
13
  authors = [
14
14
  { name = "Ravi Pina", email = "ravi@pina.org" },
@@ -1,16 +0,0 @@
1
- from django.db import migrations, models
2
-
3
-
4
- class Migration(migrations.Migration):
5
- dependencies = [
6
- ("netbox_sqlquery", "0002_query_permissions"),
7
- ("users", "0001_squashed_0011"),
8
- ]
9
-
10
- operations = [
11
- migrations.AlterField(
12
- model_name="tablepermission",
13
- name="groups",
14
- field=models.ManyToManyField(blank=True, to="users.group"),
15
- ),
16
- ]
File without changes