ipmg 3.1.2__tar.gz → 3.1.4__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 (85) hide show
  1. {ipmg-3.1.2/src/ipmg.egg-info → ipmg-3.1.4}/PKG-INFO +1 -1
  2. {ipmg-3.1.2 → ipmg-3.1.4}/pyproject.toml +1 -1
  3. {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/__init__.py +1 -1
  4. {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/infrastructure/file_io.py +13 -7
  5. {ipmg-3.1.2 → ipmg-3.1.4/src/ipmg.egg-info}/PKG-INFO +1 -1
  6. {ipmg-3.1.2 → ipmg-3.1.4}/tests/test_file_io.py +32 -0
  7. {ipmg-3.1.2 → ipmg-3.1.4}/tests/test_notify.py +3 -3
  8. {ipmg-3.1.2 → ipmg-3.1.4}/LICENSE +0 -0
  9. {ipmg-3.1.2 → ipmg-3.1.4}/README.md +0 -0
  10. {ipmg-3.1.2 → ipmg-3.1.4}/setup.cfg +0 -0
  11. {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/__main__.py +0 -0
  12. {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/cli/__init__.py +0 -0
  13. {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/cli/commands.py +0 -0
  14. {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/cli/config.py +0 -0
  15. {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/cli/parser.py +0 -0
  16. {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/core/__init__.py +0 -0
  17. {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/core/diff.py +0 -0
  18. {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/core/discovery.py +0 -0
  19. {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/core/engine.py +0 -0
  20. {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/core/health.py +0 -0
  21. {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/core/ping.py +0 -0
  22. {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/core/portscan.py +0 -0
  23. {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/core/security.py +0 -0
  24. {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/exceptions.py +0 -0
  25. {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/infrastructure/__init__.py +0 -0
  26. {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/infrastructure/database.py +0 -0
  27. {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/infrastructure/incremental.py +0 -0
  28. {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/infrastructure/notify.py +0 -0
  29. {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/reporting/__init__.py +0 -0
  30. {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/reporting/diff_report.py +0 -0
  31. {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/reporting/frames.py +0 -0
  32. {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/reporting/live.py +0 -0
  33. {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/reporting/machine.py +0 -0
  34. {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/reporting/metrics.py +0 -0
  35. {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/reporting/summary.py +0 -0
  36. {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/reporting/ui.py +0 -0
  37. {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/services/__init__.py +0 -0
  38. {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/services/history_service.py +0 -0
  39. {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/services/scan_service.py +0 -0
  40. {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/utils/__init__.py +0 -0
  41. {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/utils/helpers.py +0 -0
  42. {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/web/__init__.py +0 -0
  43. {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/web/app.py +0 -0
  44. {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/web/db.py +0 -0
  45. {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/web/manager.py +0 -0
  46. {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/web/schemas.py +0 -0
  47. {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/web/server.py +0 -0
  48. {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/web/static/css/app.css +0 -0
  49. {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/web/static/index.html +0 -0
  50. {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/web/static/js/api.js +0 -0
  51. {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/web/static/js/app.js +0 -0
  52. {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/web/static/js/charts.js +0 -0
  53. {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/web/static/js/demo.js +0 -0
  54. {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/web/static/js/views.js +0 -0
  55. {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg.egg-info/SOURCES.txt +0 -0
  56. {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg.egg-info/dependency_links.txt +0 -0
  57. {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg.egg-info/entry_points.txt +0 -0
  58. {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg.egg-info/requires.txt +0 -0
  59. {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg.egg-info/top_level.txt +0 -0
  60. {ipmg-3.1.2 → ipmg-3.1.4}/tests/test_api_contract.py +0 -0
  61. {ipmg-3.1.2 → ipmg-3.1.4}/tests/test_commands.py +0 -0
  62. {ipmg-3.1.2 → ipmg-3.1.4}/tests/test_config.py +0 -0
  63. {ipmg-3.1.2 → ipmg-3.1.4}/tests/test_database_history.py +0 -0
  64. {ipmg-3.1.2 → ipmg-3.1.4}/tests/test_diff.py +0 -0
  65. {ipmg-3.1.2 → ipmg-3.1.4}/tests/test_diff_report.py +0 -0
  66. {ipmg-3.1.2 → ipmg-3.1.4}/tests/test_discover.py +0 -0
  67. {ipmg-3.1.2 → ipmg-3.1.4}/tests/test_engine.py +0 -0
  68. {ipmg-3.1.2 → ipmg-3.1.4}/tests/test_health.py +0 -0
  69. {ipmg-3.1.2 → ipmg-3.1.4}/tests/test_history_service.py +0 -0
  70. {ipmg-3.1.2 → ipmg-3.1.4}/tests/test_incremental.py +0 -0
  71. {ipmg-3.1.2 → ipmg-3.1.4}/tests/test_ipv6.py +0 -0
  72. {ipmg-3.1.2 → ipmg-3.1.4}/tests/test_live.py +0 -0
  73. {ipmg-3.1.2 → ipmg-3.1.4}/tests/test_machine_output.py +0 -0
  74. {ipmg-3.1.2 → ipmg-3.1.4}/tests/test_metrics.py +0 -0
  75. {ipmg-3.1.2 → ipmg-3.1.4}/tests/test_parser.py +0 -0
  76. {ipmg-3.1.2 → ipmg-3.1.4}/tests/test_ping.py +0 -0
  77. {ipmg-3.1.2 → ipmg-3.1.4}/tests/test_ping_command.py +0 -0
  78. {ipmg-3.1.2 → ipmg-3.1.4}/tests/test_portscan.py +0 -0
  79. {ipmg-3.1.2 → ipmg-3.1.4}/tests/test_scan_service.py +0 -0
  80. {ipmg-3.1.2 → ipmg-3.1.4}/tests/test_ui.py +0 -0
  81. {ipmg-3.1.2 → ipmg-3.1.4}/tests/test_utils.py +0 -0
  82. {ipmg-3.1.2 → ipmg-3.1.4}/tests/test_web_api.py +0 -0
  83. {ipmg-3.1.2 → ipmg-3.1.4}/tests/test_web_db.py +0 -0
  84. {ipmg-3.1.2 → ipmg-3.1.4}/tests/test_web_manager.py +0 -0
  85. {ipmg-3.1.2 → ipmg-3.1.4}/tests/test_web_server.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ipmg
3
- Version: 3.1.2
3
+ Version: 3.1.4
4
4
  Summary: IP Management & Ping Monitoring CLI Tool
5
5
  Author: Sameer Alam
6
6
  Maintainer-email: Sameer Alam <sameeralam3127@gmail.com>
@@ -13,7 +13,7 @@ build-backend = "setuptools.build_meta"
13
13
 
14
14
  [project]
15
15
  name = "ipmg"
16
- version = "3.1.2" # Managed automatically by semantic-release
16
+ version = "3.1.4" # Managed automatically by semantic-release
17
17
  description = "IP Management & Ping Monitoring CLI Tool"
18
18
  readme = "README.md"
19
19
  requires-python = ">=3.9"
@@ -2,4 +2,4 @@
2
2
  ipmg - IP Management & Ping Monitoring Tool
3
3
  """
4
4
 
5
- __version__ = "3.1.2"
5
+ __version__ = "3.1.4"
@@ -424,16 +424,22 @@ def _xlsx_cell(value: Any) -> Any:
424
424
 
425
425
  def _render_xlsx(table: ReportTable) -> bytes:
426
426
  from openpyxl import Workbook
427
+ from openpyxl.cell import WriteOnlyCell
427
428
 
428
- workbook = Workbook()
429
- sheet = workbook.active
430
- sheet.title = "Sheet1"
429
+ # Write-only mode streams rows out. A normal sheet rescans every cell for
430
+ # max_row, which made a /16 take minutes to write (#102).
431
+ workbook = Workbook(write_only=True)
432
+ sheet = workbook.create_sheet("Sheet1")
431
433
  sheet.append(list(table.columns))
432
434
  for row in sanitize_table(table).rows:
433
- sheet.append([_xlsx_cell(row.get(column)) for column in table.columns])
434
- for cell in sheet[sheet.max_row]:
435
- if isinstance(cell.value, datetime):
436
- cell.number_format = _XLSX_DATETIME_FORMAT
435
+ cells = []
436
+ for column in table.columns:
437
+ value = _xlsx_cell(row.get(column))
438
+ if isinstance(value, datetime):
439
+ value = WriteOnlyCell(sheet, value=value)
440
+ value.number_format = _XLSX_DATETIME_FORMAT
441
+ cells.append(value)
442
+ sheet.append(cells)
437
443
  buffer = io.BytesIO()
438
444
  workbook.save(buffer)
439
445
  return buffer.getvalue()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ipmg
3
- Version: 3.1.2
3
+ Version: 3.1.4
4
4
  Summary: IP Management & Ping Monitoring CLI Tool
5
5
  Author: Sameer Alam
6
6
  Maintainer-email: Sameer Alam <sameeralam3127@gmail.com>
@@ -1,4 +1,6 @@
1
1
  import json
2
+ import time
3
+ from datetime import datetime
2
4
 
3
5
  import openpyxl
4
6
  import pytest
@@ -187,6 +189,36 @@ def test_save_results_neutralizes_formulas_in_csv_and_xlsx(tmp_path, monkeypatch
187
189
  assert raw[0]["Hostname"] == HOSTILE_HOSTNAME
188
190
 
189
191
 
192
+ def _xlsx_table(rows, checked):
193
+ return ReportTable(
194
+ rows=[
195
+ {"IP Address": f"10.0.{i // 256}.{i % 256}", "Status": "Up", "Checked At": checked}
196
+ for i in range(rows)
197
+ ],
198
+ columns=("IP Address", "Status", "Checked At"),
199
+ )
200
+
201
+
202
+ def test_xlsx_writes_a_large_scan_in_linear_time(tmp_path, monkeypatch):
203
+ monkeypatch.setattr("ipmg.infrastructure.file_io.timestamp_str", lambda: "20260628_120000")
204
+ checked = datetime(2026, 6, 28, 12, 0, 0, 123456)
205
+
206
+ def seconds_to_save(rows):
207
+ started = time.perf_counter()
208
+ save_results(_xlsx_table(rows, checked), str(tmp_path / f"scan{rows}"), ["xlsx"])
209
+ return time.perf_counter() - started
210
+
211
+ # 4x the rows should take ~4x the time. The old writer rescanned the sheet
212
+ # for every row, so it took ~15x (#102). A ratio holds on slow CI runners.
213
+ small, large = seconds_to_save(2_000), seconds_to_save(8_000)
214
+ assert large < 8 * small
215
+
216
+ sheet = openpyxl.load_workbook(tmp_path / "scan8000_20260628_120000.xlsx").active
217
+ assert sheet.max_row == 8_001
218
+ assert sheet["C2"].value == checked.replace(microsecond=123000)
219
+ assert sheet["C2"].number_format == "YYYY-MM-DD HH:MM:SS"
220
+
221
+
190
222
  @pytest.mark.parametrize(
191
223
  "name", ["targts.txt", "hosts.list", "hosts.csv", "hosts.json", "hosts.xlsx", "hosts.xls"]
192
224
  )
@@ -254,7 +254,6 @@ def test_smtp_user_with_security_none_is_allowed_for_loopback_hosts(loopback_hos
254
254
  assert settings.port == 25
255
255
 
256
256
 
257
-
258
257
  # ---------------------------------------------------------------- messages
259
258
 
260
259
 
@@ -553,7 +552,9 @@ def test_send_email_follows_the_security_setting(diff, fake_smtp, security, user
553
552
 
554
553
 
555
554
  def test_send_email_refuses_cleartext_auth_on_remote_host(diff, fake_smtp):
556
- settings = SmtpSettings("mail.example.test", 25, "none", "ipmg@x.test", ("ops@x.test",), "u", "pw")
555
+ settings = SmtpSettings(
556
+ "mail.example.test", 25, "none", "ipmg@x.test", ("ops@x.test",), "u", "pw"
557
+ )
557
558
 
558
559
  with pytest.raises(NotifyError, match=r"starttls.*ssl"):
559
560
  send_email(settings, email_message(diff, settings))
@@ -567,4 +568,3 @@ def test_send_email_allows_cleartext_auth_on_loopback_host(diff, fake_smtp):
567
568
  send_email(settings, email_message(diff, settings))
568
569
 
569
570
  assert fake_smtp[0].calls == [("login", "u", "pw"), ("send", "ops@x.test"), "quit"]
570
-
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes