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.
- {ipmg-3.1.2/src/ipmg.egg-info → ipmg-3.1.4}/PKG-INFO +1 -1
- {ipmg-3.1.2 → ipmg-3.1.4}/pyproject.toml +1 -1
- {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/__init__.py +1 -1
- {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/infrastructure/file_io.py +13 -7
- {ipmg-3.1.2 → ipmg-3.1.4/src/ipmg.egg-info}/PKG-INFO +1 -1
- {ipmg-3.1.2 → ipmg-3.1.4}/tests/test_file_io.py +32 -0
- {ipmg-3.1.2 → ipmg-3.1.4}/tests/test_notify.py +3 -3
- {ipmg-3.1.2 → ipmg-3.1.4}/LICENSE +0 -0
- {ipmg-3.1.2 → ipmg-3.1.4}/README.md +0 -0
- {ipmg-3.1.2 → ipmg-3.1.4}/setup.cfg +0 -0
- {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/__main__.py +0 -0
- {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/cli/__init__.py +0 -0
- {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/cli/commands.py +0 -0
- {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/cli/config.py +0 -0
- {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/cli/parser.py +0 -0
- {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/core/__init__.py +0 -0
- {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/core/diff.py +0 -0
- {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/core/discovery.py +0 -0
- {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/core/engine.py +0 -0
- {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/core/health.py +0 -0
- {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/core/ping.py +0 -0
- {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/core/portscan.py +0 -0
- {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/core/security.py +0 -0
- {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/exceptions.py +0 -0
- {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/infrastructure/__init__.py +0 -0
- {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/infrastructure/database.py +0 -0
- {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/infrastructure/incremental.py +0 -0
- {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/infrastructure/notify.py +0 -0
- {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/reporting/__init__.py +0 -0
- {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/reporting/diff_report.py +0 -0
- {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/reporting/frames.py +0 -0
- {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/reporting/live.py +0 -0
- {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/reporting/machine.py +0 -0
- {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/reporting/metrics.py +0 -0
- {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/reporting/summary.py +0 -0
- {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/reporting/ui.py +0 -0
- {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/services/__init__.py +0 -0
- {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/services/history_service.py +0 -0
- {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/services/scan_service.py +0 -0
- {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/utils/__init__.py +0 -0
- {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/utils/helpers.py +0 -0
- {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/web/__init__.py +0 -0
- {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/web/app.py +0 -0
- {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/web/db.py +0 -0
- {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/web/manager.py +0 -0
- {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/web/schemas.py +0 -0
- {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/web/server.py +0 -0
- {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/web/static/css/app.css +0 -0
- {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/web/static/index.html +0 -0
- {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/web/static/js/api.js +0 -0
- {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/web/static/js/app.js +0 -0
- {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/web/static/js/charts.js +0 -0
- {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/web/static/js/demo.js +0 -0
- {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg/web/static/js/views.js +0 -0
- {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg.egg-info/SOURCES.txt +0 -0
- {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg.egg-info/dependency_links.txt +0 -0
- {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg.egg-info/entry_points.txt +0 -0
- {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg.egg-info/requires.txt +0 -0
- {ipmg-3.1.2 → ipmg-3.1.4}/src/ipmg.egg-info/top_level.txt +0 -0
- {ipmg-3.1.2 → ipmg-3.1.4}/tests/test_api_contract.py +0 -0
- {ipmg-3.1.2 → ipmg-3.1.4}/tests/test_commands.py +0 -0
- {ipmg-3.1.2 → ipmg-3.1.4}/tests/test_config.py +0 -0
- {ipmg-3.1.2 → ipmg-3.1.4}/tests/test_database_history.py +0 -0
- {ipmg-3.1.2 → ipmg-3.1.4}/tests/test_diff.py +0 -0
- {ipmg-3.1.2 → ipmg-3.1.4}/tests/test_diff_report.py +0 -0
- {ipmg-3.1.2 → ipmg-3.1.4}/tests/test_discover.py +0 -0
- {ipmg-3.1.2 → ipmg-3.1.4}/tests/test_engine.py +0 -0
- {ipmg-3.1.2 → ipmg-3.1.4}/tests/test_health.py +0 -0
- {ipmg-3.1.2 → ipmg-3.1.4}/tests/test_history_service.py +0 -0
- {ipmg-3.1.2 → ipmg-3.1.4}/tests/test_incremental.py +0 -0
- {ipmg-3.1.2 → ipmg-3.1.4}/tests/test_ipv6.py +0 -0
- {ipmg-3.1.2 → ipmg-3.1.4}/tests/test_live.py +0 -0
- {ipmg-3.1.2 → ipmg-3.1.4}/tests/test_machine_output.py +0 -0
- {ipmg-3.1.2 → ipmg-3.1.4}/tests/test_metrics.py +0 -0
- {ipmg-3.1.2 → ipmg-3.1.4}/tests/test_parser.py +0 -0
- {ipmg-3.1.2 → ipmg-3.1.4}/tests/test_ping.py +0 -0
- {ipmg-3.1.2 → ipmg-3.1.4}/tests/test_ping_command.py +0 -0
- {ipmg-3.1.2 → ipmg-3.1.4}/tests/test_portscan.py +0 -0
- {ipmg-3.1.2 → ipmg-3.1.4}/tests/test_scan_service.py +0 -0
- {ipmg-3.1.2 → ipmg-3.1.4}/tests/test_ui.py +0 -0
- {ipmg-3.1.2 → ipmg-3.1.4}/tests/test_utils.py +0 -0
- {ipmg-3.1.2 → ipmg-3.1.4}/tests/test_web_api.py +0 -0
- {ipmg-3.1.2 → ipmg-3.1.4}/tests/test_web_db.py +0 -0
- {ipmg-3.1.2 → ipmg-3.1.4}/tests/test_web_manager.py +0 -0
- {ipmg-3.1.2 → ipmg-3.1.4}/tests/test_web_server.py +0 -0
|
@@ -13,7 +13,7 @@ build-backend = "setuptools.build_meta"
|
|
|
13
13
|
|
|
14
14
|
[project]
|
|
15
15
|
name = "ipmg"
|
|
16
|
-
version = "3.1.
|
|
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"
|
|
@@ -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
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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
|
-
|
|
434
|
-
for
|
|
435
|
-
|
|
436
|
-
|
|
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,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(
|
|
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
|
|
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
|