JhHardwareWRS_BackPoint/tests/test_usage_stats.py

904 lines
28 KiB
Python

from datetime import date, datetime
from io import BytesIO
from types import SimpleNamespace
import json
from openpyxl import load_workbook
from sqlalchemy import BigInteger, create_engine
from sqlalchemy.ext.compiler import compiles
from sqlalchemy.orm import sessionmaker
from app.database import Base
from app.models import (
ProductionReport,
ProductionReportAllocation,
ProductionReportItem,
ReportStatus,
)
from app.routers import usage_stats as usage_stats_router
from app.routers.usage_stats import (
_build_usage_stats_export_response,
_filter_usage_rows,
_matches_target,
_sort_usage_rows,
_to_detail_out,
_to_schema_row,
)
from app.schemas import UsageStatsDetailOut, UsageStatsRow
from app.services import usage_stats as usage_stats_service
from app.services.usage_stats import UsageStatRow
from app.services.usage_stats import build_usage_stats, split_cleaning_device_nos
from app.services.usage_stats_export import export_usage_stats_rows
@compiles(BigInteger, "sqlite")
def _compile_big_integer_for_sqlite(type_, compiler, **kw):
return "INTEGER"
def _sqlite_db():
engine = create_engine("sqlite+pysqlite:///:memory:", future=True)
Base.metadata.create_all(engine)
SessionLocal = sessionmaker(bind=engine, future=True)
return SessionLocal()
def _report(*items):
return SimpleNamespace(items=list(items))
def _item(**overrides):
values = {
"attendance_point_name": "嘉恒",
"product_name": "产品A",
"process_name": "1",
"stamping_method": "普通",
"operator_count": 1,
"device_no": "28#",
"allocated_minutes": 0,
"good_qty": 0,
}
values.update(overrides)
return SimpleNamespace(**values)
def _db_report(
*,
id: int,
attendance_point_name: str = "嘉恒",
status: ReportStatus = ReportStatus.approved,
is_voided: bool = False,
) -> ProductionReport:
return ProductionReport(
id=id,
session_id=id,
attendance_point_name=attendance_point_name,
employee_phone="13800000000",
report_date=date(2026, 7, 24),
start_at=datetime(2026, 7, 24, 20, 0),
end_at=datetime(2026, 7, 25, 2, 0),
duration_minutes=360,
break_minutes=0,
effective_minutes=360,
total_good_qty=100,
total_output_qty=100,
actual_beat=1,
standard_beat=1,
expected_workload=0,
pace_rate=0,
workload_rate=0,
status=status,
is_voided=is_voided,
submitted_at=datetime(2026, 7, 25, 2, 1),
)
def _db_item(
*,
id: int,
report_id: int,
attendance_point_name: str = "嘉恒",
device_no: str = "28#",
) -> ProductionReportItem:
return ProductionReportItem(
id=id,
report_id=report_id,
attendance_point_name=attendance_point_name,
device_no=device_no,
project_no="P1",
product_name="产品A",
process_name="1",
stamping_method="普通",
operator_count=1,
process_unit_price_yuan=1,
standard_beat=1,
standard_workload=0,
good_qty=100,
defect_qty=0,
scrap_qty=0,
allocated_minutes=120,
)
def _db_allocation(
*,
id: int,
report_id: int,
report_item_id: int,
attendance_point_name: str = "嘉恒",
allocation_date: date = date(2026, 7, 25),
effective_minutes: float = 120,
good_qty: float = 100,
) -> ProductionReportAllocation:
return ProductionReportAllocation(
id=id,
report_id=report_id,
report_item_id=report_item_id,
attendance_point_name=attendance_point_name,
employee_phone="13800000000",
allocation_date=allocation_date,
day_minutes=effective_minutes,
overtime_minutes=0,
night_minutes=0,
effective_minutes=effective_minutes,
good_qty=good_qty,
defect_qty=0,
scrap_qty=0,
changeover_count=0,
reference_wage=0,
)
def test_split_cleaning_device_nos_supports_chinese_and_ascii_commas():
assert split_cleaning_device_nos("清洗机A、清洗机B, 清洗机C") == ["清洗机A", "清洗机B", "清洗机C"]
assert split_cleaning_device_nos(None) == []
assert split_cleaning_device_nos("") == []
def test_split_cleaning_device_nos_deduplicates_in_order():
assert split_cleaning_device_nos("A、A、B") == ["A", "B"]
def test_build_usage_stats_for_stamping_device_uses_allocated_minutes():
rows = build_usage_stats(
reports=[_report(_item(device_no="28#", allocated_minutes=75, good_qty=12))],
category="device",
equipment_type_by_key={("嘉恒", "28#"): "冲压设备"},
)
assert len(rows) == 1
row = rows[0]
assert row.name == "28#"
assert row.metric_kind == "minutes"
assert row.value == 75
assert row.report_count == 1
assert row.object_type == "冲压设备"
assert row.id
assert "device" in row.id
assert "嘉恒" in row.id
assert "28#" in row.id
assert "minutes" in row.id
def test_build_usage_stats_for_cleaning_device_splits_quantity_evenly():
rows = build_usage_stats(
reports=[
_report(
_item(
device_no="清洗机A、清洗机B",
stamping_method="清洗",
good_qty=100,
allocated_minutes=0,
)
)
],
category="device",
equipment_type_by_key={("嘉恒", "清洗机A"): "清洗设备", ("嘉恒", "清洗机B"): "清洗设备"},
)
assert [(row.name, row.metric_kind, row.value, row.report_count) for row in rows] == [
("清洗机A", "quantity", 50, 1),
("清洗机B", "quantity", 50, 1),
]
assert [row.object_type for row in rows] == ["清洗设备", "清洗设备"]
def test_build_usage_stats_for_cleaning_device_deduplicates_before_splitting_quantity():
rows = build_usage_stats(
reports=[
_report(
_item(
device_no="清洗机A、清洗机A、清洗机B",
stamping_method="清洗",
good_qty=100,
)
)
],
category="device",
equipment_type_by_key={},
)
assert [(row.name, row.value, row.report_count) for row in rows] == [
("清洗机A", 50, 1),
("清洗机B", 50, 1),
]
def test_build_usage_stats_for_device_skips_misc_items():
rows = build_usage_stats(
reports=[
_report(
_item(
product_name="处理杂活",
process_name="杂活",
stamping_method="处理杂活",
device_no="28#",
allocated_minutes=30,
)
)
],
category="device",
equipment_type_by_key={("嘉恒", "28#"): "冲压设备"},
)
assert rows == []
def test_build_usage_stats_for_non_cleaning_device_ignores_cleaning_equipment_type():
rows = build_usage_stats(
reports=[_report(_item(device_no="清洗机A", allocated_minutes=75, good_qty=12))],
category="device",
equipment_type_by_key={("嘉恒", "清洗机A"): "清洗设备"},
)
assert len(rows) == 1
assert rows[0].name == "清洗机A"
assert rows[0].metric_kind == "minutes"
assert rows[0].value == 75
assert rows[0].object_type == "清洗设备"
def test_build_usage_stats_for_cleaning_device_marks_cleaning_tag():
rows = build_usage_stats(
reports=[
_report(
_item(
device_no="清洗机A",
stamping_method="清洗",
good_qty=100,
allocated_minutes=0,
)
)
],
category="device",
equipment_type_by_key={("嘉恒", "清洗机A"): "清洗设备"},
)
assert len(rows) == 1
assert "清洗" in rows[0].tags
assert "清洗设备" not in rows[0].tags
assert rows[0].object_type == "清洗设备"
def test_build_usage_stats_for_mold_merges_by_point_product_process_and_stamping_method():
rows = build_usage_stats(
reports=[
_report(
_item(stamping_method="连续模", allocated_minutes=30),
_item(stamping_method="连续模", allocated_minutes=45),
)
],
category="mold",
equipment_type_by_key={},
)
assert len(rows) == 1
row = rows[0]
assert row.attendance_point_name == "嘉恒"
assert row.product_name == "产品A"
assert row.process_name == "1"
assert row.stamping_method == "连续模"
assert row.metric_kind == "minutes"
assert row.value == 75
assert row.report_count == 2
assert row.object_type == "连续模"
assert "连续模" in row.tags
def test_build_usage_stats_for_mold_ids_distinguish_point_and_process():
rows = build_usage_stats(
reports=[
_report(
_item(attendance_point_name="嘉恒", process_name="1", allocated_minutes=30),
_item(attendance_point_name="二厂", process_name="1", allocated_minutes=40),
_item(attendance_point_name="嘉恒", process_name="2", allocated_minutes=50),
)
],
category="mold",
equipment_type_by_key={},
)
ids = {row.id for row in rows}
assert len(rows) == 3
assert len(ids) == 3
def test_build_usage_stats_preserves_numeric_zero_identity_fields():
rows = build_usage_stats(
reports=[_report(_item(product_name=0, process_name=0, allocated_minutes=10))],
category="mold",
equipment_type_by_key={},
)
assert len(rows) == 1
assert rows[0].product_name == "0"
assert rows[0].process_name == "0"
assert json.loads(rows[0].id)[3] == "0"
assert json.loads(rows[0].id)[4] == "0"
def test_build_usage_stats_uses_structured_ids_when_fields_contain_delimiter():
rows = build_usage_stats(
reports=[
_report(
_item(attendance_point_name="A||B", product_name="C", process_name="1", allocated_minutes=10),
_item(attendance_point_name="A", product_name="B||C", process_name="1", allocated_minutes=20),
)
],
category="mold",
equipment_type_by_key={},
)
ids = {row.id for row in rows}
assert len(ids) == 2
assert all(json.loads(row_id) for row_id in ids)
assert {tuple(json.loads(row_id)[:5]) for row_id in ids} == {
("mold", "A||B", "A||B / C / 1 / 普通", "C", "1"),
("mold", "A", "A / B||C / 1 / 普通", "B||C", "1"),
}
def test_build_usage_stats_for_cleaning_mold_uses_good_quantity():
rows = build_usage_stats(
reports=[_report(_item(stamping_method="清洗", allocated_minutes=80, good_qty=64))],
category="mold",
equipment_type_by_key={},
)
assert len(rows) == 1
assert rows[0].metric_kind == "quantity"
assert rows[0].value == 64
assert rows[0].object_type == "清洗模具"
assert "清洗" in rows[0].tags
def test_build_usage_stats_for_mold_skips_misc_and_marks_multi_person():
rows = build_usage_stats(
reports=[
_report(
_item(
product_name="处理杂活",
process_name="杂活",
stamping_method="处理杂活",
allocated_minutes=30,
),
_item(operator_count=2, allocated_minutes=40),
)
],
category="mold",
equipment_type_by_key={},
)
assert len(rows) == 1
assert rows[0].product_name == "产品A"
assert rows[0].value == 40
assert rows[0].object_type == "多人工序"
assert "多人协作" in rows[0].tags
def test_build_usage_stats_for_non_cleaning_device_defaults_object_type_to_stamping_device():
rows = build_usage_stats(
reports=[_report(_item(device_no="29#", allocated_minutes=25))],
category="device",
equipment_type_by_key={},
)
assert len(rows) == 1
assert rows[0].object_type == "冲压设备"
def test_build_usage_stats_for_cleaning_device_defaults_object_type_to_cleaning_device():
rows = build_usage_stats(
reports=[
_report(
_item(
device_no="清洗机Z",
stamping_method="清洗",
good_qty=10,
)
)
],
category="device",
equipment_type_by_key={},
)
assert len(rows) == 1
assert rows[0].object_type == "清洗设备"
def test_usage_stats_dedupes_report_count_for_split_allocations():
allocation_a = SimpleNamespace(
report_id=1,
effective_minutes=350,
good_qty=350,
item=_item(device_no="28#", allocated_minutes=0, good_qty=0),
)
allocation_b = SimpleNamespace(
report_id=1,
effective_minutes=300,
good_qty=300,
item=_item(device_no="28#", allocated_minutes=0, good_qty=0),
)
rows = usage_stats_service.build_usage_stats_from_allocations(
allocations=[allocation_a, allocation_b],
category="device",
equipment_type_by_key={("嘉恒", "28#"): "冲压设备"},
)
assert len(rows) == 1
assert rows[0].value == 650
assert rows[0].report_count == 1
def test_filter_usage_rows_searches_all_visible_fields():
rows = [
UsageStatRow(
id="device-1",
category="device",
name="28#",
metric_kind="minutes",
object_type="冲压设备",
value=360,
report_count=2,
attendance_point_name="嘉恒",
),
UsageStatRow(
id="mold-1",
category="mold",
name="旭升 / 产品B / 2 / 连续模",
metric_kind="minutes",
object_type="连续模",
value=45,
report_count=9,
attendance_point_name="旭升",
product_name="产品B",
process_name="2",
stamping_method="连续模",
tags=["连续模", "多人协作"],
),
]
assert _filter_usage_rows(rows, "") == rows
assert [row.id for row in _filter_usage_rows(rows, "嘉恒")] == ["device-1"]
assert [row.id for row in _filter_usage_rows(rows, "冲压")] == ["device-1"]
assert [row.id for row in _filter_usage_rows(rows, "产品B")] == ["mold-1"]
assert [row.id for row in _filter_usage_rows(rows, "多人协作")] == ["mold-1"]
assert [row.id for row in _filter_usage_rows(rows, "360")] == ["device-1"]
assert [row.id for row in _filter_usage_rows(rows, "9")] == ["mold-1"]
def test_export_usage_stats_rows_writes_device_and_mold_sheets():
device_rows = [
UsageStatRow(
id="device-minutes",
category="device",
name="28#",
metric_kind="minutes",
object_type="冲压设备",
value=360,
report_count=2,
attendance_point_name="嘉恒",
),
UsageStatRow(
id="device-quantity",
category="device",
name="清洗机A",
metric_kind="quantity",
object_type="清洗设备",
value=30,
report_count=1,
attendance_point_name="嘉恒",
),
]
mold_rows = [
UsageStatRow(
id="mold-minutes",
category="mold",
name="嘉恒 / 产品A / 1 / 普通",
metric_kind="minutes",
object_type="普通模具",
value=45,
report_count=3,
attendance_point_name="嘉恒",
product_name="产品A",
process_name="1",
stamping_method="普通",
),
UsageStatRow(
id="mold-quantity",
category="mold",
name="嘉恒 / 产品B / 2 / 清洗",
metric_kind="quantity",
object_type="清洗模具",
value=18,
report_count=1,
attendance_point_name="嘉恒",
product_name="产品B",
process_name="2",
stamping_method="清洗",
),
]
workbook = load_workbook(BytesIO(export_usage_stats_rows(device_rows, mold_rows)))
assert workbook.sheetnames == ["设备统计", "模具统计"]
device_sheet = workbook["设备统计"]
mold_sheet = workbook["模具统计"]
assert [cell.value for cell in device_sheet[1]] == ["考勤点", "设备号", "设备类型", "使用时长(分钟)", "使用时长(小时)", "使用时长(天)", "清洗数量", "报工次数"]
assert [cell.value for cell in mold_sheet[1]] == ["考勤点", "产品名称", "工序", "冲压方式", "使用时长(分钟)", "使用时长(小时)", "使用时长(天)", "清洗数量", "报工次数"]
assert [cell.value for cell in device_sheet[2]] == ["嘉恒", "28#", "冲压设备", 360, 6, 0.25, 0, 2]
assert [cell.value for cell in device_sheet[3]] == ["嘉恒", "清洗机A", "清洗设备", 0, 0, 0, 30, 1]
assert [cell.value for cell in mold_sheet[2]] == ["嘉恒", "产品A", "1", "普通", 45, 0.75, 0.03, 0, 3]
assert [cell.value for cell in mold_sheet[3]] == ["嘉恒", "产品B", "2", "清洗", 0, 0, 0, 18, 1]
def test_build_usage_stats_export_response_returns_xlsx_attachment():
device_rows = [
UsageStatRow(
id="device-minutes",
category="device",
name="28#",
metric_kind="minutes",
object_type="冲压设备",
value=75,
report_count=2,
attendance_point_name="嘉恒",
)
]
mold_rows = [
UsageStatRow(
id="mold-minutes",
category="mold",
name="嘉恒 / 产品A / 1 / 普通",
metric_kind="minutes",
object_type="普通模具",
value=45,
report_count=3,
attendance_point_name="嘉恒",
product_name="产品A",
process_name="1",
stamping_method="普通",
)
]
response = _build_usage_stats_export_response(device_rows, mold_rows)
assert response.media_type == "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
assert "usage-stats.xlsx" in response.headers["Content-Disposition"]
workbook = load_workbook(BytesIO(response.body))
assert workbook.sheetnames == ["设备统计", "模具统计"]
def test_sort_usage_rows_by_value_desc_then_report_count_desc_then_name():
rows = [
UsageStatRow(id="b", category="device", name="B", metric_kind="minutes", object_type="冲压设备", value=10, report_count=1),
UsageStatRow(id="c", category="device", name="C", metric_kind="minutes", object_type="冲压设备", value=20, report_count=1),
UsageStatRow(id="a", category="device", name="A", metric_kind="minutes", object_type="冲压设备", value=20, report_count=3),
]
sorted_rows = _sort_usage_rows(rows, "value")
assert [row.name for row in sorted_rows] == ["A", "C", "B"]
def test_sort_usage_rows_by_count_desc_then_value_desc_then_name():
rows = [
UsageStatRow(id="b", category="device", name="B", metric_kind="minutes", object_type="冲压设备", value=20, report_count=2),
UsageStatRow(id="c", category="device", name="C", metric_kind="minutes", object_type="冲压设备", value=30, report_count=1),
UsageStatRow(id="a", category="device", name="A", metric_kind="minutes", object_type="冲压设备", value=30, report_count=2),
]
sorted_rows = _sort_usage_rows(rows, "count")
assert [row.name for row in sorted_rows] == ["A", "B", "C"]
def test_to_schema_row_maps_usage_stat_fields():
row = UsageStatRow(
id="row-1",
category="mold",
name="嘉恒 / 产品A / 1 / 普通",
metric_kind="minutes",
object_type="普通模具",
value=12.34,
report_count=5,
tags=["多人协作"],
attendance_point_name="嘉恒",
product_name="产品A",
process_name="1",
stamping_method="普通",
)
schema_row = _to_schema_row(row)
assert isinstance(schema_row, UsageStatsRow)
assert schema_row.model_dump() == {
"id": "row-1",
"category": "mold",
"name": "嘉恒 / 产品A / 1 / 普通",
"metric_kind": "minutes",
"object_type": "普通模具",
"value": 12.34,
"report_count": 5,
"tags": ["多人协作"],
"attendance_point_name": "嘉恒",
"product_name": "产品A",
"process_name": "1",
"stamping_method": "普通",
}
def test_to_detail_out_maps_target_identity_fields():
row = UsageStatRow(
id="target-1",
category="mold",
name="嘉恒 / 产品A / 1 / 连续模",
metric_kind="minutes",
object_type="连续模",
value=88,
report_count=4,
tags=["连续模"],
attendance_point_name="嘉恒",
product_name="产品A",
process_name="1",
stamping_method="连续模",
)
detail = _to_detail_out(row)
assert isinstance(detail, UsageStatsDetailOut)
assert detail.model_dump() == {
"id": "target-1",
"category": "mold",
"attendance_point_name": "嘉恒",
"name": "嘉恒 / 产品A / 1 / 连续模",
"object_type": "连续模",
"metric_kind": "minutes",
"value": 88,
"report_count": 4,
"product_name": "产品A",
"process_name": "1",
"stamping_method": "连续模",
"tags": ["连续模"],
"daily_rows": [],
"report_rows": [],
}
def test_usage_stats_detail_out_identity_fields_have_empty_defaults():
detail = UsageStatsDetailOut(category="device")
assert detail.id == ""
assert detail.product_name == ""
assert detail.process_name == ""
assert detail.stamping_method == ""
assert detail.tags == []
def test_matches_target_allows_accessible_point_when_attendance_point_name_is_empty():
row = UsageStatRow(
id="row-1",
category="device",
name="28#",
metric_kind="minutes",
object_type="冲压设备",
attendance_point_name="嘉恒",
)
assert _matches_target(
row,
id="",
category="device",
attendance_point_name="",
name="28#",
product_name="",
process_name="",
stamping_method="",
metric_kind="",
)
def test_matches_target_uses_metric_kind_to_disambiguate_same_device_name():
minutes_row = UsageStatRow(
id="minutes-id",
category="device",
name="清洗机A",
metric_kind="minutes",
object_type="清洗设备",
attendance_point_name="嘉恒",
)
quantity_row = UsageStatRow(
id="quantity-id",
category="device",
name="清洗机A",
metric_kind="quantity",
object_type="清洗设备",
attendance_point_name="嘉恒",
)
target_args = {
"id": "",
"category": "device",
"attendance_point_name": "",
"name": "清洗机A",
"product_name": "",
"process_name": "",
"stamping_method": "",
"metric_kind": "quantity",
}
assert not _matches_target(minutes_row, **target_args)
assert _matches_target(quantity_row, **target_args)
def test_matches_target_uses_id_before_ambiguous_device_fields():
minutes_row = UsageStatRow(
id="minutes-id",
category="device",
name="清洗机A",
metric_kind="minutes",
object_type="清洗设备",
attendance_point_name="嘉恒",
)
quantity_row = UsageStatRow(
id="quantity-id",
category="device",
name="清洗机A",
metric_kind="quantity",
object_type="清洗设备",
attendance_point_name="嘉恒",
)
target_args = {
"id": "minutes-id",
"category": "device",
"attendance_point_name": "",
"name": "清洗机A",
"product_name": "",
"process_name": "",
"stamping_method": "",
"metric_kind": "quantity",
}
assert _matches_target(minutes_row, **target_args)
assert not _matches_target(quantity_row, **target_args)
def test_usage_stats_detail_rows_use_allocation_date_and_dedupe_daily_report_count():
report = SimpleNamespace(
id=7,
report_date=date(2026, 7, 24),
employee_phone="13800000000",
employee=SimpleNamespace(name="张三"),
)
item = _item(device_no="28#", allocated_minutes=0, good_qty=0)
allocations = [
SimpleNamespace(
report_id=7,
allocation_date=date(2026, 7, 25),
effective_minutes=350,
good_qty=350,
report=report,
item=item,
),
SimpleNamespace(
report_id=7,
allocation_date=date(2026, 7, 25),
effective_minutes=300,
good_qty=300,
report=report,
item=item,
),
]
detail = usage_stats_router._build_usage_stats_detail_from_allocations(
allocations=allocations,
category="device",
target_args={
"id": "",
"category": "device",
"attendance_point_name": "",
"name": "28#",
"product_name": "",
"process_name": "",
"stamping_method": "",
"metric_kind": "minutes",
},
equipment_type_by_key={("嘉恒", "28#"): "冲压设备"},
)
assert detail.value == 650
assert detail.report_count == 1
assert [(row.report_date, row.value, row.report_count) for row in detail.daily_rows] == [
(date(2026, 7, 25), 650, 1)
]
assert [
(row.report_id, row.report_date, row.value, row.report_count)
for row in detail.report_rows
] == [(7, date(2026, 7, 25), 650, 1)]
def test_approved_allocations_query_requires_approved_unvoided_matching_point_and_item_report():
db = _sqlite_db()
try:
db.add_all(
[
_db_report(id=1),
_db_item(id=101, report_id=1),
_db_allocation(id=1001, report_id=1, report_item_id=101),
_db_report(id=2, status=ReportStatus.pending),
_db_item(id=102, report_id=2),
_db_allocation(id=1002, report_id=2, report_item_id=102),
_db_report(id=3, is_voided=True),
_db_item(id=103, report_id=3),
_db_allocation(id=1003, report_id=3, report_item_id=103),
_db_report(id=4, attendance_point_name="二厂"),
_db_item(id=104, report_id=4, attendance_point_name="二厂"),
_db_allocation(
id=1004,
report_id=4,
report_item_id=104,
attendance_point_name="二厂",
),
_db_report(id=5),
_db_item(id=105, report_id=5),
_db_allocation(
id=1005,
report_id=5,
report_item_id=105,
attendance_point_name="二厂",
),
_db_allocation(id=1006, report_id=1, report_item_id=102),
_db_allocation(
id=1007,
report_id=1,
report_item_id=101,
allocation_date=date(2026, 7, 26),
effective_minutes=999,
good_qty=999,
),
]
)
db.commit()
allocations = db.scalars(
usage_stats_router._approved_allocations_query(
["嘉恒"],
date(2026, 7, 25),
date(2026, 7, 25),
)
).all()
assert [allocation.id for allocation in allocations] == [1001]
rows = usage_stats_service.build_usage_stats_from_allocations(
allocations=allocations,
category="device",
equipment_type_by_key={("嘉恒", "28#"): "冲压设备"},
)
assert len(rows) == 1
assert rows[0].value == 120
assert rows[0].report_count == 1
finally:
db.close()