conda-pack 并非 conda 的核心组成部分,而是 conda 生态系统中的一个独立工具包, 用于打包和迁移 conda 虚拟环境。 安装与获取: conda-pack 可通过 conda 或 pip 等包管理器进行安装, 例如使用 conda install -c conda-forge conda-pack 命令 收费情况: conda-pack 本身是免费的开源工具,无需付费即可使用。 该工具的主要功能是将整个 conda 环境(包括所有依赖包和二进制文件)打包成一个归档文件(如 .tar.gz), 便于在无网络环境或不同机器间迁移环境,避免重新下载和安装依赖。 |
- 所安装的包离线下线,以保证更换环境时,或者在无网环境下方便安装
- pip使用wheel
- conda 使用conda-pack
- micromamba 自动下载
|
|
执行uv sync 时,会在.venv下生成项目对应的固定路径,永远牵连着这个项目
这在迁移时,如果项目路径变换,可能会出问题
Thinking
根因彻底确认了!虚拟环境中有一个 editable 安装文件:
.venv/Lib/site-packages/__editable__.inspectai-1.0.0.pth
|
|
|
|
|
|
安装 pip install uv micromamba install uv uv --version 整个电脑同一时间只能有一个uv在运行,第2个uv运行会被锁住 uv sync --python 3.12 uv sync --index https://mirrors.tuna.tsinghua.edu.cn/pypi/simple/ 后端uv sync报错 ``` PS D:\git\zhinengjiancha\inspectai> uv sync error: Querying Python at `D:\git\zhinengjiancha\inspectai\.venv\Scripts\python.exe` failed with exit status exit code: 103 [stderr] No Python at '"C:\Users\itoracle_17068343192\AppData\Roaming\uv\python\cpython-3.12.12-windows-x86_64-none\python.exe' ``` 这是我从另外一个电脑上COPY过来的,有没有办法,以最小的代价解决这个问题,而不是删除.venv文件重新安装 # 先用系统 Python 重新定位 venv,这会删除已有的依赖 python -m venv --clear .venv - 如果uv安装的.venv文件是从另外的电脑上复制过来的,需要重新定位venv ``` # 这并不会删除已有的依赖 uv venv --relocate .venv # 然后 uv sync ``` 镜像 ``` # 设置全局镜像源 uv pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple ``` 或在项目的 pyproject.toml 中配置 ``` [tool.uv] index-url = "https://pypi.tuna.tsinghua.edu.cn/simple" ``` pyproject.toml
```
[project]
name = "simple-api"
version = "0.1.0"
description = "一个简单的 FastAPI 学习示例项目"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"fastapi>=0.115.0",
"uvicorn>=0.30.0",
"pydantic>=2.0.0",
"pyinstaller>=6.0.0",
"email-validator>=2.3.0",
]
[tool.uv]
index-url = "https://pypi.tuna.tsinghua.edu.cn/simple"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
```
|
|
## 通过requirements.txt安装依赖
```
# 1. 初始化项目,生成 pyproject.toml
uv init
# 2. 从 requirements.txt 添加依赖
uv add -r requirements.txt
```
- 操作记录
```
xt@qisan:/ai/wks/elec/doc/skills$ uv init
Initialized project `skills`
xt@qisan:/ai/wks/elec/doc/skills$ uv add -r requirements.txt
Using CPython 3.12.0 interpreter at: /wks/python/micromamba/envs/py312/bin/python3.12
Creating virtual environment at: .venv
```
## 通过pyproject.toml安装依赖
指定 Python 版本 ``` mkdir my_project cd my_project echo "3.12" > .python-version ```创建 pyproject.toml
```
[project]
name = "my-project"
version = "0.1.0"
description = "我的项目"
requires-python = ">=3.12"
dependencies = [
"fastapi>=0.115.0",
"uvicorn>=0.30.0",
]
[tool.uv]
index-url = "https://pypi.tuna.tsinghua.edu.cn/simple"
```
同步依赖 uv sync 这一步会自动: ✅ 创建 .venv 虚拟环境 ✅ 安装所有依赖 ✅ 生成 uv.lock 锁文件 |
|
激活虚拟环境 - Linux / macOS - source .venv/bin/activate - Windows - .venv\Scripts\activate 运行 Python 代码 - 方法 1: 激活环境后运行 - source .venv/bin/activate - python main.py - 方法 2: 使用 uv run(无需激活环境) - uv run python main.py |
|
| 命令 | 行为 | 是否修改 `pyproject.toml` | 是否生成/更新 `uv.lock` | 适用场景 |
| :--- | :--- | :---: | :---: | :--- |
| `uv add <包名>` | 解析依赖、安装包、记录依赖、锁定版本 | ✅ 是 | ✅ 是 | 标准项目开发(推荐)。确保依赖被正式记录,方便团队协作和环境复现。 |
| `uv pip install <包名>` | 仅将包安装到当前激活的虚拟环境中 | ❌ 否 | ❌ 否 | 临时测试、CI/CD 中根据现有文件安装、或管理非 `uv` 管理的项目。行为完全等同于 `pip install`。 |
添加新依赖 ``` # 添加到生产环境 uv add pandas # 添加开发依赖(仅开发时使用) uv add --dev pytest black # 指定版本 uv add "requests==2.32.0" # 指定版本范围 uv add "fastapi>=0.115.0" ``` pyproject.toml 会自动更新
```
[project]
dependencies = [
"fastapi>=0.115.0",
"pandas", # 新添加
]
[project.optional-dependencies]
dev = [
"pytest", # 新添加
"black", # 新添加
]
```
移除/更新依赖 ``` uv remove pandas ``` ``` # 更新所有依赖到最新版本 uv lock --upgrade # 只更新某个包 uv lock --upgrade-package fastapi # 交互式更新(选择更新哪些包) uv lock --upgrade interactive ``` 查看依赖信息 ``` # 列出已安装的包 uv pip list # 查看包的详细信息 uv pip show fastapi # 查看依赖树 uv pip tree # 检查依赖冲突 uv check ``` |
``` # 创建新虚拟环境(指定 Python 版本) uv venv --python 3.12 # 查找可用的 Python 版本 uv python list # 安装特定 Python 版本 uv python install 3.11 ``` 运行命令 ``` # 运行 Python 脚本 uv run python script.py # 运行测试 uv run pytest # 运行格式化工具 uv run black . # 运行任何已安装的命令行工具 uv run fastapi dev main.py ``` 导出 requirements.txt ``` # 导出为 requirements.txt(与 pip 兼容) uv pip compile pyproject.toml -o requirements.txt # 从 requirements.txt 安装 uv pip install -r requirements.txt ``` |
```
| 功能 | 命令 | 说明 |
|------|------|------|
| **初始化** | | |
| 安装依赖 | `uv sync` | 首次安装或同步依赖 |
| 添加依赖 | `uv add package` | 添加新依赖 |
| 添加开发依赖 | `uv add --dev package` | 添加开发依赖 |
| 移除依赖 | `uv remove package` | 移除依赖 |
| 更新依赖 | `uv lock --upgrade` | 更新所有依赖 |
| **运行** | | |
| 运行脚本 | `uv run python script.py` | 无需激活环境 |
| 运行测试 | `uv run pytest` | 运行测试工具 |
| **查看** | | |
| 列出包 | `uv pip list` | 查看已安装包 |
| 查看包信息 | `uv pip show package` | 查看包详情 |
| 依赖树 | `uv pip tree` | 查看依赖关系 |
| **环境** | | |
| Python 版本 | `uv python list` | 查看可用 Python |
| 安装 Python | `uv python install 3.11` | 安装特定版本 |
| 创建虚拟环境 | `uv venv` | 创建新虚拟环境 |
---
```
|
|
- uv export(推荐)
```
# 导出 requirements.txt 格式到标准输出
uv export --format requirements.txt > requirements.txt
# 或者直接指定输出文件
uv export --format requirements.txt --output-file requirements.txt
```
这个命令会基于 uv.lock (锁文件) 导出依赖,确保导出的版本与当前项目解析的完全一致
导入的文件内容类似下面这样
```
-e .
aiohappyeyeballs==2.6.1 \
--hash=sha256:c3
```
- uv pip freeze
```
# 将当前虚拟环境中安装的所有包导出到 requirements.txt
uv pip freeze > requirements.txt
```
这个命令会列出当前环境中所有已安装的包及其精确版本,并将其写入文件,
- 效果完全等同于 pip freeze > requirements.txt。
- 但实际上只会导出主环境没有,而当前环境有的包,或者版本不一致的包,其他包不会导出。
- 而pip freeze > requirements.txt会将当前环境中所有包都导出,不管主环境有没有,版本一致不一致都会导出。
|
|
- 国内源 · windows PowerShell
```
# 1. 创建配置目录
New-Item -Path "$env:APPDATA\uv" -ItemType Directory -Force
# 2. 写入清华源配置
$content = @"
[[index]]
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
default = true
"@
Set-Content -Path "$env:APPDATA\uv\uv.toml" -Value $content
Write-Host "✅ 国内镜像源配置完成!" -ForegroundColor Green
```
- 国内源 · Linux / macOS
```
export UV_INDEX_URL="https://pypi.tuna.tsinghua.edu.cn/simple"
```
```
[pip]
index-url = "https://pypi.tuna.tsinghua.edu.cn/simple"
```
- 回归原来的源
```
uv sync --index-url https://pypi.org/simple
```
或
```
export UV_INDEX_URL="https://pypi.org/simple"
```
|
|
|
|
|
|
simple_api.spec
```
import os
from PyInstaller.utils.hooks import collect_submodules
# 项目根目录
project_root = os.path.abspath(os.getcwd())
# 收集所有子模块
hiddenimports = collect_submodules('simple_api')
# 主分析配置
a = Analysis(
['src/simple_api/main.py'],
pathex=[os.path.join(project_root, 'src')],
binaries=[],
hiddenimports=hiddenimports,
hookspath=[],
runtime_hooks=[],
excludes=[],
datas=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=None,
)
pyz = PYZ(a.pure, a.zipped_data, cipher=None)
exe = EXE(
pyz,
a.scripts,
[],
exclude_binaries=True,
name='simple_api_app',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
console=True,
)
coll = COLLECT(
exe,
a.binaries,
a.zipfiles,
a.datas,
strip=False,
upx=True,
name='simple_api_app',
)
```
执行打包 pyinstaller simple_api.spec |
|
Linux / macOS
cd dist/simple_api_app
./simple_api_app
```
xt@qisan:/ai/wks/work2/uv_demo$ ls dist/simple_api_app/
_internal simple_api_app
xt@qisan:/ai/wks/work2/uv_demo$ ./dist/simple_api_app/simple_api_app
Simple API 开发服务器启动中...
============================================================
API 地址: http://127.0.0.1:8000
API 文档: http://127.0.0.1:8000/docs
ReDoc 文档: http://127.0.0.1:8000/redoc
============================================================
INFO: Will watch for changes in these directories: ['/ai/wks/work2/uv_demo']
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO: Started reloader process [14068] using WatchFiles
```
Windows cd dist\simple_api_app simple_api_app.exe |
### 1. 单文件打包(不推荐)
```python
# simple_api_single_file.spec
exe = EXE(
pyz,
a.scripts,
a.binaries, # 包含二进制
a.zipfiles,
a.datas,
[],
name='simple_api_app',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
console=True,
onefile=True, # 单文件模式
)
```
**优点**: 单个可执行文件
**缺点**: 启动慢(需要解压到临时目录)
### 2. 添加图标
```python
exe = EXE(
# ... 其他配置
icon='path/to/icon.ico', # Windows 图标
)
```
### 3. 添加版本信息
创建 `version_info.txt`:
```txt
VSVersionInfo(
ffi=FixedFileInfo(
filevers=(0, 1, 0, 0),
prodvers=(0, 1, 0, 0),
mask=0x3f,
flags=0x0,
OS=0x40004,
fileType=0x1,
subtype=0x0,
date=(0, 0)
),
kids=[
StringFileInfo(
[
StringTable(
u'040904B0',
[
StringStruct(u'CompanyName', u'My Company'),
StringStruct(u'FileDescription', u'Simple API Application'),
StringStruct(u'FileVersion', u'0.1.0'),
StringStruct(u'InternalName', u'simple_api'),
StringStruct(u'LegalCopyright', u'Copyright 2026'),
StringStruct(u'OriginalFilename', u'simple_api_app.exe'),
StringStruct(u'ProductName', u'Simple API'),
StringStruct(u'ProductVersion', u'0.1.0'),
])
]
),
VarFileInfo([VarStruct(u'Translation', [1033, 1200])])
]
)
```
使用:
```python
exe = EXE(
# ... 其他配置
version='version_info.txt',
)
```
### 4. 添加数据文件
```python
a = Analysis(
# ... 其他配置
datas=[
('config.json', '.'), # 打包配置文件
('templates', 'templates'), # 打包模板目录
('static', 'static'), # 打包静态文件
],
)
```
在代码中访问打包的数据文件:
```python
import sys
import os
def get_resource_path(relative_path):
"""获取打包后的资源路径"""
if hasattr(sys, '_MEIPASS'):
# PyInstaller 打包后的路径
return os.path.join(sys._MEIPASS, relative_path)
# 开发环境的路径
return os.path.join(os.path.dirname(__file__), relative_path)
# 使用
config_path = get_resource_path('config.json')
```
---
|
### 1. 减小体积
```python
a = Analysis(
# ... 其他配置
excludes=[
'test', # 排除测试模块
'pytest',
'tkinter', # 排除 GUI 库
'matplotlib', # 排除不需要的大型库
],
)
```
### 2. UPX 压缩
```python
exe = EXE(
# ... 其他配置
upx=True, # 启用 UPX 压缩(默认)
upx_exclude=[], # 排除 UPX 压缩的文件
)
```
### 3. 去除调试符号
```python
exe = EXE(
# ... 其他配置
strip=True, # 去除调试符号(减小体积)
)
```
---
|
|
|
|
|
|
|
|
|
pnpm安装依赖包时,直接COPY项目会存在问题,那么使用yarn安装依赖包, 是不是就可以直接COPY项目到另外一台机器相同的目录下,就可以正常启动了? pnpm 是一个注重性能的 Node.js 包管理工具, 其核心设计理念是通过使用硬链接和符号链接来管理依赖,从而节省磁盘空间并提高安装效率。 它与 npm、Yarn 等是并列的包管理器,而非某个公司的镜像产品。 淘宝团队开发的是 cnpm,它是 npm 的一个镜像版本,主要用于加速中国大陆地区的包下载速度。 而 pnpm 是一个独立的、开源的包管理器项目,由社区维护,其官方仓库位于 GitHub。
是的,你的思路基本正确!使用 yarn v1 可以直接复制项目到另一台机器。让我详细解释:
pnpm vs yarn 对比
pnpm 的问题
源机器:
node_modules/
└── .pnpm/
└── (硬链接指向 D:\.pnpm-store\v10)
复制到新机器后:
node_modules/
└── .pnpm/
└── (硬链接失效!文件找不到)
原因:pnpm 使用硬链接节省空间,依赖在全局 store 中
yarn v1 的优势
源机器:
node_modules/
└── package/ (真实文件,完整复制)
复制到新机器后:
node_modules/
└── package/ (仍然可用!)
原因:yarn v1 将所有依赖直接放在项目 node_modules 中
使用 yarn 的迁移方案
1. 在源机器上使用 yarn
cd D:\wks\aisql\electron_chat2sql_frontend
# 安装 yarn (如果还没安装)
npm install -g yarn
# 删除 pnpm lock 文件(如果有)
rm pnpm-lock.yaml
# 使用 yarn 安装依赖
yarn install
# 现在可以直接复制整个项目
2. 直接复制项目
# 复制整个项目到新机器相同位置
Copy-Item -Path "D:\wks\aisql\electron_chat2sql_frontend" `
-Destination "E:\wks\aisql\electron_chat2sql_frontend" `
-Recurse -Force
3. 在新机器上直接运行
cd D:\wks\aisql\electron_chat2sql_frontend
yarn run dev
不需要重新安装依赖!
注意事项
✅ 优点
特性 pnpm yarn v1
复制后可用 ❌ ✅
磁盘占用 小 大
安装速度 快 慢
迁移简单度 复杂 简单
⚠️ 限制条件
Node.js 版本:新机器需要相同或兼容的 Node.js 版本
操作系统:跨平台可能有问题(Windows → Linux)
二进制文件:某些原生模块可能需要重新编译
yarn 版本:需要使用 yarn v1,不是 yarn v2/v3
推荐方案对比
方案 A:继续使用 pnpm + 离线包
优点:磁盘占用小,安装快
缺点:需要特殊脚本处理 store
适用:磁盘空间有限,频繁安装
方案 B:改用 yarn v1 直接复制
优点:简单粗暴,复制即用
缺点:磁盘占用大,安装慢
适用:磁盘充足,追求简单
快速切换到 yarn
# 1. 安装 yarn
npm install -g yarn
# 2. 清理现有依赖
rm pnpm-lock.yaml
rm -r node_modules -Force
# 3. 使用 yarn 安装
yarn install
# 4. 测试
yarn run dev
# 5. 直接复制整个项目到新机器即可
总结:如果你追求简单迁移,yarn v1 确实是更好的选择;
如果追求磁盘空间和安装效率,继续使用 pnpm 但需要配合我们刚才的离线包方案。
|
|
yarn v1是yarn的版本吗?
是的,yarn v1 是 yarn 的一个主要版本。让我解释一下 yarn 的版本历史:
Yarn 版本说明
Yarn v1(Classic Yarn)- 推荐用于你的场景
# 安装特定版本
npm install -g yarn@1
# 查看版本
yarn -v
# 输出:1.22.19 或类似的 1.x.x
特点:
所有依赖直接放在项目的 node_modules 中
复制项目后可以直接使用
使用 yarn.lock 文件
Yarn v2/v3/v4(Yarn Berry)- 不适合
# 这些版本使用不同的机制
npm install -g yarn@berry
# 或
yarn set version berry
特点:
使用 Plug'n'Play (PnP) 机制
依赖不放在 node_modules 中
和 pnpm 一样有迁移问题
如何确认和安装 Yarn v1
1. 检查当前版本
# 如果已安装 yarn
yarn -v
# 输出示例:
# 1.22.19 ← 这是 v1,可以直接复制项目
# 2.4.0 ← 这是 v2,不适合你的场景
# 3.1.0 ← 这是 v3,不适合你的场景
# 4.0.0 ← 这是 v4,不适合你的场景
2. 安装 Yarn v1
# 方式 1:安装 yarn v1(推荐)
npm install -g yarn@1.22.19
# 方式 2:安装最新 v1 版本
npm install -g yarn@1
# 方式 3:安装 yarn(默认就是 v1)
npm install -g yarn
3. 在项目中使用 Yarn v1
cd D:\wks\aisql\electron_chat2sql_frontend
# 如果之前用的是 pnpm,需要先清理
rm pnpm-lock.yaml
rm -r node_modules -Force
# 使用 yarn 安装依赖
yarn install
# 现在可以复制整个项目到新机器
快速验证流程
# 1. 安装 yarn v1
npm install -g yarn@1
# 2. 确认版本
yarn -v
# 应该显示 1.x.x
# 3. 在项目中使用
cd D:\wks\aisql\electron_chat2sql_frontend
rm pnpm-lock.yaml
yarn install
# 4. 测试运行
yarn run dev
# 5. 复制整个项目到新机器相同位置
# 在新机器上直接运行:
yarn run dev
版本对比总结
版本 依赖位置 可直接复制 适用场景
yarn v1 node_modules ✅ 是 离线迁移
yarn v2+ PnP 缓存 ❌ 否 在线开发
pnpm 全局 store ❌ 否 磁盘节省
总结:你需要安装 yarn@1(yarn v1),这样复制项目后就可以直接使用了,不需要任何特殊处理。
|
|
|
|
|
|
|
|
|
|
|
|
|
micromamba activate py312 micromamba install uv 这一步可以使用全局安装一个uv也可以 ``` (py312) PS D:\wks\aisql\chat2sql-develop\src\chat2sql> uv --version uv 0.9.26 ``` ``` # 这一步会在项目下生成一个.venv环境 uv sync ``` 进入环境·windows 修改 PowerShell 执行策略(管理员权限) Set-ExecutionPolicy RemoteSigned -Scope CurrentUser cd D:\wks\aisql\chat2sql-develop .venv\Scripts\activate PS D:\wks\aisql\chat2sql-develop> .venv\Scripts\activate cd .\src\chat2sql\ python .\main.py |
启动 ``` PS D:\wks\aisql\app\ChatSQL\mysql> .\start.bat ``` 连接 mysql.exe -uroot -pmemect2025 ``` PS C:\Users\itora> mysql.exe -uroot -pmemect2025 Server version: 8.1.0 MySQL Community Server - GPL mysql> mysql> use memect; Database changed mysql> show tables; +----------------------+ | Tables_in_memect | +----------------------+ | alembic_version | | cache_info | | chart_config | | conversation | | conversation_group | | dashboard | | dashboard_widget | | db_config | | favorite | | llm_config | | rule_base | | rule_match | | sql_execution_record | | tb_base | | tb_match | | user_info | +----------------------+ 16 rows in set (0.01 sec) ```
mysqladmin.exe -u root -p shutdown
For further information on MySQL or additional documentation, visit
http://dev.mysql.com/doc/
For additional downloads and the source of MySQL, visit
http://dev.mysql.com/downloads/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
打包·PyInstaller cd D:\wks\aipkg\chat2sql-develop .\.venv\Scripts\activate pyinstaller .\chat2sql.spec - 注意事项 - 打包时不能使用热启动 启动 ```bash # 方式1:双击运行 # 直接双击 dist/chat2sql_app/chat2sql_app.exe # 方式2:命令行运行 cd dist/chat2sql_app ./chat2sql_app.exe # 方式3:带参数运行 ./chat2sql_app.exe --host 0.0.0.0 --port 8000 ``` 依赖包处理 先删除.venv再重新安装 ``` uv --version uv 0.9.30 (ea4560831 2026-02-04) uv sync .venv\Scripts\activate ``` 启动测试 - mysql - D:\wks\aipkg\electron_chat2sql_frontend\mysql - 项目启动测试 - cd D:\wks\aisql\chat2sql-develop - python dev.py |
|
使用外部目录替换打包文件中的目录
- 场景 | 使用的 Prompts 目录 | 说明 |
- **直接运行 exe** | 内嵌目录 (`sys._MEIPASS`) | 使用打包时的 prompts |
- **通过 Electron 启动** | 取决于 `.env` 配置 | 如果设置了 `PROMPT_PATH=./prompts`,则使用外部目录 |
- **开发环境调试** | `./chat2sql/prompts/` | 相对路径 |
运行目录os.getcwd()
- 当前工作目录 os.getcwd() 程序运行时所在的目录(启动目录)
- 当前文件目录 os.path.dirname(os.path.abspath(__file__)) 当前 .py 文件所在的目录
- 项目根目录 无直接方法,需自定义 通常指项目最顶层目录
/myproject/
├── main.py
└── sub/
└── module.py
如果在 /home/user 目录下运行:
cd /home/user
python /myproject/sub/module.py
那么:
os.getcwd() → /home/user(启动目录)
os.path.dirname(os.path.abspath(__file__)) → /myproject/sub(文件所在目录)
项目根目录 → /myproject(需要额外逻辑判断)
|
|
|
|
|
|
|
|
|
|
|
|
|
- mysql启动 - 项目依赖Mysql,在启动时会连接mysql数据库;因此要先启动mysql数据库并确保可以连接 - mysql.exe -h127.0.0.1 -uroot -pmemect2025 环境切换 ``` cd D:\wks\elec\fastapi .\.venv\Scripts\activate python .\dev.py ```
|
alembic迁移
```
# Step 1: 创建数据库,创建数据库是必须,否则迁移会失败,因为alembic会连接数据库并检查alembic_version表是否存在
mysql -u root -p -e "CREATE DATABASE fastapi_db1 CHARACTER SET utf8mb4;"
# Step 2: 创建迁移脚本(您缺少的步骤);
# 在version目录下会生成一个新的python迁移脚本,里面包含了根据当前模型自动生成的数据库变更代码
alembic revision --autogenerate -m "init_db"
# 或使用:python scripts/create_migration.py "init_db"
# Step 3: 执行迁移,执行上一步生成的迁移脚本,
# alembic会连接数据库并执行迁移脚本中的变更代码,并将本次迁移使用的版本号插入alembic_version表
alembic upgrade head
# 或重启应用自动执行:python dev.py
# Step 4: 验证
mysql -u root -p -e "USE fastapi_db1; SHOW TABLES;"
# 应该看到:alembic_version 和 users
```
新增字段:menu_string
```
# 数据库表模型
class UserBase(SQLModel):
"""用户基础字段"""
username: str = SQLField(index=True, unique=True, max_length=50)
email: EmailStr = SQLField(index=True, unique=True, max_length=100)
full_name: Optional[str] = SQLField(default=None, max_length=100)
is_active: bool = SQLField(default=True)
is_superuser: bool = SQLField(default=False)
menu_string: Optional[str] = SQLField(default=None, max_length=255) # 新增菜单权限字段
class User(UserBase, table=True):
"""用户表模型"""
__tablename__ = "users"
id: Optional[int] = SQLField(default=None, primary_key=True)
created_at: datetime = SQLField(default_factory=datetime.now)
updated_at: datetime = SQLField(default_factory=datetime.now)
```
```
(fastapi-base) PS D:\wks\elec\fastapi> alembic current
INFO [alembic.runtime.migration] Context impl MySQLImpl.
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
5e938133d636
```
```bash
# === 添加新字段完整流程 ===
# 1. 修改模型
vim src/fastapi_base/models/users.py
# 2. 生成迁移脚本
alembic revision --autogenerate -m "user add menu_string field"
# 3. 检查生成的迁移
cat alembic/versions/2026_02_25_1042_c121030d39dc_user_add_menu_string_field.py
# 4. 执行迁移
alembic upgrade head
# 5. 验证结果
alembic current
mysql -u root -p -e "USE fastapi_base; DESCRIBE users;"
# === 如果需要回滚 ===
# 查看迁移历史
alembic history
# 回滚到上一个版本
alembic downgrade -1
# 或回滚到指定版本
alembic downgrade 5e938133d636
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
使用 field_validator(推荐)
```python
from pydantic import field_validator
class Settings(BaseSettings):
# 定义为可选字段
PROMPT_PATH: str | None = None
@field_validator('PROMPT_PATH', mode='before')
@classmethod
def set_prompt_path(cls, v):
if v is not None:
# 如果环境变量/.env 提供了值,使用它
return v
# 否则使用默认逻辑
if hasattr(sys, '_MEIPASS'):
return os.path.join(getattr(sys, "_MEIPASS"), 'prompts/')
return './chat2sql/prompts/'
```
- 如果 `.env` 有 `PROMPT_PATH=./prompts`,使用 `.env` 的值
- 如果 `.env` 没有,根据 `_MEIPASS` 动态判断
|
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ Settings 实例化流程 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ Settings() 被调用 │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 步骤1:扫描类中的类型注解字段 │ │
│ │ │ │
│ │ 扫描到: │ │
│ │ API_PREFIX: str = '/api/v1' ← 普通字段 │ │
│ │ MYSQL_HOST: str = "127.0.0.1" ← 普通字段 │ │
│ │ PROMPT_PATH (property) ← 跳过!这是方法 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 步骤2:从环境变量/.env 加载值 │ │
│ │ │ │
│ │ .env 中的 PROMPT_PATH=./prompts │ │
│ │ │ │
│ │ Pydantic 查找类中是否有 PROMPT_PATH 字段定义... │ │
│ │ │ │
│ │ 找到的是 property 方法,不是字段定义 │ │
│ │ → 忽略 .env 中的 PROMPT_PATH │ │
│ │ → 不存储这个值 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 步骤3:访问 settings.PROMPT_PATH │ │
│ │ │ │
│ │ 调用 property 方法,返回计算结果: │ │
│ │ - 打包环境:sys._MEIPASS/prompts/ │ │
│ │ - 开发环境:./chat2sql/prompts/ │ │
│ │ │ │
│ │ .env 中的值被完全忽略! │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
```
|
```python
# test_settings.py
import os
from pydantic_settings import BaseSettings
from pydantic import computed_field
class TestSettings(BaseSettings):
class Config:
env_file = '.env'
extra = "ignore"
# 普通字段 - 可被覆盖
NORMAL_FIELD: str = "default_value"
# 计算字段 - 不可被覆盖
@computed_field
@property
def COMPUTED_FIELD(self) -> str:
return "computed_value"
# 创建 .env 文件
with open('.env', 'w') as f:
f.write('NORMAL_FIELD=from_env\n')
f.write('COMPUTED_FIELD=from_env\n') # 这个会被忽略
settings = TestSettings()
print(f"NORMAL_FIELD: {settings.NORMAL_FIELD}") # → "from_env" (被覆盖)
print(f"COMPUTED_FIELD: {settings.COMPUTED_FIELD}") # → "computed_value" (未覆盖)
```
|
场景 | 使用的 Prompts 目录 | 说明 | - 直接运行 exe | 内嵌目录 (`sys._MEIPASS`) | 使用打包时的 prompts | - 通过 Electron 启动 | 取决于 `.env` 配置 | 如果设置了 `PROMPT_PATH=./prompts`,则使用外部目录 | - 开发环境调试 | `./chat2sql/prompts/` | 相对路径 | 直接运行 #### 问题1:直接运行 exe 时,.env 是否生效? **答案:不生效**(但原因需要澄清) ``` ┌─────────────────────────────────────────────────────────────────────────────┐ │ 直接运行 exe 的流程 │ ├─────────────────────────────────────────────────────────────────────────────┤ │ │ │ 1. 用户双击 chat2sql_app.exe │ │ │ │ 2. PyInstaller 运行时: │ │ - 创建临时目录 sys._MEIPASS(如 C:\Users\xxx\AppData\Local\Temp\_MEIxxx)│ │ - 解压打包的文件到临时目录 │ │ │ │ 3. Settings 类加载: │ │ - 读取 .env 文件:os.path.join(os.getcwd(), '.env') │ │ - os.getcwd() = exe 所在目录(如 D:\...\python\) │ │ - 所以 .env 文件 IS 被读取了! │ │ │ │ 4. 但是: │ │ - PROMPT_PATH 是 @computed_field @property │ │ - .env 中的 PROMPT_PATH 值被忽略 │ │ - 访问 settings.PROMPT_PATH 时执行代码中的计算逻辑 │ │ - 检测到 sys._MEIPASS 存在,返回内嵌目录 │ │ │ │ 结论:.env 被读取了,但 PROMPT_PATH 值不生效(因为是计算属性) │ │ │ └─────────────────────────────────────────────────────────────────────────────┘ ```
#### 问题2:Electron 启动时,.env 是否生效?
**答案:同样不生效**(原因相同)
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ Electron 启动的流程 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 1. Electron main.js 执行: │
│ backendProcess = spawn(backendPath, [], { │
│ cwd: pythonDir // 工作目录设为 python 目录 │
│ }) │
│ │
│ 2. chat2sql_app.exe 启动: │
│ - os.getcwd() = D:\...\electron_chat2sql_frontend\python\ │
│ - sys._MEIPASS 存在(因为是打包的 exe) │
│ │
│ 3. Settings 类加载: │
│ - 读取 .env:D:\...\electron_chat2sql_frontend\python\.env │
│ - .env 文件 IS 被读取了! │
│ │
│ 4. 但是: │
│ - PROMPT_PATH 是 @computed_field @property │
│ - .env 中的 PROMPT_PATH=./prompts 被忽略 │
│ - 返回 sys._MEIPASS/prompts/(内嵌目录) │
│ │
│ 结论:无论怎么启动,.env 的 PROMPT_PATH 都不生效 │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
```
#### 问题3:exe 能否访问外部文件?
**用户的理解:**
> "因为没有使用 sys.executable,所以无法访问 exe 外部的文件,_MEIPASS 只能访问解压后的目录"
**这个理解是错误的!**
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ 常见误解 vs 实际情况 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ❌ 错误理解: │
│ ───────────── │
│ "PyInstaller 打包的 exe 只能访问 _MEIPASS 目录" │
│ "_MEIPASS 是隔离的沙箱" │
│ "exe 无法读取外部 .env 文件" │
│ │
│ │
│ ✅ 实际情况: │
│ ───────────── │
│ PyInstaller 打包的 exe 和普通 Python 程序一样: │
│ │
│ 1. 可以访问任何文件系统路径 │
│ - os.getcwd() → 当前工作目录 │
│ - 可以读取外部 .env 文件 │
│ - 可以读写任意位置的文件 │
│ │
│ 2. _MEIPASS 只是资源存放位置 │
│ - 不是隔离环境 │
│ - 只是告诉程序"打包的资源在这里" │
│ │
│ 3. 代码中的路径访问都正常工作: │
│ - open('D:/data/file.txt') ✅ 可以 │
│ - open('.env') ✅ 可以(相对路径) │
│ - os.getcwd() ✅ 返回工作目录 │
│ │
│ │
│ .env 不生效的真正原因: │
│ ─────────────────── │
│ 不是"无法访问外部文件",而是代码设计问题: │
│ - PROMPT_PATH 定义为 @computed_field @property │
│ - 这是只读计算属性,无法被 .env 覆盖 │
│ - 与文件访问能力无关 │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
```
|
必须修改代码,将 `@computed_field @property` 改为可覆盖的字段:
```python
from pydantic import field_validator
class Settings(BaseSettings):
# 改为普通字段 + 验证器
PROMPT_PATH: str | None = None
@field_validator('PROMPT_PATH', mode='before')
@classmethod
def set_prompt_path(cls, v):
if v is not None:
# .env 提供了值,使用它
return v
# 否则使用默认逻辑
if hasattr(sys, '_MEIPASS'):
return os.path.join(getattr(sys, "_MEIPASS"), 'prompts/')
return './chat2sql/prompts/'
```
修改后的效果:
场景 | .env 配置 | PROMPT_PATH 结果 |
Electron 启动 | `PROMPT_PATH=./prompts` | `D:\...\python\prompts/`(外部目录) |
Electron 启动 | 未配置 | `sys._MEIPASS/prompts/`(内嵌目录) |
直接运行 exe | `PROMPT_PATH=./prompts` | `D:\...\python\prompts/`(外部目录) |
直接运行 exe | 未配置 | `sys._MEIPASS/prompts/`(内嵌目录) |
#### 总结
问题 | 答案 |
**exe 能访问外部文件吗?** | ✅ 能,与普通程序无区别 |
**_MEIPASS 是沙箱吗?** | ❌ 不是,只是资源解压目录 |
**.env 被读取了吗?** | ✅ 被读取了 |
**PROMPT_PATH 为什么不生效?** | 因为是 `@computed_field @property`(只读计算属性) |
**如何让 .env 生效?** | 修改代码,使用 `field_validator` |
核心要点:
1. 问题不在于"文件访问能力"
2. 问题在于"代码设计"——使用了不可覆盖的计算属性
3. PyInstaller 打包不会限制文件系统访问
4. 如果需要外部配置生效,必须修改代码
|
### 9.1 问题:Config 类是否可以省略
用户的问题:示例代码中没有 Config 类,为什么也能读取 .env 文件?
```python
# 没有 Config 类的示例
class Settings(BaseSettings):
PROMPT_PATH: str | None = None
@field_validator('PROMPT_PATH', mode='before')
@classmethod
def set_prompt_path(cls, v):
# ...
```
**答案:是的,Config 可以省略(如果使用默认配置)**
### 9.2 Pydantic Settings 默认行为
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ BaseSettings 默认行为 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ pydantic-settings 的 BaseSettings 默认会: │
│ │
│ 1. 自动读取系统环境变量 │
│ - os.environ 中的所有变量 │
│ │
│ 2. 自动读取当前目录的 .env 文件 │
│ - 默认路径:os.getcwd()/.env │
│ - 即当前工作目录下的 .env 文件 │
│ │
│ 3. 默认配置 │
│ - env_file: '.env'(当前目录) │
│ - env_file_encoding: 'utf-8' │
│ - env_prefix: ''(无前缀) │
│ - env_nested_delimiter: None │
│ - extra: 'ignore' │
│ │
│ 所以如果: │
│ - .env 文件在当前工作目录 │
│ - 使用 utf-8 编码 │
│ - 不需要特殊配置 │
│ │
│ 那么可以不写 Config 类! │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
```
### 9.3 什么时候需要 Config 类
```python
class Settings(BaseSettings):
class Config:
env_file = '.env' # 指定 .env 文件路径(非默认时需要)
env_file_encoding = 'gbk' # 指定编码(非 utf-8 时需要)
env_prefix = 'APP_' # 环境变量前缀(需要时配置)
extra = 'forbid' # 额外字段处理策略
case_sensitive = True # 区分大小写
```
需要 Config 的场景:
场景 | 需要配置 |
.env 在当前目录 | ❌ 不需要(默认) |
.env 在其他目录 | ✅ 需要指定 `env_file` |
非 utf-8 编码 | ✅ 需要指定 `env_file_encoding` |
环境变量需要前缀(如 `APP_MYSQL_HOST`) | ✅ 需要指定 `env_prefix` |
禁止未定义字段 | ✅ 需要指定 `extra = 'forbid'` |
### 9.4 Config 类已弃用,推荐使用 model_config
**pydantic v2 的变化**:`class Config` 已被弃用,推荐使用 `model_config`。
#### 旧写法(已弃用但仍然支持)
```python
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
MYSQL_HOST: str = "127.0.0.1"
class Config:
env_file = '.env'
env_file_encoding = 'utf-8'
extra = 'ignore'
```
#### 新写法(推荐)
```python
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
# 使用 model_config 字典
model_config = SettingsConfigDict(
env_file='.env',
env_file_encoding='utf-8',
extra='ignore',
env_prefix='', # 环境变量前缀
case_sensitive=False, # 是否区分大小写
env_nested_delimiter='__', # 嵌套变量分隔符
)
MYSQL_HOST: str = "127.0.0.1"
```
### 9.5 完整示例:新写法 + field_validator
```python
import os
import sys
from pydantic_settings import BaseSettings, SettingsConfigDict
from pydantic import field_validator
class Settings(BaseSettings):
# 使用新的 model_config 配置方式
model_config = SettingsConfigDict(
env_file=os.path.join(os.getcwd(), '.env'), # .env 文件路径
env_file_encoding='utf-8', # 编码
extra='ignore', # 忽略未定义字段
env_ignore_empty=True, # 忽略空值
)
# 静态字段
API_PREFIX: str = '/api/v1'
API_HOST: str = '127.0.0.1'
API_PORT: int = 8888
# MySQL 配置
MYSQL_HOST: str = "127.0.0.1"
MYSQL_PORT: int = 3306
MYSQL_USERNAME: str = "root"
MYSQL_PASSWORD: str = "memect2025"
MYSQL_DATABASE: str = "memect"
# 可被 .env 覆盖的 PROMPT_PATH
PROMPT_PATH: str | None = None
@field_validator('PROMPT_PATH', mode='before')
@classmethod
def set_prompt_path(cls, v):
"""如果 .env 未提供值,使用默认逻辑"""
if v is not None:
return v # 使用 .env 的值
# 默认逻辑
if hasattr(sys, '_MEIPASS'):
return os.path.join(getattr(sys, "_MEIPASS"), 'prompts/')
return './chat2sql/prompts/'
# 其他计算字段
@property
def SQLALCHEMY_DATABASE_URI(self) -> str:
return f"mysql+pymysql://{self.MYSQL_USERNAME}:{self.MYSQL_PASSWORD}@{self.MYSQL_HOST}:{self.MYSQL_PORT}/{self.MYSQL_DATABASE}"
settings = Settings()
```
### 9.6 SettingsConfigDict 所有选项
```python
from pydantic_settings import SettingsConfigDict
model_config = SettingsConfigDict(
# .env 文件配置
env_file='.env', # .env 文件路径,可以是字符串或 PathLike
env_file_encoding='utf-8', # .env 文件编码
# 环境变量配置
env_prefix='', # 环境变量前缀,如 'APP_' → 读取 APP_MYSQL_HOST
env_nested_delimiter=None, # 嵌套分隔符,如 '__' → MYSQL__HOST = {'MYSQL': {'HOST': ...}}
env_parse_none_str=None, # 解析为 None 的字符串
env_parse_enums=None, # 是否解析枚举
# 字段处理
extra='ignore', # 'ignore' | 'allow' | 'forbid'
case_sensitive=False, # 是否区分大小写
# 其他
env_ignore_empty=True, # 是否忽略空环境变量
cli_parse_args=None, # CLI 参数解析
cli_prog_name=None, # CLI 程序名
cli_avoid_json=None, # 避免 JSON 解析
cli_hide_none_type=None, # 隐藏 None 类型
cli_shortcuts=None, # CLI 快捷方式
secrets_dir=None, # secrets 目录
json_schema_extra=None, # JSON Schema 额外信息
)
```
### 9.7 对比总结
方式 | 写法 | 状态 |
**不写 Config** | 无 | ✅ 使用默认配置 |
**class Config** | 嵌套类 | ⚠️ 已弃用但支持 |
**model_config** | 字典 + SettingsConfigDict | ✅ 推荐写法 |
### 9.8 当前项目建议
当前项目 `D:\wks\aisql\chat2sql-develop\src\chat2sql\core\settings.py` 使用的是旧写法:
```python
class Settings(BaseSettings):
class Config:
env_file = os.path.join(os.getcwd(), '.env')
env_file_encoding = "utf-8"
extra = "ignore"
env_ignore_empty = True
```
**建议**:迁移到新写法:
```python
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(
env_file=os.path.join(os.getcwd(), '.env'),
env_file_encoding="utf-8",
extra="ignore",
env_ignore_empty=True,
)
```
### 9.9 总结
问题 | 答案 |
**Config 可以省略吗?** | ✅ 可以,如果使用默认配置(.env 在当前目录) |
**默认 .env 位置?** | `os.getcwd()/.env`(当前工作目录) |
**Config 已弃用?** | ⚠️ 是的,推荐使用 `model_config = SettingsConfigDict(...)` |
**新写法优势?** | 类型提示更好、IDE 支持更好、与 pydantic v2 一致 |
|
|
|
|
|