Compare commits
No commits in common. 'f663435843a23de44a369d9e940c49d63c0b0d17' and '8b5e43f4915cef786208a8132460f87b8b191765' have entirely different histories.
f663435843
...
8b5e43f491
@ -1,6 +0,0 @@
|
||||
# context_compression: off | beta
|
||||
context_compression: off
|
||||
# review_mode: off | standard | thorough
|
||||
review_mode: off
|
||||
# auto_transition: true | false
|
||||
auto_transition: true
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,52 +0,0 @@
|
||||
"""
|
||||
配置迁移 - JSON 配置文件 → MySQL
|
||||
"""
|
||||
import logging
|
||||
|
||||
from app.config_store import get_config_store, CONFIG_FILES, DEFAULT_CONFIGS
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
CONFIG_KEYS = ["symbols", "ai"]
|
||||
|
||||
|
||||
def migrate_configs_to_mysql():
|
||||
"""
|
||||
将 JSON 配置文件迁移到 MySQL。
|
||||
|
||||
Returns:
|
||||
bool: 迁移是否成功
|
||||
"""
|
||||
store = get_config_store()
|
||||
|
||||
if not store.storage_manager.check_mysql():
|
||||
logger.warning("MySQL 不可用,跳过配置迁移")
|
||||
return True
|
||||
|
||||
if store.session_maker is None:
|
||||
logger.warning("MySQL session 未初始化,跳过配置迁移")
|
||||
return True
|
||||
|
||||
migrated_count = 0
|
||||
skipped_count = 0
|
||||
|
||||
for key in CONFIG_KEYS:
|
||||
try:
|
||||
if store._config_exists_in_mysql(key):
|
||||
logger.info(f"MySQL 配置 [{key}] 已存在,跳过迁移")
|
||||
skipped_count += 1
|
||||
continue
|
||||
|
||||
value = store._load_json_from_file(key)
|
||||
if value is None:
|
||||
value = DEFAULT_CONFIGS.get(key, {})
|
||||
logger.info(f"JSON 配置 [{key}] 不存在或损坏,使用默认值迁移")
|
||||
store._save_to_mysql(key, value)
|
||||
migrated_count += 1
|
||||
logger.info(f"配置 [{key}] 已从 JSON 迁移到 MySQL")
|
||||
except Exception as e:
|
||||
logger.error(f"迁移配置 [{key}] 失败: {e}")
|
||||
return False
|
||||
|
||||
logger.info(f"配置迁移完成: 迁移 {migrated_count} 项,跳过 {skipped_count} 项")
|
||||
return True
|
||||
@ -1,39 +0,0 @@
|
||||
"""
|
||||
数据缓冲平台 - MySQL 数据库连接
|
||||
"""
|
||||
import logging
|
||||
import urllib.parse
|
||||
from sqlalchemy import create_engine, text
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from app.config import MYSQL_HOST, MYSQL_PORT, MYSQL_USER, MYSQL_PASSWORD, MYSQL_DATABASE
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
mysql_engine = None
|
||||
MySQLSessionLocal = None
|
||||
|
||||
|
||||
def init_mysql():
|
||||
"""初始化 MySQL 连接引擎,失败时返回 None"""
|
||||
global mysql_engine, MySQLSessionLocal
|
||||
try:
|
||||
encoded_password = urllib.parse.quote_plus(MYSQL_PASSWORD)
|
||||
url = f"mysql+pymysql://{MYSQL_USER}:{encoded_password}@{MYSQL_HOST}:{MYSQL_PORT}/{MYSQL_DATABASE}?charset=utf8mb4"
|
||||
mysql_engine = create_engine(url, pool_pre_ping=True, pool_recycle=3600)
|
||||
with mysql_engine.connect() as conn:
|
||||
conn.execute(text("SELECT 1"))
|
||||
MySQLSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=mysql_engine)
|
||||
logger.info("MySQL 连接初始化成功")
|
||||
return mysql_engine
|
||||
except Exception as e:
|
||||
logger.warning(f"MySQL 初始化失败: {e}")
|
||||
mysql_engine = None
|
||||
MySQLSessionLocal = None
|
||||
return None
|
||||
|
||||
|
||||
def get_mysql_session():
|
||||
"""获取 MySQL 会话"""
|
||||
if MySQLSessionLocal is None:
|
||||
raise RuntimeError("MySQL 未初始化")
|
||||
return MySQLSessionLocal()
|
||||
@ -1,31 +0,0 @@
|
||||
import logging
|
||||
import redis
|
||||
from app.config import REDIS_HOST, REDIS_PORT, REDIS_DB, REDIS_PASSWORD
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
redis_client = None
|
||||
|
||||
|
||||
def init_redis():
|
||||
"""初始化 Redis 客户端,失败时返回 None"""
|
||||
global redis_client
|
||||
try:
|
||||
redis_client = redis.Redis(
|
||||
host=REDIS_HOST, port=REDIS_PORT, db=REDIS_DB,
|
||||
password=REDIS_PASSWORD or None,
|
||||
decode_responses=True,
|
||||
socket_connect_timeout=5,
|
||||
)
|
||||
redis_client.ping()
|
||||
logger.info("Redis 连接初始化成功")
|
||||
return redis_client
|
||||
except Exception as e:
|
||||
logger.warning(f"Redis 初始化失败: {e}")
|
||||
redis_client = None
|
||||
return None
|
||||
|
||||
|
||||
def get_redis():
|
||||
"""获取 Redis 客户端实例"""
|
||||
return redis_client
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,24 +0,0 @@
|
||||
workflow: full
|
||||
phase: archive
|
||||
context_compression: off
|
||||
build_mode: executing-plans
|
||||
build_pause: null
|
||||
subagent_dispatch: null
|
||||
tdd_mode: tdd
|
||||
review_mode: thorough
|
||||
isolation: branch
|
||||
verify_mode: full
|
||||
auto_transition: true
|
||||
base_ref: 82de2f9
|
||||
design_doc: docs/superpowers/specs/2026-07-04-analysis-storage-refactor-design.md
|
||||
plan: docs/superpowers/plans/2026-07-04-analysis-storage-refactor.md
|
||||
verify_result: pass
|
||||
verification_report: docs/superpowers/reports/2026-07-04-analysis-storage-refactor-verify.md
|
||||
branch_status: handled
|
||||
created_at: 2026-07-04
|
||||
verified_at: 2026-07-04
|
||||
archived: true
|
||||
handoff_context: null
|
||||
handoff_hash: null
|
||||
build_command: python -m pytest tests/ -v
|
||||
verify_command: python -m pytest tests/ -v
|
||||
@ -1,3 +0,0 @@
|
||||
name: analysis-storage-refactor
|
||||
created: 2026-07-04
|
||||
status: active
|
||||
@ -1,24 +0,0 @@
|
||||
workflow: full
|
||||
phase: archive
|
||||
context_compression: off
|
||||
build_mode: subagent-driven-development
|
||||
build_pause: null
|
||||
subagent_dispatch: confirmed
|
||||
tdd_mode: tdd
|
||||
review_mode: thorough
|
||||
isolation: worktree
|
||||
verify_mode: full
|
||||
auto_transition: true
|
||||
base_ref: 8b5e43f4915cef786208a8132460f87b8b191765
|
||||
design_doc: docs/superpowers/specs/2026-07-04-storage-cache-refactor-design.md
|
||||
plan: docs/superpowers/plans/2026-07-04-storage-cache-refactor.md
|
||||
verify_result: pass
|
||||
verification_report: docs/superpowers/reports/2026-07-04-storage-cache-refactor-verify.md
|
||||
branch_status: handled
|
||||
created_at: 2026-07-04
|
||||
verified_at: 2026-07-04T15:00:00
|
||||
archived: true
|
||||
handoff_context: openspec/changes/storage-cache-refactor/.comet/handoff/design-context.json
|
||||
handoff_hash: 1064fdf5186e511848878541bdad663ac6b521ee5721807d01eaf3481bb0cd5a
|
||||
build_command: python -m pytest tests/ -v
|
||||
verify_command: python -m pytest tests/ -v
|
||||
@ -1,17 +0,0 @@
|
||||
{
|
||||
"change": "storage-cache-refactor",
|
||||
"phase": "design",
|
||||
"mode": "compact",
|
||||
"canonical_spec": "openspec",
|
||||
"generated_by": "comet-handoff.sh",
|
||||
"context_hash": "1064fdf5186e511848878541bdad663ac6b521ee5721807d01eaf3481bb0cd5a",
|
||||
"files": [
|
||||
{ "path": "openspec/changes/storage-cache-refactor/proposal.md", "sha256": "759de20d4e94b3228ee94d8607740de2949c24935933884584b85d3d855569ad" },
|
||||
{ "path": "openspec/changes/storage-cache-refactor/design.md", "sha256": "293fba52d3bef2adc55fa3d8667879fc7806918922f2f273d05efbc870c862ac" },
|
||||
{ "path": "openspec/changes/storage-cache-refactor/tasks.md", "sha256": "295733c66c5b2599cac51356e7344d17c579e674e667ec24c6b77d62ee519fcd" },
|
||||
{ "path": "openspec/changes/storage-cache-refactor/specs/dual-write-consistency/spec.md", "sha256": "6fd0c00433c73329adc93f97ea5a8b744d9a74148d7db40f30c843e92480b91e" },
|
||||
{ "path": "openspec/changes/storage-cache-refactor/specs/mysql-persistence/spec.md", "sha256": "f23680f51345a871dddbf3d18eacf6a53de1a20945d02516ffc55a2eb500af75" },
|
||||
{ "path": "openspec/changes/storage-cache-refactor/specs/redis-cache-layer/spec.md", "sha256": "a0b5c7023c88339957ce5576ad326474fd0ab913756c223573766cbc9e23f761" },
|
||||
{ "path": "openspec/changes/storage-cache-refactor/specs/storage-fallback/spec.md", "sha256": "feaf531250861c109bc48d06d684ff2520cb48269397211bdcefe7cda9013714" }
|
||||
]
|
||||
}
|
||||
@ -1,2 +0,0 @@
|
||||
schema: spec-driven
|
||||
created: 2026-07-04
|
||||
@ -1,41 +0,0 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: MySQL 连接管理
|
||||
系统应当提供 MySQL 连接管理能力,包括连接初始化、连接池管理和表结构初始化。
|
||||
|
||||
#### Scenario: MySQL 连接初始化成功
|
||||
- **WHEN** 应用启动且 MySQL 配置有效
|
||||
- **THEN** 系统成功建立 MySQL 连接并创建必要的表结构
|
||||
|
||||
#### Scenario: MySQL 连接初始化失败
|
||||
- **WHEN** 应用启动但 MySQL 服务不可用
|
||||
- **THEN** 系统输出错误日志并标记 MySQL 为不可用状态,但不阻止应用启动
|
||||
|
||||
### Requirement: 行情数据持久化存储
|
||||
系统应当使用 MySQL 作为行情数据的主要持久化存储,替代 SQLite。
|
||||
|
||||
#### Scenario: 行情数据写入 MySQL
|
||||
- **WHEN** 刷新接口接收到行情数据更新请求
|
||||
- **THEN** 系统将数据写入 MySQL 的 `market_data` 表
|
||||
|
||||
#### Scenario: 行情数据从 MySQL 读取
|
||||
- **WHEN** Redis 缓存未命中且 MySQL 可用
|
||||
- **THEN** 系统从 MySQL 读取行情数据并返回
|
||||
|
||||
### Requirement: MySQL 表结构迁移
|
||||
系统应当提供从 SQLite 到 MySQL 的数据迁移能力,保证历史数据不丢失。
|
||||
|
||||
#### Scenario: 首次启动数据迁移
|
||||
- **WHEN** 应用首次启动且 MySQL 可用但表为空
|
||||
- **THEN** 系统从 SQLite 读取历史数据并迁移到 MySQL
|
||||
|
||||
#### Scenario: 迁移完成
|
||||
- **WHEN** 数据迁移完成
|
||||
- **THEN** 系统输出迁移成功日志,后续读写直接操作 MySQL
|
||||
|
||||
### Requirement: MySQL 事务支持
|
||||
系统应当使用 MySQL 事务保证数据写入的原子性和一致性。
|
||||
|
||||
#### Scenario: 批量写入事务
|
||||
- **WHEN** 刷新接口需要写入多个品种的行情数据
|
||||
- **THEN** 系统使用事务保证所有数据要么全部写入成功,要么全部回滚
|
||||
@ -1,24 +0,0 @@
|
||||
workflow: full
|
||||
phase: archive
|
||||
context_compression: off
|
||||
build_mode: executing-plans
|
||||
build_pause: null
|
||||
subagent_dispatch: null
|
||||
tdd_mode: tdd
|
||||
review_mode: thorough
|
||||
isolation: branch
|
||||
verify_mode: full
|
||||
auto_transition: true
|
||||
base_ref: f8d5ecd
|
||||
design_doc: docs/superpowers/specs/2026-07-04-config-to-mysql-design.md
|
||||
plan: docs/superpowers/plans/2026-07-04-config-to-mysql.md
|
||||
verify_result: pass
|
||||
verification_report: docs/superpowers/reports/2026-07-04-config-to-mysql-verify.md
|
||||
branch_status: handled
|
||||
created_at: 2026-07-04
|
||||
verified_at: 2026-07-05
|
||||
archived: true
|
||||
handoff_context: null
|
||||
handoff_hash: null
|
||||
build_command: python -m pytest tests/ -v
|
||||
verify_command: python -m pytest tests/ -v
|
||||
@ -1,3 +0,0 @@
|
||||
name: config-to-mysql
|
||||
created: 2026-07-04
|
||||
status: active
|
||||
@ -1,41 +0,0 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: MySQL 连接管理
|
||||
系统应当提供 MySQL 连接管理能力,包括连接初始化、连接池管理和表结构初始化。
|
||||
|
||||
#### Scenario: MySQL 连接初始化成功
|
||||
- **WHEN** 应用启动且 MySQL 配置有效
|
||||
- **THEN** 系统成功建立 MySQL 连接并创建必要的表结构
|
||||
|
||||
#### Scenario: MySQL 连接初始化失败
|
||||
- **WHEN** 应用启动但 MySQL 服务不可用
|
||||
- **THEN** 系统输出错误日志并标记 MySQL 为不可用状态,但不阻止应用启动
|
||||
|
||||
### Requirement: 行情数据持久化存储
|
||||
系统应当使用 MySQL 作为行情数据的主要持久化存储,替代 SQLite。
|
||||
|
||||
#### Scenario: 行情数据写入 MySQL
|
||||
- **WHEN** 刷新接口接收到行情数据更新请求
|
||||
- **THEN** 系统将数据写入 MySQL 的 `market_data` 表
|
||||
|
||||
#### Scenario: 行情数据从 MySQL 读取
|
||||
- **WHEN** Redis 缓存未命中且 MySQL 可用
|
||||
- **THEN** 系统从 MySQL 读取行情数据并返回
|
||||
|
||||
### Requirement: MySQL 表结构迁移
|
||||
系统应当提供从 SQLite 到 MySQL 的数据迁移能力,保证历史数据不丢失。
|
||||
|
||||
#### Scenario: 首次启动数据迁移
|
||||
- **WHEN** 应用首次启动且 MySQL 可用但表为空
|
||||
- **THEN** 系统从 SQLite 读取历史数据并迁移到 MySQL
|
||||
|
||||
#### Scenario: 迁移完成
|
||||
- **WHEN** 数据迁移完成
|
||||
- **THEN** 系统输出迁移成功日志,后续读写直接操作 MySQL
|
||||
|
||||
### Requirement: MySQL 事务支持
|
||||
系统应当使用 MySQL 事务保证数据写入的原子性和一致性。
|
||||
|
||||
#### Scenario: 批量写入事务
|
||||
- **WHEN** 刷新接口需要写入多个品种的行情数据
|
||||
- **THEN** 系统使用事务保证所有数据要么全部写入成功,要么全部回滚
|
||||
@ -1,157 +0,0 @@
|
||||
"""
|
||||
数据缓冲平台 - SQLite 到 MySQL 数据迁移脚本测试
|
||||
"""
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from app.models import Base, MarketData, SymbolTimestamp
|
||||
|
||||
|
||||
def _create_sqlite_source(tmp_path):
|
||||
"""创建包含测试数据的文件 SQLite 源数据库。"""
|
||||
db_path = tmp_path / "test_source.db"
|
||||
engine = create_engine(f"sqlite:///{db_path}")
|
||||
MarketData.__table__.create(bind=engine)
|
||||
SymbolTimestamp.__table__.create(bind=engine)
|
||||
Session = sessionmaker(bind=engine)
|
||||
session = Session()
|
||||
fetched_at = datetime(2026, 7, 4, 10, 0, 0)
|
||||
session.add_all(
|
||||
[
|
||||
MarketData(
|
||||
symbol="AG2606",
|
||||
data_type="futures",
|
||||
period="5min",
|
||||
candles_json='[{"open": 100, "close": 110}]',
|
||||
current_price=123.45,
|
||||
fetched_at=fetched_at,
|
||||
candle_count=1,
|
||||
),
|
||||
MarketData(
|
||||
symbol="AG2606",
|
||||
data_type="futures",
|
||||
period="15min",
|
||||
candles_json='[{"open": 105, "close": 115}]',
|
||||
current_price=123.45,
|
||||
fetched_at=fetched_at,
|
||||
candle_count=1,
|
||||
),
|
||||
SymbolTimestamp(
|
||||
symbol="AG2606",
|
||||
data_type="futures",
|
||||
last_refresh_at=fetched_at,
|
||||
refresh_count=5,
|
||||
),
|
||||
]
|
||||
)
|
||||
session.commit()
|
||||
session.close()
|
||||
return engine
|
||||
|
||||
|
||||
def _make_mysql_session(market_data_count=0, symbol_timestamp_count=0):
|
||||
"""构造模拟 MySQL 会话,支持记录写入和计数查询。"""
|
||||
mysql_session = MagicMock()
|
||||
mysql_session.__enter__ = MagicMock(return_value=mysql_session)
|
||||
mysql_session.__exit__ = MagicMock(return_value=False)
|
||||
|
||||
added_market_data = []
|
||||
added_symbol_timestamps = []
|
||||
|
||||
def mock_query(model):
|
||||
query = MagicMock()
|
||||
if model is MarketData:
|
||||
query.count.return_value = market_data_count
|
||||
elif model is SymbolTimestamp:
|
||||
query.count.return_value = symbol_timestamp_count
|
||||
return query
|
||||
|
||||
mysql_session.query.side_effect = mock_query
|
||||
mysql_session.add_all.side_effect = lambda records: (
|
||||
added_market_data.extend(records)
|
||||
if records and isinstance(records[0], MarketData)
|
||||
else added_symbol_timestamps.extend(records)
|
||||
)
|
||||
|
||||
return mysql_session, added_market_data, added_symbol_timestamps
|
||||
|
||||
|
||||
class TestMigrateSqliteToMysql:
|
||||
"""migrate_sqlite_to_mysql 行为测试。"""
|
||||
|
||||
def test_migrates_data_when_mysql_tables_are_empty(self, caplog, tmp_path):
|
||||
"""MySQL 表为空时,应从 SQLite 迁移全部数据。"""
|
||||
sqlite_engine = _create_sqlite_source(tmp_path)
|
||||
mysql_session, added_market_data, added_symbol_timestamps = _make_mysql_session(
|
||||
market_data_count=0, symbol_timestamp_count=0
|
||||
)
|
||||
|
||||
with patch("app.migration.create_engine", return_value=sqlite_engine):
|
||||
with patch(
|
||||
"app.migration.MySQLSessionLocal", return_value=mysql_session
|
||||
):
|
||||
with caplog.at_level(logging.INFO, logger="app.migration"):
|
||||
from app.migration import migrate_sqlite_to_mysql
|
||||
|
||||
result = migrate_sqlite_to_mysql()
|
||||
|
||||
assert result is True
|
||||
assert len(added_market_data) == 2
|
||||
assert len(added_symbol_timestamps) == 1
|
||||
assert added_market_data[0].symbol == "AG2606"
|
||||
assert added_market_data[0].period == "5min"
|
||||
assert added_symbol_timestamps[0].symbol == "AG2606"
|
||||
assert "迁移完成" in caplog.text
|
||||
assert "market_data: 2" in caplog.text
|
||||
assert "symbol_timestamps: 1" in caplog.text
|
||||
mysql_session.commit.assert_called_once()
|
||||
|
||||
def test_skips_migration_when_mysql_has_data(self, caplog, tmp_path):
|
||||
"""MySQL 表非空时,应跳过迁移并输出提示日志。"""
|
||||
sqlite_engine = _create_sqlite_source(tmp_path)
|
||||
mysql_session, added_market_data, added_symbol_timestamps = _make_mysql_session(
|
||||
market_data_count=1, symbol_timestamp_count=1
|
||||
)
|
||||
|
||||
with patch("app.migration.create_engine", return_value=sqlite_engine):
|
||||
with patch(
|
||||
"app.migration.MySQLSessionLocal", return_value=mysql_session
|
||||
):
|
||||
with caplog.at_level(logging.INFO, logger="app.migration"):
|
||||
from app.migration import migrate_sqlite_to_mysql
|
||||
|
||||
result = migrate_sqlite_to_mysql()
|
||||
|
||||
assert result is False
|
||||
assert len(added_market_data) == 0
|
||||
assert len(added_symbol_timestamps) == 0
|
||||
assert "MySQL 已存在数据" in caplog.text
|
||||
assert "跳过迁移" in caplog.text
|
||||
mysql_session.commit.assert_not_called()
|
||||
|
||||
def test_migrates_empty_table_when_other_table_has_data(self, caplog, tmp_path):
|
||||
"""按表分别判断:仅空表需要迁移,非空表跳过。"""
|
||||
sqlite_engine = _create_sqlite_source(tmp_path)
|
||||
mysql_session, added_market_data, added_symbol_timestamps = _make_mysql_session(
|
||||
market_data_count=1, symbol_timestamp_count=0
|
||||
)
|
||||
|
||||
with patch("app.migration.create_engine", return_value=sqlite_engine):
|
||||
with patch(
|
||||
"app.migration.MySQLSessionLocal", return_value=mysql_session
|
||||
):
|
||||
with caplog.at_level(logging.INFO, logger="app.migration"):
|
||||
from app.migration import migrate_sqlite_to_mysql
|
||||
|
||||
result = migrate_sqlite_to_mysql()
|
||||
|
||||
assert result is True
|
||||
assert len(added_market_data) == 0
|
||||
assert len(added_symbol_timestamps) == 1
|
||||
assert added_symbol_timestamps[0].symbol == "AG2606"
|
||||
mysql_session.commit.assert_called_once()
|
||||
@ -1,23 +0,0 @@
|
||||
"""
|
||||
数据缓冲平台 - MySQL 数据库连接测试
|
||||
"""
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from app.mysql_database import init_mysql
|
||||
|
||||
|
||||
def test_init_mysql_url_encodes_password():
|
||||
"""MySQL 密码应使用 urllib.parse.quote_plus 进行 URL 编码。"""
|
||||
with patch("app.mysql_database.create_engine") as mock_create_engine:
|
||||
mock_engine = MagicMock()
|
||||
mock_create_engine.return_value = mock_engine
|
||||
with patch("app.mysql_database.MYSQL_PASSWORD", "p@ss/w+ord"):
|
||||
with patch("app.mysql_database.MYSQL_USER", "user"):
|
||||
with patch("app.mysql_database.MYSQL_HOST", "host"):
|
||||
with patch("app.mysql_database.MYSQL_PORT", "3306"):
|
||||
with patch("app.mysql_database.MYSQL_DATABASE", "buffer"):
|
||||
init_mysql()
|
||||
|
||||
url = mock_create_engine.call_args[0][0]
|
||||
assert "p%40ss%2Fw%2Bord" in url
|
||||
assert "p@ss" not in url
|
||||
Loading…
Reference in new issue