netbox-sqlquery 0.1.5__tar.gz → 0.1.7__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.5 → netbox_sqlquery-0.1.7}/PKG-INFO +1 -1
  2. {netbox_sqlquery-0.1.5 → netbox_sqlquery-0.1.7}/netbox_sqlquery/__init__.py +41 -17
  3. {netbox_sqlquery-0.1.5 → netbox_sqlquery-0.1.7}/netbox_sqlquery/abstract_schema.py +38 -2
  4. {netbox_sqlquery-0.1.5 → netbox_sqlquery-0.1.7}/netbox_sqlquery/management/commands/sqlquery_create_views.py +9 -1
  5. {netbox_sqlquery-0.1.5 → netbox_sqlquery-0.1.7}/netbox_sqlquery.egg-info/PKG-INFO +1 -1
  6. {netbox_sqlquery-0.1.5 → netbox_sqlquery-0.1.7}/pyproject.toml +1 -1
  7. {netbox_sqlquery-0.1.5 → netbox_sqlquery-0.1.7}/LICENSE +0 -0
  8. {netbox_sqlquery-0.1.5 → netbox_sqlquery-0.1.7}/README.md +0 -0
  9. {netbox_sqlquery-0.1.5 → netbox_sqlquery-0.1.7}/netbox_sqlquery/access.py +0 -0
  10. {netbox_sqlquery-0.1.5 → netbox_sqlquery-0.1.7}/netbox_sqlquery/api/__init__.py +0 -0
  11. {netbox_sqlquery-0.1.5 → netbox_sqlquery-0.1.7}/netbox_sqlquery/api/serializers.py +0 -0
  12. {netbox_sqlquery-0.1.5 → netbox_sqlquery-0.1.7}/netbox_sqlquery/api/urls.py +0 -0
  13. {netbox_sqlquery-0.1.5 → netbox_sqlquery-0.1.7}/netbox_sqlquery/api/views.py +0 -0
  14. {netbox_sqlquery-0.1.5 → netbox_sqlquery-0.1.7}/netbox_sqlquery/filtersets.py +0 -0
  15. {netbox_sqlquery-0.1.5 → netbox_sqlquery-0.1.7}/netbox_sqlquery/forms.py +0 -0
  16. {netbox_sqlquery-0.1.5 → netbox_sqlquery-0.1.7}/netbox_sqlquery/management/__init__.py +0 -0
  17. {netbox_sqlquery-0.1.5 → netbox_sqlquery-0.1.7}/netbox_sqlquery/management/commands/__init__.py +0 -0
  18. {netbox_sqlquery-0.1.5 → netbox_sqlquery-0.1.7}/netbox_sqlquery/migrations/0001_initial.py +0 -0
  19. {netbox_sqlquery-0.1.5 → netbox_sqlquery-0.1.7}/netbox_sqlquery/migrations/0002_query_permissions.py +0 -0
  20. {netbox_sqlquery-0.1.5 → netbox_sqlquery-0.1.7}/netbox_sqlquery/migrations/__init__.py +0 -0
  21. {netbox_sqlquery-0.1.5 → netbox_sqlquery-0.1.7}/netbox_sqlquery/models.py +0 -0
  22. {netbox_sqlquery-0.1.5 → netbox_sqlquery-0.1.7}/netbox_sqlquery/navigation.py +0 -0
  23. {netbox_sqlquery-0.1.5 → netbox_sqlquery-0.1.7}/netbox_sqlquery/preferences.py +0 -0
  24. {netbox_sqlquery-0.1.5 → netbox_sqlquery-0.1.7}/netbox_sqlquery/query.py +0 -0
  25. {netbox_sqlquery-0.1.5 → netbox_sqlquery-0.1.7}/netbox_sqlquery/schema.py +0 -0
  26. {netbox_sqlquery-0.1.5 → netbox_sqlquery-0.1.7}/netbox_sqlquery/static/netbox_sqlquery/editor.js +0 -0
  27. {netbox_sqlquery-0.1.5 → netbox_sqlquery-0.1.7}/netbox_sqlquery/static/netbox_sqlquery/icon.LICENSE +0 -0
  28. {netbox_sqlquery-0.1.5 → netbox_sqlquery-0.1.7}/netbox_sqlquery/static/netbox_sqlquery/icon.svg +0 -0
  29. {netbox_sqlquery-0.1.5 → netbox_sqlquery-0.1.7}/netbox_sqlquery/tables.py +0 -0
  30. {netbox_sqlquery-0.1.5 → netbox_sqlquery-0.1.7}/netbox_sqlquery/templates/netbox_sqlquery/query.html +0 -0
  31. {netbox_sqlquery-0.1.5 → netbox_sqlquery-0.1.7}/netbox_sqlquery/templates/netbox_sqlquery/saved_query.html +0 -0
  32. {netbox_sqlquery-0.1.5 → netbox_sqlquery-0.1.7}/netbox_sqlquery/tests/__init__.py +0 -0
  33. {netbox_sqlquery-0.1.5 → netbox_sqlquery-0.1.7}/netbox_sqlquery/tests/test_access.py +0 -0
  34. {netbox_sqlquery-0.1.5 → netbox_sqlquery-0.1.7}/netbox_sqlquery/tests/test_api.py +0 -0
  35. {netbox_sqlquery-0.1.5 → netbox_sqlquery-0.1.7}/netbox_sqlquery/tests/test_models.py +0 -0
  36. {netbox_sqlquery-0.1.5 → netbox_sqlquery-0.1.7}/netbox_sqlquery/tests/test_views.py +0 -0
  37. {netbox_sqlquery-0.1.5 → netbox_sqlquery-0.1.7}/netbox_sqlquery/urls.py +0 -0
  38. {netbox_sqlquery-0.1.5 → netbox_sqlquery-0.1.7}/netbox_sqlquery/views.py +0 -0
  39. {netbox_sqlquery-0.1.5 → netbox_sqlquery-0.1.7}/netbox_sqlquery.egg-info/SOURCES.txt +0 -0
  40. {netbox_sqlquery-0.1.5 → netbox_sqlquery-0.1.7}/netbox_sqlquery.egg-info/dependency_links.txt +0 -0
  41. {netbox_sqlquery-0.1.5 → netbox_sqlquery-0.1.7}/netbox_sqlquery.egg-info/top_level.txt +0 -0
  42. {netbox_sqlquery-0.1.5 → netbox_sqlquery-0.1.7}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: netbox-sqlquery
3
- Version: 0.1.5
3
+ Version: 0.1.7
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
@@ -1,14 +1,9 @@
1
1
  import logging
2
- import sys
3
2
 
4
3
  from netbox.plugins import PluginConfig
5
4
 
6
5
  logger = logging.getLogger("netbox_sqlquery")
7
6
 
8
- # Management commands that modify schema — creating views during these
9
- # can block migrations when a view depends on a column being altered.
10
- _SKIP_VIEWS_COMMANDS = {"migrate", "makemigrations"}
11
-
12
7
 
13
8
  class NetBoxSQLQueryConfig(PluginConfig):
14
9
  name = "netbox_sqlquery"
@@ -17,7 +12,7 @@ class NetBoxSQLQueryConfig(PluginConfig):
17
12
  "SQL query interface for NetBox with syntax highlighting,"
18
13
  " abstract views, and role-based access control"
19
14
  )
20
- version = "0.1.5"
15
+ version = "0.1.7"
21
16
  author = "Ravi Pina"
22
17
  author_email = "ravi@pina.org"
23
18
  base_url = "sqlquery"
@@ -46,25 +41,40 @@ class NetBoxSQLQueryConfig(PluginConfig):
46
41
  # Register navigation based on top_level_menu setting
47
42
  self._register_navigation()
48
43
 
49
- # Hook post_migrate so views are (re)created after schema changes.
50
- from django.db.models.signals import post_migrate
44
+ # Drop views before migrations so PostgreSQL can alter columns
45
+ # that the views depend on. Recreate them after migrations
46
+ # complete against the new schema. The views are read-only
47
+ # projections and contain no data, so this is always safe.
48
+ from django.db.models.signals import post_migrate, pre_migrate
51
49
 
52
- post_migrate.connect(self._create_views, sender=self)
50
+ pre_migrate.connect(self._drop_views, sender=self)
51
+ post_migrate.connect(self._create_views_forced, sender=self)
53
52
 
54
- # For normal app startup (gunicorn/uvicorn), create views directly.
55
- # Skip during management commands that modify schema views that
56
- # reference columns being altered will cause PostgreSQL to reject
57
- # the migration with "cannot alter type of a column used by a view".
58
- running_command = sys.argv[1] if len(sys.argv) > 1 else None
59
- if running_command not in _SKIP_VIEWS_COMMANDS:
60
- self._create_views(sender=self)
53
+ # For normal app startup (gunicorn/uvicorn), skip expensive view
54
+ # creation if views already exist just populate the table map.
55
+ self._create_views(sender=self)
56
+
57
+ @staticmethod
58
+ def _drop_views(sender, **kwargs):
59
+ try:
60
+ from .abstract_schema import drop_views
61
+
62
+ dropped = drop_views()
63
+ if dropped:
64
+ logger.info(
65
+ "Dropped %d abstract SQL view(s) before migration.",
66
+ len(dropped),
67
+ )
68
+ except Exception as exc:
69
+ logger.warning("Could not drop abstract SQL views: %s", exc)
61
70
 
62
71
  @staticmethod
63
72
  def _create_views(sender, **kwargs):
73
+ """Normal startup — skips creation if views already exist."""
64
74
  try:
65
75
  from .abstract_schema import ensure_views
66
76
 
67
- ensure_views()
77
+ ensure_views() # force=False: fast path when views exist
68
78
  except Exception as exc:
69
79
  logger.warning(
70
80
  "Could not create abstract SQL views: %s. "
@@ -72,6 +82,20 @@ class NetBoxSQLQueryConfig(PluginConfig):
72
82
  exc,
73
83
  )
74
84
 
85
+ @staticmethod
86
+ def _create_views_forced(sender, **kwargs):
87
+ """Post-migrate — always rebuild views against the new schema."""
88
+ try:
89
+ from .abstract_schema import ensure_views
90
+
91
+ ensure_views(force=True)
92
+ except Exception as exc:
93
+ logger.warning(
94
+ "Could not create abstract SQL views after migration: %s. "
95
+ "Run 'manage.py sqlquery_create_views --force' manually.",
96
+ exc,
97
+ )
98
+
75
99
  def _register_navigation(self):
76
100
  from netbox.plugins.registration import register_menu, register_menu_items
77
101
 
@@ -136,6 +136,30 @@ COLUMN_RENAMES = {
136
136
  ABSTRACT_TO_TABLES = {}
137
137
 
138
138
 
139
+ def _views_exist():
140
+ """Fast check: do any nb_* abstract views exist in the database?"""
141
+ with connection.cursor() as cursor:
142
+ cursor.execute("""
143
+ SELECT 1 FROM information_schema.views
144
+ WHERE table_schema = 'public' AND table_name LIKE 'nb\\_%'
145
+ LIMIT 1
146
+ """)
147
+ return cursor.fetchone() is not None
148
+
149
+
150
+ def _populate_table_map():
151
+ """Populate ABSTRACT_TO_TABLES from model metadata without FK introspection.
152
+
153
+ Used on normal startup when views already exist — avoids the expensive
154
+ information_schema queries that block gunicorn workers.
155
+ """
156
+ ABSTRACT_TO_TABLES.clear()
157
+ for model in get_included_models():
158
+ table_name = model._meta.db_table
159
+ view_name = VIEW_NAME_OVERRIDES.get(table_name, f"nb_{table_name}")
160
+ ABSTRACT_TO_TABLES[view_name] = {table_name}
161
+
162
+
139
163
  def _get_view_name(model):
140
164
  """Generate the nb_* view name for a model."""
141
165
  label = f"{model._meta.app_label}.{model._meta.model_name}"
@@ -165,6 +189,7 @@ def _get_table_columns(table_name):
165
189
  def _get_fk_map(table_name):
166
190
  """Get FK column -> target table mapping from information_schema."""
167
191
  with connection.cursor() as cursor:
192
+ cursor.execute("SET LOCAL statement_timeout = '30s'")
168
193
  cursor.execute(
169
194
  """
170
195
  SELECT kcu.column_name, ccu.table_name
@@ -366,8 +391,19 @@ def get_included_models():
366
391
  return models
367
392
 
368
393
 
369
- def ensure_views(dry_run=False):
370
- """Create or replace all abstract views. Returns list of (view_name, sql) tuples."""
394
+ def ensure_views(dry_run=False, force=False):
395
+ """Create or replace all abstract views. Returns list of (view_name, sql) tuples.
396
+
397
+ When *force* is False (the default) and views already exist in the database,
398
+ skips the expensive FK introspection and DDL — only populates the in-process
399
+ ABSTRACT_TO_TABLES map from model metadata. Pass *force=True* after
400
+ migrations or when views need to be rebuilt.
401
+ """
402
+ if not force and not dry_run and _views_exist():
403
+ logger.debug("Abstract views already exist; skipping creation.")
404
+ _populate_table_map()
405
+ return []
406
+
371
407
  results = []
372
408
  ABSTRACT_TO_TABLES.clear()
373
409
 
@@ -17,6 +17,11 @@ class Command(BaseCommand):
17
17
  action="store_true",
18
18
  help="Drop all nb_* views instead of creating them",
19
19
  )
20
+ parser.add_argument(
21
+ "--force",
22
+ action="store_true",
23
+ help="Rebuild views even if they already exist",
24
+ )
20
25
 
21
26
  def handle(self, *args, **options):
22
27
  if options["drop"]:
@@ -26,7 +31,10 @@ class Command(BaseCommand):
26
31
  self.stdout.write(self.style.SUCCESS(f"Dropped {len(dropped)} views"))
27
32
  return
28
33
 
29
- results = ensure_views(dry_run=options["dry_run"])
34
+ results = ensure_views(
35
+ dry_run=options["dry_run"],
36
+ force=options["force"] or options["dry_run"],
37
+ )
30
38
 
31
39
  for view_name, sql in results:
32
40
  if options["dry_run"]:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: netbox-sqlquery
3
- Version: 0.1.5
3
+ Version: 0.1.7
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "netbox-sqlquery"
7
- version = "0.1.5"
7
+ version = "0.1.7"
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"
File without changes