672 lines
22 KiB
Python
672 lines
22 KiB
Python
#############################################################################
|
||
|
||
# Author: Cerys
|
||
# Update: 2026-03-06
|
||
|
||
#############################################################################
|
||
|
||
import sqlite3
|
||
import pymysql
|
||
import copy
|
||
import functools
|
||
import re
|
||
from pymysql.constants import CLIENT
|
||
from pymysql.cursors import DictCursor
|
||
from typing import List, Dict, Any, Optional, Union
|
||
from contextlib import contextmanager
|
||
|
||
# ================= 统一执行装饰器 =================
|
||
def db_execute_wrapper(func):
|
||
"""
|
||
统一执行装饰器:
|
||
1. 负责自动获取/检查连接
|
||
2. 自动处理非事务状态下的 commit 和 rollback
|
||
3. 针对 MySQL 处理 Gone Away 断连重连重试机制
|
||
"""
|
||
@functools.wraps(func)
|
||
def wrapper(self, *args, **kwargs):
|
||
self.__connect__()
|
||
try:
|
||
res = func(self, *args, **kwargs)
|
||
return res
|
||
except Exception as e:
|
||
# 针对 MySQL 的长连接断开 (OperationalError) 尝试进行一次重试
|
||
if getattr(self, '_is_mysql', False) and not self._in_transaction:
|
||
if isinstance(e, pymysql.err.OperationalError):
|
||
try:
|
||
# 【修复】废弃 reconnect=True,直接销毁并重新初始化连接
|
||
self.conn = None
|
||
self.__connect__()
|
||
|
||
res = func(self, *args, **kwargs)
|
||
return res
|
||
except Exception:
|
||
if self.conn:
|
||
self.conn.rollback()
|
||
raise # 【修复】直接 raise,保留完整的错误堆栈 Traceback
|
||
|
||
# 常规错误回滚
|
||
if getattr(self, 'conn', None) and not getattr(self, '_in_transaction', False):
|
||
self.conn.rollback()
|
||
raise # 【修复】直接 raise,保留完整的错误堆栈 Traceback
|
||
return wrapper
|
||
# =================================================
|
||
|
||
class BaseDriver:
|
||
"""
|
||
基类:负责定义接口类型,让 VS Code 能识别返回值
|
||
"""
|
||
def __init__(self):
|
||
self.conn = None
|
||
# 子类需设置这两个属性
|
||
self.param_mark = '?'
|
||
self._is_mysql = False
|
||
self._transaction_depth = 0
|
||
|
||
# -------------- 核心执行方法 (清晰直白) --------------
|
||
|
||
@db_execute_wrapper
|
||
def fetch_all(self, sql: str, params: tuple = ()) -> List[dict]:
|
||
"""查询多条:返回字典列表"""
|
||
cursor = self.conn.cursor()
|
||
try:
|
||
cursor.execute(sql, params)
|
||
rows = cursor.fetchall()
|
||
if not rows:
|
||
return []
|
||
# 兼容性处理:如果是对象(SQLite Row)则转dict,如果是dict(MySQL)则直接用
|
||
return [dict(row) for row in rows] if rows else []
|
||
finally:
|
||
cursor.close()
|
||
|
||
@db_execute_wrapper
|
||
def fetch_one(self, sql: str, params: tuple = ()) -> Optional[dict]:
|
||
"""查询单条:返回字典 或 None"""
|
||
cursor = self.conn.cursor()
|
||
try:
|
||
cursor.execute(sql, params)
|
||
row = cursor.fetchone()
|
||
return dict(row) if row else None
|
||
finally:
|
||
cursor.close()
|
||
|
||
@db_execute_wrapper
|
||
def execute(self, sql: str, params: tuple = ()) -> int:
|
||
"""增删改:返回影响行数"""
|
||
cursor = self.conn.cursor()
|
||
try:
|
||
cursor.execute(sql, params)
|
||
if not self._in_transaction:
|
||
self.conn.commit()
|
||
return cursor.rowcount
|
||
finally:
|
||
cursor.close()
|
||
|
||
@db_execute_wrapper
|
||
def execute_insert(self, sql: str, params: tuple = ()) -> int:
|
||
"""插入:返回自增ID"""
|
||
cursor = self.conn.cursor()
|
||
try:
|
||
cursor.execute(sql, params)
|
||
if not self._in_transaction:
|
||
self.conn.commit()
|
||
return cursor.lastrowid
|
||
finally:
|
||
cursor.close()
|
||
|
||
@db_execute_wrapper
|
||
def execute_many(self, sql: str, params_list: List[tuple]) -> int:
|
||
"""批量执行:返回影响行数"""
|
||
cursor = self.conn.cursor()
|
||
try:
|
||
cursor.executemany(sql, params_list)
|
||
if not self._in_transaction:
|
||
self.conn.commit()
|
||
return cursor.rowcount
|
||
finally:
|
||
cursor.close()
|
||
|
||
@db_execute_wrapper
|
||
def execute_raw(self, sql: str, params: tuple = ()) -> Union[List[dict], int]:
|
||
"""混合执行:根据是否返回结果集自动判断"""
|
||
cursor = self.conn.cursor()
|
||
try:
|
||
cursor.execute(sql, params)
|
||
# 如果有 description 说明是 SELECT 类查询
|
||
if cursor.description:
|
||
rows = cursor.fetchall()
|
||
return [dict(row) for row in rows] if rows else []
|
||
else:
|
||
if not self._in_transaction:
|
||
self.conn.commit()
|
||
return cursor.rowcount
|
||
finally:
|
||
cursor.close()
|
||
|
||
@db_execute_wrapper
|
||
def execute_script(self, sql_script: str) -> None:
|
||
"""简单的脚本执行,不支持复杂的存储过程分隔符"""
|
||
self.__connect__()
|
||
cursor = self.conn.cursor()
|
||
try:
|
||
# 开启多语句支持通常需要在 connect 时指定 client_flag
|
||
# 或者手动分割
|
||
statements = [s.strip() for s in sql_script.split(';') if s.strip()]
|
||
for sql in statements:
|
||
cursor.execute(sql)
|
||
if not self._in_transaction:
|
||
self.conn.commit()
|
||
finally:
|
||
cursor.close()
|
||
|
||
class MysqlDriver(BaseDriver):
|
||
def __init__(self, host, port, user, password, database, charset='utf8mb4'):
|
||
super().__init__()
|
||
self.param_mark = '%s' # MySQL 占位符
|
||
self.quote_mark = '`'
|
||
self.config = {
|
||
'host': host,
|
||
'port': port,
|
||
'user': user,
|
||
'password': password,
|
||
'database': database,
|
||
'charset': charset,
|
||
'autocommit': False,
|
||
'cursorclass': DictCursor,
|
||
'client_flag': CLIENT.MULTI_STATEMENTS
|
||
}
|
||
self._is_mysql = True
|
||
self.__connect__()
|
||
|
||
@property
|
||
def _in_transaction(self) -> bool:
|
||
"""判断当前是否处于事务中"""
|
||
return self._transaction_depth > 0
|
||
|
||
def __connect__(self) -> None:
|
||
if self.conn is None:
|
||
self.conn = pymysql.connect(**self.config)
|
||
else:
|
||
try:
|
||
# 【修复】去除已在 PyMySQL 新版废弃的 reconnect=True 参数
|
||
self.conn.ping(reconnect=False)
|
||
except Exception:
|
||
self.conn = pymysql.connect(**self.config)
|
||
|
||
# =============== 事务控制方法 ===============
|
||
def begin(self) -> None:
|
||
self.__connect__()
|
||
if self._transaction_depth == 0:
|
||
self.conn.begin()
|
||
self._transaction_depth += 1
|
||
|
||
def commit(self) -> None:
|
||
if self._transaction_depth > 0:
|
||
self._transaction_depth -= 1
|
||
if self._transaction_depth == 0 and self.conn:
|
||
self.conn.commit()
|
||
|
||
def rollback(self) -> None:
|
||
# 安全清理嵌套层级,防止局部回滚后引发后续误提交
|
||
if self._transaction_depth > 0:
|
||
self._transaction_depth = 0
|
||
if self.conn:
|
||
self.conn.rollback()
|
||
# ============================================
|
||
|
||
def close(self) -> None:
|
||
if self.conn:
|
||
self.conn.close()
|
||
self.conn = None
|
||
|
||
|
||
class SqliteDriver(BaseDriver):
|
||
def __init__(self, db_path: str):
|
||
super().__init__()
|
||
self.param_mark = '?' # SQLite 占位符
|
||
self.quote_mark = '"'
|
||
self.db_path = db_path
|
||
self._is_mysql = False
|
||
self.__connect__()
|
||
|
||
@property
|
||
def _in_transaction(self) -> bool:
|
||
return self._transaction_depth > 0
|
||
|
||
def __connect__(self) -> None:
|
||
if not self.conn:
|
||
self.conn = sqlite3.connect(self.db_path, check_same_thread=False, timeout=10.0)
|
||
self.conn.row_factory = sqlite3.Row
|
||
self.conn.execute('PRAGMA journal_mode=WAL;')
|
||
self.conn.execute('PRAGMA synchronous=NORMAL;')
|
||
|
||
@db_execute_wrapper
|
||
def execute_script(self, sql_script: str) -> None:
|
||
"""SQLite 原生支持脚本执行,更安全且无需手动分割"""
|
||
# 注意:sqlite3.Cursor.executescript 不遵循 commit 逻辑,它会直接提交
|
||
# 所以这里不需要 conn.commit(),但为了保持一致性逻辑,还是走装饰器
|
||
cursor = self.conn.cursor()
|
||
try:
|
||
cursor.executescript(sql_script)
|
||
# SQLite executescript 会自动 commit,不需要手动再 commit
|
||
finally:
|
||
cursor.close()
|
||
|
||
# =============== 事务控制方法 ===============
|
||
def begin(self) -> None:
|
||
self.__connect__()
|
||
self._transaction_depth += 1
|
||
|
||
def commit(self) -> None:
|
||
if self._transaction_depth > 0:
|
||
self._transaction_depth -= 1
|
||
if self._transaction_depth == 0 and self.conn:
|
||
self.conn.commit()
|
||
|
||
def rollback(self) -> None:
|
||
if self._transaction_depth > 0:
|
||
self._transaction_depth = 0
|
||
if self.conn:
|
||
self.conn.rollback()
|
||
# ============================================
|
||
|
||
def close(self) -> None:
|
||
if self.conn:
|
||
self.conn.close()
|
||
self.conn = None
|
||
|
||
|
||
class Query:
|
||
"""查询构建器:负责拼装 SQL,且支持链式调用不可变对象"""
|
||
def __init__(self, driver, table_name: str):
|
||
self.driver = driver
|
||
self.table_name = table_name
|
||
self._wheres: List[tuple] = [] # 【修改】存为 tuple: (逻辑符, 语句) 支持 OR
|
||
self._params: List[Any] = []
|
||
self._orders: List[str] = []
|
||
self._joins: List[str] = []
|
||
self._limit: Optional[int] = None
|
||
self._offset: Optional[int] = None
|
||
|
||
self.mark = getattr(driver, 'param_mark', '?')
|
||
self.quote = getattr(driver, 'quote_mark', '')
|
||
|
||
def __copy_instance(self) -> 'Query':
|
||
"""【核心】创建当前对象的深拷贝,用于链式调用不污染原对象"""
|
||
new_query = copy.copy(self)
|
||
new_query._wheres = self._wheres[:]
|
||
new_query._params = self._params[:]
|
||
new_query._orders = self._orders[:]
|
||
new_query._joins = self._joins[:]
|
||
return new_query
|
||
|
||
def _q(self, field: str) -> str:
|
||
field = field.strip()
|
||
if field == '*' or '(' in field or ')' in field:
|
||
return field
|
||
|
||
# 简单的防止注入校验
|
||
if re.search(r'[;\'"\s\-]', field):
|
||
raise ValueError(f"非法字段名: {field}")
|
||
|
||
# 支持 table.column 格式
|
||
if '.' in field:
|
||
table, col = field.split('.', 1)
|
||
return f"{self.quote}{table}{self.quote}.{self.quote}{col}{self.quote}"
|
||
|
||
return f"{self.quote}{field}{self.quote}"
|
||
|
||
# ================= 链式构建方法 (返回新对象) =================
|
||
|
||
def where(self, key: str, value: Any, operator: str = '=') -> 'Query':
|
||
"""
|
||
单条件判断
|
||
|
||
:param key: 字段名
|
||
:param value: 值
|
||
:param operator: 运算符
|
||
|
||
"""
|
||
new_q = self.__copy_instance()
|
||
condition = f"{self._q(key)} {operator} {self.mark}"
|
||
new_q._wheres.append(("AND", condition))
|
||
new_q._params.append(value)
|
||
return new_q
|
||
|
||
def or_where(self, key: str, value: Any, operator: str = '=') -> 'Query':
|
||
"""
|
||
OR 条件判断
|
||
方法同where
|
||
"""
|
||
new_q = self.__copy_instance()
|
||
condition = f"{self._q(key)} {operator} {self.mark}"
|
||
new_q._wheres.append(("OR", condition))
|
||
new_q._params.append(value)
|
||
return new_q
|
||
|
||
def where_all(self, condition: Dict[str, Any], operator: str = '=', logic: str = 'AND') -> 'Query':
|
||
"""
|
||
多条件判断
|
||
|
||
:param condition:param condition: { 字段名: 值 }
|
||
:param operator: 运算符 (传入字典建议保持默认 `=`)
|
||
:param logic: 逻辑运算符内部连接(AND/OR)
|
||
|
||
"""
|
||
new_q = self.__copy_instance()
|
||
clauses = []
|
||
for key, value in condition.items():
|
||
clauses.append(f"{self._q(key)} {operator} {self.mark}")
|
||
new_q._params.append(value)
|
||
|
||
if clauses:
|
||
full_condition = f"({f' {logic.upper()} '.join(clauses)})"
|
||
# 作为一个整体条件加入,默认外部通过 AND 连接
|
||
new_q._wheres.append(("AND", full_condition))
|
||
return new_q
|
||
|
||
def where_in(self, key: str, values: List[Any]) -> 'Query':
|
||
"""
|
||
IN 语句支持
|
||
|
||
:param key: 字段名
|
||
:param values: 匹配值列表
|
||
"""
|
||
if not values:
|
||
raise ValueError("whereIn 的 values 参数不能为空列表")
|
||
new_q = self.__copy_instance()
|
||
placeholders = ", ".join([self.mark] * len(values))
|
||
condition = f"{self._q(key)} IN ({placeholders})"
|
||
new_q._wheres.append(("AND", condition))
|
||
new_q._params.extend(values)
|
||
return new_q
|
||
|
||
def where_raw(self, sql: str, params: tuple = ()) -> 'Query':
|
||
"""
|
||
原生条件语句 (支持复杂逻辑查询)
|
||
注意:调用方需自行通过 params 传递参数以防注入
|
||
|
||
:param sql: SQL 字符串,如 "age > ? AND (status = ? OR role = ?)"
|
||
:param params: 对应占位符的参数元组
|
||
"""
|
||
new_q = self.__copy_instance()
|
||
new_q._wheres.append(("AND", f"({sql})"))
|
||
new_q._params.extend(params)
|
||
return new_q
|
||
|
||
def join(self, table: str, on: str, join_type: str = 'LEFT') -> 'Query':
|
||
"""
|
||
链表查询
|
||
|
||
:param table: 要链接的表名
|
||
:param on: 链接条件
|
||
:param join_type: 链接类型(LEFT/INNER/RIGHT)
|
||
|
||
"""
|
||
new_q = self.__copy_instance()
|
||
# 注意: join 的 on 条件由开发者自己写 raw sql,需自行注意安全
|
||
new_q._joins.append(f"{join_type.upper()} JOIN {self._q(table)} ON {on}")
|
||
return new_q
|
||
|
||
def order(self, field: str, direction: str = 'ASC') -> 'Query':
|
||
"""
|
||
排序
|
||
|
||
:param field: 排序字段
|
||
:param direction: 排序方向(ASC/DESC)
|
||
|
||
"""
|
||
direction = direction.upper()
|
||
if direction not in ('ASC', 'DESC'):
|
||
raise ValueError("排序方向只能是 ASC 或 DESC")
|
||
|
||
new_q = self.__copy_instance()
|
||
# _q() 内部已经加上了正则校验,防止 order by 注入
|
||
new_q._orders.append(f"{self._q(field)} {direction}")
|
||
return new_q
|
||
|
||
def limit(self, limit: int, offset: int = 0) -> 'Query':
|
||
"""
|
||
分页
|
||
|
||
:param limit: 每页数量
|
||
:param offset: 第几页 (从0开始)
|
||
|
||
"""
|
||
new_q = self.__copy_instance()
|
||
new_q._limit = int(limit)
|
||
new_q._offset = int(offset)
|
||
return new_q
|
||
|
||
# ================= 终结方法 (执行并返回结果) =================
|
||
|
||
def select(self, fields: str = '*') -> List[dict]:
|
||
"""
|
||
执行查询返回列表
|
||
|
||
:param fields: 查询字段
|
||
|
||
"""
|
||
sql = self._build_select_sql(fields)
|
||
return self.driver.fetch_all(sql, tuple(self._params))
|
||
|
||
def find(self, fields: str = '*') -> Optional[dict]:
|
||
"""
|
||
执行查询返回单条
|
||
|
||
:param fields: 查询字段
|
||
|
||
"""
|
||
temp_q = self.__copy_instance()
|
||
temp_q._limit = 1
|
||
temp_q._offset = 0
|
||
|
||
sql = temp_q._build_select_sql(fields)
|
||
return self.driver.fetch_one(sql, tuple(temp_q._params))
|
||
|
||
def count(self) -> int:
|
||
"""统计数量"""
|
||
sql = self._build_select_sql("COUNT(*) AS cnt")
|
||
res = self.driver.fetch_one(sql, tuple(self._params))
|
||
return res.get('cnt', 0) if res else 0
|
||
|
||
def insert(self, data: Dict[str, Any], replace: bool = False) -> int:
|
||
"""
|
||
插入单条数据
|
||
|
||
:param data: { 字段名: 值 }
|
||
:param replace: 是否使用 REPLACE INTO(存在则覆盖)
|
||
:return: 影响行数/自增主键ID
|
||
|
||
"""
|
||
keys = [self._q(k) for k in data.keys()]
|
||
values = list(data.values())
|
||
placeholders = ",".join([self.mark] * len(values))
|
||
columns = ",".join(keys)
|
||
|
||
action = "REPLACE" if replace else "INSERT"
|
||
sql = f"{action} INTO {self._q(self.table_name)} ({columns}) VALUES ({placeholders})"
|
||
return self.driver.execute_insert(sql, tuple(values))
|
||
|
||
def insert_all(self, data: List[Dict[str, Any]], replace: bool = False) -> int:
|
||
"""
|
||
批量插入
|
||
每条数据key结构必须相同
|
||
|
||
:param data: [{ 字段名: 值 }, ...]
|
||
:param replace: 是否使用 REPLACE INTO(存在则覆盖)
|
||
:return: 影响行数
|
||
|
||
"""
|
||
if not data: return 0
|
||
|
||
raw_keys = list(data[0].keys())
|
||
keys = [self._q(k) for k in raw_keys]
|
||
columns = ",".join(keys)
|
||
placeholders = ",".join([self.mark] * len(keys))
|
||
|
||
params_list = [tuple(d.get(k) for k in raw_keys) for d in data]
|
||
|
||
action = "REPLACE" if replace else "INSERT"
|
||
sql = f"{action} INTO {self._q(self.table_name)} ({columns}) VALUES ({placeholders})"
|
||
|
||
return self.driver.execute_many(sql, params_list)
|
||
|
||
def update(self, data: Dict[str, Any]) -> int:
|
||
"""
|
||
更新符合条件的所有行数据
|
||
|
||
:param data: { 字段名: 值 }
|
||
:return: 影响行数
|
||
|
||
"""
|
||
if not data:
|
||
return 0 # 避免生成无效 SQL
|
||
|
||
set_clauses = []
|
||
set_params = []
|
||
|
||
for key, value in data.items():
|
||
set_clauses.append(f"{self._q(key)} = {self.mark}")
|
||
set_params.append(value)
|
||
|
||
set_sql = ", ".join(set_clauses)
|
||
where_sql = self._build_where_sql()
|
||
if not where_sql:
|
||
raise ValueError("WHERE 条件不存在,为防止全表更新操作已被拦截!")
|
||
|
||
full_params = set_params + self._params
|
||
sql = f"UPDATE {self._q(self.table_name)} SET {set_sql} {where_sql}"
|
||
return self.driver.execute(sql, tuple(full_params))
|
||
|
||
def delete(self) -> int:
|
||
"""
|
||
删除符合条件的所有行数据
|
||
:return: 影响行数
|
||
|
||
"""
|
||
where_sql = self._build_where_sql()
|
||
if not where_sql:
|
||
raise ValueError("WHERE 条件不存在,为防止全表删除操作已被拦截!")
|
||
sql = f"DELETE FROM {self._q(self.table_name)} {where_sql}"
|
||
return self.driver.execute(sql, tuple(self._params))
|
||
|
||
# ================= 内部辅助方法 =================
|
||
|
||
def _build_where_sql(self) -> str:
|
||
if not self._wheres:
|
||
return ""
|
||
|
||
# 【修改】适配新的 Tuple(logic, condition) 结构以支持 OR
|
||
where_parts = []
|
||
for i, (logic, condition) in enumerate(self._wheres):
|
||
if i == 0:
|
||
where_parts.append(condition) # 首个条件不需要 AND/OR 前缀
|
||
else:
|
||
where_parts.append(f"{logic} {condition}")
|
||
|
||
return "WHERE " + " ".join(where_parts)
|
||
|
||
def _build_select_sql(self, fields: str) -> str:
|
||
parts = [f"SELECT {fields} FROM {self._q(self.table_name)}"]
|
||
|
||
if self._joins:
|
||
parts.extend(self._joins)
|
||
|
||
where_sql = self._build_where_sql()
|
||
if where_sql:
|
||
parts.append(where_sql)
|
||
|
||
if self._orders:
|
||
parts.append("ORDER BY " + ", ".join(self._orders))
|
||
|
||
if self._limit is not None:
|
||
parts.append(f"LIMIT {self._limit}")
|
||
|
||
if self._offset is not None:
|
||
parts.append(f"OFFSET {self._offset}")
|
||
|
||
return " ".join(parts)
|
||
|
||
|
||
class Db:
|
||
"""数据库入口"""
|
||
def __init__(self, db_path: str, config: dict = None, db_type: str = None):
|
||
config = config or {}
|
||
|
||
if db_type is None:
|
||
if db_path.lower() == 'mysql' or 'host' in config:
|
||
db_type = 'mysql'
|
||
else:
|
||
db_type = 'sqlite'
|
||
|
||
if db_type == 'mysql':
|
||
self.driver = MysqlDriver(**config)
|
||
elif db_type == 'sqlite':
|
||
self.driver = SqliteDriver(db_path) # 【修复】采用正确的命名
|
||
else:
|
||
raise ValueError("db_type 参数错误: 应为 'mysql' 或 'sqlite'")
|
||
|
||
@contextmanager
|
||
def transaction(self):
|
||
"""
|
||
事务上下文管理器
|
||
with db.transaction():
|
||
db.table('A').insert(...)
|
||
db.table('B').update(...)
|
||
"""
|
||
self.driver.begin()
|
||
try:
|
||
yield self
|
||
self.driver.commit()
|
||
except Exception:
|
||
self.driver.rollback()
|
||
raise # 【修复】直接 raise 避免截断真实报错的 Traceback
|
||
|
||
def table(self, table_name: str) -> Query:
|
||
"""
|
||
获取表操作对象
|
||
|
||
:param table_name: 表名
|
||
|
||
"""
|
||
return Query(self.driver, table_name)
|
||
|
||
def execute_raw(self, sql: str, params: tuple = ()) -> Union[List[dict], int]:
|
||
"""
|
||
执行原生 SQL
|
||
|
||
:param sql: SQL 语句
|
||
:param params: 参数
|
||
|
||
"""
|
||
return self.driver.execute_raw(sql, params)
|
||
|
||
def execute_many_raw(self, sql: str, params_list: List[tuple]) -> int:
|
||
"""
|
||
批量执行原生 SQL
|
||
|
||
:param sql: SQL 语句
|
||
:param params_list: 参数列表
|
||
|
||
"""
|
||
return self.driver.execute_many(sql, params_list)
|
||
|
||
def execute_script(self, sql_script: str) -> None:
|
||
"""
|
||
执行多条 SQL 语句(如建表)
|
||
|
||
:param sql_script: SQL 脚本
|
||
|
||
"""
|
||
self.driver.execute_script(sql_script)
|
||
|
||
def close(self) -> None:
|
||
self.driver.close()
|
||
|
||
# Context Manager 支持
|
||
def __enter__(self):
|
||
return self
|
||
|
||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||
self.close() |