chore: 清理历史资料并规范工程目录与开发规则
为减少仓库噪声和重复规则,整理当前开发入口并移除已废弃资料。 - 精简根 AGENTS,补充四个应用规则与 README,统一规范入口 - 归整部署文件、SOP 原稿和参考资源,修正迁移后的脚本路径 - 取消虚拟环境、工具状态、业务导出及备份的跟踪,本机所需原件保留 - 移除停用的 graphify 配置、脚本与产物 - 删除历史业务文档、执行计划、OpenSpec 业务变更和移动 UI 原型 - 清理失效引用,历史细胞产品导入改为必须显式指定 --file 验证:git diff --cached --check、OpenSpec 严格校验、导入参数隔离检查 3/3 通过; 迁移文件完整性和现行链接已核对。未执行部署、业务数据导入或服务启停。
This commit is contained in:
1 parent
8321246765
commit
b220298f89
4824 files changed
+571
-2450335
No files matched your search
+1
-18
@@ -1,20 +1,3 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"graphify-backend": {
|
||||
"command": "/opt/anaconda3/bin/python",
|
||||
"args": [
|
||||
"-m",
|
||||
"graphify.serve",
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/graphify-out/graph.json"
|
||||
]
|
||||
},
|
||||
"graphify-frontend": {
|
||||
"command": "/opt/anaconda3/bin/python",
|
||||
"args": [
|
||||
"-m",
|
||||
"graphify.serve",
|
||||
"/Users/wuzilong/work/00code/zkxw/web-admin/graphify-out/graph.json"
|
||||
]
|
||||
}
|
||||
}
|
||||
"mcpServers": {}
|
||||
}
|
||||
@@ -1,21 +1,12 @@
|
||||
---
|
||||
description: OpenSpec 工程规范与 zkxw SHALL 级约束入口
|
||||
description: zkxw 全局开发规则与 OpenSpec 入口
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# OpenSpec(zkxw)
|
||||
# 工程入口
|
||||
|
||||
- 使用 OpenSpec CLI 管理变更时,遵守仓库 `openspec/config.yaml` 与既有 `changes/` 结构;新能力在 `proposal.md` 的 Capabilities 中与 `specs/<kebab-name>/spec.md` 一一对应。
|
||||
- **实现或评审代码**时,对照当前生效的 spec(`### Requirement` / `#### Scenario`),并与 `docs/中科兴皖需求V0.3.md`、两份 UI 规范交叉核对,避免与 SHALL 冲突。
|
||||
|
||||
**路径(活跃变更)**
|
||||
|
||||
- `openspec/changes/zkxw-engineering-standards/specs/*/spec.md`:`architecture-conventions`、`api-conventions`、`ui-conventions`、`coding-conventions`、`database-conventions`、`workflow-conventions`、`permission-conventions`。
|
||||
|
||||
**路径(归档后基线)**
|
||||
|
||||
- 若已归档:`openspec/specs/<capability>/spec.md` 为长期基线;与 `changes/` 并存时 **以 `openspec/specs/` 为准**。
|
||||
|
||||
**技能入口(可选)**
|
||||
|
||||
- `.cursor/commands/opsx-*.md` 与 `.cursor/skills/openspec-*/SKILL.md` 用于 propose / apply / explore / archive 流程。
|
||||
- 阅读根 `AGENTS.md` 及目标应用的 `AGENTS.md`,从 `docs/README.md` 查对应规范,不自动加载全部历史方案。
|
||||
- 使用 OpenSpec 时遵守 `openspec/config.yaml`;新能力在 proposal 的 Capabilities 与 `specs/<capability>/spec.md` 一一对应。
|
||||
- 工程规范当前位于 `openspec/changes/zkxw-engineering-standards/specs/`。`openspec/specs/` 尚未建立长期基线,不能只因目录存在就认为归档完成。
|
||||
- 归档必须核对并合并当前有效 Requirement/Scenario,保留未完成或被替代事项的说明,更新所有规范入口。
|
||||
- `.cursor/commands/opsx-*.md` 与 `.cursor/skills/openspec-*/SKILL.md` 保留为工具流程入口。
|
||||
@@ -1,26 +1,11 @@
|
||||
---
|
||||
description: web-admin 列表主标题竖条与新增编辑弹窗(Element Plus)约定
|
||||
description: web-admin 页面模式、表格与弹窗规范入口
|
||||
globs: web-admin/**/*.vue,web-admin/**/*.scss
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# web-admin · 列表标题与 CRUD 弹窗
|
||||
# Web UI 入口
|
||||
|
||||
细则以 `docs/web-admin-ui-spec-fresh-mint.md` §7、§9.3.1 为准;以下为仓库**已落地**约定,新增或改造列表/弹窗时须遵守。
|
||||
|
||||
## 列表面板主标题(绿色竖条)
|
||||
|
||||
- 需要标题前 **主色竖条**(`h2.title-bar-heading` + `::before`)时:在 `web-admin/src/App.vue` 的 **`panelTitleBarTextRules`** 里**写入完整标题文案**(如「租户列表」),不要只靠自动图标逻辑。
|
||||
- 勿用会误配的短正则:「租户」含「用户」子串,会命中用户类图标;**竖条标题一律显式列入 `panelTitleBarTextRules`**。
|
||||
|
||||
## 新增 / 编辑 `el-dialog`
|
||||
|
||||
- 根节点加 **`class="dialog-form-layout"`**(样式在 `src/assets/styles/core/app.scss`):`max-height: calc(100vh - 100px)`,**仅** `.el-dialog__body` 纵向滚动;头、底栏不随内容滚动;水平垂直居中依赖 **`align-center`** 与全局 `ElConfigProvider`。
|
||||
- 建议 **`append-to-body`** + **`modal-class="protocol-dialog-mask"`**(与采集协议单 `ProtocolFormDialog` 一致);所有带 `dialog-form-layout` 的弹窗遮罩在 `app.scss` 另有 **`:has()` 全局兜底**(顶对齐 + 视口顶距 16px)。
|
||||
- 字段少用 **`width` 约 560~720px**;字段很多时 **加宽** `width`,栅格用 **`.form-grid-2`(默认)~`.form-grid-5`(列数上限 5)**。
|
||||
- 表单 **`label-position="top"`**;长字段、备注用 **`grid-column: 1 / -1`** 或 **`.form-col-span-2`**;栅格内 **`el-form-item` 宜 `margin-bottom: 0`**,纵向间距靠栅格 **`gap`**。
|
||||
- **勿**在页面 `scoped` 里重复定义 `.form-grid-2`、`.form-col-span-2`、`.sort-field`、`.form-tip`(已在 `app.scss` 全局提供)。
|
||||
|
||||
## 底栏
|
||||
|
||||
- 取消 / 确定使用 **Close / Check** 图标 + 文案,图标与字间距 **6px**(与规范及现有页一致)。
|
||||
- 阅读 `web-admin/AGENTS.md`;其中包含标题竖条、表格、弹窗、字段注释与上传等专用约束。
|
||||
- 详细设计以 `docs/web-admin-ui-spec-fresh-mint.md` 为准,新页面先按 `docs/web-admin-ui-pattern-index.md` 选择 P01–P27 与参考源码。
|
||||
- 复用全局样式和指定模板,不在本规则重复维护数值或模板代码。
|
||||
+6
-122
@@ -1,124 +1,8 @@
|
||||
# zkxw 工程 Cursor 规则
|
||||
# Cursor 工程规则入口
|
||||
|
||||
本仓库含 Web 管理端(`web-admin`)、小程序端(`miniprogram`)等子工程。实现或修改 UI 时,**以文档为权威来源**,本文件仅作快速索引与强约束摘要。
|
||||
本文件只适配 Cursor,不独立复制工程约束。
|
||||
|
||||
## 权威文档(必读路径)
|
||||
|
||||
- **Web 管理端(Fresh Mint)**:`docs/web-admin-ui-spec-fresh-mint.md`
|
||||
适用范围:`web-admin`(Vue 3 + Element Plus + SCSS/Tailwind)。色值、布局、列表、弹窗、树形路径等均以该文档及其中引用的 `fresh-mint-green.html` 为准。
|
||||
- **界面模式索引(选型 + 参考页)**:`docs/web-admin-ui-pattern-index.md`
|
||||
新功能开发前按「功能特征 → 模式编号 P01–P27 → Hash/源码」选型;与 Demo 模板页、业务参考页对照。
|
||||
- **小程序端(三墙 Demo 体系)**:`docs/miniprogram-ui-spec-sanqiang.md`
|
||||
适用范围:`miniprogram`(客户端 / 供应商端 / 配送员端)。视觉以该文档及 `mobile-ui/sanqiang-*.html` 为准。
|
||||
|
||||
修改对应子工程前,若涉及界面,应先检索上述文档中的章节(类名、Token、结构顺序)。
|
||||
|
||||
## OpenSpec 工程规范
|
||||
|
||||
详见 **Cursor 项目规则**:`.cursor/rules/openspec-zkxw-standards.mdc`(`alwaysApply: true`,与 Cursor 2.x 推荐方式一致)。根目录本文件保留 UI 与工程摘要;OpenSpec 路径与 SHALL 门禁以该 `.mdc` 为准。
|
||||
|
||||
---
|
||||
|
||||
## Web 管理端(`web-admin`)摘要
|
||||
|
||||
**列表主标题竖条、新增/编辑弹窗两列~五列、`dialog-form-layout` 限高与正文滚动** 等实现约定:见 Cursor 规则 **`.cursor/rules/web-admin-crud-dialog.mdc`**(编辑 `web-admin` 下 `.vue` / `.scss` 时由 Cursor 按 glob 自动附加该规则)。
|
||||
|
||||
### 技术栈与实现方式
|
||||
|
||||
- Vue 3 + Element Plus;主题与皮肤通过 **CSS 变量 + Element 主题变量 + 全局覆盖** 对齐规范,**勿随意改 Token 色值**。
|
||||
- 树形主数据全路径:`web-admin/src/utils/tree-path.ts`(如 `buildTreePathLabelFromFlat`、`buildPathLabelMapFromTreeRoots`、`resolveTreePathDisplay`)。路径分隔符统一为 **` / `**(半角斜杠两侧各一空格)。
|
||||
|
||||
### 页面级 UI 模板(固定五页 + 工作台入口)
|
||||
|
||||
上述页面均挂在侧栏 **「Demo」** 菜单下。新开发的列表/表单页**对照以下页面实现**,勿另起一套布局与类名习惯;仍须同时遵守 `docs/web-admin-ui-spec-fresh-mint.md`。
|
||||
|
||||
| 用途 | Hash 路由 | 源码 |
|
||||
|------|-----------|------|
|
||||
| 工作台(入口与说明) | `/#/demo/console` | `web-admin/src/views/dashboard/console/index.vue` |
|
||||
| 标准列表(筛选、工具栏、表格、分页) | `/#/demo/unit` | `web-admin/src/views/system/unit/index.vue` |
|
||||
| 复杂业务列表(状态、操作列等) | `/#/demo/order` | `web-admin/src/views/system/order/index.vue` |
|
||||
| 商品类列表与查看弹窗联动 | `/#/demo/product` | `web-admin/src/views/system/product/index.vue` |
|
||||
| 树形主数据 | `/#/demo/category` | `web-admin/src/views/system/category/index.vue` |
|
||||
| 系统用户类 CRUD | `/#/demo/user` | `web-admin/src/views/system/user/index.vue` |
|
||||
|
||||
商品**只读查看弹窗**结构对齐:`web-admin/src/components/business/product-view-dialog/index.vue`(由商品管理页打开,可与上表第三行一并参照)。
|
||||
|
||||
### 视觉与 Token(与规范一致)
|
||||
|
||||
- 主色链:`--primary` `#10b981`、`--primary-dark` `#059669`、`--primary-bg` `#ecfdf5`;中性字与边框见规范第 2 节表格。
|
||||
- 风格:清爽 B 端、薄荷绿 + Slate;**避免一屏多种高饱和色**;次要信息用 muted 与 `.tag.muted`。
|
||||
- 字体:Inter + 系统栈;正文 14px / line-height 1.5。
|
||||
|
||||
### 布局与页面骨架
|
||||
|
||||
- 侧栏 220px、顶栏 56px、内容区 padding 24px;面板圆角 10px,输入/按钮等 8px 级圆角见规范。
|
||||
- 列表页典型顺序:**筛选 `filter-block`(可选)→ 列表工具栏 → `list-body` / `list-wrap`(含 `el-table`)→ 分页**。
|
||||
- **单标题**:列表面板仅一个 `panel-top` 主标题;筛选区内不要再加「筛选条件」等重复标题。
|
||||
- **单分隔线**:工具栏与列表之间靠 `list-body` 顶边框;列表与分页之间保留列表底边与分页顶边规则,避免双线叠加(见规范列表与分页章节)。
|
||||
|
||||
### 筛选与表单
|
||||
|
||||
- 筛选栅格、label 11px、控件高度 36px、placeholder 规则等见规范第 5 节。
|
||||
- 筛选区内部:**不要用横向分割线** 区分多行条件与展开区,仅用 gap / margin。
|
||||
- 弹窗表单:**`label-position="top"`**;两列可用 `form-grid-2`;长字段 `grid-column: 1 / -1`。
|
||||
- 弹窗底栏:取消/确定建议 **Close / Check** 图标,图标与文字间距 **6px**;footer 按钮高度与圆角等见规范 `el-dialog` 小节。
|
||||
- 长表单:`el-form` 建议 **`scroll-to-error`** 与 **`scroll-into-view-options`**;校验失败可配合 `ElMessageBox.alert` 提示(与已有页一致)。
|
||||
- 遮罩:**无背景模糊**(`backdrop-filter: none`、`modal-class` 等与现有 `order-module-dialog-mask` 等策略一致)。
|
||||
|
||||
### 列表工具栏与表格
|
||||
|
||||
- 工具栏:**「已选 N 项」在最前**,其后从左到右排列主按钮与批量操作;**禁止**用 `space-between` 把整组按钮推到行末。
|
||||
- **同一行仅第一个主行动** 使用主色实心(`type="primary"` / `btn-main`);批量删除、导入导出等为中性描边。
|
||||
- **操作列**:`fixed="right"`,预留足够宽度(建议 min-width ≥ 260px),避免按钮被裁切。
|
||||
- 主表:`list-table` + `stripe`;与参考页「订单列表」`/#/demo/order` 一致时 **勿随意加 `border`**(以规范及上表模板页为准)。
|
||||
- 分页:表格与 **`el-pagination`** 同放在 **`list-body.list-wrap`** 内;外层 **`.pagination.list-pager-bar`**,`margin-top: 0`,上边框与 padding 见规范第 7.9 节。
|
||||
|
||||
### 树形选择器与查看态
|
||||
|
||||
- 下拉树节点文案:**本级名称**;选中后输入框、表格列、导出、查看弹窗:**全路径**(见规范树形与第 5.6 节)。
|
||||
|
||||
### 状态与业务色
|
||||
|
||||
- 进行中/主状态:浅绿底 + 深绿字;警告琥珀 `#d97706` 系;危险红系用于强提醒;与订单/配送枚举映射时保持语义一致(见规范第 12 节)。
|
||||
|
||||
---
|
||||
|
||||
## 小程序端(`miniprogram`)摘要
|
||||
|
||||
### Token 与适配
|
||||
|
||||
- 品牌绿:`#0a9f6d` / `#087f57` / `#e9f9f2`;大麦黄辅助:`#d6a343` / `#fff7e8`;中性背景与边框见规范第 2.1 节。
|
||||
- 主基线宽度 **375px**,兼容 **414px**;间距节奏 8–16;卡片圆角 12–16px,大容器 18–22px。
|
||||
|
||||
### 三端统一骨架
|
||||
|
||||
- 顺序一般为:Header(绿渐变)→ 统计/提示(可选)→ 功能宫格 → 主列表/任务 → 底部固定区(客户端:结算条 + TabBar)。
|
||||
- Header:绿色渐变、标题左对齐、操作右对齐、下缘大圆角(约 20–24px)。
|
||||
|
||||
### 组件与媒体(强约束)
|
||||
|
||||
- 列表缩略图:**1:1**,`aspectFill`;轮播:**4:3**,`aspectFill`;详情图:**widthFix**;视频建议 720p 内、尽量短(见规范第 7.0 节)。
|
||||
- 状态标签语义:`warning` 琥珀待处理、`success` 绿色进行中或完成、`danger` 红色异常;避免同屏过多高饱和色。
|
||||
- 底部结算条:数量 → 总价 → 主按钮;主按钮视觉优先级最高。
|
||||
- 触达区域不小于 **44×44**;注意横滑与纵向滚动冲突(尤其一级分类横滑)。
|
||||
|
||||
### 从 Demo 到页面
|
||||
|
||||
- 规范第 11 节给出 Demo class 到 `pages/**/*.vue` 的建议类名;优先统一 Token 与骨架,再收敛公共样式到 `src/styles` 或公共组件。
|
||||
- 新增页面类型应先在 `docs/miniprogram-ui-spec-sanqiang.md` 补充结构说明再落代码(见规范第 10 节)。
|
||||
|
||||
---
|
||||
|
||||
## 通用开发原则
|
||||
|
||||
- **最小必要改动**:不扩大范围、不做无关重构;新增代码与周边文件的命名、类型、导入风格保持一致。
|
||||
- **跨端颜色不要混用**:管理端用 Fresh Mint 链(`#10b981` 等);小程序用三墙 Token(`#0a9f6d` 等);除非产品明确要求对齐,否则不要简单复制另一端的色值。
|
||||
- **后端/API 变更**:若 UI 改动依赖接口字段(如树节点 id、分类全路径),需同步评估接口与类型定义,避免前端仅末级展示造成歧义。
|
||||
- 提交信息使用完整句子,说明「改了什么、为何」,避免无意义前缀堆砌。
|
||||
|
||||
---
|
||||
|
||||
## 语言
|
||||
|
||||
- 与用户沟通时:若用户使用简体中文,助手回复使用**简体中文**。
|
||||
- 代码注释与用户可见文案:遵循项目现有语言习惯(中文业务文案以产品为准)。
|
||||
- 先读根 `AGENTS.md`,再读修改目录的 `AGENTS.md`:`backend/`、`web-admin/`、`mobile-h5/`、`mobile-oa-h5/`。
|
||||
- 跨端任务读取涉及端的规则;UI 规范、页面模式和数据约束从对应 AGENTS 与 `docs/README.md` 按需查阅。
|
||||
- OpenSpec 流程入口为 `openspec/config.yaml` 与 `.cursor/rules/openspec-zkxw-standards.mdc`;正式规范与历史变更应区分。
|
||||
- 不维护第二套颜色、弹窗或业务规则;发现冲突记录依据,不依据旧项目名称和不存在的目录实施功能。
|
||||
+4
-3
@@ -3,7 +3,6 @@
|
||||
|
||||
backend/
|
||||
miniprogram/
|
||||
mobile-ui/
|
||||
docs/
|
||||
openspec/
|
||||
.cursor/
|
||||
@@ -15,8 +14,6 @@ dist/
|
||||
web-admin/node_modules
|
||||
web-admin/dist
|
||||
web-admin/.git
|
||||
web-admin/graphify-out
|
||||
web-admin/src/graphify-out
|
||||
web-admin/.husky
|
||||
web-admin/CHANGELOG*
|
||||
web-admin/demo-*.html
|
||||
@@ -36,3 +33,7 @@ mobile-oa-h5/.env.*
|
||||
|
||||
# 小程序原生工程不进 web 镜像构建上下文
|
||||
mobile-h5/
|
||||
|
||||
# 本机资料与非运行时原稿不进入镜像构建上下文
|
||||
.local/
|
||||
resources/
|
||||
+14
-6
@@ -30,6 +30,10 @@ Desktop.ini
|
||||
|
||||
# 依赖与构建产物
|
||||
node_modules/
|
||||
venv/
|
||||
.venv/
|
||||
unpackage/
|
||||
.pnpm-store/
|
||||
dist/
|
||||
build/
|
||||
out/
|
||||
@@ -56,6 +60,9 @@ __pycache__/
|
||||
*.key
|
||||
*.p12
|
||||
secrets/
|
||||
|
||||
# 本机资料:证书、凭据、真实业务数据和备份
|
||||
/.local/
|
||||
生产环境信息/root密码
|
||||
生产环境信息/*_nginx/
|
||||
生产环境信息/微信生产.env.snippet
|
||||
@@ -67,6 +74,9 @@ secrets/
|
||||
logs/
|
||||
tmp/
|
||||
temp/
|
||||
.tmp/
|
||||
.tmp-*/
|
||||
.playwright-mcp/
|
||||
*.tmp
|
||||
*.bak
|
||||
*.cache
|
||||
@@ -96,12 +106,10 @@ exports/
|
||||
# 本地进程 PID(如 backend/app.pid,由 start-*.sh 写入,勿提交)
|
||||
*.pid
|
||||
|
||||
# Graphify:AST 解析缓存(体积大、可本地再生成;常见于 backend/app/、web-admin/src/ 下)
|
||||
**/graphify-out/cache/
|
||||
|
||||
# Graphify:增量检测/状态文件(commit hook 或 update-graph.sh 触发后产生)
|
||||
**/.graphify_detect.json
|
||||
**/.graphify_incremental.json
|
||||
# 已停用工具的本机残留不再入库
|
||||
**/graphify-out/
|
||||
**/.graphify-out/
|
||||
**/.graphify*.json
|
||||
|
||||
# 本地密钥(勿提交)
|
||||
测试环境机器的账号密码
|
||||
@@ -1,5 +0,0 @@
|
||||
- generic [ref=e1]:
|
||||
- generic "Toggle devtools panel" [ref=e2] [cursor=pointer]:
|
||||
- img [ref=e3]
|
||||
- generic "Toggle Component Inspector" [ref=e8] [cursor=pointer]:
|
||||
- img [ref=e9]
|
||||
@@ -1,119 +0,0 @@
|
||||
- generic [active] [ref=e13]:
|
||||
- generic [ref=e64]:
|
||||
- complementary [ref=e65]:
|
||||
- generic [ref=e67]:
|
||||
- generic [ref=e68] [cursor=pointer]:
|
||||
- img "中科兴皖" [ref=e70]
|
||||
- paragraph [ref=e71]: 中科兴皖
|
||||
- menubar [ref=e75]:
|
||||
- menuitem "工作台" [ref=e76] [cursor=pointer]:
|
||||
- generic [ref=e78]: 工作台
|
||||
- menuitem "运营支撑" [expanded] [ref=e79]:
|
||||
- generic [ref=e80] [cursor=pointer]:
|
||||
- img [ref=e83]
|
||||
- generic [ref=e85]: 运营支撑
|
||||
- img [ref=e87]
|
||||
- menu [ref=e89]:
|
||||
- menuitem "洁净室管理" [ref=e90] [cursor=pointer]:
|
||||
- generic [ref=e91]: 洁净室管理
|
||||
- menuitem "库位管理" [ref=e92] [cursor=pointer]:
|
||||
- generic [ref=e93]: 库位管理
|
||||
- menuitem "展馆预约" [ref=e94] [cursor=pointer]:
|
||||
- generic [ref=e95]: 展馆预约
|
||||
- menuitem "demo" [ref=e96] [cursor=pointer]:
|
||||
- generic [ref=e97]: demo
|
||||
- menuitem "基础信息管理" [expanded] [ref=e98]:
|
||||
- generic [ref=e99] [cursor=pointer]:
|
||||
- img [ref=e102]
|
||||
- generic [ref=e104]: 基础信息管理
|
||||
- img [ref=e106]
|
||||
- menu [ref=e108]:
|
||||
- menuitem "客户管理" [ref=e109] [cursor=pointer]:
|
||||
- generic [ref=e110]: 客户管理
|
||||
- menuitem "细胞样本类型管理" [ref=e111] [cursor=pointer]:
|
||||
- generic [ref=e112]: 细胞样本类型管理
|
||||
- menuitem "采集地区及医院管理" [ref=e113] [cursor=pointer]:
|
||||
- generic [ref=e114]: 采集地区及医院管理
|
||||
- menuitem "采集协议号管理" [ref=e115] [cursor=pointer]:
|
||||
- generic [ref=e116]: 采集协议号管理
|
||||
- menuitem "样本编码管理" [ref=e117] [cursor=pointer]:
|
||||
- generic [ref=e118]: 样本编码管理
|
||||
- menuitem "细胞存储管理" [expanded] [ref=e119]:
|
||||
- generic [ref=e120] [cursor=pointer]:
|
||||
- img [ref=e123]
|
||||
- generic [ref=e125]: 细胞存储管理
|
||||
- img [ref=e127]
|
||||
- menu [ref=e129]:
|
||||
- menuitem "采集协议单" [ref=e130] [cursor=pointer]:
|
||||
- generic [ref=e131]: 采集协议单
|
||||
- menuitem "采集协议单审核" [ref=e132] [cursor=pointer]:
|
||||
- generic [ref=e133]: 采集协议单审核
|
||||
- menuitem "收款确认(财务)" [ref=e134] [cursor=pointer]:
|
||||
- generic [ref=e135]: 收款确认(财务)
|
||||
- menuitem "退费确认(财务)" [ref=e136] [cursor=pointer]:
|
||||
- generic [ref=e137]: 退费确认(财务)
|
||||
- menuitem "分期缴费预警" [ref=e138] [cursor=pointer]:
|
||||
- generic [ref=e139]: 分期缴费预警
|
||||
- menuitem "工作流管理" [expanded] [ref=e140]:
|
||||
- generic [ref=e141] [cursor=pointer]:
|
||||
- img [ref=e144]
|
||||
- generic [ref=e147]: 工作流管理
|
||||
- img [ref=e149]
|
||||
- menu [ref=e151]:
|
||||
- menuitem "流程配置" [ref=e152] [cursor=pointer]:
|
||||
- generic [ref=e153]: 流程配置
|
||||
- menuitem "待办中心" [ref=e154] [cursor=pointer]:
|
||||
- generic [ref=e155]: 待办中心
|
||||
- menuitem "已办中心" [ref=e156] [cursor=pointer]:
|
||||
- generic [ref=e157]: 已办中心
|
||||
- menuitem "我的申请" [ref=e158] [cursor=pointer]:
|
||||
- generic [ref=e159]: 我的申请
|
||||
- menuitem "请假申请" [ref=e160] [cursor=pointer]:
|
||||
- generic [ref=e161]: 请假申请
|
||||
- main [ref=e162]:
|
||||
- generic [ref=e164]:
|
||||
- generic [ref=e165]:
|
||||
- generic [ref=e166]:
|
||||
- img [ref=e169] [cursor=pointer]
|
||||
- img [ref=e173] [cursor=pointer]
|
||||
- navigation "breadcrumb" [ref=e175]:
|
||||
- list [ref=e176]:
|
||||
- listitem [ref=e177]:
|
||||
- generic [ref=e179]: 工作台
|
||||
- generic [ref=e180]:
|
||||
- img [ref=e183] [cursor=pointer]
|
||||
- img [ref=e187] [cursor=pointer]
|
||||
- generic [ref=e188]:
|
||||
- list [ref=e190]:
|
||||
- listitem [ref=e191] [cursor=pointer]: 工作台
|
||||
- img [ref=e195] [cursor=pointer]
|
||||
- generic [ref=e199]:
|
||||
- generic [ref=e200]:
|
||||
- heading "工作台" [level=1] [ref=e201]
|
||||
- paragraph [ref=e202]: 以下为固定参考页,新增管理端界面请对照布局与类名;视觉以文档为准。
|
||||
- generic [ref=e203]:
|
||||
- heading "快速入口" [level=2] [ref=e205]: 快速入口
|
||||
- generic [ref=e207]:
|
||||
- paragraph [ref=e208]: "详见仓库根目录 `.cursorrules` 中「页面级 UI 模板(固定五页)」。"
|
||||
- generic [ref=e209]:
|
||||
- button "商品单位" [ref=e210] [cursor=pointer]:
|
||||
- generic [ref=e211]: 商品单位
|
||||
- button "用户管理" [ref=e212] [cursor=pointer]:
|
||||
- generic [ref=e213]: 用户管理
|
||||
- button "商品分类" [ref=e214] [cursor=pointer]:
|
||||
- generic [ref=e215]: 商品分类
|
||||
- button "租户管理" [ref=e216] [cursor=pointer]:
|
||||
- generic [ref=e217]: 租户管理
|
||||
- button "客户管理" [ref=e218] [cursor=pointer]:
|
||||
- generic [ref=e219]: 客户管理
|
||||
- generic [ref=e1]:
|
||||
- generic "Toggle devtools panel" [ref=e2] [cursor=pointer]:
|
||||
- img [ref=e3]
|
||||
- generic "Toggle Component Inspector" [ref=e8] [cursor=pointer]:
|
||||
- img [ref=e9]
|
||||
- alert [ref=e220]:
|
||||
- img [ref=e222]
|
||||
- generic [ref=e224]:
|
||||
- heading "登录成功" [level=2] [ref=e225]
|
||||
- paragraph [ref=e227]: 欢迎回来,系统管理员!
|
||||
- img [ref=e229] [cursor=pointer]
|
||||
@@ -1,29 +0,0 @@
|
||||
- generic [active] [ref=e13]:
|
||||
- generic [ref=e231]:
|
||||
- generic [ref=e233]:
|
||||
- img "中科兴皖" [ref=e235]
|
||||
- heading "一体化管理门户" [level=1] [ref=e236]
|
||||
- paragraph [ref=e237]: 安全 · 高效 · 可扩展
|
||||
- generic [ref=e238]:
|
||||
- generic [ref=e240]: 系统管理
|
||||
- generic [ref=e242]: 权限配置
|
||||
- generic [ref=e244]: 数据统计
|
||||
- generic [ref=e246]:
|
||||
- generic [ref=e247]:
|
||||
- heading "欢迎登录" [level=2] [ref=e248]
|
||||
- paragraph [ref=e249]: 请输入您的账号信息
|
||||
- generic [ref=e250]:
|
||||
- generic [ref=e254]:
|
||||
- img [ref=e257]
|
||||
- textbox "请输入用户名" [ref=e259]
|
||||
- generic [ref=e263]:
|
||||
- img [ref=e266]
|
||||
- textbox "请输入密码" [ref=e269]
|
||||
- button "登 录" [ref=e272] [cursor=pointer]:
|
||||
- generic [ref=e273]: 登 录
|
||||
- generic [ref=e274]: 中科兴皖 © 2026
|
||||
- generic [ref=e1]:
|
||||
- generic "Toggle devtools panel" [ref=e2] [cursor=pointer]:
|
||||
- img [ref=e3]
|
||||
- generic "Toggle Component Inspector" [ref=e8] [cursor=pointer]:
|
||||
- img [ref=e9]
|
||||
@@ -1,29 +0,0 @@
|
||||
- generic [active] [ref=e13]:
|
||||
- generic [ref=e231]:
|
||||
- generic [ref=e233]:
|
||||
- img "中科兴皖" [ref=e235]
|
||||
- heading "一体化管理门户" [level=1] [ref=e236]
|
||||
- paragraph [ref=e237]: 安全 · 高效 · 可扩展
|
||||
- generic [ref=e238]:
|
||||
- generic [ref=e240]: 系统管理
|
||||
- generic [ref=e242]: 权限配置
|
||||
- generic [ref=e244]: 数据统计
|
||||
- generic [ref=e246]:
|
||||
- generic [ref=e247]:
|
||||
- heading "欢迎登录" [level=2] [ref=e248]
|
||||
- paragraph [ref=e249]: 请输入您的账号信息
|
||||
- generic [ref=e250]:
|
||||
- generic [ref=e254]:
|
||||
- img [ref=e257]
|
||||
- textbox "请输入用户名" [ref=e259]
|
||||
- generic [ref=e263]:
|
||||
- img [ref=e266]
|
||||
- textbox "请输入密码" [ref=e269]
|
||||
- button "登 录" [ref=e272] [cursor=pointer]:
|
||||
- generic [ref=e273]: 登 录
|
||||
- generic [ref=e274]: 中科兴皖 © 2026
|
||||
- generic [ref=e1]:
|
||||
- generic "Toggle devtools panel" [ref=e2] [cursor=pointer]:
|
||||
- img [ref=e3]
|
||||
- generic "Toggle Component Inspector" [ref=e8] [cursor=pointer]:
|
||||
- img [ref=e9]
|
||||
@@ -1,119 +0,0 @@
|
||||
- generic [active] [ref=e13]:
|
||||
- generic [ref=e280]:
|
||||
- complementary [ref=e281]:
|
||||
- generic [ref=e283]:
|
||||
- generic [ref=e284] [cursor=pointer]:
|
||||
- img "中科兴皖" [ref=e286]
|
||||
- paragraph [ref=e287]: 中科兴皖
|
||||
- menubar [ref=e291]:
|
||||
- menuitem "工作台" [ref=e292] [cursor=pointer]:
|
||||
- generic [ref=e294]: 工作台
|
||||
- menuitem "运营支撑" [expanded] [ref=e295]:
|
||||
- generic [ref=e296] [cursor=pointer]:
|
||||
- img [ref=e299]
|
||||
- generic [ref=e301]: 运营支撑
|
||||
- img [ref=e303]
|
||||
- menu [ref=e305]:
|
||||
- menuitem "洁净室管理" [ref=e306] [cursor=pointer]:
|
||||
- generic [ref=e307]: 洁净室管理
|
||||
- menuitem "库位管理" [ref=e308] [cursor=pointer]:
|
||||
- generic [ref=e309]: 库位管理
|
||||
- menuitem "展馆预约" [ref=e310] [cursor=pointer]:
|
||||
- generic [ref=e311]: 展馆预约
|
||||
- menuitem "demo" [ref=e312] [cursor=pointer]:
|
||||
- generic [ref=e313]: demo
|
||||
- menuitem "基础信息管理" [expanded] [ref=e314]:
|
||||
- generic [ref=e315] [cursor=pointer]:
|
||||
- img [ref=e318]
|
||||
- generic [ref=e320]: 基础信息管理
|
||||
- img [ref=e322]
|
||||
- menu [ref=e324]:
|
||||
- menuitem "客户管理" [ref=e325] [cursor=pointer]:
|
||||
- generic [ref=e326]: 客户管理
|
||||
- menuitem "细胞样本类型管理" [ref=e327] [cursor=pointer]:
|
||||
- generic [ref=e328]: 细胞样本类型管理
|
||||
- menuitem "采集地区及医院管理" [ref=e329] [cursor=pointer]:
|
||||
- generic [ref=e330]: 采集地区及医院管理
|
||||
- menuitem "采集协议号管理" [ref=e331] [cursor=pointer]:
|
||||
- generic [ref=e332]: 采集协议号管理
|
||||
- menuitem "样本编码管理" [ref=e333] [cursor=pointer]:
|
||||
- generic [ref=e334]: 样本编码管理
|
||||
- menuitem "细胞存储管理" [expanded] [ref=e335]:
|
||||
- generic [ref=e336] [cursor=pointer]:
|
||||
- img [ref=e339]
|
||||
- generic [ref=e341]: 细胞存储管理
|
||||
- img [ref=e343]
|
||||
- menu [ref=e345]:
|
||||
- menuitem "采集协议单" [ref=e346] [cursor=pointer]:
|
||||
- generic [ref=e347]: 采集协议单
|
||||
- menuitem "采集协议单审核" [ref=e348] [cursor=pointer]:
|
||||
- generic [ref=e349]: 采集协议单审核
|
||||
- menuitem "收款确认(财务)" [ref=e350] [cursor=pointer]:
|
||||
- generic [ref=e351]: 收款确认(财务)
|
||||
- menuitem "退费确认(财务)" [ref=e352] [cursor=pointer]:
|
||||
- generic [ref=e353]: 退费确认(财务)
|
||||
- menuitem "分期缴费预警" [ref=e354] [cursor=pointer]:
|
||||
- generic [ref=e355]: 分期缴费预警
|
||||
- menuitem "工作流管理" [expanded] [ref=e356]:
|
||||
- generic [ref=e357] [cursor=pointer]:
|
||||
- img [ref=e360]
|
||||
- generic [ref=e363]: 工作流管理
|
||||
- img [ref=e365]
|
||||
- menu [ref=e367]:
|
||||
- menuitem "流程配置" [ref=e368] [cursor=pointer]:
|
||||
- generic [ref=e369]: 流程配置
|
||||
- menuitem "待办中心" [ref=e370] [cursor=pointer]:
|
||||
- generic [ref=e371]: 待办中心
|
||||
- menuitem "已办中心" [ref=e372] [cursor=pointer]:
|
||||
- generic [ref=e373]: 已办中心
|
||||
- menuitem "我的申请" [ref=e374] [cursor=pointer]:
|
||||
- generic [ref=e375]: 我的申请
|
||||
- menuitem "请假申请" [ref=e376] [cursor=pointer]:
|
||||
- generic [ref=e377]: 请假申请
|
||||
- main [ref=e378]:
|
||||
- generic [ref=e380]:
|
||||
- generic [ref=e381]:
|
||||
- generic [ref=e382]:
|
||||
- img [ref=e385] [cursor=pointer]
|
||||
- img [ref=e389] [cursor=pointer]
|
||||
- navigation "breadcrumb" [ref=e391]:
|
||||
- list [ref=e392]:
|
||||
- listitem [ref=e393]:
|
||||
- generic [ref=e395]: 工作台
|
||||
- generic [ref=e396]:
|
||||
- img [ref=e399] [cursor=pointer]
|
||||
- img [ref=e403] [cursor=pointer]
|
||||
- generic [ref=e404]:
|
||||
- list [ref=e406]:
|
||||
- listitem [ref=e407] [cursor=pointer]: 工作台
|
||||
- img [ref=e411] [cursor=pointer]
|
||||
- generic [ref=e415]:
|
||||
- generic [ref=e416]:
|
||||
- heading "工作台" [level=1] [ref=e417]
|
||||
- paragraph [ref=e418]: 以下为固定参考页,新增管理端界面请对照布局与类名;视觉以文档为准。
|
||||
- generic [ref=e419]:
|
||||
- heading "快速入口" [level=2] [ref=e421]: 快速入口
|
||||
- generic [ref=e423]:
|
||||
- paragraph [ref=e424]: "详见仓库根目录 `.cursorrules` 中「页面级 UI 模板(固定五页)」。"
|
||||
- generic [ref=e425]:
|
||||
- button "商品单位" [ref=e426] [cursor=pointer]:
|
||||
- generic [ref=e427]: 商品单位
|
||||
- button "用户管理" [ref=e428] [cursor=pointer]:
|
||||
- generic [ref=e429]: 用户管理
|
||||
- button "商品分类" [ref=e430] [cursor=pointer]:
|
||||
- generic [ref=e431]: 商品分类
|
||||
- button "租户管理" [ref=e432] [cursor=pointer]:
|
||||
- generic [ref=e433]: 租户管理
|
||||
- button "客户管理" [ref=e434] [cursor=pointer]:
|
||||
- generic [ref=e435]: 客户管理
|
||||
- generic [ref=e1]:
|
||||
- generic "Toggle devtools panel" [ref=e2] [cursor=pointer]:
|
||||
- img [ref=e3]
|
||||
- generic "Toggle Component Inspector" [ref=e8] [cursor=pointer]:
|
||||
- img [ref=e9]
|
||||
- alert [ref=e436]:
|
||||
- img [ref=e438]
|
||||
- generic [ref=e440]:
|
||||
- heading "登录成功" [level=2] [ref=e441]
|
||||
- paragraph [ref=e443]: 欢迎回来,系统管理员!
|
||||
- img [ref=e445] [cursor=pointer]
|
||||
@@ -1,29 +0,0 @@
|
||||
- generic [active] [ref=e13]:
|
||||
- generic [ref=e447]:
|
||||
- generic [ref=e449]:
|
||||
- img "中科兴皖" [ref=e451]
|
||||
- heading "一体化管理门户" [level=1] [ref=e452]
|
||||
- paragraph [ref=e453]: 安全 · 高效 · 可扩展
|
||||
- generic [ref=e454]:
|
||||
- generic [ref=e456]: 系统管理
|
||||
- generic [ref=e458]: 权限配置
|
||||
- generic [ref=e460]: 数据统计
|
||||
- generic [ref=e462]:
|
||||
- generic [ref=e463]:
|
||||
- heading "欢迎登录" [level=2] [ref=e464]
|
||||
- paragraph [ref=e465]: 请输入您的账号信息
|
||||
- generic [ref=e466]:
|
||||
- generic [ref=e470]:
|
||||
- img [ref=e473]
|
||||
- textbox "请输入用户名" [ref=e475]
|
||||
- generic [ref=e479]:
|
||||
- img [ref=e482]
|
||||
- textbox "请输入密码" [ref=e485]
|
||||
- button "登 录" [ref=e488] [cursor=pointer]:
|
||||
- generic [ref=e489]: 登 录
|
||||
- generic [ref=e490]: 中科兴皖 © 2026
|
||||
- generic [ref=e1]:
|
||||
- generic "Toggle devtools panel" [ref=e2] [cursor=pointer]:
|
||||
- img [ref=e3]
|
||||
- generic "Toggle Component Inspector" [ref=e8] [cursor=pointer]:
|
||||
- img [ref=e9]
|
||||
@@ -1,29 +0,0 @@
|
||||
- generic [active] [ref=e13]:
|
||||
- generic [ref=e447]:
|
||||
- generic [ref=e449]:
|
||||
- img "中科兴皖" [ref=e451]
|
||||
- heading "一体化管理门户" [level=1] [ref=e452]
|
||||
- paragraph [ref=e453]: 安全 · 高效 · 可扩展
|
||||
- generic [ref=e454]:
|
||||
- generic [ref=e456]: 系统管理
|
||||
- generic [ref=e458]: 权限配置
|
||||
- generic [ref=e460]: 数据统计
|
||||
- generic [ref=e462]:
|
||||
- generic [ref=e463]:
|
||||
- heading "欢迎登录" [level=2] [ref=e464]
|
||||
- paragraph [ref=e465]: 请输入您的账号信息
|
||||
- generic [ref=e466]:
|
||||
- generic [ref=e470]:
|
||||
- img [ref=e473]
|
||||
- textbox "请输入用户名" [ref=e475]
|
||||
- generic [ref=e479]:
|
||||
- img [ref=e482]
|
||||
- textbox "请输入密码" [ref=e485]
|
||||
- button "登 录" [ref=e488] [cursor=pointer]:
|
||||
- generic [ref=e489]: 登 录
|
||||
- generic [ref=e490]: 中科兴皖 © 2026
|
||||
- generic [ref=e1]:
|
||||
- generic "Toggle devtools panel" [ref=e2] [cursor=pointer]:
|
||||
- img [ref=e3]
|
||||
- generic "Toggle Component Inspector" [ref=e8] [cursor=pointer]:
|
||||
- img [ref=e9]
|
||||
@@ -1,119 +0,0 @@
|
||||
- generic [active] [ref=e13]:
|
||||
- generic [ref=e496]:
|
||||
- complementary [ref=e497]:
|
||||
- generic [ref=e499]:
|
||||
- generic [ref=e500] [cursor=pointer]:
|
||||
- img "中科兴皖" [ref=e502]
|
||||
- paragraph [ref=e503]: 中科兴皖
|
||||
- menubar [ref=e507]:
|
||||
- menuitem "工作台" [ref=e508] [cursor=pointer]:
|
||||
- generic [ref=e510]: 工作台
|
||||
- menuitem "运营支撑" [expanded] [ref=e511]:
|
||||
- generic [ref=e512] [cursor=pointer]:
|
||||
- img [ref=e515]
|
||||
- generic [ref=e517]: 运营支撑
|
||||
- img [ref=e519]
|
||||
- menu [ref=e521]:
|
||||
- menuitem "洁净室管理" [ref=e522] [cursor=pointer]:
|
||||
- generic [ref=e523]: 洁净室管理
|
||||
- menuitem "库位管理" [ref=e524] [cursor=pointer]:
|
||||
- generic [ref=e525]: 库位管理
|
||||
- menuitem "展馆预约" [ref=e526] [cursor=pointer]:
|
||||
- generic [ref=e527]: 展馆预约
|
||||
- menuitem "demo" [ref=e528] [cursor=pointer]:
|
||||
- generic [ref=e529]: demo
|
||||
- menuitem "基础信息管理" [expanded] [ref=e530]:
|
||||
- generic [ref=e531] [cursor=pointer]:
|
||||
- img [ref=e534]
|
||||
- generic [ref=e536]: 基础信息管理
|
||||
- img [ref=e538]
|
||||
- menu [ref=e540]:
|
||||
- menuitem "客户管理" [ref=e541] [cursor=pointer]:
|
||||
- generic [ref=e542]: 客户管理
|
||||
- menuitem "细胞样本类型管理" [ref=e543] [cursor=pointer]:
|
||||
- generic [ref=e544]: 细胞样本类型管理
|
||||
- menuitem "采集地区及医院管理" [ref=e545] [cursor=pointer]:
|
||||
- generic [ref=e546]: 采集地区及医院管理
|
||||
- menuitem "采集协议号管理" [ref=e547] [cursor=pointer]:
|
||||
- generic [ref=e548]: 采集协议号管理
|
||||
- menuitem "样本编码管理" [ref=e549] [cursor=pointer]:
|
||||
- generic [ref=e550]: 样本编码管理
|
||||
- menuitem "细胞存储管理" [expanded] [ref=e551]:
|
||||
- generic [ref=e552] [cursor=pointer]:
|
||||
- img [ref=e555]
|
||||
- generic [ref=e557]: 细胞存储管理
|
||||
- img [ref=e559]
|
||||
- menu [ref=e561]:
|
||||
- menuitem "采集协议单" [ref=e562] [cursor=pointer]:
|
||||
- generic [ref=e563]: 采集协议单
|
||||
- menuitem "采集协议单审核" [ref=e564] [cursor=pointer]:
|
||||
- generic [ref=e565]: 采集协议单审核
|
||||
- menuitem "收款确认(财务)" [ref=e566] [cursor=pointer]:
|
||||
- generic [ref=e567]: 收款确认(财务)
|
||||
- menuitem "退费确认(财务)" [ref=e568] [cursor=pointer]:
|
||||
- generic [ref=e569]: 退费确认(财务)
|
||||
- menuitem "分期缴费预警" [ref=e570] [cursor=pointer]:
|
||||
- generic [ref=e571]: 分期缴费预警
|
||||
- menuitem "工作流管理" [expanded] [ref=e572]:
|
||||
- generic [ref=e573] [cursor=pointer]:
|
||||
- img [ref=e576]
|
||||
- generic [ref=e579]: 工作流管理
|
||||
- img [ref=e581]
|
||||
- menu [ref=e583]:
|
||||
- menuitem "流程配置" [ref=e584] [cursor=pointer]:
|
||||
- generic [ref=e585]: 流程配置
|
||||
- menuitem "待办中心" [ref=e586] [cursor=pointer]:
|
||||
- generic [ref=e587]: 待办中心
|
||||
- menuitem "已办中心" [ref=e588] [cursor=pointer]:
|
||||
- generic [ref=e589]: 已办中心
|
||||
- menuitem "我的申请" [ref=e590] [cursor=pointer]:
|
||||
- generic [ref=e591]: 我的申请
|
||||
- menuitem "请假申请" [ref=e592] [cursor=pointer]:
|
||||
- generic [ref=e593]: 请假申请
|
||||
- main [ref=e594]:
|
||||
- generic [ref=e596]:
|
||||
- generic [ref=e597]:
|
||||
- generic [ref=e598]:
|
||||
- img [ref=e601] [cursor=pointer]
|
||||
- img [ref=e605] [cursor=pointer]
|
||||
- navigation "breadcrumb" [ref=e607]:
|
||||
- list [ref=e608]:
|
||||
- listitem [ref=e609]:
|
||||
- generic [ref=e611]: 工作台
|
||||
- generic [ref=e612]:
|
||||
- img [ref=e615] [cursor=pointer]
|
||||
- img [ref=e619] [cursor=pointer]
|
||||
- generic [ref=e620]:
|
||||
- list [ref=e622]:
|
||||
- listitem [ref=e623] [cursor=pointer]: 工作台
|
||||
- img [ref=e627] [cursor=pointer]
|
||||
- generic [ref=e631]:
|
||||
- generic [ref=e632]:
|
||||
- heading "工作台" [level=1] [ref=e633]
|
||||
- paragraph [ref=e634]: 以下为固定参考页,新增管理端界面请对照布局与类名;视觉以文档为准。
|
||||
- generic [ref=e635]:
|
||||
- heading "快速入口" [level=2] [ref=e637]: 快速入口
|
||||
- generic [ref=e639]:
|
||||
- paragraph [ref=e640]: "详见仓库根目录 `.cursorrules` 中「页面级 UI 模板(固定五页)」。"
|
||||
- generic [ref=e641]:
|
||||
- button "商品单位" [ref=e642] [cursor=pointer]:
|
||||
- generic [ref=e643]: 商品单位
|
||||
- button "用户管理" [ref=e644] [cursor=pointer]:
|
||||
- generic [ref=e645]: 用户管理
|
||||
- button "商品分类" [ref=e646] [cursor=pointer]:
|
||||
- generic [ref=e647]: 商品分类
|
||||
- button "租户管理" [ref=e648] [cursor=pointer]:
|
||||
- generic [ref=e649]: 租户管理
|
||||
- button "客户管理" [ref=e650] [cursor=pointer]:
|
||||
- generic [ref=e651]: 客户管理
|
||||
- generic [ref=e1]:
|
||||
- generic "Toggle devtools panel" [ref=e2] [cursor=pointer]:
|
||||
- img [ref=e3]
|
||||
- generic "Toggle Component Inspector" [ref=e8] [cursor=pointer]:
|
||||
- img [ref=e9]
|
||||
- alert [ref=e652]:
|
||||
- img [ref=e654]
|
||||
- generic [ref=e656]:
|
||||
- heading "登录成功" [level=2] [ref=e657]
|
||||
- paragraph [ref=e659]: 欢迎回来,系统管理员!
|
||||
- img [ref=e661] [cursor=pointer]
|
||||
-151
@@ -1,151 +0,0 @@
|
||||
<h2>细胞存储管理(2.2.6 ~ 2.2.10)模块总览</h2>
|
||||
<p class="subtitle">用于对齐:菜单结构、核心数据表、关键状态流转与后续页面原型拆分</p>
|
||||
|
||||
<div class="section">
|
||||
<div class="label">菜单(本次范围)</div>
|
||||
<div class="cards">
|
||||
<div class="card">
|
||||
<div class="card-image">
|
||||
<div class="mockup">
|
||||
<div class="mockup-header">2.2.6 采集协议单</div>
|
||||
<div class="mockup-body">
|
||||
<div class="mock-content">
|
||||
<div>列表:协议号 / 状态 / 采集专员 / 登记日期 / 创建时间</div>
|
||||
<div class="placeholder" style="margin-top:10px">详情抽屉/弹窗(基础信息 + 样本 + 缴费 + 附件)</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h3>采集协议单</h3>
|
||||
<p>主表:<code>biz_collection_protocol</code>,关联采集信息/样本/运输/当事人/缴费等子表</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-image">
|
||||
<div class="mockup">
|
||||
<div class="mockup-header">2.2.7 采集协议单审核</div>
|
||||
<div class="mockup-body">
|
||||
<div class="mock-content">
|
||||
<div>列表:待审核 / 已审核(通过/退回)</div>
|
||||
<div class="placeholder" style="margin-top:10px">审核弹窗:审核意见 + 操作(通过/退回)</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h3>协议单审核</h3>
|
||||
<p>表:<code>biz_protocol_audit</code>(按协议单维度记录审核结果/备注/审核人)</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-image">
|
||||
<div class="mockup">
|
||||
<div class="mockup-header">2.2.8 收款确认(财务)</div>
|
||||
<div class="mockup-body">
|
||||
<div class="mock-content">
|
||||
<div>列表:待确认缴费(按缴费明细)</div>
|
||||
<div class="placeholder" style="margin-top:10px">确认弹窗:确认附件 + 备注</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h3>收款确认</h3>
|
||||
<p>表:<code>biz_protocol_payment_detail</code>(<code>pay_status=dfj/jfqrz/yjf</code> 等)</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-image">
|
||||
<div class="mockup">
|
||||
<div class="mockup-header">2.2.9 退费确认(财务)</div>
|
||||
<div class="mockup-body">
|
||||
<div class="mock-content">
|
||||
<div>列表:待确认退费(按缴费明细)</div>
|
||||
<div class="placeholder" style="margin-top:10px">确认弹窗:退费附件 + 备注</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h3>退费确认</h3>
|
||||
<p>表:同上(<code>pay_status=tfqrz/ytf</code>)+ 财务确认附件字段</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-image">
|
||||
<div class="mockup">
|
||||
<div class="mockup-header">2.2.10 分期缴费预警</div>
|
||||
<div class="mockup-body">
|
||||
<div class="mock-content">
|
||||
<div>列表:即将到期/已逾期(按分期明细)</div>
|
||||
<div class="placeholder" style="margin-top:10px">快捷操作:提醒/标记处理(可选)</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h3>分期缴费预警</h3>
|
||||
<p>字段:<code>installment_due_date</code>(明细应缴日)+ 状态过滤(未缴费/确认中)</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="label">关键状态流转(建议先对齐)</div>
|
||||
<div class="split">
|
||||
<div class="mockup">
|
||||
<div class="mockup-header">协议单状态(biz_collection_protocol.protocol_status)</div>
|
||||
<div class="mockup-body">
|
||||
<div class="mock-content">
|
||||
<p><b>dsx</b> 待生效 → <b>sxz</b> 生效中 →(到期)→ <b>ygq</b> 已过期</p>
|
||||
<p>任意阶段可 → <b>yzf</b> 已作废(毁约/异常终止/作废)</p>
|
||||
<p>可选:→ <b>zky</b> 转科研(后续再做,当前仅保留字段/入口)</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mockup">
|
||||
<div class="mockup-header">缴费明细状态(biz_protocol_payment_detail.pay_status)</div>
|
||||
<div class="mockup-body">
|
||||
<div class="mock-content">
|
||||
<p><b>dfj</b> 待缴费 → <b>jfqrz</b> 缴费确认中 → <b>yjf</b> 已缴费</p>
|
||||
<p><b>tfqrz</b> 退费确认中 → <b>ytf</b> 已退费(通常由业务发起后进入)</p>
|
||||
<p class="subtitle" style="margin-top:10px">收款确认页面主要处理:<b>jfqrz → yjf</b>;退费确认页面主要处理:<b>tfqrz → ytf</b></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="label">下一步我需要你确认的 1 个问题</div>
|
||||
<div class="mockup">
|
||||
<div class="mockup-header">审核模块(2.2.7)的定义边界</div>
|
||||
<div class="mockup-body">
|
||||
<div class="options">
|
||||
<div class="option" data-choice="audit-1" onclick="toggleSelect(this)">
|
||||
<div class="letter">A</div>
|
||||
<div class="content">
|
||||
<h3>仅“协议附件/缴费截图”审核</h3>
|
||||
<p>审核通过后仅写 <code>biz_protocol_audit</code> 记录,不改协议单主状态(状态仍由业务流程控制)</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="option" data-choice="audit-2" onclick="toggleSelect(this)">
|
||||
<div class="letter">B</div>
|
||||
<div class="content">
|
||||
<h3>审核通过会驱动协议单生效</h3>
|
||||
<p>审核通过时同步将 <code>protocol_status</code> 从 <code>dsx</code> 推到 <code>sxz</code>(并记录生效时间/描述)</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="subtitle" style="margin-top:10px">你在浏览器里点选 A 或 B 即可(也可以回终端用文字说明)。</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
<h2>2.2.10 分期缴费用户预警:规则确认</h2>
|
||||
<p class="subtitle">目标:确定“预警/逾期”的判定口径,便于后端列表筛选与前端状态标签统一</p>
|
||||
|
||||
<div class="section">
|
||||
<div class="label">数据来源(已确定)</div>
|
||||
<div class="mockup">
|
||||
<div class="mockup-header">biz_protocol_payment_detail</div>
|
||||
<div class="mockup-body">
|
||||
<div class="mock-content">
|
||||
<p>按 <code>installment_due_date</code>(YYYYMMDD)计算;仅展示分期明细(<code>installment_no</code> 不为空)</p>
|
||||
<p>过滤状态:通常只看 <code>dfj</code>(待缴费)与(可选)<code>jfqrz</code>(确认中)</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="label">请选择预警口径(单选)</div>
|
||||
<div class="options">
|
||||
<div class="option" data-choice="warn-7" onclick="toggleSelect(this)">
|
||||
<div class="letter">A</div>
|
||||
<div class="content">
|
||||
<h3>预警=未来 7 天内到期;逾期=已超过到期日</h3>
|
||||
<p>最常见口径;页面分两块:即将到期(0~7天) / 已逾期(<0天)</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="option" data-choice="warn-3" onclick="toggleSelect(this)">
|
||||
<div class="letter">B</div>
|
||||
<div class="content">
|
||||
<h3>预警=未来 3 天内到期;逾期=已超过到期日</h3>
|
||||
<p>更紧迫;减少列表噪音</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="option" data-choice="warn-15" onclick="toggleSelect(this)">
|
||||
<div class="letter">C</div>
|
||||
<div class="content">
|
||||
<h3>预警=未来 15 天内到期;逾期=已超过到期日</h3>
|
||||
<p>更早提醒;适合人工跟进周期长的场景</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="option" data-choice="warn-custom" onclick="toggleSelect(this)">
|
||||
<div class="letter">D</div>
|
||||
<div class="content">
|
||||
<h3>预警天数可配置(字典/配置表/环境变量)</h3>
|
||||
<p>灵活,但实现与运维成本更高;需要补一处配置入口</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="label">另外 1 个小点(默认建议)</div>
|
||||
<div class="mockup">
|
||||
<div class="mockup-header">确认:是否把“确认中 jfqrz”也纳入预警列表?</div>
|
||||
<div class="mockup-body">
|
||||
<div class="options">
|
||||
<div class="option" data-choice="include-jfqrz-no" onclick="toggleSelect(this)">
|
||||
<div class="letter">1</div>
|
||||
<div class="content">
|
||||
<h3>不纳入</h3>
|
||||
<p>预警只针对“待缴费 dfj”,避免和财务确认页面重复</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="option" data-choice="include-jfqrz-yes" onclick="toggleSelect(this)">
|
||||
<div class="letter">2</div>
|
||||
<div class="content">
|
||||
<h3>纳入</h3>
|
||||
<p>把“待缴费 + 确认中”都算未完成付款,有助于跟进</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="subtitle" style="margin-top:10px">你可以点 A/B/C/D,再点 1 或 2;也可以直接在终端回复(例如:A + 1)。</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
{"reason":"owner process exited","timestamp":1778664344125}
|
||||
@@ -1,225 +1,55 @@
|
||||
# AGENTS.md — zkxw (陇耘鲜达)
|
||||
# AGENTS.md — 中科兴皖一体化门户(zkxw)
|
||||
|
||||
Two-package monorepo: Python backend + Vue 3 admin frontend. Named "sanqiang" internally.
|
||||
## 工程与查阅范围
|
||||
|
||||
## Repository Layout
|
||||
共享 FastAPI 后端的四应用仓库。项目说明见 [README](README.md),文档入口见 [docs/README](docs/README.md)。
|
||||
|
||||
```
|
||||
backend/ FastAPI backend (Python 3.9, venv)
|
||||
web-admin/ Vue 3 admin panel (pnpm, Vite 7, Element Plus, Tailwind CSS 4)
|
||||
docs/ UI specs, SQL schemas, requirements docs
|
||||
openspec/ OpenSpec: `config.yaml`; active changes under `openspec/changes/*/`
|
||||
.cursorrules Detailed UI conventions and constraints (Chinese)
|
||||
```
|
||||
| 目录 | 职责 | 专用规则 |
|
||||
| --- | --- | --- |
|
||||
| `backend/` | API、权限、业务数据、工作流与 OA 引擎 | [backend/AGENTS.md](backend/AGENTS.md) |
|
||||
| `web-admin/` | Vue 3 管理端 | [web-admin/AGENTS.md](web-admin/AGENTS.md) |
|
||||
| `mobile-h5/` | HBuilderX uni-app,小程序及移动宿主 | [mobile-h5/AGENTS.md](mobile-h5/AGENTS.md) |
|
||||
| `mobile-oa-h5/` | Vue/Vant H5,OA 表单与市场业务 | [mobile-oa-h5/AGENTS.md](mobile-oa-h5/AGENTS.md) |
|
||||
|
||||
## OpenSpec engineering standards
|
||||
修改前阅读目标目录规则;跨端改动同时阅读涉及端的规则。
|
||||
|
||||
Full specs: `openspec/changes/zkxw-engineering-standards/specs/*/spec.md`. Below are the key SHALL items every coding session MUST follow.
|
||||
## 工作方式
|
||||
|
||||
### UI Conventions (web-admin + miniprogram)
|
||||
- 用户使用中文时用简体中文沟通;代码命名、注释、格式遵循所在模块。
|
||||
- 仅做任务必要的改动,不夹带无关重构或全仓库格式化;保留用户已有改动。
|
||||
- **不得自行停止、启动或重启 `web-admin` 开发服务与 `backend` 服务。** 需要重启时告知用户,由用户操作。
|
||||
- 提交使用 Conventional Commits,完整说明“改了什么、为什么”。
|
||||
- 不把真实凭据、业务导出、虚拟环境、构建产物和工具临时状态新增到版本管理。
|
||||
- 清理文件前核查代码、测试、脚本、部署及文档引用;不能只凭名称、日期或搜索无结果删除。
|
||||
|
||||
- **Visual authority**: `web-admin` SHALL follow `docs/web-admin-ui-spec-fresh-mint.md` (Fresh Mint palette, `--primary #10b981`). `miniprogram` SHALL follow `docs/miniprogram-ui-spec-sanqiang.md` (三墙 palette, brand green `#0a9f6d`).
|
||||
- **No cross-app color mixing**: `web-admin` SHALL NOT use `#0a9f6d` as primary; `miniprogram` SHALL NOT use `#10b981` as primary.
|
||||
- **Tree path display**: Dropdown/tree nodes show **node name only**; input fields, table columns, exports, and view dialogs SHALL show **full ancestor path**. Separator: ` / ` (space-slash-space). See `web-admin/src/utils/tree-path.ts`.
|
||||
- **Dialog forms**: `label-position="top"`. No backdrop blur (`backdrop-filter: none`). Use `.dialog-form-layout` with `max-height: calc(100vh - 100px)`, only body scrolls.
|
||||
- **Dialog grid**: Default 2-column (`.form-grid-2`). Max 5 columns. Full-width fields use `.form-col-span-2` (`grid-column: 1 / -1`). Narrow viewport SHALL fallback to fewer columns.
|
||||
- **List panel heading**: `panel-top` → `h2.title-bar-heading` SHALL show primary-color vertical bar via `::before`. Ambiguous titles (e.g. "租户" containing "户") SHALL be registered in `App.vue` `panelTitleBarTextRules`.
|
||||
- **Miniprogram media**: Thumbnails 1:1 `aspectFill`; carousel 4:3 `aspectFill`; detail images `widthFix`. Min tap target 44×44px.
|
||||
- **Sidebar menu icons**: First-level menu items SHALL show icons before the name; second-level and deeper items SHALL NOT show icons (text only). Implemented via `level === 0` check in `SidebarSubmenu.vue`.
|
||||
- **Tab bar (WorkTab) icons**: Tab bar tabs SHALL NOT show icons — only text titles. `meta.icon` is for sidebar only.
|
||||
- **Form field annotations**: Field hints (e.g. "数值越小越靠前") SHALL appear as small text after the label via `<template #label>字段名<span class="form-label-tip">注释</span></template>`, NOT beside the input control. Style class: `.form-label-tip` (12px, `--mint-text-muted`).
|
||||
- **Attachment upload card layout**: Upload trigger SHALL be a card (140×96px, dashed border `#a7f3d0`) inline as the first item in the same flex row as uploaded attachment cards; select file → immediate upload via `fetchUploadAttachment`; preview/download/delete on cards; hint below the row. Authority: `ProtocolFormDialog.vue` «协议附件». See `openspec/changes/zkxw-engineering-standards/specs/ui-conventions/spec.md` §「附件上传卡片化布局」for full Vue template + SCSS + interaction rules.
|
||||
- **Button icons**: All `web-admin` `el-button` with visible text labels SHALL include a leading `<el-icon>` from `@element-plus/icons-vue` (explicit import). See `openspec/.../ui-conventions/spec.md` §「按钮须携带图标」.
|
||||
- **Sort fields (sort_order/orderNum)**: web-admin 列表、新增弹窗、编辑弹窗中默认不展示排序字段。后续根据功能需要自行添加。
|
||||
- **Status/conclusion/type fields as Tags**: All status, conclusion, and type fields in table columns and view dialogs SHALL render as `<el-tag>` with semantic colors. Tables: use `<el-tag :type="..." size="small">` in column templates. SystemViewDialog: use `tagType` property on `ViewField`. Semantic colors: `success`=正常/启用/已完成/通过, `warning`=进行中/待确认/已过期, `danger`=异常/已作废/拒绝/不通过/质检待执行/质检待结论, `info`=待生效/停用/默认, `primary`=特殊分类/来源类型. SHALL NOT use plain text or custom `<span class="tag">`. See `openspec/.../ui-conventions/spec.md` "状态、结论与类型字段使用 Tag 标签展示".
|
||||
- **Template pages** — new pages SHALL match these exactly:
|
||||
- Standard list: `src/views/system/unit/index.vue`
|
||||
- Complex list: `src/views/system/order/index.vue`
|
||||
- Product list + view dialog: `src/views/system/product/index.vue`
|
||||
- Tree master data: `src/views/system/category/index.vue`
|
||||
- User CRUD: `src/views/system/user/index.vue`
|
||||
- **UI pattern index** — before implementing a new page, developers SHALL select a pattern code (P01–P27) per `docs/web-admin-ui-pattern-index.md` and OpenSpec `ui-conventions` §「界面模式索引与参考页选型」; PRs SHALL cite pattern code and reference page path.
|
||||
## 跨端数据与权限底线
|
||||
|
||||
### API Conventions (backend)
|
||||
- 业务查询必须按当前用户 `tenant_no` 隔离并应用数据权限;写入租户来自认证上下文,不信任客户端覆盖。多租户管理例外须有安全评审。
|
||||
- 菜单和数据权限使用所有有效角色的**并集**;选中子菜单必须包含祖先菜单。Web 与移动菜单使用各自 `parent_id=0` 的虚拟根。
|
||||
- 租户管理仅 `super_admin` 可访问,其他系统管理运行在租户上下文。
|
||||
- 业务数据软删除(`del_flag=1`),查询默认排除已删除数据。
|
||||
- 保存子行使用 `backend/app/crud/child_row_sync.py`:原行更新、新行插入、只软删除移除行,禁止全删重建。拒绝其他父记录或租户的 ID;复制时生成新 ID。
|
||||
- 细胞存储样本跨表匹配使用 **`tenant_no + sample_code`**,不用可变化的协议关联 ID 作为匹配键。
|
||||
- 业务附件使用 MinIO;单字段多附件按英文逗号分隔 URL,前后端解析一致。
|
||||
- 数据库变更维护 `docs/sql/`,同时检查 API Schema、CRUD、前端字段及类型;不绕过鉴权、加密、审计中间件。
|
||||
|
||||
- **Cell-storage sample cross-table key**: Matching `biz_protocol_sample` to downstream rows (sample reception, QC work orders, etc.) SHALL use `tenant_no` + `sample_code` only—not `collection_protocol_id` or `protocol_sample_id` (protocol may change; sample code does not). See `openspec/changes/zkxw-engineering-standards/specs/api-conventions/spec.md` §「细胞存储样本跨表关联键」.
|
||||
- **Tenant isolation**: All business data queries SHALL filter by current user's `tenant_no`. All write/update endpoints SHALL set `tenant_no` to the current user's tenant. MUST NOT trust client-submitted tenant overrides (unless multi-tenant admin API with security review).
|
||||
- **Data permission**: All business queries SHALL apply data scope filtering based on the union of all roles assigned to the current user (menu + data permissions both use union semantics).
|
||||
- **Default sort**: Lists SHALL sort by `create_time DESC` when no custom sort is specified.
|
||||
- **Route registration**: New route modules SHALL be registered in `app/api/__init__.py` AND `app/main.py`. SHALL NOT bypass auth/crypto/audit middleware.
|
||||
## 规范入口
|
||||
|
||||
### Database Conventions
|
||||
- 工程 SHALL 要求:[OpenSpec 工程标准](openspec/changes/zkxw-engineering-standards/specs/)。按改动范围查阅 API、数据库、权限、编码、架构、工作流及 UI 专题。
|
||||
- Web UI:[Fresh Mint 规范](docs/web-admin-ui-spec-fresh-mint.md)、[P01–P27 模式索引](docs/web-admin-ui-pattern-index.md)。主色 `#10b981`,不得混入移动端主色。
|
||||
- 移动 UI:[移动端规范](docs/miniprogram-ui-spec-sanqiang.md)。规范品牌绿 `#0a9f6d`;现有 Token 存在差异,按各端 AGENTS 记录处理,禁止直接套用 Web 主色。
|
||||
- Cursor 配置仅作入口,不再独立维护另一套规则。规范与实现冲突需查明依据,不以代码现状默认为新规范。
|
||||
- OpenSpec 长期规范尚未完成归档;不要因 `openspec/specs/` 目录存在就认定其中已有基线。
|
||||
|
||||
- **Engine**: MySQL 5.7, charset utf8mb4.
|
||||
- **Public fields** (every business table SHALL include these after business fields): `id` VARCHAR(64) UUID, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag` (default `0`).
|
||||
- **Field order**: `id` → business fields → public fields (`tenant_no` through `del_flag`).
|
||||
- **Soft delete**: SHALL NOT physically delete business data. Delete = set `del_flag=1`. Queries SHALL exclude `del_flag=1` by default.
|
||||
- **Primary key**: `id` SHALL be `VARCHAR(64) NOT NULL`, UUID value.
|
||||
## 验证与交付
|
||||
|
||||
### Architecture Conventions
|
||||
- 执行与改动范围相符的检查,报告实际结果和未执行项;文档变更检查链接与引用,业务变更检查受影响测试及构建。
|
||||
- CRUD 字段一致性按 [开发指南](docs/dev-field-consistency.md) 检查;根目录命令是 `python backend/scripts/check_field_consistency.py`。该工具仅覆盖注册实体,已知失效路径见该指南。
|
||||
- 使用源码、引用检索和测试核对影响范围。已移除的 graphify 不再是开发依赖或交付步骤,不安装、运行或恢复其自动 hook。
|
||||
|
||||
- **Monorepo structure**: Python backend (`backend/`) + Vue 3 web-admin (`web-admin/`) + uni-app miniprogram (`miniprogram/`). Shared backend and tenant model.
|
||||
- **File attachments**: SHALL use object storage (MinIO). Multiple attachments in one field SHALL be comma-separated URLs.
|
||||
- **Authoritative docs**: `docs/中科兴皖需求V0.3.md` (requirements), `docs/web-admin-ui-spec-fresh-mint.md` (web UI), `docs/miniprogram-ui-spec-sanqiang.md` (miniprogram UI).
|
||||
## 维护边界
|
||||
|
||||
### Coding Conventions
|
||||
|
||||
- **Minimal changes**: Commits SHALL contain only what's needed. No unrelated refactoring or formatting.
|
||||
- **Frontend (web-admin)**: pnpm, Prettier (single quotes, no semicolons, 2-space indent). Vue/Element Plus auto-imports — SHALL NOT manually import auto-imported APIs/components.
|
||||
- **Backend**: Python 3.9, FastAPI + SQLAlchemy + Pydantic v2. Routes stay thin; business logic goes in `app/services/` and `app/crud/`. Config via `pydantic-settings` + `.env` files.
|
||||
- **DB changes**: SHALL maintain SQL in `docs/sql/`. Schema changes SHALL align with `database-conventions` public field rules.
|
||||
- **Commit messages**: SHALL be complete sentences describing "what changed and why". Follow Conventional Commits (`feat|fix|docs|…`).
|
||||
- **Field consistency (CRUD ↔ Schema ↔ Frontend)**: When implementing CRUD for any entity, the `_row_to_dict` function is the **single source of truth** for all readable fields. The CreateBody/UpdateBody Pydantic schemas and the frontend `formData` initialization SHALL include every writable field present in `_row_to_dict` (excluding computed/read-only fields like `id`, `createTime`, `updateTime`, `stockQty`). Run `python scripts/check_field_consistency.py` after implementation to verify. See `docs/dev-field-consistency.md` for details.
|
||||
- **Child-row save(禁止整表删除再新增)**: 保存子表时 SHALL 按行处理:已有行就地更新,只插入新增行,只软删除用户去掉的行。客户端可以提交稳定 `id`,字段变化时该行必须保留原 `id`。请求未带 `id` 时,先用稳定业务键(样本编码、物料编码、条码、组分 id 等)匹配一条尚未占用的有效行,匹配不到再插入新 UUID。属于其他父记录或其他租户的 `id` SHALL 拒绝。SHALL NOT 在保存时把子表全部软删除再整批插入新行——其他表会拿这些 id 做外键(`production_step_output_id`、`protocol_sample_id`、`donation_sample_id`),整批换 id 会把关联打断。删除父记录时可以级联软删除其子行。复制或克隆父记录时 SHALL 为子行分配新 id。实现入口:`backend/app/crud/child_row_sync.py` 的 `sync_child_rows`。
|
||||
|
||||
### Permission Conventions
|
||||
|
||||
- **Role-menu**: Selecting a child menu SHALL auto-include all ancestor menus (no orphan leaves).
|
||||
- **Data scope**: Per role — all / own dept / own dept and below / self only / custom. Custom uses `sys_role_data_scope_detail` with `scope_type` (dept/user) + `scope_value`.
|
||||
- **Multi-role union**: A user with multiple roles gets the **union** of menu permissions and the **union** of data scopes across all roles.
|
||||
- **Dual menu trees**: Web and miniprogram each have a separate top-level virtual root (`parent_id=0`). New menus SHALL be placed under the correct root.
|
||||
- **Tenant management**: Accessible ONLY by `super_admin`. All other system management runs within tenant context.
|
||||
|
||||
### Workflow Conventions (target design, not yet implemented)
|
||||
|
||||
- **Engine**: SHALL be implemented in Python backend as an independent, configurable approval/flow subsystem.
|
||||
- **Node types**: Start, End, Approver, Conditional Branch (multi-branch supported). Extensible but SHALL NOT break published definitions.
|
||||
- **Approver strategies**: Specified users, specified roles, or SQL-based resolution. Per-node: "any one passes" or "all must pass".
|
||||
- **Versioning**: Each flow definition has a version. Running instances SHALL stick to their launch version; new versions only affect new launches.
|
||||
- **Task centers**: System SHALL provide "Pending Tasks" and "Completed Tasks" views.
|
||||
|
||||
## Backend (`backend/`)
|
||||
|
||||
### Stack
|
||||
FastAPI · SQLAlchemy · Pydantic v2 · MySQL (pymysql) · Redis · MinIO · JWT auth · Alembic migrations
|
||||
|
||||
### Commands
|
||||
```bash
|
||||
source venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
cp .env.example .env.local # fill in DB, Redis, MinIO, etc.
|
||||
./start-local.sh # uvicorn --reload on 127.0.0.1:8000
|
||||
./start-prod.sh start # production on :8111 (reads .env.production, PID-managed)
|
||||
./start-prod.sh status|stop|restart|logs
|
||||
./start-test.sh start # test on :8112 (reads .env.test, PID-managed)
|
||||
./start-test.sh activate # source .venv/bin/activate (interactive shell)
|
||||
./start-test.sh status|stop|restart|logs
|
||||
```
|
||||
|
||||
### Entrypoint & Structure
|
||||
- Entrypoint: `app/main.py` → `app.main:app`
|
||||
- **`app/api/`** — Route files: `public.py` (no auth), `menu.py`, `admin.py`. Register new routers in `app/api/__init__.py` and `app/main.py`.
|
||||
- **`app/core/`** — Config (`config.py`), DB (`database.py`), auth (`auth.py`, `security.py`), middleware (`api_crypto_middleware.py`, `api_request_log_middleware.py`), RSA login crypto (`login_crypto.py`)
|
||||
- **`app/models/`** — SQLAlchemy models (`models.py`, `base.py`)
|
||||
- **`app/schemas/`** — Pydantic schemas per domain (auth, order, product, user, etc.)
|
||||
- **`app/crud/`** — Database CRUD operations (`base.py` has generic base)
|
||||
- **`app/services/`** — Business logic layer
|
||||
- **`app/tasks/`** — APScheduler scheduled tasks
|
||||
- **`scripts/`** — SQL migrations and one-off scripts
|
||||
|
||||
### Key Patterns
|
||||
- API prefix: `/api` (set via `API_V1_PREFIX` in config). Routes: `/api/public/*`, `/api/admin/*`, `/api/supplier/*`, `/api/client/*`, `/api/deliver/*`
|
||||
- Auth: JWT + RSA-encrypted login. Login RSA key auto-generated or set via `LOGIN_RSA_PRIVATE_KEY_PEM` env var.
|
||||
- API encryption middleware: symmetric crypto layer controlled by `API_ENCRYPT_*` env vars. Excluded paths listed in `API_ENCRYPT_EXCLUDE_PATHS`.
|
||||
- Config: pydantic-settings (`app/core/config.py`), reads from `.env` / `.env.local` / `.env.production` / `.env.test` (via `ENV_FILE`).
|
||||
- Database init / schema baseline: `docs/sql/zkxw.sql`(从当前库导出的全量 SQL)。Default admin: `admin` / `111111`, role `super_admin`.
|
||||
- Swagger docs at `/docs`, ReDoc at `/redoc`.
|
||||
- Tests: minimal, only `tests/test_api_crypto.py`. Run with `pytest`.
|
||||
|
||||
## Frontend (`web-admin/`)
|
||||
|
||||
### Stack
|
||||
Vue 3 · TypeScript (strict: false, noImplicitAny: false) · Vite 7 · Element Plus · Tailwind CSS 4 · Pinia · vue-router (hash mode) · SCSS · Axios · pnpm
|
||||
|
||||
### Commands
|
||||
```bash
|
||||
pnpm install # if fails: pnpm install --ignore-scripts
|
||||
pnpm dev # Vite dev server (port from .env.development)
|
||||
pnpm build # production build
|
||||
pnpm build:typecheck # typecheck + build (vue-tsc --noEmit)
|
||||
pnpm lint # ESLint check
|
||||
pnpm fix # ESLint --fix
|
||||
pnpm lint:prettier # Prettier format all
|
||||
pnpm lint:stylelint # Stylelint fix SCSS/Vue
|
||||
pnpm commit # cz-git interactive commit
|
||||
pnpm clean:dev # strip demo data for fresh start
|
||||
```
|
||||
|
||||
### Entrypoint & Structure
|
||||
- Entrypoint: `src/main.ts`
|
||||
- **`src/views/system/`** — Main business pages: `category/`, `order/`, `product/`, `tenant/`, `unit/`, `user/`
|
||||
- **`src/api/`** — Axios API modules: `auth.ts`, `admin.ts`, `business.ts`, `system.ts`, `chinaRegion.ts`
|
||||
- **`src/store/modules/`** — Pinia stores: `user.ts`, `menu.ts`, `setting.ts`, `table.ts`, `worktab.ts`
|
||||
- **`src/router/modules/`** — Route modules: `index.ts`, `sqxp.ts`, `exception.ts`
|
||||
- **`src/utils/tree-path.ts`** — Tree path utilities (`buildTreePathLabelFromFlat`, etc.). Separator: ` / ` (spaced slash).
|
||||
|
||||
### Path Aliases (tsconfig + vite)
|
||||
`@/*` → `src/*`, `@views/*`, `@imgs/*`, `@icons/*`, `@utils/*`, `@stores/*`, `@plugins/*`, `@styles/*`
|
||||
|
||||
### Key Patterns
|
||||
- **Auto-imports**: `unplugin-auto-import` + `unplugin-vue-components` — Vue, vue-router, pinia, @vueuse/core APIs and Element Plus components are auto-imported. Don't manually import them.
|
||||
- **Element Plus**: auto-imported resolvers — use components directly, no manual imports.
|
||||
- **Tailwind CSS 4**: via `@tailwindcss/vite` plugin.
|
||||
- **SCSS**: global mixin/data injected via `vite.config.ts` `additionalData` (uses `@styles/core/el-light.scss` and `@styles/core/mixin.scss`).
|
||||
- **API proxy**: dev server proxies `/api` → `VITE_API_PROXY_URL` (localhost:8000). Also `/web` and `/mini` proxies configured.
|
||||
- **Env files**: `.env.development` (local), `.env.production` (prod). Uses `VITE_*` prefix.
|
||||
- **API encryption**: `VITE_API_ENCRYPT_ENABLED` toggles request/response crypto in dev. Production has it on.
|
||||
- **Hash routing**: URLs use `/#/` format.
|
||||
- **Access mode**: `VITE_ACCESS_MODE=frontend` (fixed frontend menu mode).
|
||||
|
||||
### Code Style
|
||||
- **Prettier**: single quotes, no semicolons, 2-space indent, trailing comma: none, `vueIndentScriptAndStyle: true`
|
||||
- **ESLint**: extends recommended + typescript + vue essential + prettier. `@typescript-eslint/no-explicit-any` is OFF.
|
||||
- **Stylelint**: SCSS + Vue + recess-order property sorting.
|
||||
- **Commit**: Conventional commits via `cz-git` + `commitlint`. Types: `feat|fix|docs|style|refactor|perf|test|build|ci|revert|chore|wip`.
|
||||
- **Husky + lint-staged**: pre-commit auto-runs ESLint, Prettier, Stylelint on staged files.
|
||||
|
||||
## UI Design Authority
|
||||
|
||||
**The `.cursorrules` file and `docs/` are the authoritative source for all UI decisions.**
|
||||
|
||||
- **Web admin UI spec**: `docs/web-admin-ui-spec-fresh-mint.md` + `docs/fresh-mint-green.html`
|
||||
- **Template pages** (match these exactly for new pages):
|
||||
- Standard list: `src/views/system/unit/index.vue` (`/#/demo/unit`)
|
||||
- Complex list: `src/views/system/order/index.vue` (`/#/demo/order`)
|
||||
- Product list + view dialog: `src/views/system/product/index.vue`
|
||||
- Tree master data: `src/views/system/category/index.vue`
|
||||
- User CRUD: `src/views/system/user/index.vue`
|
||||
- Console/dashboard: `src/views/dashboard/console/index.vue`
|
||||
- **Color tokens** (web-admin): Primary `#10b981`, dark `#059669`, bg `#ecfdf5`. Fresh Mint palette. **Do NOT use miniprogram colors in web-admin.**
|
||||
- **Tree display**: dropdown shows node name; selected/input/table shows full path (spaced slash separator).
|
||||
|
||||
## Knowledge Graph (代码知识图谱)
|
||||
|
||||
项目已生成代码知识图谱,用于理解模块间依赖关系、追踪修改影响范围。
|
||||
|
||||
### 图谱位置
|
||||
|
||||
| 项目 | 报告 | 可视化 | 数据 |
|
||||
|------|------|--------|------|
|
||||
| 后端 | `backend/graphify-out/GRAPH_REPORT.md` | `backend/graphify-out/graph.html` | `backend/graphify-out/graph.json` |
|
||||
| 前端 | `web-admin/graphify-out/GRAPH_REPORT.md` | `web-admin/graphify-out/graph.html` | `web-admin/graphify-out/graph.json` |
|
||||
|
||||
### 使用方式
|
||||
|
||||
- **浏览器**:直接打开 `*/graphify-out/graph.html` 查看交互式图谱
|
||||
- **AI Agent 查询**:已配置 MCP 服务(graphify),可通过工具调用查询图谱
|
||||
- **手动更新**:`./scripts/update-graph.sh [backend|frontend|all]`
|
||||
|
||||
### Agent 更新规则
|
||||
|
||||
完成代码修改后(新建文件、修改函数签名、新增/删除 import、重构模块),Agent SHALL 执行 `./scripts/update-graph.sh` 增量更新图谱,确保图谱与代码保持同步。如果只修改了单个项目(backend 或 web-admin),只需更新对应项目即可。
|
||||
|
||||
## Important Constraints
|
||||
|
||||
- **Language**: User communication in Chinese (简体中文) when user writes in Chinese.
|
||||
- **Minimal changes**: Don't expand scope, don't refactor unrelated code. Match surrounding naming/type/import style.
|
||||
- **Cross-app colors**: Web admin uses Fresh Mint (`#10b981`), miniprogram uses 三墙 (`#0a9f6d`). Don't mix.
|
||||
- **No backdrop blur on modals**: `backdrop-filter: none`.
|
||||
- **Dialog forms**: `label-position="top"`.
|
||||
- **Table action column**: `fixed="right"`, min-width ≥ 260px.
|
||||
- **Table column widths**: All `el-table-column` SHALL use `min-width` (not `width`) for proportional scaling when viewport resizes.
|
||||
- **Toolbar**: "已选 N 项" first, then primary action button (only one `type="primary"` per toolbar row), no `space-between` push.
|
||||
- **Database changes**: always pair with schema SQL in `docs/sql/` and evaluate API/TypeScript type impact.
|
||||
- **Child-row save**: SHALL NOT soft-delete every child row and insert new rows on save. Update in place, insert new rows, soft-delete only removed rows. See Coding Conventions.
|
||||
- **进程重启**:修改 bug 时,如果前端或后端需要重启,Agent 只告知用户,由用户自行重启。Agent 不得自行停止、启动或重启 `web-admin` 开发服务与 `backend` 服务。
|
||||
- 当前 SQL 初始化基线不完整,部分测试引用的迁移文件已缺失;未验证前不宣称可从空库部署。
|
||||
- 业务历史方案、执行计划、Demo 与整理报告已清理;保留四个应用目录、现行工程规范及被部署或脚本读取的 SQL、SOP 等资源。
|
||||
- 文档优先更新现有 README 或规范入口,避免重复生成历史业务方案和整理报告。
|
||||
- 更改规则应同步唯一的对应入口,不在多个文件复制完整细则。
|
||||
@@ -0,0 +1,50 @@
|
||||
# 中科兴皖一体化门户(zkxw)
|
||||
|
||||
面向细胞存储、科研捐赠、生产质检、中试服务及 OA 审批的多租户业务系统。四个应用共享后端与权限模型;`sanqiang`、`sqxp`、陇耘鲜达等名称是历史标识,不代表独立服务。
|
||||
|
||||
## 应用与入口
|
||||
|
||||
| 应用 | 技术与用途 | 开发说明 |
|
||||
| --- | --- | --- |
|
||||
| `backend/` | Python 3.9 / FastAPI,业务 API、认证、权限、工作流及 OA | [后端 README](backend/README.md) |
|
||||
| `web-admin/` | Vue 3 / TypeScript / Element Plus,Web 管理端 | [Web README](web-admin/README.md) |
|
||||
| `mobile-h5/` | HBuilderX uni-app Vue 3,移动工作台及 WebView 宿主 | [uni-app README](mobile-h5/README.md) |
|
||||
| `mobile-oa-h5/` | Vue 3 / Vant,OA 与市场业务 H5 | [H5 README](mobile-oa-h5/README.md) |
|
||||
|
||||
Web 与移动端访问同一后端;移动宿主通过 WebView 打开 H5 表单和详情。Web 使用 Hash 路由,H5 使用 `/oa-h5/` 下的 History 路由。Docker 的 Web 镜像包含 Web 管理端和移动 H5,小程序由 HBuilderX 独立编译。
|
||||
|
||||
后端入口为 `backend/app/main.py`。默认 API 总前缀 `/api`,业务模块主要位于 `/web/...` 与 `/mini/...`;以对应路由定义为准。数据服务使用 MySQL 5.7、Redis 与 MinIO。
|
||||
|
||||
## 开发导航
|
||||
|
||||
- 开发约束:[根 AGENTS](AGENTS.md)及各应用的 AGENTS。
|
||||
- UI、模式、SQL、SOP 与开发指南:[文档导航](docs/README.md)。
|
||||
- 工程规范:[OpenSpec 工程标准](openspec/changes/zkxw-engineering-standards/specs/)。当前仍在变更目录,尚未完成长期基线归档。
|
||||
|
||||
首次开发按各应用 README 准备依赖与环境配置,不复用 Git 中的虚拟环境。启动、停止、重启服务由用户执行,Agent 仅告知需要的操作。
|
||||
|
||||
## 目录职责
|
||||
|
||||
| 路径 | 内容 |
|
||||
| --- | --- |
|
||||
| 四个应用目录 | 业务源码、各端测试、各端专用工具 |
|
||||
| `docs/` | 当前规范、开发指南、SQL 与 SOP 模板 |
|
||||
| `openspec/` | 保留工程标准,历史业务变更记录已清理 |
|
||||
| `deploy/docker/` | Docker 构建、Compose、Nginx 与交付说明 |
|
||||
| `scripts/` | 跨项目工具;发布脚本有外部影响,不能当作普通检查运行 |
|
||||
| `deploy/test/`、`deploy/production/` | 测试部署与生产操作资料;凭据和证书在忽略的 `.local/deploy/` |
|
||||
| `resources/` | SOP 原稿、图片和导入规则,见 [资源导航](resources/README.md) |
|
||||
| `.local/` | 本机证书、环境配置、业务表格与备份,不进入 Git 或 Docker 上下文 |
|
||||
|
||||
## 部署入口与已知限制
|
||||
|
||||
- [Docker 交付说明](deploy/docker/README.md)、[测试环境说明](deploy/test/README.md)。
|
||||
- `.drone.yml` 由 tag 触发生产交付包构建,调用 `scripts/package-docker-deploy.sh`,不是日常检查流水线。
|
||||
- **当前 `docs/sql/zkxw.sql` 只包含 3 张表的建表语句,但打包和 Compose 仍引用它作为初始化文件。尚未验证空库完整部署,请勿把它视为完整可用的全库基线。** 既有部署文档中的相关说法待专项修正。
|
||||
- 部分测试引用的迁移 SQL 已缺失;字段检查器的覆盖限制见[字段一致性指南](docs/dev-field-consistency.md),移动色板差异见移动端 AGENTS。不能通过删除测试、覆盖现网数据或改变 UI 来规避这些问题。
|
||||
|
||||
## 版本管理
|
||||
|
||||
提交源码、必要模板和脱敏配置示例;依赖、构建产物、日志、本地工具状态及真实业务备份不新增入库。真实数据存放在本机 `.local/`,导入操作显式指定输入。graphify 已停用,不再生成或提交知识图谱,也不安装或运行其 hook。
|
||||
|
||||
本机凭据位于 `.local/deploy/`,表格与 TSV 位于 `.local/data/`,备份位于 `.local/backups/`。业务历史文档及整理报告已删除;后续优先维护现有规范和 README。
|
||||
@@ -8,8 +8,6 @@ __pycache__/
|
||||
logs/
|
||||
uploads/
|
||||
temp_files/
|
||||
graphify-out/
|
||||
app/graphify-out/
|
||||
**/__pycache__/
|
||||
**/.pytest_cache/
|
||||
*.md
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
# 后端开发约束
|
||||
|
||||
适用 `backend/`,与[根规则](../AGENTS.md)同时生效。环境与命令见 [README](README.md)。
|
||||
|
||||
## 分层与入口
|
||||
|
||||
- Python 3.9、FastAPI、SQLAlchemy、Pydantic v2;配置通过 `app/core/config.py` 的 pydantic-settings 与环境文件读取。
|
||||
- 路由保持薄层;业务逻辑在 `app/services/`,持久化在 `app/crud/`,模型与 Schema 在 `app/models/`、`app/schemas/`;既有 API 内联 Schema 按周边方式维护。
|
||||
- 新路由模块必须同时在 `app/api/__init__.py` 导出并在 `app/main.py` 注册;保持鉴权、API 加密和请求审计。
|
||||
- API 总前缀由 `API_V1_PREFIX` 控制,默认 `/api`;实际模块主要使用 `/web/...`、`/mini/...`,以路由定义为准。
|
||||
- 列表无自定义排序时使用 `create_time DESC`。
|
||||
|
||||
## 数据库与权限
|
||||
|
||||
- MySQL 5.7、utf8mb4。主键 `id VARCHAR(64) NOT NULL`,值为 UUID。
|
||||
- 字段顺序:`id` → 业务字段 → `tenant_no`、`order_num`、`remark`、`create_by`、`create_dept_id`、`create_time`、`update_by`、`update_time`、`del_flag`;`del_flag` 默认 `0`。
|
||||
- 查询按租户与数据权限过滤,默认排除 `del_flag=1`;写入租户从当前用户取得。禁止业务物理删除。
|
||||
- 数据范围支持全部、本部门、本部门及下级、本人、自定义;自定义使用 `sys_role_data_scope_detail.scope_type`(dept/user)和 `scope_value`。
|
||||
- 权限取有效角色并集,包括现有职位继承角色链;复用 `app/services/web_auth_roles.py`、`app/core/data_scope.py`,不要另造单角色判断。
|
||||
- SQL 变更维护在 `../docs/sql/`,区分结构迁移、种子数据与一次性数据修复;有写库行为的 `scripts/` 不是普通检查命令。
|
||||
|
||||
## 子行与样本关联
|
||||
|
||||
- 通过 `app/crud/child_row_sync.py` 的 `sync_child_rows` 按行保存,已有行保持原 ID。
|
||||
- 请求无 ID 时,用稳定业务键(样本编码、物料编码、条码、组分 ID 等)匹配尚未占用的有效行,匹配不到才生成 UUID。
|
||||
- 拒绝其他父记录或其他租户的 ID。只软删除用户移除的行;删除父记录可级联软删除,复制或克隆父记录必须为子行生成新 ID。
|
||||
- 禁止先把全部子行软删除再插入,否则会破坏 `production_step_output_id`、`protocol_sample_id`、`donation_sample_id` 等外部引用。
|
||||
- 细胞存储样本与接收、质检等下游匹配使用 `tenant_no + sample_code`,不使用 `collection_protocol_id` 或 `protocol_sample_id` 作为跨表匹配键。
|
||||
|
||||
## 字段一致性
|
||||
|
||||
- `_row_to_dict` 是实体可读字段基准;CreateBody、UpdateBody、前端 formData 必须包含其中所有可写字段。
|
||||
- 排除 `id`、`tenantNo`、`createTime`、`updateTime`、`stockQty` 等服务端或计算字段;租户不能成为客户端可写字段。
|
||||
- 逐层核对 Schema → API Handler → CRUD 参数 → ORM 赋值;字段映射不只检查返回值。
|
||||
- 修改后从仓库根目录运行 `python backend/scripts/check_field_consistency.py`,并按[指南](../docs/dev-field-consistency.md)核对受影响实体。检查器只覆盖注册实体,不能代替新增实体检查。
|
||||
- 当前检查器有两个前端路径失效,详情见[字段一致性指南](../docs/dev-field-consistency.md)。失败需区分既有问题和新改动,不能隐藏结果。
|
||||
|
||||
## 工作流与 OA
|
||||
|
||||
- `app/services/workflow_engine.py` 与 `app/services/oa_engine.py` 均有实现;不得将其描述为尚未实现或当作重复文件删除。
|
||||
- 流程由后端独立、可配置地执行;支持开始、结束、审批人、多条件分支,扩展不得破坏已发布定义。
|
||||
- 审批人策略遵循指定用户、角色或受控 SQL 解析,以及任一通过/全员通过的正式要求;不能让前端独占流程状态机。
|
||||
- 运行实例绑定发起版本,新发布版本只影响新实例;维护待办、已办中心的权限与状态一致性。
|
||||
- 具体要求见 [workflow-conventions](../openspec/changes/zkxw-engineering-standards/specs/workflow-conventions/spec.md),不同引擎的能力差异应单独核实。
|
||||
|
||||
## 验证与工具
|
||||
|
||||
- 在隔离数据环境执行受影响测试,不运行清库、导入、重置密码等脚本作为通用验收。
|
||||
- 环境与文档清理不重启服务;影响范围通过源码检索和测试核对。
|
||||
- `requirements.txt` 保存运行依赖,`venv/`、`.venv/` 是本地环境,不能纳入 Git。测试工具安装在开发环境,不为清理改动覆盖现有环境。
|
||||
+31
-79
@@ -1,99 +1,51 @@
|
||||
# zkxw 后端骨架(FastAPI)
|
||||
# 中科兴皖后端
|
||||
|
||||
基于 FastAPI 的精简后端:**Web 鉴权**(登录 RSA、登录、刷新 Token、登出、当前用户、菜单 code)、**静态运营菜单树**、**运营端路由占位**。原叁强鲜配业务 API 已移除,可按模块在 `app/api/admin.py` 等位置扩展。
|
||||
Python 3.9 + FastAPI + SQLAlchemy + Pydantic v2。共享 MySQL 5.7、Redis、MinIO,提供认证、权限、市场协议、存储、生产、质检、中试及 OA 等 API。
|
||||
|
||||
数据库结构与种子数据基线见:`docs/sql/zkxw.sql`(全库导出;默认账号 `admin` / `111111`,角色 `super_admin`)。
|
||||
## 结构
|
||||
|
||||
## 技术栈
|
||||
- `app/main.py`:应用入口、路由与中间件注册。
|
||||
- `app/api/`:接口;`app/schemas/`:请求与响应结构(部分旧模块在 API 中定义)。
|
||||
- `app/services/`:业务服务与两套现有流程引擎;`app/crud/`:数据操作。
|
||||
- `app/models/`:模型;`app/core/`:认证、配置、数据库与权限。
|
||||
- `app/tasks/`:定时任务;`tests/`:回归测试;`scripts/`:检查及一次性运维工具。
|
||||
|
||||
- FastAPI - 现代高性能异步 Web 框架
|
||||
- SQLAlchemy - ORM 数据库映射
|
||||
- Pydantic - 数据验证
|
||||
- JWT - 用户认证
|
||||
- MySQL/PostgreSQL - 数据库
|
||||
规则见 [AGENTS](AGENTS.md),工程关系见[根 README](../README.md)。依赖包含 Alembic,但仓库目前没有可直接运行的 Alembic 迁移目录,不能据此假定可执行 `alembic upgrade head`。
|
||||
|
||||
## 项目结构
|
||||
## 准备环境
|
||||
|
||||
```
|
||||
backend/
|
||||
├── app/
|
||||
│ ├── api/ # API 路由
|
||||
│ │ ├── admin/ # 运营端接口
|
||||
│ │ ├── supplier/ # 供应商端接口
|
||||
│ │ ├── client/ # 客户端接口
|
||||
│ │ └── deliver/ # 配送员端接口
|
||||
│ ├── core/ # 核心配置
|
||||
│ │ ├── config.py # 应用配置
|
||||
│ │ ├── security.py # 安全认证
|
||||
│ │ └── database.py # 数据库连接
|
||||
│ ├── models/ # 数据库模型
|
||||
│ ├── schemas/ # Pydantic schemas
|
||||
│ ├── services/ # 业务逻辑
|
||||
│ └── main.py # 应用入口
|
||||
├── requirements.txt
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 1. 创建虚拟环境
|
||||
以下命令从 `backend/` 执行。已有环境无需重复创建或覆盖配置。
|
||||
|
||||
```bash
|
||||
python -m venv venv
|
||||
venv\Scripts\activate # Windows
|
||||
# source venv/bin/activate # Linux/Mac
|
||||
python3.9 -m venv venv
|
||||
venv/bin/python -m pip install -r requirements.txt
|
||||
venv/bin/python -m pip check
|
||||
```
|
||||
|
||||
### 2. 安装依赖
|
||||
首次配置时复制 `.env.example` 为 `.env`,按团队提供的本地环境填写。默认读取 `backend/.env`;其他环境通过 `ENV_FILE` 指定。不要提交实际凭据或把测试环境指向生产数据。
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
虚拟环境由每台机器重建,不纳入 Git。测试工具不是运行依赖;需要 pytest 时安装到开发虚拟环境,再从本目录执行,例如 `venv/bin/python -m pytest tests/test_child_row_sync.py`。完整测试集需先确认配置及隔离数据环境。
|
||||
|
||||
### 3. 配置环境变量
|
||||
## 服务命令(由用户执行)
|
||||
|
||||
复制 `.env.example` 为 `.env` 并修改配置:
|
||||
| 环境 | 命令入口 | 默认监听/配置 |
|
||||
| --- | --- | --- |
|
||||
| 本地 | `./start.sh --host 127.0.0.1 --port 8000 --reload` | `.env`,脚本默认 host 原为 `0.0.0.0`,此处显式绑定本机 |
|
||||
| 生产 | `./start-prod.sh start` | `0.0.0.0:8111`、`.env.production`、`venv/` |
|
||||
| 测试 | `./start-test.sh start` | `127.0.0.1:10002`、`.env.test`、`.venv/` |
|
||||
|
||||
```env
|
||||
# 数据库配置
|
||||
DATABASE_URL=mysql+pymysql://root:password@localhost:3306/sanqiang
|
||||
生产/测试脚本另支持 `status`、`stop`、`restart`、`logs`。Agent 不自行启停或重启服务。不存在 `start-local.sh`;旧测试端口 8112 不能代替当前脚本默认值。
|
||||
|
||||
# JWT配置
|
||||
SECRET_KEY=your-secret-key-change-in-production
|
||||
ALGORITHM=HS256
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES=30
|
||||
## API 与数据
|
||||
|
||||
# 应用配置
|
||||
APP_NAME=陇耘鲜达
|
||||
DEBUG=true
|
||||
```
|
||||
默认前缀 `/api`;Web 管理接口主要为 `/api/web/...`,移动接口主要为 `/api/mini/...`。公共登录路径以 `app/api/public.py` 为准。JWT、RSA 登录加密、API 加密和审计由现有中间件与配置控制。
|
||||
|
||||
### 4. 启动服务
|
||||
`/health` 是健康检查;`/docs`、`/redoc` 仅在 DEBUG 启用时提供。
|
||||
|
||||
```bash
|
||||
uvicorn app.main:app --reload --port 8000
|
||||
uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
|
||||
SQL 位于 [docs/sql](../docs/sql/)。当前初始化基线不完整,部分历史迁移缺失;尚未验证从空库部署。不能直接向现有业务库导入根目录备份或运行清库、重置密码、导入脚本作为验证。
|
||||
|
||||
```
|
||||
## 检查
|
||||
|
||||
服务启动后,访问:
|
||||
- API 文档:http://127.0.0.1:8000/docs
|
||||
- ReDoc 文档:http://127.0.0.1:8000/redoc
|
||||
|
||||
## API 路由分组
|
||||
|
||||
| 前缀 | 说明 | 认证方式 |
|
||||
|------|------|----------|
|
||||
| `/api/admin` | 运营端接口 | JWT Token |
|
||||
| `/api/supplier` | 供应商端接口 | JWT Token |
|
||||
| `/api/client` | 客户端接口 | JWT Token |
|
||||
| `/api/deliver` | 配送员端接口 | JWT Token |
|
||||
| `/api/public` | 公共接口(登录等) | 无需认证 |
|
||||
|
||||
## 开发规范
|
||||
|
||||
- 使用 Pydantic 进行数据验证
|
||||
- 使用 SQLAlchemy ORM 进行数据库操作
|
||||
- 所有接口遵循 RESTful 规范
|
||||
- 密码使用 bcrypt 加密存储
|
||||
- 使用 JWT Token 进行用户认证
|
||||
- 运行受影响测试;测试数量随代码变化,不把旧报告视为当前通过证明。
|
||||
- CRUD 修改从根目录运行 `python backend/scripts/check_field_consistency.py`,同时人工核对新增或未注册实体;已知覆盖限制见[字段一致性指南](../docs/dev-field-consistency.md)。
|
||||
- `scripts/` 中的 migration、seed、reset、clear、import 工具可能写库,先读用途和数据范围,不批量执行。
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -1 +0,0 @@
|
||||
{"0": "Community 0", "1": "Community 1", "2": "Community 2", "3": "Community 3", "4": "Community 4", "5": "Community 5", "6": "Community 6", "7": "Community 7", "8": "Community 8", "9": "Community 9", "10": "Community 10", "11": "Community 11", "12": "Community 12", "13": "Community 13", "14": "Community 14", "15": "Community 15", "16": "Community 16", "17": "Community 17", "18": "Community 18", "19": "Community 19", "20": "Community 20", "21": "Community 21", "22": "Community 22", "23": "Community 23", "24": "Community 24", "25": "Community 25", "26": "Community 26", "27": "Community 27", "28": "Community 28", "29": "Community 29", "30": "Community 30", "31": "Community 31", "32": "Community 32", "33": "Community 33", "34": "Community 34", "35": "Community 35", "36": "Community 36", "37": "Community 37", "38": "Community 38", "39": "Community 39", "40": "Community 40", "41": "Community 41", "42": "Community 42", "43": "Community 43", "44": "Community 44", "45": "Community 45", "46": "Community 46", "47": "Community 47", "48": "Community 48", "49": "Community 49", "50": "Community 50", "51": "Community 51", "52": "Community 52", "53": "Community 53", "54": "Community 54", "55": "Community 55", "56": "Community 56", "57": "Community 57", "58": "Community 58"}
|
||||
@@ -1 +0,0 @@
|
||||
/opt/anaconda3/bin/python
|
||||
@@ -1,305 +0,0 @@
|
||||
# Graph Report - app (2026-10-29)
|
||||
|
||||
## Corpus Check
|
||||
- Large corpus: 320 files · ~394,483 words. Semantic extraction will be expensive (many Claude tokens). Consider running on a subfolder, or use --no-semantic to run AST-only.
|
||||
|
||||
## Summary
|
||||
- 6609 nodes · 36569 edges · 44 communities detected
|
||||
- Extraction: 32% EXTRACTED · 68% INFERRED · 0% AMBIGUOUS · INFERRED: 25020 edges (avg confidence: 0.6)
|
||||
- Token cost: 0 input · 0 output
|
||||
|
||||
## Community Hubs (Navigation)
|
||||
- [[_COMMUNITY_Community 0|Community 0]]
|
||||
- [[_COMMUNITY_Community 1|Community 1]]
|
||||
- [[_COMMUNITY_Community 2|Community 2]]
|
||||
- [[_COMMUNITY_Community 3|Community 3]]
|
||||
- [[_COMMUNITY_Community 4|Community 4]]
|
||||
- [[_COMMUNITY_Community 5|Community 5]]
|
||||
- [[_COMMUNITY_Community 6|Community 6]]
|
||||
- [[_COMMUNITY_Community 7|Community 7]]
|
||||
- [[_COMMUNITY_Community 8|Community 8]]
|
||||
- [[_COMMUNITY_Community 9|Community 9]]
|
||||
- [[_COMMUNITY_Community 10|Community 10]]
|
||||
- [[_COMMUNITY_Community 11|Community 11]]
|
||||
- [[_COMMUNITY_Community 12|Community 12]]
|
||||
- [[_COMMUNITY_Community 13|Community 13]]
|
||||
- [[_COMMUNITY_Community 14|Community 14]]
|
||||
- [[_COMMUNITY_Community 15|Community 15]]
|
||||
- [[_COMMUNITY_Community 16|Community 16]]
|
||||
- [[_COMMUNITY_Community 17|Community 17]]
|
||||
- [[_COMMUNITY_Community 18|Community 18]]
|
||||
- [[_COMMUNITY_Community 19|Community 19]]
|
||||
- [[_COMMUNITY_Community 20|Community 20]]
|
||||
- [[_COMMUNITY_Community 21|Community 21]]
|
||||
- [[_COMMUNITY_Community 22|Community 22]]
|
||||
- [[_COMMUNITY_Community 23|Community 23]]
|
||||
- [[_COMMUNITY_Community 24|Community 24]]
|
||||
- [[_COMMUNITY_Community 25|Community 25]]
|
||||
- [[_COMMUNITY_Community 26|Community 26]]
|
||||
- [[_COMMUNITY_Community 27|Community 27]]
|
||||
- [[_COMMUNITY_Community 28|Community 28]]
|
||||
- [[_COMMUNITY_Community 29|Community 29]]
|
||||
- [[_COMMUNITY_Community 30|Community 30]]
|
||||
- [[_COMMUNITY_Community 31|Community 31]]
|
||||
- [[_COMMUNITY_Community 32|Community 32]]
|
||||
- [[_COMMUNITY_Community 33|Community 33]]
|
||||
- [[_COMMUNITY_Community 34|Community 34]]
|
||||
- [[_COMMUNITY_Community 35|Community 35]]
|
||||
- [[_COMMUNITY_Community 36|Community 36]]
|
||||
- [[_COMMUNITY_Community 39|Community 39]]
|
||||
- [[_COMMUNITY_Community 40|Community 40]]
|
||||
- [[_COMMUNITY_Community 41|Community 41]]
|
||||
- [[_COMMUNITY_Community 42|Community 42]]
|
||||
- [[_COMMUNITY_Community 43|Community 43]]
|
||||
- [[_COMMUNITY_Community 44|Community 44]]
|
||||
- [[_COMMUNITY_Community 47|Community 47]]
|
||||
|
||||
## God Nodes (most connected - your core abstractions)
|
||||
1. `success_response()` - 842 edges
|
||||
2. `SysUser` - 756 edges
|
||||
3. `error_response()` - 710 edges
|
||||
4. `BizQcTaskResult` - 396 edges
|
||||
5. `BizProductionMaterial` - 379 edges
|
||||
6. `BizProductionMaterialUsage` - 371 edges
|
||||
7. `BizQcMaterialUsage` - 369 edges
|
||||
8. `BizQcMaterial` - 359 edges
|
||||
9. `BizProductionMaterialStockBatch` - 358 edges
|
||||
10. `BizMaterialUsage` - 355 edges
|
||||
|
||||
## Surprising Connections (you probably didn't know these)
|
||||
- `统一记录 API 请求/响应出入参的中间件` --uses--> `SysApiRequestLog` [INFERRED]
|
||||
core/api_request_log_middleware.py → models/models.py
|
||||
- `菜单 code 权限守卫(V0.3:通过 sys_user_role → sys_role_menu → sys_menu.perms)` --uses--> `SysUser` [INFERRED]
|
||||
core/menu_guard.py → models/models.py
|
||||
- `运营端角色守卫:admin / super_admin 可访问。` --uses--> `SysUser` [INFERRED]
|
||||
core/menu_guard.py → models/models.py
|
||||
- `读取用户授权的菜单权限标识集合(与前端 meta.menuCode / perms 对齐)。` --uses--> `SysUser` [INFERRED]
|
||||
core/menu_guard.py → models/models.py
|
||||
- `接口菜单权限守卫(已统一放行 menuCode 校验,仅保留运营端登录)。` --uses--> `SysUser` [INFERRED]
|
||||
core/menu_guard.py → models/models.py
|
||||
|
||||
## Communities
|
||||
|
||||
### Community 0 - "Community 0"
|
||||
Cohesion: 0.01
|
||||
Nodes (1175): _assert_user_tenant_access(), attachment_download_url(), create_dept(), create_dict_data(), create_dict_type(), create_express_company(), create_item_unit(), create_menu() (+1167 more)
|
||||
|
||||
### Community 1 - "Community 1"
|
||||
Cohesion: 0.0
|
||||
Nodes (901): BaseSettings, Config, secret_key_must_be_strong(), Settings, generate_uuid(), allocate_next_biz_no(), _allocate_sqlite_sequence(), _format_biz_no() (+893 more)
|
||||
|
||||
### Community 2 - "Community 2"
|
||||
Cohesion: 0.01
|
||||
Nodes (735): AttachmentDownloadUrlBody, DeptCreateBody, DeptUpdateBody, DictDataCreateBody, DictDataUpdateBody, DictTypeCreateBody, DictTypeUpdateBody, ExpressCompanyCreateBody (+727 more)
|
||||
|
||||
### Community 3 - "Community 3"
|
||||
Cohesion: 0.07
|
||||
Nodes (479): CellSampleTypeCreateBody, CellSampleTypeUpdateBody, CollectionRegionCreateBody, CollectionRegionUpdateBody, CustomerCreateBody, CustomerUpdateBody, HospitalCreateBody, HospitalUpdateBody (+471 more)
|
||||
|
||||
### Community 4 - "Community 4"
|
||||
Cohesion: 0.04
|
||||
Nodes (427): AuditCreateBody, BatchDetailIdsBody, BatchRefundFinanceSubmitBody, FinanceConfirmBody, FullmoonHandleBody, FullmoonTodoInsuranceBody, InvoiceRequestCompleteBody, InvoiceRequestCreateBody (+419 more)
|
||||
|
||||
### Community 5 - "Community 5"
|
||||
Cohesion: 0.01
|
||||
Nodes (365): _normalize_date_bound(), assert_protocol_sensitive_fields(), create_collection_protocol(), _persist_protocol_sub_resources(), _resolve_consent_form_urls(), _resolve_health_survey_form_urls(), _resolve_protocol_attachments(), update_collection_protocol() (+357 more)
|
||||
|
||||
### Community 6 - "Community 6"
|
||||
Cohesion: 0.01
|
||||
Nodes (328): _body_to_payload(), _get_current_user_id(), get_production_reception_by_sample_code(), get_production_reception_detail(), _get_tenant_no(), post_production_reception(), put_production_reception(), _resolve_reception_id() (+320 more)
|
||||
|
||||
### Community 7 - "Community 7"
|
||||
Cohesion: 0.02
|
||||
Nodes (312): 截止日期:仅日期时扩到当天 23:59:59。, 发起前预览审批链(条件齐后解析最终路径与审批人)。, 兼容旧 GET:无 formData,含条件分支时多为待确定占位。, _app_to_dict(), _copied_app_name(), copy_app(), create_app(), create_group() (+304 more)
|
||||
|
||||
### Community 8 - "Community 8"
|
||||
Cohesion: 0.01
|
||||
Nodes (256): list_ethnics_for_select(), get_designer_data(), apply_data_scope(), _get_child_dept_ids(), _resolve_user_role_scopes(), delete(), get_multi_with_filters(), count_pending_protocol_invoice_requests() (+248 more)
|
||||
|
||||
### Community 9 - "Community 9"
|
||||
Cohesion: 0.02
|
||||
Nodes (188): _register_attachment_routes(), _register_freight_routes(), _register_payment_routes(), _register_presign_routes(), _register_protocol_routes(), mp_bind_status(), 小程序鉴权:微信登录 / 绑定 openid。, 当前用户是否已绑定微信服务号 openid。 (+180 more)
|
||||
|
||||
### Community 10 - "Community 10"
|
||||
Cohesion: 0.02
|
||||
Nodes (173): _ancestor_link_from_cell_product_input(), _ancestor_sample_chain_link(), _apply_cell_product_status_change(), _build_ancestor_chain(), _build_descendant_chain(), _build_facility_path_label(), build_split_product_codes(), create_cell_product() (+165 more)
|
||||
|
||||
### Community 11 - "Community 11"
|
||||
Cohesion: 0.03
|
||||
Nodes (140): _save_sample_payments(), deduct_for_step(), _display_remark(), list_stock_logs(), _parse_receiver_purpose_from_remark(), 耗材变动记录 CRUD(biz_consumable_stock_log), 撤销步骤此前 step_save 扣减,便于重新填报。, 扣减耗材库存并生成变动记录。调用方控制事务,本函数不 commit。 (+132 more)
|
||||
|
||||
### Community 12 - "Community 12"
|
||||
Cohesion: 0.04
|
||||
Nodes (97): get_customer_service_dict(), _get_tenant_no(), _get_current_user_id(), get_sample_insurance(), get_sample_storage_cert(), _get_tenant_no(), put_sample_insurance(), put_sample_storage_cert() (+89 more)
|
||||
|
||||
### Community 13 - "Community 13"
|
||||
Cohesion: 0.06
|
||||
Nodes (105): AccessLogCreateBody, AccessLogUpdateBody, CleanRoomCreateBody, CleanRoomUpdateBody, ExhibitionAppointmentCreateBody, ExhibitionAppointmentUpdateBody, list_storage_slot_stats(), OccupancyCreateBody (+97 more)
|
||||
|
||||
### Community 14 - "Community 14"
|
||||
Cohesion: 0.05
|
||||
Nodes (58): _error_json(), lifespan(), 未捕获异常:统一 JSON 500,确保跨域场景下仍有 Access-Control-Allow-Origin, 数据库异常:返回 JSON 并保留 CORS 头,避免浏览器只报跨域错误, sqlalchemy_exception_handler(), unhandled_exception_handler(), BaseHTTPMiddleware, ApiCryptoError (+50 more)
|
||||
|
||||
### Community 15 - "Community 15"
|
||||
Cohesion: 0.05
|
||||
Nodes (52): 验证密码(哈希非 bcrypt 或损坏时返回 False,避免抛错), verify_password(), 按行同步子表。 带 ID 的行原地修改,没有 ID 时按业务键对上尚未占用的原行,其余才新增。 本次提交里没有的活动行才软删除。不要整表软删后再插入,否则外键, 按关联 id 更新关系行。重新加回已删除的关联时恢复原行,避免唯一索引冲突。, _sort_key(), sync_link_rows(), _bound_user_names_by_post_ids(), create_post() (+44 more)
|
||||
|
||||
### Community 16 - "Community 16"
|
||||
Cohesion: 0.09
|
||||
Nodes (50): complete_application(), create_basic_experiment_sale_schedules(), create_infusion_plan(), create_schedule(), _create_schedule_products(), _format_yyyy_mm_dd(), _generate_application_no(), get_application() (+42 more)
|
||||
|
||||
### Community 17 - "Community 17"
|
||||
Cohesion: 0.1
|
||||
Nodes (38): get_detail(), _load_sample_context(), _row_to_dict(), upsert(), _with_prefill(), dumps_payload(), empty_detail(), loads_payload() (+30 more)
|
||||
|
||||
### Community 18 - "Community 18"
|
||||
Cohesion: 0.11
|
||||
Nodes (35): batch_create_sterilize_logs(), batch_create_wash_logs(), batch_discard_garments(), batch_soft_delete_garments(), _count_map(), create_garment(), create_sterilize_log(), create_wash_log() (+27 more)
|
||||
|
||||
### Community 19 - "Community 19"
|
||||
Cohesion: 0.12
|
||||
Nodes (11): get_wechat_pay_client(), get_wechat_pay_v2(), 微信支付工具模块 支持微信支付 V3 API 文档参考: https://pay.weixin.qq.com/docs/v3/apis/index.ht, 统一下单接口 (V3) Args: out_trade_no: 商户订单号 total, 申请退款 Args: out_trade_no: 商户订单号 total_amount, 小程序调起支付的二次签名参数(与公众号 JSAPI 规则一致), 统一下单 (V2 API) Args: out_trade_no: 商户订单号 tot, 生成签名 V3 (SHA256 with RSA) (+3 more)
|
||||
|
||||
### Community 20 - "Community 20"
|
||||
Cohesion: 0.13
|
||||
Nodes (23): batch_create_checkups(), _checkup_project_fee(), _checkup_project_name(), create_checkup(), delete_actual_item(), delete_reserve_item(), get_checkup_by_id(), list_actual_items() (+15 more)
|
||||
|
||||
### Community 21 - "Community 21"
|
||||
Cohesion: 0.19
|
||||
Nodes (21): apply_overtime_kit_hours(), _build_rest_day_segments(), _build_workday_windows(), calc_overtime_hours(), _day_at_minutes(), _day_start_ms(), _extract_rules(), _format_ymd() (+13 more)
|
||||
|
||||
### Community 22 - "Community 22"
|
||||
Cohesion: 0.17
|
||||
Nodes (12): BusinessException, InvalidStatusTransitionException, log_delivery_operation(), log_operation(), log_order_operation(), log_settlement_operation(), log_user_operation(), OrderLockedException (+4 more)
|
||||
|
||||
### Community 23 - "Community 23"
|
||||
Cohesion: 0.2
|
||||
Nodes (19): _code_exists(), create_material(), _dec(), _fmt_dt(), get_material(), get_material_entity(), get_my_usages(), _is_enabled_status() (+11 more)
|
||||
|
||||
### Community 24 - "Community 24"
|
||||
Cohesion: 0.19
|
||||
Nodes (18): _field_label(), _is_non_empty_str(), _is_required(), _is_valid_receive_account_item(), normalize_account_fields(), _pick(), _pick_order_num(), _pick_str() (+10 more)
|
||||
|
||||
### Community 25 - "Community 25"
|
||||
Cohesion: 0.25
|
||||
Nodes (15): create_protocol_refund_record(), delete_protocol_refund_record(), _get_current_user_id(), _get_tenant_no(), list_protocol_refund_records(), _protocol_accessible(), update_protocol_refund_record(), create_refund_record() (+7 more)
|
||||
|
||||
### Community 26 - "Community 26"
|
||||
Cohesion: 0.22
|
||||
Nodes (15): apply_trip_kit_days(), calc_trip_leg_days(), _day_index(), _end_offset(), _extract_rules(), is_half_day_multiple(), 出差套件:半日制行程时长(与前端 oa-trip-days.ts 对齐), 遍历 tripKit:行程未手改则覆盖 duration;出差天数未手改则覆盖为各行程时长之和。 (+7 more)
|
||||
|
||||
### Community 27 - "Community 27"
|
||||
Cohesion: 0.32
|
||||
Nodes (12): _attach_owner_names(), create_account(), _filtered_query(), get_account(), list_accounts(), list_options(), _owner_name_map(), OA 收款账户 CRUD(oa_receive_account) (+4 more)
|
||||
|
||||
### Community 28 - "Community 28"
|
||||
Cohesion: 0.24
|
||||
Nodes (11): deduct_for_step(), _display_remark(), list_stock_logs(), _parse_receiver_purpose_from_remark(), 试剂变动记录 CRUD(biz_reagent_stock_log), 撤销步骤此前 step_save 扣减,便于重新填报。, 扣减试剂库存并生成变动记录。调用方控制事务,本函数不 commit。, _resolve_receiver_purpose() (+3 more)
|
||||
|
||||
### Community 29 - "Community 29"
|
||||
Cohesion: 0.29
|
||||
Nodes (2): BaseModel, SoftDeleteModel
|
||||
|
||||
### Community 30 - "Community 30"
|
||||
Cohesion: 0.38
|
||||
Nodes (6): derive_legacy_status_from_three(), derive_three_status_from_legacy(), 阶段A兼容:从旧 status 推导三字段。 约束:旧 status 本身混合了支付/配送/订单生命周期,因此只能尽力推断; 若业务侧已有更精确, 阶段A兼容:从三字段反推旧 status(用于旧前端/旧筛选参数兼容)。, ThreeStatus, _to_decimal()
|
||||
|
||||
### Community 31 - "Community 31"
|
||||
Cohesion: 0.4
|
||||
Nodes (5): batch_matches_stock_status(), normalize_batch_stock_status(), 质检物料入库批次:按剩余数量过滤的语义辅助。, 纯函数判定(供单测);list_batches 用 SQL 等价条件。, 返回 'available' | 'exhausted';全部 / 空 / 非法 → None(不加剩余数量条件)。
|
||||
|
||||
### Community 32 - "Community 32"
|
||||
Cohesion: 0.5
|
||||
Nodes (2): _auto_fill_create_dept_id(), ORM before_insert 钩子:当 create_dept_id 为空时,自动从请求上下文填充当前用户部门 ID
|
||||
|
||||
### Community 33 - "Community 33"
|
||||
Cohesion: 0.67
|
||||
Nodes (1): 转正套件:入职日 + 试用期月数 → 建议转正日(YYYY-MM-DD)。
|
||||
|
||||
### Community 34 - "Community 34"
|
||||
Cohesion: 1.0
|
||||
Nodes (1): 质检物料金额查看授权:专用角色与菜单权限常量。
|
||||
|
||||
### Community 35 - "Community 35"
|
||||
Cohesion: 1.0
|
||||
Nodes (1): 请求级别的用户上下文变量 通过 contextvars 在请求处理链路中传递当前用户信息, 供 SQLAlchemy 事件监听器等底层机制使用,避免逐层手动传
|
||||
|
||||
### Community 36 - "Community 36"
|
||||
Cohesion: 1.0
|
||||
Nodes (1): API 路由包(骨架:公共鉴权 + 静态菜单 + 运营端占位)
|
||||
|
||||
### Community 39 - "Community 39"
|
||||
Cohesion: 1.0
|
||||
Nodes (1): 发送订单状态通知 Args: openid: 用户openid ord
|
||||
|
||||
### Community 40 - "Community 40"
|
||||
Cohesion: 1.0
|
||||
Nodes (1): 发送支付成功通知 Args: openid: 用户openid ord
|
||||
|
||||
### Community 41 - "Community 41"
|
||||
Cohesion: 1.0
|
||||
Nodes (1): 发送配送通知 Args: openid: 配送员openid task
|
||||
|
||||
### Community 42 - "Community 42"
|
||||
Cohesion: 1.0
|
||||
Nodes (1): 发送配送时效预警 Args: openid: 配送员openid ta
|
||||
|
||||
### Community 43 - "Community 43"
|
||||
Cohesion: 1.0
|
||||
Nodes (1): 发送退款结果通知 Args: openid: 用户openid ord
|
||||
|
||||
### Community 44 - "Community 44"
|
||||
Cohesion: 1.0
|
||||
Nodes (1): 发送催单通知给配送员 Args: deliver_openid: 配送员openid
|
||||
|
||||
### Community 47 - "Community 47"
|
||||
Cohesion: 1.0
|
||||
Nodes (1): 接受 YYYYMMDD / YYYY-MM-DD,统一规范为 CHAR(8) YYYYMMDD。
|
||||
|
||||
## Knowledge Gaps
|
||||
- **213 isolated node(s):** `刷新会话 TTL(refreshToken 续期;已登录用户每次 API 校验通过后滑动续期)`, `写入 refreshToken(用于 one-time 使用/吊销)`, `根据 token 获取当前用户(包含 Redis 会话校验;会话滑动续期)`, `严格判断当前租户用户是否拥有指定移动市场权限码。`, `为移动市场 API 提供租户化、精确匹配的菜单权限依赖。` (+208 more)
|
||||
These have ≤1 connection - possible missing edges or undocumented components.
|
||||
- **Thin community `Community 29`** (8 nodes): `BaseModel`, `.to_dict()`, `get_active()`, `base.py`, `SoftDeleteModel`, `.restore()`, `.soft_delete()`, `__tablename__()`
|
||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||
- **Thin community `Community 32`** (4 nodes): `_auto_fill_create_dept_id()`, `get_db()`, `database.py`, `ORM before_insert 钩子:当 create_dept_id 为空时,自动从请求上下文填充当前用户部门 ID`
|
||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||
- **Thin community `Community 33`** (3 nodes): `add_months_ymd()`, `oa_confirmation_date.py`, `转正套件:入职日 + 试用期月数 → 建议转正日(YYYY-MM-DD)。`
|
||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||
- **Thin community `Community 34`** (2 nodes): `qc_material_amount_auth_constants.py`, `质检物料金额查看授权:专用角色与菜单权限常量。`
|
||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||
- **Thin community `Community 35`** (2 nodes): `context.py`, `请求级别的用户上下文变量 通过 contextvars 在请求处理链路中传递当前用户信息, 供 SQLAlchemy 事件监听器等底层机制使用,避免逐层手动传`
|
||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||
- **Thin community `Community 36`** (2 nodes): `__init__.py`, `API 路由包(骨架:公共鉴权 + 静态菜单 + 运营端占位)`
|
||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||
- **Thin community `Community 39`** (1 nodes): `发送订单状态通知 Args: openid: 用户openid ord`
|
||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||
- **Thin community `Community 40`** (1 nodes): `发送支付成功通知 Args: openid: 用户openid ord`
|
||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||
- **Thin community `Community 41`** (1 nodes): `发送配送通知 Args: openid: 配送员openid task`
|
||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||
- **Thin community `Community 42`** (1 nodes): `发送配送时效预警 Args: openid: 配送员openid ta`
|
||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||
- **Thin community `Community 43`** (1 nodes): `发送退款结果通知 Args: openid: 用户openid ord`
|
||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||
- **Thin community `Community 44`** (1 nodes): `发送催单通知给配送员 Args: deliver_openid: 配送员openid`
|
||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||
- **Thin community `Community 47`** (1 nodes): `接受 YYYYMMDD / YYYY-MM-DD,统一规范为 CHAR(8) YYYYMMDD。`
|
||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||
|
||||
## Suggested Questions
|
||||
_Questions this graph is uniquely positioned to answer:_
|
||||
|
||||
- **Why does `SysUser` connect `Community 2` to `Community 0`, `Community 1`, `Community 3`, `Community 4`, `Community 7`, `Community 8`, `Community 9`, `Community 10`, `Community 11`, `Community 12`, `Community 13`, `Community 15`, `Community 27`, `Community 28`?**
|
||||
_High betweenness centrality (0.118) - this node is a cross-community bridge._
|
||||
- **Why does `success_response()` connect `Community 0` to `Community 2`, `Community 5`, `Community 6`, `Community 8`, `Community 9`, `Community 12`, `Community 13`, `Community 25`?**
|
||||
_High betweenness centrality (0.050) - this node is a cross-community bridge._
|
||||
- **Why does `OaEngineError` connect `Community 7` to `Community 0`, `Community 1`, `Community 2`?**
|
||||
_High betweenness centrality (0.035) - this node is a cross-community bridge._
|
||||
- **Are the 1209 inferred relationships involving `str` (e.g. with `unhandled_exception_handler()` and `get_current_user_by_token()`) actually correct?**
|
||||
_`str` has 1209 INFERRED edges - model-reasoned connections that need verification._
|
||||
- **Are the 841 inferred relationships involving `success_response()` (e.g. with `list_storage_release_reviews()` and `get_storage_release_review_detail()`) actually correct?**
|
||||
_`success_response()` has 841 INFERRED edges - model-reasoned connections that need verification._
|
||||
- **Are the 754 inferred relationships involving `SysUser` (e.g. with `菜单 code 权限守卫(V0.3:通过 sys_user_role → sys_role_menu → sys_menu.perms)` and `运营端角色守卫:admin / super_admin 可访问。`) actually correct?**
|
||||
_`SysUser` has 754 INFERRED edges - model-reasoned connections that need verification._
|
||||
- **Are the 708 inferred relationships involving `ValueError` (e.g. with `secret_key_must_be_strong()` and `assert_protocol_status_transition()`) actually correct?**
|
||||
_`ValueError` has 708 INFERRED edges - model-reasoned connections that need verification._
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"runs": [
|
||||
{
|
||||
"date": "2026-05-19T22:53:04.223489+00:00",
|
||||
"input_tokens": 0,
|
||||
"output_tokens": 0,
|
||||
"files": 134
|
||||
}
|
||||
],
|
||||
"total_input_tokens": 0,
|
||||
"total_output_tokens": 0
|
||||
}
|
||||
@@ -1,276 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>graphify - graphify-out/graph.html</title>
|
||||
<script src="https://unpkg.com/vis-network/standalone/umd/vis-network.min.js"></script>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { background: #0f0f1a; color: #e0e0e0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; display: flex; height: 100vh; overflow: hidden; }
|
||||
#graph { flex: 1; }
|
||||
#sidebar { width: 280px; background: #1a1a2e; border-left: 1px solid #2a2a4e; display: flex; flex-direction: column; overflow: hidden; }
|
||||
#search-wrap { padding: 12px; border-bottom: 1px solid #2a2a4e; }
|
||||
#search { width: 100%; background: #0f0f1a; border: 1px solid #3a3a5e; color: #e0e0e0; padding: 7px 10px; border-radius: 6px; font-size: 13px; outline: none; }
|
||||
#search:focus { border-color: #4E79A7; }
|
||||
#search-results { max-height: 140px; overflow-y: auto; padding: 4px 12px; border-bottom: 1px solid #2a2a4e; display: none; }
|
||||
.search-item { padding: 4px 6px; cursor: pointer; border-radius: 4px; font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.search-item:hover { background: #2a2a4e; }
|
||||
#info-panel { padding: 14px; border-bottom: 1px solid #2a2a4e; min-height: 140px; }
|
||||
#info-panel h3 { font-size: 13px; color: #aaa; margin-bottom: 8px; text-transform: uppercase; letter-spacing: 0.05em; }
|
||||
#info-content { font-size: 13px; color: #ccc; line-height: 1.6; }
|
||||
#info-content .field { margin-bottom: 5px; }
|
||||
#info-content .field b { color: #e0e0e0; }
|
||||
#info-content .empty { color: #555; font-style: italic; }
|
||||
.neighbor-link { display: block; padding: 2px 6px; margin: 2px 0; border-radius: 3px; cursor: pointer; font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; border-left: 3px solid #333; }
|
||||
.neighbor-link:hover { background: #2a2a4e; }
|
||||
#neighbors-list { max-height: 160px; overflow-y: auto; margin-top: 4px; }
|
||||
#legend-wrap { flex: 1; overflow-y: auto; padding: 12px; }
|
||||
#legend-wrap h3 { font-size: 13px; color: #aaa; margin-bottom: 10px; text-transform: uppercase; letter-spacing: 0.05em; }
|
||||
.legend-item { display: flex; align-items: center; gap: 8px; padding: 4px 0; cursor: pointer; border-radius: 4px; font-size: 12px; }
|
||||
.legend-item:hover { background: #2a2a4e; padding-left: 4px; }
|
||||
.legend-item.dimmed { opacity: 0.35; }
|
||||
.legend-dot { width: 12px; height: 12px; border-radius: 50%; flex-shrink: 0; }
|
||||
.legend-label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.legend-count { color: #666; font-size: 11px; }
|
||||
#stats { padding: 10px 14px; border-top: 1px solid #2a2a4e; font-size: 11px; color: #555; }
|
||||
#legend-controls { display: flex; gap: 6px; margin-bottom: 8px; }
|
||||
#legend-controls button { flex: 1; background: #0f0f1a; border: 1px solid #3a3a5e; color: #aaa; padding: 4px 0; border-radius: 4px; font-size: 11px; cursor: pointer; }
|
||||
#legend-controls button:hover { border-color: #4E79A7; color: #e0e0e0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="graph"></div>
|
||||
<div id="sidebar">
|
||||
<div id="search-wrap">
|
||||
<input id="search" type="text" placeholder="Search nodes..." autocomplete="off">
|
||||
<div id="search-results"></div>
|
||||
</div>
|
||||
<div id="info-panel">
|
||||
<h3>Node Info</h3>
|
||||
<div id="info-content"><span class="empty">Click a node to inspect it</span></div>
|
||||
</div>
|
||||
<div id="legend-wrap">
|
||||
<h3>Communities</h3>
|
||||
<div id="legend-controls">
|
||||
<button onclick="toggleAllCommunities(false)">Show All</button>
|
||||
<button onclick="toggleAllCommunities(true)">Hide All</button>
|
||||
</div>
|
||||
<div id="legend"></div>
|
||||
</div>
|
||||
<div id="stats">4990 nodes · 27487 edges · 48 communities</div>
|
||||
</div>
|
||||
<script>
|
||||
const RAW_NODES = [{"id": "init_py", "label": "__init__.py", "color": {"background": "#B07AA1", "border": "#B07AA1", "highlight": {"background": "#ffffff", "border": "#B07AA1"}}, "size": 10.0, "font": {"size": 0, "color": "#ffffff"}, "title": "__init__.py", "community": 36, "community_name": "Community 36", "source_file": "__init__.py", "file_type": "code", "degree": 0}, {"id": "main_py", "label": "main.py", "color": {"background": "#B07AA1", "border": "#B07AA1", "highlight": {"background": "#ffffff", "border": "#B07AA1"}}, "size": 10.2, "font": {"size": 0, "color": "#ffffff"}, "title": "main.py", "community": 6, "community_name": "Community 6", "source_file": "main.py", "file_type": "code", "degree": 7}, {"id": "app_main_lifespan", "label": "lifespan()", "color": {"background": "#59A14F", "border": "#59A14F", "highlight": {"background": "#ffffff", "border": "#59A14F"}}, "size": 10.1, "font": {"size": 0, "color": "#ffffff"}, "title": "lifespan()", "community": 14, "community_name": "Community 14", "source_file": "main.py", "file_type": "code", "degree": 4}, {"id": "app_main_error_json", "label": "_error_json()", "color": {"background": "#B07AA1", "border": "#B07AA1", "highlight": {"background": "#ffffff", "border": "#B07AA1"}}, "size": 10.1, "font": {"size": 0, "color": "#ffffff"}, "title": "_error_json()", "community": 6, "community_name": "Community 6", "source_file": "main.py", "file_type": "code", "degree": 3}, {"id": "app_main_sqlalchemy_exception_handler", "label": "sqlalchemy_exception_handler()", "color": {"background": "#B07AA1", "border": "#B07AA1", "highlight": {"background": "#ffffff", "border": "#B07AA1"}}, "size": 10.1, "font": {"size": 0, "color": "#ffffff"}, "title": "sqlalchemy_exception_handler()", "community": 6, "community_name": "Community 6", "source_file": "main.py", "file_type": "code", "degree": 4}, {"id": "app_main_unhandled_exception_handler", "label": "unhandled_exception_handler()", "color": {"background": "#B07AA1", "border": "#B07AA1", "highlight": {"background": "#ffffff", "border": "#B07AA1"}}, "size": 10.2, "font": {"size": 0, "color": "#ffffff"}, "title": "unhandled_exception_handler()", "community": 6, "community_name": "Community 6", "source_file": "main.py", "file_type": "code", "degree": 5}, {"id": "app_main_mask_secret", "label": "_mask_secret()", "color": {"background": "#B07AA1", "border": "#B07AA1", "highlight": {"background": "#ffffff", "border": "#B07AA1"}}, "size": 10.0, "font": {"size": 0, "color": "#ffffff"}, "title": "_mask_secret()", "community": 6, "community_name": "Community 6", "source_file": "main.py", "file_type": "code", "degree": 1}, {"id": "app_main_root", "label": "root()", "color": {"background": "#B07AA1", "border": "#B07AA1", "highlight": {"background": "#ffffff", "border": "#B07AA1"}}, "size": 10.0, "font": {"size": 0, "color": "#ffffff"}, "title": "root()", "community": 6, "community_name": "Community 6", "source_file": "main.py", "file_type": "code", "degree": 1}, {"id": "app_main_health_check", "label": "health_check()", "color": {"background": "#B07AA1", "border": "#B07AA1", "highlight": {"background": "#ffffff", "border": "#B07AA1"}}, "size": 10.0, "font": {"size": 0, "color": "#ffffff"}, "title": "health_check()", "community": 6, "community_name": "Community 6", "source_file": "main.py", "file_type": "code", "degree": 1}, {"id": "app_main_rationale_81", "label": "\u6570\u636e\u5e93\u5f02\u5e38\uff1a\u8fd4\u56de JSON \u5e76\u4fdd\u7559 CORS \u5934\uff0c\u907f\u514d\u6d4f\u89c8\u5668\u53ea\u62a5\u8de8\u57df\u9519\u8bef", "color": {"background": "#B07AA1", "border": "#B07AA1", "highlight": {"background": "#ffffff", "border": "#B07AA1"}}, "size": 10.1, "font": {"size": 0, "color": "#ffffff"}, "title": "\u6570\u636e\u5e93\u5f02\u5e38\uff1a\u8fd4\u56de JSON \u5e76\u4fdd\u7559 CORS \u5934\uff0c\u907f\u514d\u6d4f\u89c8\u5668\u53ea\u62a5\u8de8\u57df\u9519\u8bef", "community": 6, "community_name": "Community 6", "source_file": "main.py", "file_type": "rationale", "degree": 3}, {"id": "app_main_rationale_92", "label": "\u672a\u6355\u83b7\u5f02\u5e38\uff1a\u7edf\u4e00 JSON 500\uff0c\u786e\u4fdd\u8de8\u57df\u573a\u666f\u4e0b\u4ecd\u6709 Access-Control-Allow-Origin", "color": {"background": "#B07AA1", "border": "#B07AA1", "highlight": {"background": "#ffffff", "border": "#B07AA1"}}, "size": 10.1, "font": {"size": 0, "color": "#ffffff"}, "title": "\u672a\u6355\u83b7\u5f02\u5e38\uff1a\u7edf\u4e00 JSON 500\uff0c\u786e\u4fdd\u8de8\u57df\u573a\u666f\u4e0b\u4ecd\u6709 Access-Control-Allow-Origin", "community": 6, "community_name": "Community 6", "source_file": "main.py", "file_type": "rationale", "degree": 3}, {"id": "tasks_init_py", "label": "__init__.py", "color": {"background": "#59A14F", "border": "#59A14F", "highlight": {"background": "#ffffff", "border": "#59A14F"}}, "size": 10.1, "font": {"size": 0, "color": "#ffffff"}, "title": "__init__.py", "community": 14, "community_name": "Community 14", "source_file": "tasks/__init__.py", "file_type": "code", Line truncated
|
||||
const RAW_EDGES = [{"from": "main_py", "to": "app_main_lifespan", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "main_py", "to": "app_main_error_json", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "main_py", "to": "app_main_sqlalchemy_exception_handler", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "main_py", "to": "app_main_unhandled_exception_handler", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "main_py", "to": "app_main_mask_secret", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "main_py", "to": "app_main_root", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "main_py", "to": "app_main_health_check", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_main_lifespan", "to": "core_login_crypto_ensure_initialized", "label": "calls", "title": "calls [INFERRED]", "dashes": true, "width": 1, "color": {"opacity": 0.35}, "confidence": "INFERRED"}, {"from": "app_main_lifespan", "to": "tasks_init_init_scheduled_tasks", "label": "calls", "title": "calls [INFERRED]", "dashes": true, "width": 1, "color": {"opacity": 0.35}, "confidence": "INFERRED"}, {"from": "app_main_lifespan", "to": "tasks_init_shutdown_scheduler", "label": "calls", "title": "calls [INFERRED]", "dashes": true, "width": 1, "color": {"opacity": 0.35}, "confidence": "INFERRED"}, {"from": "app_main_error_json", "to": "app_main_sqlalchemy_exception_handler", "label": "calls", "title": "calls [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_main_error_json", "to": "app_main_unhandled_exception_handler", "label": "calls", "title": "calls [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_main_sqlalchemy_exception_handler", "to": "app_main_rationale_81", "label": "rationale_for", "title": "rationale_for [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_main_sqlalchemy_exception_handler", "to": "exception", "label": "calls", "title": "calls [INFERRED]", "dashes": true, "width": 1, "color": {"opacity": 0.35}, "confidence": "INFERRED"}, {"from": "app_main_unhandled_exception_handler", "to": "app_main_rationale_92", "label": "rationale_for", "title": "rationale_for [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_main_unhandled_exception_handler", "to": "exception", "label": "calls", "title": "calls [INFERRED]", "dashes": true, "width": 1, "color": {"opacity": 0.35}, "confidence": "INFERRED"}, {"from": "app_main_unhandled_exception_handler", "to": "str", "label": "calls", "title": "calls [INFERRED]", "dashes": true, "width": 1, "color": {"opacity": 0.35}, "confidence": "INFERRED"}, {"from": "app_main_rationale_81", "to": "core_api_request_log_middleware_apirequestlogmiddleware", "label": "uses", "title": "uses [INFERRED]", "dashes": true, "width": 1, "color": {"opacity": 0.35}, "confidence": "INFERRED"}, {"from": "app_main_rationale_81", "to": "core_api_crypto_middleware_apicryptomiddleware", "label": "uses", "title": "uses [INFERRED]", "dashes": true, "width": 1, "color": {"opacity": 0.35}, "confidence": "INFERRED"}, {"from": "app_main_rationale_92", "to": "core_api_request_log_middleware_apirequestlogmiddleware", "label": "uses", "title": "uses [INFERRED]", "dashes": true, "width": 1, "color": {"opacity": 0.35}, "confidence": "INFERRED"}, {"from": "app_main_rationale_92", "to": "core_api_crypto_middleware_apicryptomiddleware", "label": "uses", "title": "uses [INFERRED]", "dashes": true, "width": 1, "color": {"opacity": 0.35}, "confidence": "INFERRED"}, {"from": "tasks_init_py", "to": "tasks_init_init_scheduled_tasks", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "tasks_init_py", "to": "tasks_init_shutdown_scheduler", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "core_auth_py", "to": "core_auth_redis_session_enabled", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "core_auth_py", "to": "core_auth_get_redis_client", "labeLine truncated
|
||||
const LEGEND = [{"cid": 0, "color": "#4E79A7", "label": "Community 0", "count": 965}, {"cid": 1, "color": "#F28E2B", "label": "Community 1", "count": 761}, {"cid": 2, "color": "#E15759", "label": "Community 2", "count": 665}, {"cid": 3, "color": "#76B7B2", "label": "Community 3", "count": 594}, {"cid": 4, "color": "#59A14F", "label": "Community 4", "count": 325}, {"cid": 5, "color": "#EDC948", "label": "Community 5", "count": 198}, {"cid": 6, "color": "#B07AA1", "label": "Community 6", "count": 186}, {"cid": 7, "color": "#FF9DA7", "label": "Community 7", "count": 182}, {"cid": 8, "color": "#9C755F", "label": "Community 8", "count": 176}, {"cid": 9, "color": "#BAB0AC", "label": "Community 9", "count": 148}, {"cid": 10, "color": "#4E79A7", "label": "Community 10", "count": 141}, {"cid": 11, "color": "#F28E2B", "label": "Community 11", "count": 118}, {"cid": 12, "color": "#E15759", "label": "Community 12", "count": 106}, {"cid": 13, "color": "#76B7B2", "label": "Community 13", "count": 74}, {"cid": 14, "color": "#59A14F", "label": "Community 14", "count": 70}, {"cid": 15, "color": "#EDC948", "label": "Community 15", "count": 41}, {"cid": 16, "color": "#B07AA1", "label": "Community 16", "count": 31}, {"cid": 17, "color": "#FF9DA7", "label": "Community 17", "count": 27}, {"cid": 18, "color": "#9C755F", "label": "Community 18", "count": 23}, {"cid": 19, "color": "#BAB0AC", "label": "Community 19", "count": 21}, {"cid": 20, "color": "#4E79A7", "label": "Community 20", "count": 17}, {"cid": 21, "color": "#F28E2B", "label": "Community 21", "count": 13}, {"cid": 22, "color": "#E15759", "label": "Community 22", "count": 12}, {"cid": 23, "color": "#76B7B2", "label": "Community 23", "count": 12}, {"cid": 24, "color": "#59A14F", "label": "Community 24", "count": 11}, {"cid": 25, "color": "#EDC948", "label": "Community 25", "count": 11}, {"cid": 26, "color": "#B07AA1", "label": "Community 26", "count": 10}, {"cid": 27, "color": "#FF9DA7", "label": "Community 27", "count": 8}, {"cid": 28, "color": "#9C755F", "label": "Community 28", "count": 8}, {"cid": 29, "color": "#BAB0AC", "label": "Community 29", "count": 7}, {"cid": 30, "color": "#4E79A7", "label": "Community 30", "count": 4}, {"cid": 31, "color": "#F28E2B", "label": "Community 31", "count": 4}, {"cid": 32, "color": "#E15759", "label": "Community 32", "count": 3}, {"cid": 33, "color": "#76B7B2", "label": "Community 33", "count": 2}, {"cid": 34, "color": "#59A14F", "label": "Community 34", "count": 2}, {"cid": 35, "color": "#EDC948", "label": "Community 35", "count": 2}, {"cid": 36, "color": "#B07AA1", "label": "Community 36", "count": 1}, {"cid": 37, "color": "#FF9DA7", "label": "Community 37", "count": 1}, {"cid": 38, "color": "#9C755F", "label": "Community 38", "count": 1}, {"cid": 39, "color": "#BAB0AC", "label": "Community 39", "count": 1}, {"cid": 40, "color": "#4E79A7", "label": "Community 40", "count": 1}, {"cid": 41, "color": "#F28E2B", "label": "Community 41", "count": 1}, {"cid": 42, "color": "#E15759", "label": "Community 42", "count": 1}, {"cid": 43, "color": "#76B7B2", "label": "Community 43", "count": 1}, {"cid": 44, "color": "#59A14F", "label": "Community 44", "count": 1}, {"cid": 45, "color": "#EDC948", "label": "Community 45", "count": 1}, {"cid": 46, "color": "#B07AA1", "label": "Community 46", "count": 1}, {"cid": 47, "color": "#FF9DA7", "label": "Community 47", "count": 1}, {"cid": 48, "color": "#9C755F", "label": "Community 48", "count": 0}, {"cid": 49, "color": "#BAB0AC", "label": "Community 49", "count": 0}, {"cid": 50, "color": "#4E79A7", "label": "Community 50", "count": 0}, {"cid": 51, "color": "#F28E2B", "label": "Community 51", "count": 0}, {"cid": 52, "color": "#E15759", "label": "Community 52", "count": 0}, {"cid": 53, "color": "#76B7B2", "label": "Community 53", "count": 0}, {"cid": 54, "color": "#59A14F", "label": "Community 54", "count": 0}, {"cid": 55, "color": "#EDC948", "label": "Community 55", "count": 0}, {"cid": 56, "color": "#B07AA1", "label": "Community 56", "count": 0}, {"cid": 57, "color": "#FF9DA7", "label": "Community 57", "count": 0}, {"cid": 58, "color": "#9C755F", "label": "Community 58", "count": 0}];
|
||||
|
||||
// HTML-escape helper — prevents XSS when injecting graph data into innerHTML
|
||||
function esc(s) {
|
||||
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,''');
|
||||
}
|
||||
|
||||
// Build vis datasets
|
||||
const nodesDS = new vis.DataSet(RAW_NODES.map(n => ({
|
||||
id: n.id, label: n.label, color: n.color, size: n.size,
|
||||
font: n.font, title: n.title,
|
||||
_community: n.community, _community_name: n.community_name,
|
||||
_source_file: n.source_file, _file_type: n.file_type, _degree: n.degree,
|
||||
})));
|
||||
|
||||
const edgesDS = new vis.DataSet(RAW_EDGES.map((e, i) => ({
|
||||
id: i, from: e.from, to: e.to,
|
||||
label: '',
|
||||
title: e.title,
|
||||
dashes: e.dashes,
|
||||
width: e.width,
|
||||
color: e.color,
|
||||
arrows: { to: { enabled: true, scaleFactor: 0.5 } },
|
||||
})));
|
||||
|
||||
const container = document.getElementById('graph');
|
||||
const network = new vis.Network(container, { nodes: nodesDS, edges: edgesDS }, {
|
||||
physics: {
|
||||
enabled: true,
|
||||
solver: 'forceAtlas2Based',
|
||||
forceAtlas2Based: {
|
||||
gravitationalConstant: -60,
|
||||
centralGravity: 0.005,
|
||||
springLength: 120,
|
||||
springConstant: 0.08,
|
||||
damping: 0.4,
|
||||
avoidOverlap: 0.8,
|
||||
},
|
||||
stabilization: { iterations: 200, fit: true },
|
||||
},
|
||||
interaction: {
|
||||
hover: true,
|
||||
tooltipDelay: 100,
|
||||
hideEdgesOnDrag: true,
|
||||
navigationButtons: false,
|
||||
keyboard: false,
|
||||
},
|
||||
nodes: { shape: 'dot', borderWidth: 1.5 },
|
||||
edges: { smooth: { type: 'continuous', roundness: 0.2 }, selectionWidth: 3 },
|
||||
});
|
||||
|
||||
network.once('stabilizationIterationsDone', () => {
|
||||
network.setOptions({ physics: { enabled: false } });
|
||||
});
|
||||
|
||||
function showInfo(nodeId) {
|
||||
const n = nodesDS.get(nodeId);
|
||||
if (!n) return;
|
||||
const neighborIds = network.getConnectedNodes(nodeId);
|
||||
const neighborItems = neighborIds.map(nid => {
|
||||
const nb = nodesDS.get(nid);
|
||||
const color = nb ? nb.color.background : '#555';
|
||||
return `<span class="neighbor-link" style="border-left-color:${esc(color)}" onclick="focusNode(${JSON.stringify(nid)})">${esc(nb ? nb.label : nid)}</span>`;
|
||||
}).join('');
|
||||
document.getElementById('info-content').innerHTML = `
|
||||
<div class="field"><b>${esc(n.label)}</b></div>
|
||||
<div class="field">Type: ${esc(n._file_type || 'unknown')}</div>
|
||||
<div class="field">Community: ${esc(n._community_name)}</div>
|
||||
<div class="field">Source: ${esc(n._source_file || '-')}</div>
|
||||
<div class="field">Degree: ${n._degree}</div>
|
||||
${neighborIds.length ? `<div class="field" style="margin-top:8px;color:#aaa;font-size:11px">Neighbors (${neighborIds.length})</div><div id="neighbors-list">${neighborItems}</div>` : ''}
|
||||
`;
|
||||
}
|
||||
|
||||
function focusNode(nodeId) {
|
||||
network.focus(nodeId, { scale: 1.4, animation: true });
|
||||
network.selectNodes([nodeId]);
|
||||
showInfo(nodeId);
|
||||
}
|
||||
|
||||
// Track hovered node — hover detection is more reliable than click params
|
||||
let hoveredNodeId = null;
|
||||
network.on('hoverNode', params => {
|
||||
hoveredNodeId = params.node;
|
||||
container.style.cursor = 'pointer';
|
||||
});
|
||||
network.on('blurNode', () => {
|
||||
hoveredNodeId = null;
|
||||
container.style.cursor = 'default';
|
||||
});
|
||||
container.addEventListener('click', () => {
|
||||
if (hoveredNodeId !== null) {
|
||||
showInfo(hoveredNodeId);
|
||||
network.selectNodes([hoveredNodeId]);
|
||||
}
|
||||
});
|
||||
network.on('click', params => {
|
||||
if (params.nodes.length > 0) {
|
||||
showInfo(params.nodes[0]);
|
||||
} else if (hoveredNodeId === null) {
|
||||
document.getElementById('info-content').innerHTML = '<span class="empty">Click a node to inspect it</span>';
|
||||
}
|
||||
});
|
||||
|
||||
const searchInput = document.getElementById('search');
|
||||
const searchResults = document.getElementById('search-results');
|
||||
searchInput.addEventListener('input', () => {
|
||||
const q = searchInput.value.toLowerCase().trim();
|
||||
searchResults.innerHTML = '';
|
||||
if (!q) { searchResults.style.display = 'none'; return; }
|
||||
const matches = RAW_NODES.filter(n => n.label.toLowerCase().includes(q)).slice(0, 20);
|
||||
if (!matches.length) { searchResults.style.display = 'none'; return; }
|
||||
searchResults.style.display = 'block';
|
||||
matches.forEach(n => {
|
||||
const el = document.createElement('div');
|
||||
el.className = 'search-item';
|
||||
el.textContent = n.label;
|
||||
el.style.borderLeft = `3px solid ${n.color.background}`;
|
||||
el.style.paddingLeft = '8px';
|
||||
el.onclick = () => {
|
||||
network.focus(n.id, { scale: 1.5, animation: true });
|
||||
network.selectNodes([n.id]);
|
||||
showInfo(n.id);
|
||||
searchResults.style.display = 'none';
|
||||
searchInput.value = '';
|
||||
};
|
||||
searchResults.appendChild(el);
|
||||
});
|
||||
});
|
||||
document.addEventListener('click', e => {
|
||||
if (!searchResults.contains(e.target) && e.target !== searchInput)
|
||||
searchResults.style.display = 'none';
|
||||
});
|
||||
|
||||
const hiddenCommunities = new Set();
|
||||
|
||||
function toggleAllCommunities(hide) {
|
||||
document.querySelectorAll('.legend-item').forEach(item => {
|
||||
hide ? item.classList.add('dimmed') : item.classList.remove('dimmed');
|
||||
});
|
||||
LEGEND.forEach(c => {
|
||||
if (hide) hiddenCommunities.add(c.cid); else hiddenCommunities.delete(c.cid);
|
||||
});
|
||||
const updates = RAW_NODES.map(n => ({ id: n.id, hidden: hide }));
|
||||
nodesDS.update(updates);
|
||||
}
|
||||
|
||||
const legendEl = document.getElementById('legend');
|
||||
LEGEND.forEach(c => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'legend-item';
|
||||
item.innerHTML = `<div class="legend-dot" style="background:${c.color}"></div>
|
||||
<span class="legend-label">${c.label}</span>
|
||||
<span class="legend-count">${c.count}</span>`;
|
||||
item.onclick = () => {
|
||||
if (hiddenCommunities.has(c.cid)) {
|
||||
hiddenCommunities.delete(c.cid);
|
||||
item.classList.remove('dimmed');
|
||||
} else {
|
||||
hiddenCommunities.add(c.cid);
|
||||
item.classList.add('dimmed');
|
||||
}
|
||||
const updates = RAW_NODES
|
||||
.filter(n => n.community === c.cid)
|
||||
.map(n => ({ id: n.id, hidden: hiddenCommunities.has(c.cid) }));
|
||||
nodesDS.update(updates);
|
||||
};
|
||||
legendEl.appendChild(item);
|
||||
});
|
||||
</script>
|
||||
<script>
|
||||
// Render hyperedges as shaded regions
|
||||
const hyperedges = [];
|
||||
// afterDrawing passes ctx already transformed to network coordinate space.
|
||||
// Draw node positions raw — no manual pan/zoom/DPR math needed.
|
||||
network.on('afterDrawing', function(ctx) {
|
||||
hyperedges.forEach(h => {
|
||||
const positions = h.nodes
|
||||
.map(nid => network.getPositions([nid])[nid])
|
||||
.filter(p => p !== undefined);
|
||||
if (positions.length < 2) return;
|
||||
ctx.save();
|
||||
ctx.globalAlpha = 0.12;
|
||||
ctx.fillStyle = '#6366f1';
|
||||
ctx.strokeStyle = '#6366f1';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
// Centroid and expanded hull in network coordinates
|
||||
const cx = positions.reduce((s, p) => s + p.x, 0) / positions.length;
|
||||
const cy = positions.reduce((s, p) => s + p.y, 0) / positions.length;
|
||||
const expanded = positions.map(p => ({
|
||||
x: cx + (p.x - cx) * 1.15,
|
||||
y: cy + (p.y - cy) * 1.15
|
||||
}));
|
||||
ctx.moveTo(expanded[0].x, expanded[0].y);
|
||||
expanded.slice(1).forEach(p => ctx.lineTo(p.x, p.y));
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
ctx.globalAlpha = 0.4;
|
||||
ctx.stroke();
|
||||
// Label
|
||||
ctx.globalAlpha = 0.8;
|
||||
ctx.fillStyle = '#4f46e5';
|
||||
ctx.font = 'bold 11px sans-serif';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(h.label, cx, cy - 5);
|
||||
ctx.restore();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,322 +0,0 @@
|
||||
{
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/__init__.py": 1778578257.989357,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/main.py": 1792634982.6186624,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/tasks/__init__.py": 1792649509.101045,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/core/auth.py": 1791692287.6777437,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/core/mobile_menu_guard.py": 1791642893.5237567,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/core/api_crypto.py": 1778578258.039314,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/core/login_crypto.py": 1782186069.0708613,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/core/config.py": 1793153767.2755127,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/core/wechat_pay.py": 1778578258.0461762,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/core/api_request_log_middleware.py": 1779882001.753267,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/core/avatar_url.py": 1780540753.5481932,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/core/database.py": 1779242347.136184,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/core/security.py": 1778578258.044949,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/core/request_payload.py": 1778578258.04292,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/core/__init__.py": 1778578258.028834,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/core/response.py": 1779928224.7450857,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/core/menu_guard.py": 1791915790.8400602,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/core/db_collation.py": 1779890253.2250955,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/core/qc_material_amount_auth_constants.py": 1783919118.6811988,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/core/context.py": 1779242263.6923044,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/core/utils.py": 1780305790.0009735,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/core/protocol_status_display.py": 1792646839.6074612,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/core/storage.py": 1786527936.515132,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/core/order_status_compat.py": 1778578258.0426462,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/core/exceptions.py": 1778578258.0413945,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/core/data_scope.py": 1791643686.8535404,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/core/admin_request_json.py": 1778578258.038963,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/core/api_crypto_middleware.py": 1778581508.2309763,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/core/wechat_message.py": 1778578258.0458734,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/utils/sample_special.py": 1782890474.0661302,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/utils/production_task_name_placeholders.py": 1788230189.063661,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/utils/lab_storage_code_task_set.py": 1788233109.633378,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/models/material.py": 1782894693.6389399,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/models/production_material.py": 1782404882.9262497,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/models/qc_param.py": 1782379593.174925,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/models/models.py": 1792722215.456125,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/models/cleanroom_garment.py": 1781347080.6165943,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/models/cn_holiday.py": 1786373798.242758,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/models/__init__.py": 1791386246.9181905,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/models/research_donation.py": 1792199580.930273,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/models/qc_material.py": 1787116012.3385794,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/models/base.py": 1778578258.0821667,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/models/clinical_payment.py": 1779834956.9588954,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/schemas/storage_release_review.py": 1787110411.5106792,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/schemas/qc_task.py": 1779597960.4337523,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/schemas/process_route.py": 1793096085.6087615,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/schemas/auth.py": 1780539434.444314,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/schemas/delivery.py": 1778578258.0949655,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/schemas/outbound_release_review.py": 1779968230.0408957,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/schemas/price_list.py": 1778578258.096609,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/schemas/user.py": 1778578258.099944,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/schemas/production_work_order.py": 1788158205.6998835,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/schemas/material.py": 1781502974.2628305,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/schemas/storage_occupancy.py": 1779418054.7652438,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/schemas/quotation.py": 1778578258.0991468,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/schemas/product_parameter_value.py": 1778578258.0980055,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/schemas/product_review_media.py": 1778578258.0986822,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/schemas/product_parameter.py": 1778578258.0977957,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/schemas/production_task.py": 1793096082.4957078,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/schemas/product_detail_media.py": 1778578258.0973988,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/schemas/product_banner.py": 1778578258.097102,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/schemas/region.py": 1778578258.099429,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/schemas/inquiry.py": 1778578258.095357,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/schemas/order.py": 1778578258.0960286,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/schemas/__init__.py": 1781502992.2721803,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/schemas/market_mobile.py": 1793155336.902657,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/schemas/order_adjust.py": 1778578258.0963318,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/schemas/cell_product.py": 1782711015.11309,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/schemas/menu.py": 1778578258.0957444,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/schemas/attachment.py": 1778763456.6244864,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/schemas/category_parameter.py": 1778578258.0944862,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/schemas/common.py": 1778578258.094703,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/schemas/unit.py": 1778578258.0996897,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/schemas/qc_work_order.py": 1787193565.5439816,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/schemas/production_task_set.py": 1787043809.4506257,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/schemas/outbound_release_material.py": 1779827058.9935315,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/schemas/product.py": 1778578258.0968297,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/schemas/workflow.py": 1778633845.5937889,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/schemas/product_review.py": 1778578258.098316,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/schemas/outbound_handover.py": 1780455571.0749218,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/schemas/category.py": 1778578258.0941947,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/schemas/leave.py": 1778666088.7680702,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/storage_release_review.py": 1787110718.4798927,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/equipment.py": 1782717312.9816003,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/qc_task.py": 1786958454.8959644,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/process_route.py": 1793095936.5547593,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/sf_express_settlement.py": 1792580020.6752806,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/sample_code.py": 1793065268.5905912,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/qc_task_result.py": 1793097690.7125766,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/outbound_release_review.py": 1792646414.7740386,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/qc_material_stock_check.py": 1793098731.3404455,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/material_stock_batch.py": 1782894693.7810533,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/post.py": 1793097928.8808393,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/sample_reception_check_result.py": 1793097696.0818002,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/qc_inspection_item_template.py": 1786941802.105201,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/production_material_stock_batch.py": 1782404955.550011,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/user.py": 1793097925.7885866,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/production_work_order.py": 1792646376.0415618,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/material.py": 1781522283.6438622,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/dict_type.py": 1779265621.5212462,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/storage_occupancy.py": 1785237078.9580858,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/qc_third_party_company.py": 1781415374.1019006,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/production_material.py": 1781344099.4832826,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/material_stock_log.py": 1779281903.0942652,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/qc_material_batch_stock_status.py": 1787016977.3323748,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/production_waste_output.py": 1787021560.513884,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/tenant.py": 1779265611.0532296,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/consumable.py": 1779512620.86979,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/material_usage.py": 1782894693.7808309,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/production_material_usage.py": 1781513732.7796721,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/sop_document.py": 1779265721.9928973,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/production_task.py": 1793096185.7513871,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/inbound_sample_special.py": 1782890475.769446,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/qc_third_party_item.py": 1779265661.482352,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/sample_production_reception.py": 1788159570.8985534,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/reagent.py": 1779512669.9847593,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/qc_inbound_lab_storage_sort.py": 1786959444.2857325,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/collection_protocol.py": 1793097712.881502,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/cleanroom_garment.py": 1782264452.4901433,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/child_row_sync.py": 1793097946.2135875,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/temp_qc_target.py": 1787193516.0555077,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/qc_material_usage.py": 1783428516.0973594,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/qc_temporary_production_cluster_sort.py": 1792636895.7513638,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/biz_no_generator.py": 1791892839.5616264,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/presign_order.py": 1793095962.5634658,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/qc_material_month_template.py": 1787115871.8303692,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/sample_info_quick_register.py": 1792377880.3840506,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/pilot_infusion_confirm.py": 1793065268.0816917,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/reagent_stock_log.py": 1779515479.6599197,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/pilot_infusion_schedule_mgmt.py": 1793098096.204029,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/clean_room.py": 1781403189.1288755,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/__init__.py": 1778605691.3816392,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/production_material_stock_check.py": 1793098717.924901,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/equipment_snapshot.py": 1781524050.998141,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/storage_entry_sms_notification.py": 1781063498.05542,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/express_company.py": 1781402091.5987046,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/customer_service_dict.py": 1791859621.720187,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/pilot_application.py": 1792646362.7074053,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/qc_lab_test_item.py": 1779265673.3888667,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/pre_transit_sample.py": 1783921714.0966582,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/role.py": 1793097935.9497454,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/qc_material_stock_batch.py": 1787017176.7612765,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/material_stock_check.py": 1793098703.7762063,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/production_task_step_assignee.py": 1793065267.4658027,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/qc_sms_notification.py": 1781590614.9654794,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/production_product_type.py": 1785145511.4485562,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/qc_task_assignee.py": 1781233180.3766735,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/research_donation_sample_collection.py": 1791187401.410944,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/qc_material_amount_auth.py": 1785905319.9222975,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/oa_approval.py": 1786414220.5298004,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/cell_product.py": 1792638856.8515265,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/protocol_refund_record.py": 1790996674.1676536,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/menu.py": 1779265729.0381103,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/collection_region.py": 1791436619.5126424,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/pilot_delivery.py": 1793098096.2724218,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/sample_reception_check_item.py": 1779007909.2933013,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/sample_storage_cert.py": 1791860094.4119813,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/site.py": 1781401290.6845422,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/api_request_log.py": 1778578258.0614743,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/dict_data.py": 1782893595.9312406,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/research_donation.py": 1793097746.9649897,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/qc_material_stock_in_no.py": 1787114586.3799527,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/ethnic.py": 1779265849.547566,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/sample_freight.py": 1793097752.573005,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/production_material_formula.py": 1793095965.716362,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/pilot_checkup.py": 1793097846.6982708,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/env_detect_item.py": 1781145689.9525824,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/process_category.py": 1779265700.0606413,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/item_unit.py": 1779265653.7504313,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/qc_work_order.py": 1793094741.0946975,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/outbound_handover_inventory.py": 1781598316.476379,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/env_detect_record.py": 1786945487.5517714,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/production_task_set.py": 1793097810.1320748,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/outbound_release_material.py": 1793097901.0378923,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/process.py": 1793095920.843898,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/pilot_service.py": 1793097887.8013425,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/customer.py": 1791648050.9604611,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/workflow.py": 1779265993.677624,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/sample_customer_service.py": 1791860094.4207802,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/oa_receive_account.py": 1792579491.9561448,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/sample_sms_notification.py": 1781063495.0317714,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/sample_reception.py": 1793097696.6803784,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/consumable_pack.py": 1793097793.9003072,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/qc_inspection_batch.py": 1793065284.8892066,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/production_material_prep.py": 1793097985.9659176,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/qc_material.py": 1787114725.8477683,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/user_display.py": 1793065264.1457374,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/collection_protocol_no.py": 1793065269.7817523,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/sample_storage_cert_qc.py": 1793009250.2649956,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/outbound_handover.py": 1780455574.0993364,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/storage_facility.py": 1792638802.7313325,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/oa_instance_draft.py": 1792462713.458171,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/sample_reception_lab_code.py": 1783308340.5854337,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/cell_sample_type.py": 1781747645.1762104,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/pilot_outbound_status_sync.py": 1781595840.6955194,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/consumable_stock_log.py": 1779515477.284446,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/dept.py": 1785145511.4446244,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/base.py": 1778578258.062207,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/protocol_donation_convert.py": 1791786447.2823255,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/sample_insurance.py": 1791860094.3744645,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/protocol_sample_collection.py": 1791860264.2204025,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/protocol_sample_payment.py": 1793097761.4878137,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/qc_item_param.py": 1793097679.9535193,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/leave.py": 1779265839.500242,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/output_type.py": 1779265703.7363207,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/exhibition_appointment.py": 1778801075.5211918,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/clinical_payment.py": 1791799767.089836,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/api/storage_release_review.py": 1787110947.8175044,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/api/outbound_release_review.py": 1785303263.2759058,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/api/public.py": 1791692287.6797194,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/api/material.py": 1793098686.1687553,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/api/production_material.py": 1793098686.8677888,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/api/mini_menu.py": 1791861013.3286748,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/api/qc.py": 1793098687.6486495,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/api/market.py": 1781522283.6823432,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/api/sample_production_reception.py": 1788159565.164049,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/api/collection_protocol.py": 1793065269.7831242,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/api/cleanroom_garment.py": 1781347256.2326367,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/api/basic_info.py": 1791454077.2373106,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/api/presign_order.py": 1793001629.236552,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/api/pilot_infusion_confirm.py": 1779657043.1389203,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/api/pilot_infusion_schedule_mgmt.py": 1779940938.18027,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/api/__init__.py": 1792634972.2299473,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/api/market_mobile.py": 1792578366.9390223,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/api/storage_entry_sms_notification.py": 1780325524.9170487,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/api/customer_service_dict.py": 1791859621.7209744,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/api/pilot_application.py": 1792653386.9872339,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/api/pre_transit_sample.py": 1786938943.6929982,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/api/qc_sms_notification.py": 1780326875.7886882,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/api/research_donation_sample_collection.py": 1791187409.0427368,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/api/oa_approval.py": 1792722294.1293957,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/api/protocol_refund_record.py": 1792197256.603858,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/api/operations.py": 1785213144.2881503,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/api/menu.py": 1792580899.7326677,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/api/pilot_delivery.py": 1779655584.820777,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/api/sample_cert_insurance.py": 1793001635.2987237,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/api/admin.py": 1793240569.7858844,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/api/env_detect.py": 1786945487.513935,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/api/research_donation.py": 1793001640.0574386,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/api/ethnic.py": 1779266243.9511197,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/api/sample_freight.py": 1792580031.1094093,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/api/pilot_checkup.py": 1779935751.2658868,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/api/mini_auth.py": 1789710909.065258,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/api/pilot_service.py": 1792641868.3677006,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/api/sample_code_correction.py": 1792635822.5685863,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/api/workflow.py": 1790217176.7514315,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/api/sample_sms_notification.py": 1780325524.907156,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/api/sample_reception.py": 1782719108.3741593,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/api/sample_storage_cert_qc.py": 1792662212.518068,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/api/insurance_company.py": 1781410837.9443986,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/api/production.py": 1793096091.2640226,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/api/protocol_sample_collection.py": 1792377889.4818213,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/api/protocol_sample_payment.py": 1793001632.8994496,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/api/leave.py": 1790217143.1910717,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/api/clinical_payment.py": 1781750841.074568,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/services/oa_engine.py": 1793065275.172498,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/services/sf_express_settlement.py": 1792575921.313689,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/services/sample_info_list_enrich.py": 1793065267.798019,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/services/sample_payment_period_summary.py": 1791799261.7547023,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/services/sample_collect_wx_notify.py": 1793153978.1273012,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/services/oa_datetime_normalize.py": 1792462649.0641665,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/services/oa_overtime_hours.py": 1786373766.5113294,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/services/workflow_callback.py": 1790217147.1955774,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/services/qc_work_order_fee_summary.py": 1792078734.3604057,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/services/storage_facility_code.py": 1785213139.8646047,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/services/oa_person_select.py": 1785401156.2563787,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/services/qc_work_order_conclusion.py": 1785206750.2532814,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/services/party_customer_sync.py": 1791648050.9613898,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/services/oa_dept_select.py": 1786000695.5043027,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/services/wechat_mini_auth.py": 1789710434.6386433,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/services/sample_storage_cert_qc_prefill.py": 1793091264.6031697,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/services/oa_publish.py": 1786001976.196836,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/services/cn_holiday_sync.py": 1786412469.104896,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/services/workflow_engine.py": 1790217176.6912422,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/services/wx_qr_session.py": 1789710006.0382662,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/services/sample_transport_mode.py": 1791386325.9832487,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/services/cn_holiday.py": 1786373910.8580065,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/services/sample_info_quick_register.py": 1791860094.3744802,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/services/market_payment.py": 1792202405.459428,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/services/oa_confirmation_date.py": 1785403063.141215,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/services/web_auth_roles.py": 1791692287.6755066,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/services/oa_trip_days.py": 1785407394.0606225,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/services/oa_bank_receipt.py": 1792140951.8290772,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/services/protocol_payment_installment.py": 1791921039.0872521,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/services/protocol_sample_list_summary.py": 1791187322.7863488,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/services/oa_related_approval.py": 1787892816.3154016,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/services/wechat_mp_message.py": 1793153773.5584576,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/services/oa_leave_hours.py": 1785397787.0493405,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/services/protocol_transfer_status.py": 1791799767.0946853,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/services/payment_chase.py": 1791903365.3346775,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/services/protocol_refund_record.py": 1790996661.009547,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/services/market_sample.py": 1791712921.0372562,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/services/sample_payment_schedule_validate.py": 1791539753.4546175,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/services/sample_info_progress.py": 1791187232.4075856,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/services/oa_instance_comment.py": 1792722268.265355,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/services/sample_payment_progress.py": 1792202352.3417196,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/services/oa_resignation_kit_sync.py": 1785405020.285242,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/services/wechat_mp_auth.py": 1786615330.0733685,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/services/market_support.py": 1792574711.417624,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/services/oa_assignee.py": 1790665524.6969597,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/services/wechat_login_service.py": 1786615952.6840453,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/services/market_protocol.py": 1793154276.9704335,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/services/market_field_scope.py": 1791651783.1531875,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/services/sample_code_correction.py": 1792634957.0385876,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/services/customer_relations.py": 1791383150.5354238,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/services/oa_receive_account.py": 1792579495.1077259,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/services/market_presign.py": 1791713904.5883873,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/services/oa_owner_fields.py": 1785401151.5200977,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/services/oa_money_field.py": 1785380625.3526123,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/services/sample_storage_cert_qc.py": 1792722939.9336097,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/services/protocol_pay_mode_switch.py": 1791922115.8003666,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/services/basic_experiment_delivery_remind.py": 1792649506.2005453,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/services/market_freight.py": 1792578248.775886,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/services/oa_post_select.py": 1785394392.5716882,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/services/oa_wechat_notify.py": 1793065275.6481574,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/services/oa_confirmation_kit_sync.py": 1785403667.4270058,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/services/sample_payment_fgd_convert.py": 1791525433.8399634,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/services/oa_formula.py": 1785145511.513383,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/services/sample_customer_service_register.py": 1791860136.2147195,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/ws/__init__.py": 1778578258.105995
|
||||
}
|
||||
@@ -13,9 +13,9 @@
|
||||
|
||||
用法:
|
||||
cd backend && source venv/bin/activate
|
||||
python scripts/import_cell_products_historical.py --dry-run
|
||||
python scripts/import_cell_products_historical.py --execute
|
||||
python scripts/import_cell_products_historical.py --execute --replace # 先删后导
|
||||
python scripts/import_cell_products_historical.py --file /path/to/input.tsv # 默认仅预览
|
||||
python scripts/import_cell_products_historical.py --file /path/to/input.tsv --execute
|
||||
python scripts/import_cell_products_historical.py --file /path/to/input.tsv --execute --replace # 先软删除匹配记录再导入
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -31,7 +31,6 @@ from typing import Dict, List, Optional, Tuple
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
REPO = ROOT.parent
|
||||
DEFAULT_TSV = REPO / "docs/data/cell_products_historical_2025.tsv"
|
||||
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
@@ -53,7 +52,7 @@ PASSAGE_RE = re.compile(r"-P([1-8])$", re.IGNORECASE)
|
||||
DATE_RE = re.compile(r"^(\d{4})\.(\d{1,2})\.(\d{1,2})$")
|
||||
DATE_YEAR_ONLY_RE = re.compile(r"^(\d{4})$")
|
||||
SLOT_CODE_LINE_RE = re.compile(r"^\d+-\d+-\d+-\d+$")
|
||||
DEFAULT_EXCLUDE_SLOTS_FILE = REPO / "细胞产品库数据刷新/特别注意.md"
|
||||
DEFAULT_EXCLUDE_SLOTS_FILE = REPO / "resources/data-imports/cell-products/特别注意.md"
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -458,8 +457,8 @@ def main() -> int:
|
||||
parser.add_argument(
|
||||
"--file",
|
||||
type=Path,
|
||||
default=DEFAULT_TSV,
|
||||
help=f"TSV 数据文件(默认 {DEFAULT_TSV.relative_to(REPO)})",
|
||||
required=True,
|
||||
help="TSV 数据文件路径(必须显式指定)",
|
||||
)
|
||||
parser.add_argument("--tenant-no", default="zkxw")
|
||||
parser.add_argument("--operator", default="周斌倩", help="create_by 对应用户 nick_name")
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
"""采集协议单缴费模型 v1 → v2 迁移(生产专用,可重复执行)。
|
||||
|
||||
规则(与 docs/superpowers/specs/2026-10-15-protocol-save-auto-submit-and-v1-to-v2-migration-design.md 一致):
|
||||
迁移规则:
|
||||
|
||||
- 单样本 + 无退费:1:1 迁到 sample_payment / schedule / transfer,并置 payment_model_version=2
|
||||
- 多样本 + 无退费:默认只进人工清单;加 --multi-average 时按样本数平均分合同/应缴/转账
|
||||
|
||||
@@ -1,241 +0,0 @@
|
||||
<#
|
||||
.Synopsis
|
||||
Activate a Python virtual environment for the current PowerShell session.
|
||||
|
||||
.Description
|
||||
Pushes the python executable for a virtual environment to the front of the
|
||||
$Env:PATH environment variable and sets the prompt to signify that you are
|
||||
in a Python virtual environment. Makes use of the command line switches as
|
||||
well as the `pyvenv.cfg` file values present in the virtual environment.
|
||||
|
||||
.Parameter VenvDir
|
||||
Path to the directory that contains the virtual environment to activate. The
|
||||
default value for this is the parent of the directory that the Activate.ps1
|
||||
script is located within.
|
||||
|
||||
.Parameter Prompt
|
||||
The prompt prefix to display when this virtual environment is activated. By
|
||||
default, this prompt is the name of the virtual environment folder (VenvDir)
|
||||
surrounded by parentheses and followed by a single space (ie. '(.venv) ').
|
||||
|
||||
.Example
|
||||
Activate.ps1
|
||||
Activates the Python virtual environment that contains the Activate.ps1 script.
|
||||
|
||||
.Example
|
||||
Activate.ps1 -Verbose
|
||||
Activates the Python virtual environment that contains the Activate.ps1 script,
|
||||
and shows extra information about the activation as it executes.
|
||||
|
||||
.Example
|
||||
Activate.ps1 -VenvDir C:\Users\MyUser\Common\.venv
|
||||
Activates the Python virtual environment located in the specified location.
|
||||
|
||||
.Example
|
||||
Activate.ps1 -Prompt "MyPython"
|
||||
Activates the Python virtual environment that contains the Activate.ps1 script,
|
||||
and prefixes the current prompt with the specified string (surrounded in
|
||||
parentheses) while the virtual environment is active.
|
||||
|
||||
.Notes
|
||||
On Windows, it may be required to enable this Activate.ps1 script by setting the
|
||||
execution policy for the user. You can do this by issuing the following PowerShell
|
||||
command:
|
||||
|
||||
PS C:\> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
|
||||
|
||||
For more information on Execution Policies:
|
||||
https://go.microsoft.com/fwlink/?LinkID=135170
|
||||
|
||||
#>
|
||||
Param(
|
||||
[Parameter(Mandatory = $false)]
|
||||
[String]
|
||||
$VenvDir,
|
||||
[Parameter(Mandatory = $false)]
|
||||
[String]
|
||||
$Prompt
|
||||
)
|
||||
|
||||
<# Function declarations --------------------------------------------------- #>
|
||||
|
||||
<#
|
||||
.Synopsis
|
||||
Remove all shell session elements added by the Activate script, including the
|
||||
addition of the virtual environment's Python executable from the beginning of
|
||||
the PATH variable.
|
||||
|
||||
.Parameter NonDestructive
|
||||
If present, do not remove this function from the global namespace for the
|
||||
session.
|
||||
|
||||
#>
|
||||
function global:deactivate ([switch]$NonDestructive) {
|
||||
# Revert to original values
|
||||
|
||||
# The prior prompt:
|
||||
if (Test-Path -Path Function:_OLD_VIRTUAL_PROMPT) {
|
||||
Copy-Item -Path Function:_OLD_VIRTUAL_PROMPT -Destination Function:prompt
|
||||
Remove-Item -Path Function:_OLD_VIRTUAL_PROMPT
|
||||
}
|
||||
|
||||
# The prior PYTHONHOME:
|
||||
if (Test-Path -Path Env:_OLD_VIRTUAL_PYTHONHOME) {
|
||||
Copy-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME -Destination Env:PYTHONHOME
|
||||
Remove-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME
|
||||
}
|
||||
|
||||
# The prior PATH:
|
||||
if (Test-Path -Path Env:_OLD_VIRTUAL_PATH) {
|
||||
Copy-Item -Path Env:_OLD_VIRTUAL_PATH -Destination Env:PATH
|
||||
Remove-Item -Path Env:_OLD_VIRTUAL_PATH
|
||||
}
|
||||
|
||||
# Just remove the VIRTUAL_ENV altogether:
|
||||
if (Test-Path -Path Env:VIRTUAL_ENV) {
|
||||
Remove-Item -Path env:VIRTUAL_ENV
|
||||
}
|
||||
|
||||
# Just remove the _PYTHON_VENV_PROMPT_PREFIX altogether:
|
||||
if (Get-Variable -Name "_PYTHON_VENV_PROMPT_PREFIX" -ErrorAction SilentlyContinue) {
|
||||
Remove-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Scope Global -Force
|
||||
}
|
||||
|
||||
# Leave deactivate function in the global namespace if requested:
|
||||
if (-not $NonDestructive) {
|
||||
Remove-Item -Path function:deactivate
|
||||
}
|
||||
}
|
||||
|
||||
<#
|
||||
.Description
|
||||
Get-PyVenvConfig parses the values from the pyvenv.cfg file located in the
|
||||
given folder, and returns them in a map.
|
||||
|
||||
For each line in the pyvenv.cfg file, if that line can be parsed into exactly
|
||||
two strings separated by `=` (with any amount of whitespace surrounding the =)
|
||||
then it is considered a `key = value` line. The left hand string is the key,
|
||||
the right hand is the value.
|
||||
|
||||
If the value starts with a `'` or a `"` then the first and last character is
|
||||
stripped from the value before being captured.
|
||||
|
||||
.Parameter ConfigDir
|
||||
Path to the directory that contains the `pyvenv.cfg` file.
|
||||
#>
|
||||
function Get-PyVenvConfig(
|
||||
[String]
|
||||
$ConfigDir
|
||||
) {
|
||||
Write-Verbose "Given ConfigDir=$ConfigDir, obtain values in pyvenv.cfg"
|
||||
|
||||
# Ensure the file exists, and issue a warning if it doesn't (but still allow the function to continue).
|
||||
$pyvenvConfigPath = Join-Path -Resolve -Path $ConfigDir -ChildPath 'pyvenv.cfg' -ErrorAction Continue
|
||||
|
||||
# An empty map will be returned if no config file is found.
|
||||
$pyvenvConfig = @{ }
|
||||
|
||||
if ($pyvenvConfigPath) {
|
||||
|
||||
Write-Verbose "File exists, parse `key = value` lines"
|
||||
$pyvenvConfigContent = Get-Content -Path $pyvenvConfigPath
|
||||
|
||||
$pyvenvConfigContent | ForEach-Object {
|
||||
$keyval = $PSItem -split "\s*=\s*", 2
|
||||
if ($keyval[0] -and $keyval[1]) {
|
||||
$val = $keyval[1]
|
||||
|
||||
# Remove extraneous quotations around a string value.
|
||||
if ("'""".Contains($val.Substring(0, 1))) {
|
||||
$val = $val.Substring(1, $val.Length - 2)
|
||||
}
|
||||
|
||||
$pyvenvConfig[$keyval[0]] = $val
|
||||
Write-Verbose "Adding Key: '$($keyval[0])'='$val'"
|
||||
}
|
||||
}
|
||||
}
|
||||
return $pyvenvConfig
|
||||
}
|
||||
|
||||
|
||||
<# Begin Activate script --------------------------------------------------- #>
|
||||
|
||||
# Determine the containing directory of this script
|
||||
$VenvExecPath = Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||
$VenvExecDir = Get-Item -Path $VenvExecPath
|
||||
|
||||
Write-Verbose "Activation script is located in path: '$VenvExecPath'"
|
||||
Write-Verbose "VenvExecDir Fullname: '$($VenvExecDir.FullName)"
|
||||
Write-Verbose "VenvExecDir Name: '$($VenvExecDir.Name)"
|
||||
|
||||
# Set values required in priority: CmdLine, ConfigFile, Default
|
||||
# First, get the location of the virtual environment, it might not be
|
||||
# VenvExecDir if specified on the command line.
|
||||
if ($VenvDir) {
|
||||
Write-Verbose "VenvDir given as parameter, using '$VenvDir' to determine values"
|
||||
}
|
||||
else {
|
||||
Write-Verbose "VenvDir not given as a parameter, using parent directory name as VenvDir."
|
||||
$VenvDir = $VenvExecDir.Parent.FullName.TrimEnd("\\/")
|
||||
Write-Verbose "VenvDir=$VenvDir"
|
||||
}
|
||||
|
||||
# Next, read the `pyvenv.cfg` file to determine any required value such
|
||||
# as `prompt`.
|
||||
$pyvenvCfg = Get-PyVenvConfig -ConfigDir $VenvDir
|
||||
|
||||
# Next, set the prompt from the command line, or the config file, or
|
||||
# just use the name of the virtual environment folder.
|
||||
if ($Prompt) {
|
||||
Write-Verbose "Prompt specified as argument, using '$Prompt'"
|
||||
}
|
||||
else {
|
||||
Write-Verbose "Prompt not specified as argument to script, checking pyvenv.cfg value"
|
||||
if ($pyvenvCfg -and $pyvenvCfg['prompt']) {
|
||||
Write-Verbose " Setting based on value in pyvenv.cfg='$($pyvenvCfg['prompt'])'"
|
||||
$Prompt = $pyvenvCfg['prompt'];
|
||||
}
|
||||
else {
|
||||
Write-Verbose " Setting prompt based on parent's directory's name. (Is the directory name passed to venv module when creating the virutal environment)"
|
||||
Write-Verbose " Got leaf-name of $VenvDir='$(Split-Path -Path $venvDir -Leaf)'"
|
||||
$Prompt = Split-Path -Path $venvDir -Leaf
|
||||
}
|
||||
}
|
||||
|
||||
Write-Verbose "Prompt = '$Prompt'"
|
||||
Write-Verbose "VenvDir='$VenvDir'"
|
||||
|
||||
# Deactivate any currently active virtual environment, but leave the
|
||||
# deactivate function in place.
|
||||
deactivate -nondestructive
|
||||
|
||||
# Now set the environment variable VIRTUAL_ENV, used by many tools to determine
|
||||
# that there is an activated venv.
|
||||
$env:VIRTUAL_ENV = $VenvDir
|
||||
|
||||
if (-not $Env:VIRTUAL_ENV_DISABLE_PROMPT) {
|
||||
|
||||
Write-Verbose "Setting prompt to '$Prompt'"
|
||||
|
||||
# Set the prompt to include the env name
|
||||
# Make sure _OLD_VIRTUAL_PROMPT is global
|
||||
function global:_OLD_VIRTUAL_PROMPT { "" }
|
||||
Copy-Item -Path function:prompt -Destination function:_OLD_VIRTUAL_PROMPT
|
||||
New-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Description "Python virtual environment prompt prefix" -Scope Global -Option ReadOnly -Visibility Public -Value $Prompt
|
||||
|
||||
function global:prompt {
|
||||
Write-Host -NoNewline -ForegroundColor Green "($_PYTHON_VENV_PROMPT_PREFIX) "
|
||||
_OLD_VIRTUAL_PROMPT
|
||||
}
|
||||
}
|
||||
|
||||
# Clear PYTHONHOME
|
||||
if (Test-Path -Path Env:PYTHONHOME) {
|
||||
Copy-Item -Path Env:PYTHONHOME -Destination Env:_OLD_VIRTUAL_PYTHONHOME
|
||||
Remove-Item -Path Env:PYTHONHOME
|
||||
}
|
||||
|
||||
# Add the venv to the PATH
|
||||
Copy-Item -Path Env:PATH -Destination Env:_OLD_VIRTUAL_PATH
|
||||
$Env:PATH = "$VenvExecDir$([System.IO.Path]::PathSeparator)$Env:PATH"
|
||||
@@ -1,66 +0,0 @@
|
||||
# This file must be used with "source bin/activate" *from bash*
|
||||
# you cannot run it directly
|
||||
|
||||
deactivate () {
|
||||
# reset old environment variables
|
||||
if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then
|
||||
PATH="${_OLD_VIRTUAL_PATH:-}"
|
||||
export PATH
|
||||
unset _OLD_VIRTUAL_PATH
|
||||
fi
|
||||
if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then
|
||||
PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}"
|
||||
export PYTHONHOME
|
||||
unset _OLD_VIRTUAL_PYTHONHOME
|
||||
fi
|
||||
|
||||
# This should detect bash and zsh, which have a hash command that must
|
||||
# be called to get it to forget past commands. Without forgetting
|
||||
# past commands the $PATH changes we made may not be respected
|
||||
if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then
|
||||
hash -r 2> /dev/null
|
||||
fi
|
||||
|
||||
if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then
|
||||
PS1="${_OLD_VIRTUAL_PS1:-}"
|
||||
export PS1
|
||||
unset _OLD_VIRTUAL_PS1
|
||||
fi
|
||||
|
||||
unset VIRTUAL_ENV
|
||||
if [ ! "${1:-}" = "nondestructive" ] ; then
|
||||
# Self destruct!
|
||||
unset -f deactivate
|
||||
fi
|
||||
}
|
||||
|
||||
# unset irrelevant variables
|
||||
deactivate nondestructive
|
||||
|
||||
VIRTUAL_ENV="/Users/wuzilong/work/00code/sqxp/backend/venv"
|
||||
export VIRTUAL_ENV
|
||||
|
||||
_OLD_VIRTUAL_PATH="$PATH"
|
||||
PATH="$VIRTUAL_ENV/bin:$PATH"
|
||||
export PATH
|
||||
|
||||
# unset PYTHONHOME if set
|
||||
# this will fail if PYTHONHOME is set to the empty string (which is bad anyway)
|
||||
# could use `if (set -u; : $PYTHONHOME) ;` in bash
|
||||
if [ -n "${PYTHONHOME:-}" ] ; then
|
||||
_OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}"
|
||||
unset PYTHONHOME
|
||||
fi
|
||||
|
||||
if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then
|
||||
_OLD_VIRTUAL_PS1="${PS1:-}"
|
||||
PS1="(venv) ${PS1:-}"
|
||||
export PS1
|
||||
fi
|
||||
|
||||
# This should detect bash and zsh, which have a hash command that must
|
||||
# be called to get it to forget past commands. Without forgetting
|
||||
# past commands the $PATH changes we made may not be respected
|
||||
if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then
|
||||
hash -r 2> /dev/null
|
||||
fi
|
||||
@@ -1,25 +0,0 @@
|
||||
# This file must be used with "source bin/activate.csh" *from csh*.
|
||||
# You cannot run it directly.
|
||||
# Created by Davide Di Blasi <davidedb@gmail.com>.
|
||||
# Ported to Python 3.3 venv by Andrew Svetlov <andrew.svetlov@gmail.com>
|
||||
|
||||
alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; test "\!:*" != "nondestructive" && unalias deactivate'
|
||||
|
||||
# Unset irrelevant variables.
|
||||
deactivate nondestructive
|
||||
|
||||
setenv VIRTUAL_ENV "/Users/wuzilong/work/00code/sqxp/backend/venv"
|
||||
|
||||
set _OLD_VIRTUAL_PATH="$PATH"
|
||||
setenv PATH "$VIRTUAL_ENV/bin:$PATH"
|
||||
|
||||
|
||||
set _OLD_VIRTUAL_PROMPT="$prompt"
|
||||
|
||||
if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then
|
||||
set prompt = "(venv) $prompt"
|
||||
endif
|
||||
|
||||
alias pydoc python -m pydoc
|
||||
|
||||
rehash
|
||||
@@ -1,64 +0,0 @@
|
||||
# This file must be used with "source <venv>/bin/activate.fish" *from fish*
|
||||
# (https://fishshell.com/); you cannot run it directly.
|
||||
|
||||
function deactivate -d "Exit virtual environment and return to normal shell environment"
|
||||
# reset old environment variables
|
||||
if test -n "$_OLD_VIRTUAL_PATH"
|
||||
set -gx PATH $_OLD_VIRTUAL_PATH
|
||||
set -e _OLD_VIRTUAL_PATH
|
||||
end
|
||||
if test -n "$_OLD_VIRTUAL_PYTHONHOME"
|
||||
set -gx PYTHONHOME $_OLD_VIRTUAL_PYTHONHOME
|
||||
set -e _OLD_VIRTUAL_PYTHONHOME
|
||||
end
|
||||
|
||||
if test -n "$_OLD_FISH_PROMPT_OVERRIDE"
|
||||
functions -e fish_prompt
|
||||
set -e _OLD_FISH_PROMPT_OVERRIDE
|
||||
functions -c _old_fish_prompt fish_prompt
|
||||
functions -e _old_fish_prompt
|
||||
end
|
||||
|
||||
set -e VIRTUAL_ENV
|
||||
if test "$argv[1]" != "nondestructive"
|
||||
# Self-destruct!
|
||||
functions -e deactivate
|
||||
end
|
||||
end
|
||||
|
||||
# Unset irrelevant variables.
|
||||
deactivate nondestructive
|
||||
|
||||
set -gx VIRTUAL_ENV "/Users/wuzilong/work/00code/sqxp/backend/venv"
|
||||
|
||||
set -gx _OLD_VIRTUAL_PATH $PATH
|
||||
set -gx PATH "$VIRTUAL_ENV/bin" $PATH
|
||||
|
||||
# Unset PYTHONHOME if set.
|
||||
if set -q PYTHONHOME
|
||||
set -gx _OLD_VIRTUAL_PYTHONHOME $PYTHONHOME
|
||||
set -e PYTHONHOME
|
||||
end
|
||||
|
||||
if test -z "$VIRTUAL_ENV_DISABLE_PROMPT"
|
||||
# fish uses a function instead of an env var to generate the prompt.
|
||||
|
||||
# Save the current fish_prompt function as the function _old_fish_prompt.
|
||||
functions -c fish_prompt _old_fish_prompt
|
||||
|
||||
# With the original prompt function renamed, we can override with our own.
|
||||
function fish_prompt
|
||||
# Save the return status of the last command.
|
||||
set -l old_status $status
|
||||
|
||||
# Output the venv prompt; color taken from the blue of the Python logo.
|
||||
printf "%s%s%s" (set_color 4B8BBE) "(venv) " (set_color normal)
|
||||
|
||||
# Restore the return status of the previous command.
|
||||
echo "exit $old_status" | .
|
||||
# Output the original/"old" prompt.
|
||||
_old_fish_prompt
|
||||
end
|
||||
|
||||
set -gx _OLD_FISH_PROMPT_OVERRIDE "$VIRTUAL_ENV"
|
||||
end
|
||||
@@ -1,6 +0,0 @@
|
||||
#!/Users/wuzilong/work/00code/sqxp/backend/venv/bin/python3
|
||||
import sys
|
||||
from alembic.config import main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = sys.argv[0].removesuffix('.exe')
|
||||
sys.exit(main())
|
||||
@@ -1,6 +0,0 @@
|
||||
#!/Users/wuzilong/work/00code/sqxp/backend/venv/bin/python3
|
||||
import sys
|
||||
from dotenv.__main__ import cli
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = sys.argv[0].removesuffix('.exe')
|
||||
sys.exit(cli())
|
||||
@@ -1,6 +0,0 @@
|
||||
#!/Users/wuzilong/work/00code/sqxp/backend/venv/bin/python3
|
||||
import sys
|
||||
from httpx import main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = sys.argv[0].removesuffix('.exe')
|
||||
sys.exit(main())
|
||||
@@ -1,6 +0,0 @@
|
||||
#!/Users/wuzilong/work/00code/sqxp/backend/venv/bin/python3
|
||||
import sys
|
||||
from mako.cmd import cmdline
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = sys.argv[0].removesuffix('.exe')
|
||||
sys.exit(cmdline())
|
||||
@@ -1,6 +0,0 @@
|
||||
#!/Users/wuzilong/work/00code/zkxw/backend/venv/bin/python
|
||||
import sys
|
||||
from charset_normalizer.cli import cli_detect
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = sys.argv[0].removesuffix('.exe')
|
||||
sys.exit(cli_detect())
|
||||
@@ -1,8 +0,0 @@
|
||||
#!/Users/wuzilong/work/00code/sqxp/backend/venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from pip._internal.cli.main import main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(main())
|
||||
@@ -1,8 +0,0 @@
|
||||
#!/Users/wuzilong/work/00code/sqxp/backend/venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from pip._internal.cli.main import main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(main())
|
||||
@@ -1,8 +0,0 @@
|
||||
#!/Users/wuzilong/work/00code/sqxp/backend/venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from pip._internal.cli.main import main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(main())
|
||||
@@ -1,6 +0,0 @@
|
||||
#!/Users/wuzilong/work/00code/zkxw/backend/venv/bin/python
|
||||
import sys
|
||||
from pytest import console_main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = sys.argv[0].removesuffix('.exe')
|
||||
sys.exit(console_main())
|
||||
@@ -1,6 +0,0 @@
|
||||
#!/Users/wuzilong/work/00code/zkxw/backend/venv/bin/python
|
||||
import sys
|
||||
from pygments.cmdline import main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = sys.argv[0].removesuffix('.exe')
|
||||
sys.exit(main())
|
||||
@@ -1,6 +0,0 @@
|
||||
#!/Users/wuzilong/work/00code/sqxp/backend/venv/bin/python3
|
||||
import sys
|
||||
from rsa.cli import decrypt
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = sys.argv[0].removesuffix('.exe')
|
||||
sys.exit(decrypt())
|
||||
@@ -1,6 +0,0 @@
|
||||
#!/Users/wuzilong/work/00code/sqxp/backend/venv/bin/python3
|
||||
import sys
|
||||
from rsa.cli import encrypt
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = sys.argv[0].removesuffix('.exe')
|
||||
sys.exit(encrypt())
|
||||
@@ -1,6 +0,0 @@
|
||||
#!/Users/wuzilong/work/00code/sqxp/backend/venv/bin/python3
|
||||
import sys
|
||||
from rsa.cli import keygen
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = sys.argv[0].removesuffix('.exe')
|
||||
sys.exit(keygen())
|
||||
@@ -1,6 +0,0 @@
|
||||
#!/Users/wuzilong/work/00code/sqxp/backend/venv/bin/python3
|
||||
import sys
|
||||
from rsa.util import private_to_public
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = sys.argv[0].removesuffix('.exe')
|
||||
sys.exit(private_to_public())
|
||||
@@ -1,6 +0,0 @@
|
||||
#!/Users/wuzilong/work/00code/sqxp/backend/venv/bin/python3
|
||||
import sys
|
||||
from rsa.cli import sign
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = sys.argv[0].removesuffix('.exe')
|
||||
sys.exit(sign())
|
||||
@@ -1,6 +0,0 @@
|
||||
#!/Users/wuzilong/work/00code/sqxp/backend/venv/bin/python3
|
||||
import sys
|
||||
from rsa.cli import verify
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = sys.argv[0].removesuffix('.exe')
|
||||
sys.exit(verify())
|
||||
@@ -1,6 +0,0 @@
|
||||
#!/Users/wuzilong/work/00code/zkxw/backend/venv/bin/python
|
||||
import sys
|
||||
from pytest import console_main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = sys.argv[0].removesuffix('.exe')
|
||||
sys.exit(console_main())
|
||||
@@ -1 +0,0 @@
|
||||
python3
|
||||
@@ -1 +0,0 @@
|
||||
/Library/Developer/CommandLineTools/usr/bin/python3
|
||||
@@ -1 +0,0 @@
|
||||
python3
|
||||
@@ -1,6 +0,0 @@
|
||||
#!/Users/wuzilong/work/00code/sqxp/backend/venv/bin/python3
|
||||
import sys
|
||||
from uvicorn.main import main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = sys.argv[0].removesuffix('.exe')
|
||||
sys.exit(main())
|
||||
@@ -1,6 +0,0 @@
|
||||
#!/Users/wuzilong/work/00code/sqxp/backend/venv/bin/python3
|
||||
import sys
|
||||
from watchfiles.cli import cli
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = sys.argv[0].removesuffix('.exe')
|
||||
sys.exit(cli())
|
||||
@@ -1,6 +0,0 @@
|
||||
#!/Users/wuzilong/work/00code/sqxp/backend/venv/bin/python3
|
||||
import sys
|
||||
from websockets.cli import main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = sys.argv[0].removesuffix('.exe')
|
||||
sys.exit(main())
|
||||
BIN
Binary file not shown.
@@ -1 +0,0 @@
|
||||
pip
|
||||
@@ -1,19 +0,0 @@
|
||||
This is the MIT license: http://www.opensource.org/licenses/mit-license.php
|
||||
|
||||
Copyright (c) Alex Grönholm
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
software and associated documentation files (the "Software"), to deal in the Software
|
||||
without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or
|
||||
substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
@@ -1,138 +0,0 @@
|
||||
Metadata-Version: 2.1
|
||||
Name: APScheduler
|
||||
Version: 3.10.4
|
||||
Summary: In-process task scheduler with Cron-like capabilities
|
||||
Home-page: https://github.com/agronholm/apscheduler
|
||||
Author: Alex Grönholm
|
||||
Author-email: apscheduler@nextday.fi
|
||||
License: MIT
|
||||
Keywords: scheduling cron
|
||||
Classifier: Development Status :: 5 - Production/Stable
|
||||
Classifier: Intended Audience :: Developers
|
||||
Classifier: License :: OSI Approved :: MIT License
|
||||
Classifier: Programming Language :: Python
|
||||
Classifier: Programming Language :: Python :: 3
|
||||
Classifier: Programming Language :: Python :: 3.6
|
||||
Classifier: Programming Language :: Python :: 3.7
|
||||
Classifier: Programming Language :: Python :: 3.8
|
||||
Classifier: Programming Language :: Python :: 3.9
|
||||
Classifier: Programming Language :: Python :: 3.10
|
||||
Classifier: Programming Language :: Python :: 3.11
|
||||
Requires-Python: >=3.6
|
||||
License-File: LICENSE.txt
|
||||
Requires-Dist: six >=1.4.0
|
||||
Requires-Dist: pytz
|
||||
Requires-Dist: tzlocal !=3.*,>=2.0
|
||||
Requires-Dist: importlib-metadata >=3.6.0 ; python_version < "3.8"
|
||||
Provides-Extra: doc
|
||||
Requires-Dist: sphinx ; extra == 'doc'
|
||||
Requires-Dist: sphinx-rtd-theme ; extra == 'doc'
|
||||
Provides-Extra: gevent
|
||||
Requires-Dist: gevent ; extra == 'gevent'
|
||||
Provides-Extra: mongodb
|
||||
Requires-Dist: pymongo >=3.0 ; extra == 'mongodb'
|
||||
Provides-Extra: redis
|
||||
Requires-Dist: redis >=3.0 ; extra == 'redis'
|
||||
Provides-Extra: rethinkdb
|
||||
Requires-Dist: rethinkdb >=2.4.0 ; extra == 'rethinkdb'
|
||||
Provides-Extra: sqlalchemy
|
||||
Requires-Dist: sqlalchemy >=1.4 ; extra == 'sqlalchemy'
|
||||
Provides-Extra: testing
|
||||
Requires-Dist: pytest ; extra == 'testing'
|
||||
Requires-Dist: pytest-asyncio ; extra == 'testing'
|
||||
Requires-Dist: pytest-cov ; extra == 'testing'
|
||||
Requires-Dist: pytest-tornado5 ; extra == 'testing'
|
||||
Provides-Extra: tornado
|
||||
Requires-Dist: tornado >=4.3 ; extra == 'tornado'
|
||||
Provides-Extra: twisted
|
||||
Requires-Dist: twisted ; extra == 'twisted'
|
||||
Provides-Extra: zookeeper
|
||||
Requires-Dist: kazoo ; extra == 'zookeeper'
|
||||
|
||||
.. image:: https://github.com/agronholm/apscheduler/workflows/Python%20codeqa/test/badge.svg?branch=3.x
|
||||
:target: https://github.com/agronholm/apscheduler/actions?query=workflow%3A%22Python+codeqa%2Ftest%22+branch%3A3.x
|
||||
:alt: Build Status
|
||||
.. image:: https://coveralls.io/repos/github/agronholm/apscheduler/badge.svg?branch=3.x
|
||||
:target: https://coveralls.io/github/agronholm/apscheduler?branch=3.x
|
||||
:alt: Code Coverage
|
||||
.. image:: https://readthedocs.org/projects/apscheduler/badge/?version=3.x
|
||||
:target: https://apscheduler.readthedocs.io/en/master/?badge=3.x
|
||||
:alt: Documentation
|
||||
|
||||
Advanced Python Scheduler (APScheduler) is a Python library that lets you schedule your Python code
|
||||
to be executed later, either just once or periodically. You can add new jobs or remove old ones on
|
||||
the fly as you please. If you store your jobs in a database, they will also survive scheduler
|
||||
restarts and maintain their state. When the scheduler is restarted, it will then run all the jobs
|
||||
it should have run while it was offline [#f1]_.
|
||||
|
||||
Among other things, APScheduler can be used as a cross-platform, application specific replacement
|
||||
to platform specific schedulers, such as the cron daemon or the Windows task scheduler. Please
|
||||
note, however, that APScheduler is **not** a daemon or service itself, nor does it come with any
|
||||
command line tools. It is primarily meant to be run inside existing applications. That said,
|
||||
APScheduler does provide some building blocks for you to build a scheduler service or to run a
|
||||
dedicated scheduler process.
|
||||
|
||||
APScheduler has three built-in scheduling systems you can use:
|
||||
|
||||
* Cron-style scheduling (with optional start/end times)
|
||||
* Interval-based execution (runs jobs on even intervals, with optional start/end times)
|
||||
* One-off delayed execution (runs jobs once, on a set date/time)
|
||||
|
||||
You can mix and match scheduling systems and the backends where the jobs are stored any way you
|
||||
like. Supported backends for storing jobs include:
|
||||
|
||||
* Memory
|
||||
* `SQLAlchemy <http://www.sqlalchemy.org/>`_ (any RDBMS supported by SQLAlchemy works)
|
||||
* `MongoDB <http://www.mongodb.org/>`_
|
||||
* `Redis <http://redis.io/>`_
|
||||
* `RethinkDB <https://www.rethinkdb.com/>`_
|
||||
* `ZooKeeper <https://zookeeper.apache.org/>`_
|
||||
|
||||
APScheduler also integrates with several common Python frameworks, like:
|
||||
|
||||
* `asyncio <http://docs.python.org/3.4/library/asyncio.html>`_ (:pep:`3156`)
|
||||
* `gevent <http://www.gevent.org/>`_
|
||||
* `Tornado <http://www.tornadoweb.org/>`_
|
||||
* `Twisted <http://twistedmatrix.com/>`_
|
||||
* `Qt <http://qt-project.org/>`_ (using either
|
||||
`PyQt <http://www.riverbankcomputing.com/software/pyqt/intro>`_ ,
|
||||
`PySide6 <https://wiki.qt.io/Qt_for_Python>`_ ,
|
||||
`PySide2 <https://wiki.qt.io/Qt_for_Python>`_ or
|
||||
`PySide <http://qt-project.org/wiki/PySide>`_)
|
||||
|
||||
There are third party solutions for integrating APScheduler with other frameworks:
|
||||
|
||||
* `Django <https://github.com/jarekwg/django-apscheduler>`_
|
||||
* `Flask <https://github.com/viniciuschiele/flask-apscheduler>`_
|
||||
|
||||
|
||||
.. [#f1] The cutoff period for this is also configurable.
|
||||
|
||||
|
||||
Documentation
|
||||
-------------
|
||||
|
||||
Documentation can be found `here <https://apscheduler.readthedocs.io/>`_.
|
||||
|
||||
|
||||
Source
|
||||
------
|
||||
|
||||
The source can be browsed at `Github <https://github.com/agronholm/apscheduler/tree/3.x>`_.
|
||||
|
||||
|
||||
Reporting bugs
|
||||
--------------
|
||||
|
||||
A `bug tracker <https://github.com/agronholm/apscheduler/issues>`_ is provided by Github.
|
||||
|
||||
|
||||
Getting help
|
||||
------------
|
||||
|
||||
If you have problems or other questions, you can either:
|
||||
|
||||
* Ask in the `apscheduler <https://gitter.im/apscheduler/Lobby>`_ room on Gitter
|
||||
* Ask on the `APScheduler GitHub discussion forum <https://github.com/agronholm/apscheduler/discussions>`_, or
|
||||
* Ask on `StackOverflow <http://stackoverflow.com/questions/tagged/apscheduler>`_ and tag your
|
||||
question with the ``apscheduler`` tag
|
||||
@@ -1,84 +0,0 @@
|
||||
../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/apscheduler/__init__.cpython-39.pyc,,
|
||||
../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/apscheduler/events.cpython-39.pyc,,
|
||||
../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/apscheduler/executors/__init__.cpython-39.pyc,,
|
||||
../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/apscheduler/executors/asyncio.cpython-39.pyc,,
|
||||
../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/apscheduler/executors/base.cpython-39.pyc,,
|
||||
../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/apscheduler/executors/base_py3.cpython-39.pyc,,
|
||||
../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/apscheduler/executors/debug.cpython-39.pyc,,
|
||||
../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/apscheduler/executors/gevent.cpython-39.pyc,,
|
||||
../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/apscheduler/executors/pool.cpython-39.pyc,,
|
||||
../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/apscheduler/executors/tornado.cpython-39.pyc,,
|
||||
../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/apscheduler/executors/twisted.cpython-39.pyc,,
|
||||
../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/apscheduler/job.cpython-39.pyc,,
|
||||
../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/apscheduler/jobstores/__init__.cpython-39.pyc,,
|
||||
../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/apscheduler/jobstores/base.cpython-39.pyc,,
|
||||
../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/apscheduler/jobstores/memory.cpython-39.pyc,,
|
||||
../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/apscheduler/jobstores/mongodb.cpython-39.pyc,,
|
||||
../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/apscheduler/jobstores/redis.cpython-39.pyc,,
|
||||
../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/apscheduler/jobstores/rethinkdb.cpython-39.pyc,,
|
||||
../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/apscheduler/jobstores/sqlalchemy.cpython-39.pyc,,
|
||||
../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/apscheduler/jobstores/zookeeper.cpython-39.pyc,,
|
||||
../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/apscheduler/schedulers/__init__.cpython-39.pyc,,
|
||||
../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/apscheduler/schedulers/asyncio.cpython-39.pyc,,
|
||||
../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/apscheduler/schedulers/background.cpython-39.pyc,,
|
||||
../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/apscheduler/schedulers/base.cpython-39.pyc,,
|
||||
../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/apscheduler/schedulers/blocking.cpython-39.pyc,,
|
||||
../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/apscheduler/schedulers/gevent.cpython-39.pyc,,
|
||||
../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/apscheduler/schedulers/qt.cpython-39.pyc,,
|
||||
../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/apscheduler/schedulers/tornado.cpython-39.pyc,,
|
||||
../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/apscheduler/schedulers/twisted.cpython-39.pyc,,
|
||||
../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/apscheduler/triggers/__init__.cpython-39.pyc,,
|
||||
../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/apscheduler/triggers/base.cpython-39.pyc,,
|
||||
../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/apscheduler/triggers/combining.cpython-39.pyc,,
|
||||
../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/apscheduler/triggers/cron/__init__.cpython-39.pyc,,
|
||||
../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/apscheduler/triggers/cron/expressions.cpython-39.pyc,,
|
||||
../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/apscheduler/triggers/cron/fields.cpython-39.pyc,,
|
||||
../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/apscheduler/triggers/date.cpython-39.pyc,,
|
||||
../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/apscheduler/triggers/interval.cpython-39.pyc,,
|
||||
../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/apscheduler/util.cpython-39.pyc,,
|
||||
APScheduler-3.10.4.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||
APScheduler-3.10.4.dist-info/LICENSE.txt,sha256=YWP3mH37ONa8MgzitwsvArhivEESZRbVUu8c1DJH51g,1130
|
||||
APScheduler-3.10.4.dist-info/METADATA,sha256=ITYjDYv8SBO2ynuPiXmySCDJPjfvrFElLJoKQr58h8U,5695
|
||||
APScheduler-3.10.4.dist-info/RECORD,,
|
||||
APScheduler-3.10.4.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
APScheduler-3.10.4.dist-info/WHEEL,sha256=5sUXSg9e4bi7lTLOHcm6QEYwO5TIF1TNbTSVFVjcJcc,92
|
||||
APScheduler-3.10.4.dist-info/entry_points.txt,sha256=KMxTUp2QykDNL6w-WBU5xrk8ebroCPEBN0eZtyL3x2w,1147
|
||||
APScheduler-3.10.4.dist-info/top_level.txt,sha256=O3oMCWxG-AHkecUoO6Ze7-yYjWrttL95uHO8-RFdYvE,12
|
||||
apscheduler/__init__.py,sha256=c_KXMg1QziacYqUpDuzLY5g1mcEZvBLq1dJY7NjLoKc,452
|
||||
apscheduler/events.py,sha256=KRMTDQUS6d2uVnrQvPoz3ZPV5V9XKsCAZLsgx913FFo,3593
|
||||
apscheduler/executors/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
apscheduler/executors/asyncio.py,sha256=9m4wvRHSSYplllxAQyxWkPVcFdyFG5aZbHt5nfWKIAc,1859
|
||||
apscheduler/executors/base.py,sha256=hogiMc_t-huw6BMod0HEeY2FhRNmAAUyNNuBHvIX31M,5336
|
||||
apscheduler/executors/base_py3.py,sha256=8WOpTeX1NA-spdbEQ1oJMh5T2O_t2UdsaSnAh-iEWe0,1831
|
||||
apscheduler/executors/debug.py,sha256=15_ogSBzl8RRCfBYDnkIV2uMH8cLk1KImYmBa_NVGpc,573
|
||||
apscheduler/executors/gevent.py,sha256=aulrNmoefyBgrOkH9awRhFiXIDnSCnZ4U0o0_JXIXgc,777
|
||||
apscheduler/executors/pool.py,sha256=h4cYgKMRhjpNHmkhlogHLbmT4O_q6HePXVLmiJIHC3c,2484
|
||||
apscheduler/executors/tornado.py,sha256=DU75VaQ9R6nBuy8lbPUvDKUgsuJcZqwAvURC5vg3r6w,1780
|
||||
apscheduler/executors/twisted.py,sha256=bRoU0C4BoVcS6_BjKD5wfUs0IJpGkmLsRAcMH2rJJss,778
|
||||
apscheduler/job.py,sha256=JCRERBpfWLuomPiNNHX-jrluEwfHkdscEmz4i0Y8rao,11216
|
||||
apscheduler/jobstores/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
apscheduler/jobstores/base.py,sha256=DXzSW9XscueHZHMvy1qFiG-vYqUl_MMv0n0uBSZWXGo,4523
|
||||
apscheduler/jobstores/memory.py,sha256=ZxWiKsqfsCHFvac-6X9BztuhnuSxlOYi1dhT6g-pjQo,3655
|
||||
apscheduler/jobstores/mongodb.py,sha256=r9t2neNuzfPuf_omDm0KdkLGPZXLksiH-U3j13MIBlM,5347
|
||||
apscheduler/jobstores/redis.py,sha256=kjQDIzPXz-Yq976U9HK3aMkcCI_QRLKgTADQWKewtik,5483
|
||||
apscheduler/jobstores/rethinkdb.py,sha256=k1rSLYJqejuhQxJY3pXwHAQYcpZ1QFJsoQ8n0oEu5MM,5863
|
||||
apscheduler/jobstores/sqlalchemy.py,sha256=LIA9iSGMvuPTVqGHdztgQs4YFmYN1xqXvpJauYNK470,6529
|
||||
apscheduler/jobstores/zookeeper.py,sha256=avGLXaJGjHD0F7uG6rLJ2gg_TXNqXDEM4PqOu56f-Xg,6363
|
||||
apscheduler/schedulers/__init__.py,sha256=jM63xA_K7GSToBenhsz-SCcqfhk1pdEVb6ajwoO5Kqg,406
|
||||
apscheduler/schedulers/asyncio.py,sha256=iJO6QUo1oW16giOU_nW8WMu2b9NTWT4Tg2gY586G08w,1994
|
||||
apscheduler/schedulers/background.py,sha256=751p-f5Di6pY4x6UXlZggpxQ5k2ObJ_Q5wSeWmKHS8o,1566
|
||||
apscheduler/schedulers/base.py,sha256=hCchDyhEXCoVmCfGgD3QMrKumYYLAUwY4456tQrukAY,43780
|
||||
apscheduler/schedulers/blocking.py,sha256=8nubfJ4PoUnAkEY6WRQG4COzG4SxGyW9PjuVPhDAbsk,985
|
||||
apscheduler/schedulers/gevent.py,sha256=csPBvV75FGcboXXsdex6fCD7J54QgBddYNdWj62ZO9g,1031
|
||||
apscheduler/schedulers/qt.py,sha256=jy58cP5roWOv68ytg8fiwtxMVnZKw7a8tkCHbLWeUs8,1329
|
||||
apscheduler/schedulers/tornado.py,sha256=D9Vaq3Ee9EFiXa1jDy9tedI048gR_YT_LAFUWqO_uEw,1926
|
||||
apscheduler/schedulers/twisted.py,sha256=D5EBjjMRtMBxy0_aAURcULAI8Ky2IvCTr9tK9sO1rYk,1844
|
||||
apscheduler/triggers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
apscheduler/triggers/base.py,sha256=BvBJdOnIeVClXPXeInzYK25cN64jAc4a9IiEQucSiVk,1355
|
||||
apscheduler/triggers/combining.py,sha256=klaSoBp1kyrPX5D3gBpNTlsGKjks5QeKPW5JN_MVs30,3449
|
||||
apscheduler/triggers/cron/__init__.py,sha256=D39BQ63qWyk6XZcSuWth46ELQ3VIFpYjUHh7Kj65Z9M,9251
|
||||
apscheduler/triggers/cron/expressions.py,sha256=hu1kq0mKvivIw7U0D0Nnrbuk3q01dCuhZ7SHRPw6qhI,9184
|
||||
apscheduler/triggers/cron/fields.py,sha256=NWPClh1NgSOpTlJ3sm1TXM_ViC2qJGKWkd_vg0xsw7o,3510
|
||||
apscheduler/triggers/date.py,sha256=RrfB1PNO9G9e91p1BOf-y_TseVHQQR-KJPhNdPpAHcU,1705
|
||||
apscheduler/triggers/interval.py,sha256=ABjcZFaGYAAgdAaUQIuLr9_dLszIifu88qaXrJmdxQ4,4377
|
||||
apscheduler/util.py,sha256=aCLu_v8-c7rpY6sD7EKgxH2zYjZARiBdqKFZktaxO68,13260
|
||||
Whitespace-only changes.
@@ -1,5 +0,0 @@
|
||||
Wheel-Version: 1.0
|
||||
Generator: bdist_wheel (0.41.1)
|
||||
Root-Is-Purelib: true
|
||||
Tag: py3-none-any
|
||||
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
[apscheduler.executors]
|
||||
asyncio = apscheduler.executors.asyncio:AsyncIOExecutor [asyncio]
|
||||
debug = apscheduler.executors.debug:DebugExecutor
|
||||
gevent = apscheduler.executors.gevent:GeventExecutor [gevent]
|
||||
processpool = apscheduler.executors.pool:ProcessPoolExecutor
|
||||
threadpool = apscheduler.executors.pool:ThreadPoolExecutor
|
||||
tornado = apscheduler.executors.tornado:TornadoExecutor [tornado]
|
||||
twisted = apscheduler.executors.twisted:TwistedExecutor [twisted]
|
||||
|
||||
[apscheduler.jobstores]
|
||||
memory = apscheduler.jobstores.memory:MemoryJobStore
|
||||
mongodb = apscheduler.jobstores.mongodb:MongoDBJobStore [mongodb]
|
||||
redis = apscheduler.jobstores.redis:RedisJobStore [redis]
|
||||
rethinkdb = apscheduler.jobstores.rethinkdb:RethinkDBJobStore [rethinkdb]
|
||||
sqlalchemy = apscheduler.jobstores.sqlalchemy:SQLAlchemyJobStore [sqlalchemy]
|
||||
zookeeper = apscheduler.jobstores.zookeeper:ZooKeeperJobStore [zookeeper]
|
||||
|
||||
[apscheduler.triggers]
|
||||
and = apscheduler.triggers.combining:AndTrigger
|
||||
cron = apscheduler.triggers.cron:CronTrigger
|
||||
date = apscheduler.triggers.date:DateTrigger
|
||||
interval = apscheduler.triggers.interval:IntervalTrigger
|
||||
or = apscheduler.triggers.combining:OrTrigger
|
||||
@@ -1 +0,0 @@
|
||||
apscheduler
|
||||
@@ -1,235 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Cipher/AES.py : AES
|
||||
#
|
||||
# ===================================================================
|
||||
# The contents of this file are dedicated to the public domain. To
|
||||
# the extent that dedication to the public domain is not available,
|
||||
# everyone is granted a worldwide, perpetual, royalty-free,
|
||||
# non-exclusive license to exercise all rights associated with the
|
||||
# contents of this file for any purpose whatsoever.
|
||||
# No rights are reserved.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
||||
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
||||
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
# ===================================================================
|
||||
|
||||
import sys
|
||||
|
||||
from Crypto.Cipher import _create_cipher
|
||||
from Crypto.Util._raw_api import (load_pycryptodome_raw_lib,
|
||||
VoidPointer, SmartPointer,
|
||||
c_size_t, c_uint8_ptr)
|
||||
|
||||
from Crypto.Util import _cpu_features
|
||||
from Crypto.Random import get_random_bytes
|
||||
|
||||
MODE_ECB = 1 #: Electronic Code Book (:ref:`ecb_mode`)
|
||||
MODE_CBC = 2 #: Cipher-Block Chaining (:ref:`cbc_mode`)
|
||||
MODE_CFB = 3 #: Cipher Feedback (:ref:`cfb_mode`)
|
||||
MODE_OFB = 5 #: Output Feedback (:ref:`ofb_mode`)
|
||||
MODE_CTR = 6 #: Counter mode (:ref:`ctr_mode`)
|
||||
MODE_OPENPGP = 7 #: OpenPGP mode (:ref:`openpgp_mode`)
|
||||
MODE_CCM = 8 #: Counter with CBC-MAC (:ref:`ccm_mode`)
|
||||
MODE_EAX = 9 #: :ref:`eax_mode`
|
||||
MODE_SIV = 10 #: Synthetic Initialization Vector (:ref:`siv_mode`)
|
||||
MODE_GCM = 11 #: Galois Counter Mode (:ref:`gcm_mode`)
|
||||
MODE_OCB = 12 #: Offset Code Book (:ref:`ocb_mode`)
|
||||
MODE_KW = 13 #: Key Wrap (:ref:`kw_mode`)
|
||||
MODE_KWP = 14 #: Key Wrap with Padding (:ref:`kwp_mode`)
|
||||
|
||||
_cproto = """
|
||||
int AES_start_operation(const uint8_t key[],
|
||||
size_t key_len,
|
||||
void **pResult);
|
||||
int AES_encrypt(const void *state,
|
||||
const uint8_t *in,
|
||||
uint8_t *out,
|
||||
size_t data_len);
|
||||
int AES_decrypt(const void *state,
|
||||
const uint8_t *in,
|
||||
uint8_t *out,
|
||||
size_t data_len);
|
||||
int AES_stop_operation(void *state);
|
||||
"""
|
||||
|
||||
|
||||
# Load portable AES
|
||||
_raw_aes_lib = load_pycryptodome_raw_lib("Crypto.Cipher._raw_aes",
|
||||
_cproto)
|
||||
|
||||
# Try to load AES with AES NI instructions
|
||||
try:
|
||||
_raw_aesni_lib = None
|
||||
if _cpu_features.have_aes_ni():
|
||||
_raw_aesni_lib = load_pycryptodome_raw_lib("Crypto.Cipher._raw_aesni",
|
||||
_cproto.replace("AES",
|
||||
"AESNI"))
|
||||
# _raw_aesni may not have been compiled in
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def _create_base_cipher(dict_parameters):
|
||||
"""This method instantiates and returns a handle to a low-level
|
||||
base cipher. It will absorb named parameters in the process."""
|
||||
|
||||
use_aesni = dict_parameters.pop("use_aesni", True)
|
||||
|
||||
try:
|
||||
key = dict_parameters.pop("key")
|
||||
except KeyError:
|
||||
raise TypeError("Missing 'key' parameter")
|
||||
|
||||
if len(key) not in key_size:
|
||||
raise ValueError("Incorrect AES key length (%d bytes)" % len(key))
|
||||
|
||||
if use_aesni and _raw_aesni_lib:
|
||||
start_operation = _raw_aesni_lib.AESNI_start_operation
|
||||
stop_operation = _raw_aesni_lib.AESNI_stop_operation
|
||||
else:
|
||||
start_operation = _raw_aes_lib.AES_start_operation
|
||||
stop_operation = _raw_aes_lib.AES_stop_operation
|
||||
|
||||
cipher = VoidPointer()
|
||||
result = start_operation(c_uint8_ptr(key),
|
||||
c_size_t(len(key)),
|
||||
cipher.address_of())
|
||||
if result:
|
||||
raise ValueError("Error %X while instantiating the AES cipher"
|
||||
% result)
|
||||
return SmartPointer(cipher.get(), stop_operation)
|
||||
|
||||
|
||||
def _derive_Poly1305_key_pair(key, nonce):
|
||||
"""Derive a tuple (r, s, nonce) for a Poly1305 MAC.
|
||||
|
||||
If nonce is ``None``, a new 16-byte nonce is generated.
|
||||
"""
|
||||
|
||||
if len(key) != 32:
|
||||
raise ValueError("Poly1305 with AES requires a 32-byte key")
|
||||
|
||||
if nonce is None:
|
||||
nonce = get_random_bytes(16)
|
||||
elif len(nonce) != 16:
|
||||
raise ValueError("Poly1305 with AES requires a 16-byte nonce")
|
||||
|
||||
s = new(key[:16], MODE_ECB).encrypt(nonce)
|
||||
return key[16:], s, nonce
|
||||
|
||||
|
||||
def new(key, mode, *args, **kwargs):
|
||||
"""Create a new AES cipher.
|
||||
|
||||
Args:
|
||||
key(bytes/bytearray/memoryview):
|
||||
The secret key to use in the symmetric cipher.
|
||||
|
||||
It must be 16 (*AES-128)*, 24 (*AES-192*) or 32 (*AES-256*) bytes long.
|
||||
|
||||
For ``MODE_SIV`` only, it doubles to 32, 48, or 64 bytes.
|
||||
mode (a ``MODE_*`` constant):
|
||||
The chaining mode to use for encryption or decryption.
|
||||
If in doubt, use ``MODE_EAX``.
|
||||
|
||||
Keyword Args:
|
||||
iv (bytes/bytearray/memoryview):
|
||||
(Only applicable for ``MODE_CBC``, ``MODE_CFB``, ``MODE_OFB``,
|
||||
and ``MODE_OPENPGP`` modes).
|
||||
|
||||
The initialization vector to use for encryption or decryption.
|
||||
|
||||
For ``MODE_CBC``, ``MODE_CFB``, and ``MODE_OFB`` it must be 16 bytes long.
|
||||
|
||||
For ``MODE_OPENPGP`` mode only,
|
||||
it must be 16 bytes long for encryption
|
||||
and 18 bytes for decryption (in the latter case, it is
|
||||
actually the *encrypted* IV which was prefixed to the ciphertext).
|
||||
|
||||
If not provided, a random byte string is generated (you must then
|
||||
read its value with the :attr:`iv` attribute).
|
||||
|
||||
nonce (bytes/bytearray/memoryview):
|
||||
(Only applicable for ``MODE_CCM``, ``MODE_EAX``, ``MODE_GCM``,
|
||||
``MODE_SIV``, ``MODE_OCB``, and ``MODE_CTR``).
|
||||
|
||||
A value that must never be reused for any other encryption done
|
||||
with this key (except possibly for ``MODE_SIV``, see below).
|
||||
|
||||
For ``MODE_EAX``, ``MODE_GCM`` and ``MODE_SIV`` there are no
|
||||
restrictions on its length (recommended: **16** bytes).
|
||||
|
||||
For ``MODE_CCM``, its length must be in the range **[7..13]**.
|
||||
Bear in mind that with CCM there is a trade-off between nonce
|
||||
length and maximum message size. Recommendation: **11** bytes.
|
||||
|
||||
For ``MODE_OCB``, its length must be in the range **[1..15]**
|
||||
(recommended: **15**).
|
||||
|
||||
For ``MODE_CTR``, its length must be in the range **[0..15]**
|
||||
(recommended: **8**).
|
||||
|
||||
For ``MODE_SIV``, the nonce is optional, if it is not specified,
|
||||
then no nonce is being used, which renders the encryption
|
||||
deterministic.
|
||||
|
||||
If not provided, for modes other than ``MODE_SIV``, a random
|
||||
byte string of the recommended length is used (you must then
|
||||
read its value with the :attr:`nonce` attribute).
|
||||
|
||||
segment_size (integer):
|
||||
(Only ``MODE_CFB``).The number of **bits** the plaintext and ciphertext
|
||||
are segmented in. It must be a multiple of 8.
|
||||
If not specified, it will be assumed to be 8.
|
||||
|
||||
mac_len (integer):
|
||||
(Only ``MODE_EAX``, ``MODE_GCM``, ``MODE_OCB``, ``MODE_CCM``)
|
||||
Length of the authentication tag, in bytes.
|
||||
|
||||
It must be even and in the range **[4..16]**.
|
||||
The recommended value (and the default, if not specified) is **16**.
|
||||
|
||||
msg_len (integer):
|
||||
(Only ``MODE_CCM``). Length of the message to (de)cipher.
|
||||
If not specified, ``encrypt`` must be called with the entire message.
|
||||
Similarly, ``decrypt`` can only be called once.
|
||||
|
||||
assoc_len (integer):
|
||||
(Only ``MODE_CCM``). Length of the associated data.
|
||||
If not specified, all associated data is buffered internally,
|
||||
which may represent a problem for very large messages.
|
||||
|
||||
initial_value (integer or bytes/bytearray/memoryview):
|
||||
(Only ``MODE_CTR``).
|
||||
The initial value for the counter. If not present, the cipher will
|
||||
start counting from 0. The value is incremented by one for each block.
|
||||
The counter number is encoded in big endian mode.
|
||||
|
||||
counter (object):
|
||||
(Only ``MODE_CTR``).
|
||||
Instance of ``Crypto.Util.Counter``, which allows full customization
|
||||
of the counter block. This parameter is incompatible to both ``nonce``
|
||||
and ``initial_value``.
|
||||
|
||||
use_aesni: (boolean):
|
||||
Use Intel AES-NI hardware extensions (default: use if available).
|
||||
|
||||
Returns:
|
||||
an AES object, of the applicable mode.
|
||||
"""
|
||||
|
||||
kwargs["add_aes_modes"] = True
|
||||
return _create_cipher(sys.modules[__name__], key, mode, *args, **kwargs)
|
||||
|
||||
|
||||
# Size of a data block (in bytes)
|
||||
block_size = 16
|
||||
# Size of a key (in bytes)
|
||||
key_size = (16, 24, 32)
|
||||
@@ -1,156 +0,0 @@
|
||||
from typing import Dict, Optional, Tuple, Union, overload
|
||||
from typing_extensions import Literal
|
||||
|
||||
Buffer=bytes|bytearray|memoryview
|
||||
|
||||
from Crypto.Cipher._mode_ecb import EcbMode
|
||||
from Crypto.Cipher._mode_cbc import CbcMode
|
||||
from Crypto.Cipher._mode_cfb import CfbMode
|
||||
from Crypto.Cipher._mode_ofb import OfbMode
|
||||
from Crypto.Cipher._mode_ctr import CtrMode
|
||||
from Crypto.Cipher._mode_openpgp import OpenPgpMode
|
||||
from Crypto.Cipher._mode_ccm import CcmMode
|
||||
from Crypto.Cipher._mode_eax import EaxMode
|
||||
from Crypto.Cipher._mode_gcm import GcmMode
|
||||
from Crypto.Cipher._mode_siv import SivMode
|
||||
from Crypto.Cipher._mode_ocb import OcbMode
|
||||
|
||||
MODE_ECB: Literal[1]
|
||||
MODE_CBC: Literal[2]
|
||||
MODE_CFB: Literal[3]
|
||||
MODE_OFB: Literal[5]
|
||||
MODE_CTR: Literal[6]
|
||||
MODE_OPENPGP: Literal[7]
|
||||
MODE_CCM: Literal[8]
|
||||
MODE_EAX: Literal[9]
|
||||
MODE_SIV: Literal[10]
|
||||
MODE_GCM: Literal[11]
|
||||
MODE_OCB: Literal[12]
|
||||
|
||||
# MODE_ECB
|
||||
@overload
|
||||
def new(key: Buffer,
|
||||
mode: Literal[1],
|
||||
use_aesni : bool = ...) -> \
|
||||
EcbMode: ...
|
||||
|
||||
# MODE_CBC
|
||||
@overload
|
||||
def new(key: Buffer,
|
||||
mode: Literal[2],
|
||||
iv : Optional[Buffer] = ...,
|
||||
use_aesni : bool = ...) -> \
|
||||
CbcMode: ...
|
||||
|
||||
@overload
|
||||
def new(key: Buffer,
|
||||
mode: Literal[2],
|
||||
IV : Optional[Buffer] = ...,
|
||||
use_aesni : bool = ...) -> \
|
||||
CbcMode: ...
|
||||
|
||||
# MODE_CFB
|
||||
@overload
|
||||
def new(key: Buffer,
|
||||
mode: Literal[3],
|
||||
iv : Optional[Buffer] = ...,
|
||||
segment_size : int = ...,
|
||||
use_aesni : bool = ...) -> \
|
||||
CfbMode: ...
|
||||
|
||||
@overload
|
||||
def new(key: Buffer,
|
||||
mode: Literal[3],
|
||||
IV : Optional[Buffer] = ...,
|
||||
segment_size : int = ...,
|
||||
use_aesni : bool = ...) -> \
|
||||
CfbMode: ...
|
||||
|
||||
# MODE_OFB
|
||||
@overload
|
||||
def new(key: Buffer,
|
||||
mode: Literal[5],
|
||||
iv : Optional[Buffer] = ...,
|
||||
use_aesni : bool = ...) -> \
|
||||
OfbMode: ...
|
||||
|
||||
@overload
|
||||
def new(key: Buffer,
|
||||
mode: Literal[5],
|
||||
IV : Optional[Buffer] = ...,
|
||||
use_aesni : bool = ...) -> \
|
||||
OfbMode: ...
|
||||
|
||||
# MODE_CTR
|
||||
@overload
|
||||
def new(key: Buffer,
|
||||
mode: Literal[6],
|
||||
nonce : Optional[Buffer] = ...,
|
||||
initial_value : Union[int, Buffer] = ...,
|
||||
counter : Dict = ...,
|
||||
use_aesni : bool = ...) -> \
|
||||
CtrMode: ...
|
||||
|
||||
# MODE_OPENPGP
|
||||
@overload
|
||||
def new(key: Buffer,
|
||||
mode: Literal[7],
|
||||
iv : Optional[Buffer] = ...,
|
||||
use_aesni : bool = ...) -> \
|
||||
OpenPgpMode: ...
|
||||
|
||||
@overload
|
||||
def new(key: Buffer,
|
||||
mode: Literal[7],
|
||||
IV : Optional[Buffer] = ...,
|
||||
use_aesni : bool = ...) -> \
|
||||
OpenPgpMode: ...
|
||||
|
||||
# MODE_CCM
|
||||
@overload
|
||||
def new(key: Buffer,
|
||||
mode: Literal[8],
|
||||
nonce : Optional[Buffer] = ...,
|
||||
mac_len : int = ...,
|
||||
assoc_len : int = ...,
|
||||
use_aesni : bool = ...) -> \
|
||||
CcmMode: ...
|
||||
|
||||
# MODE_EAX
|
||||
@overload
|
||||
def new(key: Buffer,
|
||||
mode: Literal[9],
|
||||
nonce : Optional[Buffer] = ...,
|
||||
mac_len : int = ...,
|
||||
use_aesni : bool = ...) -> \
|
||||
EaxMode: ...
|
||||
|
||||
# MODE_GCM
|
||||
@overload
|
||||
def new(key: Buffer,
|
||||
mode: Literal[10],
|
||||
nonce : Optional[Buffer] = ...,
|
||||
use_aesni : bool = ...) -> \
|
||||
SivMode: ...
|
||||
|
||||
# MODE_SIV
|
||||
@overload
|
||||
def new(key: Buffer,
|
||||
mode: Literal[11],
|
||||
nonce : Optional[Buffer] = ...,
|
||||
mac_len : int = ...,
|
||||
use_aesni : bool = ...) -> \
|
||||
GcmMode: ...
|
||||
|
||||
# MODE_OCB
|
||||
@overload
|
||||
def new(key: Buffer,
|
||||
mode: Literal[12],
|
||||
nonce : Optional[Buffer] = ...,
|
||||
mac_len : int = ...,
|
||||
use_aesni : bool = ...) -> \
|
||||
OcbMode: ...
|
||||
|
||||
|
||||
block_size: int
|
||||
key_size: Tuple[int, int, int]
|
||||
@@ -1,175 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Cipher/ARC2.py : ARC2.py
|
||||
#
|
||||
# ===================================================================
|
||||
# The contents of this file are dedicated to the public domain. To
|
||||
# the extent that dedication to the public domain is not available,
|
||||
# everyone is granted a worldwide, perpetual, royalty-free,
|
||||
# non-exclusive license to exercise all rights associated with the
|
||||
# contents of this file for any purpose whatsoever.
|
||||
# No rights are reserved.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
||||
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
||||
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
# ===================================================================
|
||||
"""
|
||||
Module's constants for the modes of operation supported with ARC2:
|
||||
|
||||
:var MODE_ECB: :ref:`Electronic Code Book (ECB) <ecb_mode>`
|
||||
:var MODE_CBC: :ref:`Cipher-Block Chaining (CBC) <cbc_mode>`
|
||||
:var MODE_CFB: :ref:`Cipher FeedBack (CFB) <cfb_mode>`
|
||||
:var MODE_OFB: :ref:`Output FeedBack (OFB) <ofb_mode>`
|
||||
:var MODE_CTR: :ref:`CounTer Mode (CTR) <ctr_mode>`
|
||||
:var MODE_OPENPGP: :ref:`OpenPGP Mode <openpgp_mode>`
|
||||
:var MODE_EAX: :ref:`EAX Mode <eax_mode>`
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
from Crypto.Cipher import _create_cipher
|
||||
from Crypto.Util.py3compat import byte_string
|
||||
from Crypto.Util._raw_api import (load_pycryptodome_raw_lib,
|
||||
VoidPointer, SmartPointer,
|
||||
c_size_t, c_uint8_ptr)
|
||||
|
||||
_raw_arc2_lib = load_pycryptodome_raw_lib(
|
||||
"Crypto.Cipher._raw_arc2",
|
||||
"""
|
||||
int ARC2_start_operation(const uint8_t key[],
|
||||
size_t key_len,
|
||||
size_t effective_key_len,
|
||||
void **pResult);
|
||||
int ARC2_encrypt(const void *state,
|
||||
const uint8_t *in,
|
||||
uint8_t *out,
|
||||
size_t data_len);
|
||||
int ARC2_decrypt(const void *state,
|
||||
const uint8_t *in,
|
||||
uint8_t *out,
|
||||
size_t data_len);
|
||||
int ARC2_stop_operation(void *state);
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def _create_base_cipher(dict_parameters):
|
||||
"""This method instantiates and returns a handle to a low-level
|
||||
base cipher. It will absorb named parameters in the process."""
|
||||
|
||||
try:
|
||||
key = dict_parameters.pop("key")
|
||||
except KeyError:
|
||||
raise TypeError("Missing 'key' parameter")
|
||||
|
||||
effective_keylen = dict_parameters.pop("effective_keylen", 1024)
|
||||
|
||||
if len(key) not in key_size:
|
||||
raise ValueError("Incorrect ARC2 key length (%d bytes)" % len(key))
|
||||
|
||||
if not (40 <= effective_keylen <= 1024):
|
||||
raise ValueError("'effective_key_len' must be at least 40 and no larger than 1024 "
|
||||
"(not %d)" % effective_keylen)
|
||||
|
||||
start_operation = _raw_arc2_lib.ARC2_start_operation
|
||||
stop_operation = _raw_arc2_lib.ARC2_stop_operation
|
||||
|
||||
cipher = VoidPointer()
|
||||
result = start_operation(c_uint8_ptr(key),
|
||||
c_size_t(len(key)),
|
||||
c_size_t(effective_keylen),
|
||||
cipher.address_of())
|
||||
if result:
|
||||
raise ValueError("Error %X while instantiating the ARC2 cipher"
|
||||
% result)
|
||||
|
||||
return SmartPointer(cipher.get(), stop_operation)
|
||||
|
||||
|
||||
def new(key, mode, *args, **kwargs):
|
||||
"""Create a new RC2 cipher.
|
||||
|
||||
:param key:
|
||||
The secret key to use in the symmetric cipher.
|
||||
Its length can vary from 5 to 128 bytes; the actual search space
|
||||
(and the cipher strength) can be reduced with the ``effective_keylen`` parameter.
|
||||
:type key: bytes, bytearray, memoryview
|
||||
|
||||
:param mode:
|
||||
The chaining mode to use for encryption or decryption.
|
||||
:type mode: One of the supported ``MODE_*`` constants
|
||||
|
||||
:Keyword Arguments:
|
||||
* **iv** (*bytes*, *bytearray*, *memoryview*) --
|
||||
(Only applicable for ``MODE_CBC``, ``MODE_CFB``, ``MODE_OFB``,
|
||||
and ``MODE_OPENPGP`` modes).
|
||||
|
||||
The initialization vector to use for encryption or decryption.
|
||||
|
||||
For ``MODE_CBC``, ``MODE_CFB``, and ``MODE_OFB`` it must be 8 bytes long.
|
||||
|
||||
For ``MODE_OPENPGP`` mode only,
|
||||
it must be 8 bytes long for encryption
|
||||
and 10 bytes for decryption (in the latter case, it is
|
||||
actually the *encrypted* IV which was prefixed to the ciphertext).
|
||||
|
||||
If not provided, a random byte string is generated (you must then
|
||||
read its value with the :attr:`iv` attribute).
|
||||
|
||||
* **nonce** (*bytes*, *bytearray*, *memoryview*) --
|
||||
(Only applicable for ``MODE_EAX`` and ``MODE_CTR``).
|
||||
|
||||
A value that must never be reused for any other encryption done
|
||||
with this key.
|
||||
|
||||
For ``MODE_EAX`` there are no
|
||||
restrictions on its length (recommended: **16** bytes).
|
||||
|
||||
For ``MODE_CTR``, its length must be in the range **[0..7]**.
|
||||
|
||||
If not provided for ``MODE_EAX``, a random byte string is generated (you
|
||||
can read it back via the ``nonce`` attribute).
|
||||
|
||||
* **effective_keylen** (*integer*) --
|
||||
Optional. Maximum strength in bits of the actual key used by the ARC2 algorithm.
|
||||
If the supplied ``key`` parameter is longer (in bits) of the value specified
|
||||
here, it will be weakened to match it.
|
||||
If not specified, no limitation is applied.
|
||||
|
||||
* **segment_size** (*integer*) --
|
||||
(Only ``MODE_CFB``).The number of **bits** the plaintext and ciphertext
|
||||
are segmented in. It must be a multiple of 8.
|
||||
If not specified, it will be assumed to be 8.
|
||||
|
||||
* **mac_len** : (*integer*) --
|
||||
(Only ``MODE_EAX``)
|
||||
Length of the authentication tag, in bytes.
|
||||
It must be no longer than 8 (default).
|
||||
|
||||
* **initial_value** : (*integer*) --
|
||||
(Only ``MODE_CTR``). The initial value for the counter within
|
||||
the counter block. By default it is **0**.
|
||||
|
||||
:Return: an ARC2 object, of the applicable mode.
|
||||
"""
|
||||
|
||||
return _create_cipher(sys.modules[__name__], key, mode, *args, **kwargs)
|
||||
|
||||
MODE_ECB = 1
|
||||
MODE_CBC = 2
|
||||
MODE_CFB = 3
|
||||
MODE_OFB = 5
|
||||
MODE_CTR = 6
|
||||
MODE_OPENPGP = 7
|
||||
MODE_EAX = 9
|
||||
|
||||
# Size of a data block (in bytes)
|
||||
block_size = 8
|
||||
# Size of a key (in bytes)
|
||||
key_size = range(5, 128 + 1)
|
||||
@@ -1,35 +0,0 @@
|
||||
from typing import Union, Dict, Iterable, Optional
|
||||
|
||||
Buffer = bytes|bytearray|memoryview
|
||||
|
||||
from Crypto.Cipher._mode_ecb import EcbMode
|
||||
from Crypto.Cipher._mode_cbc import CbcMode
|
||||
from Crypto.Cipher._mode_cfb import CfbMode
|
||||
from Crypto.Cipher._mode_ofb import OfbMode
|
||||
from Crypto.Cipher._mode_ctr import CtrMode
|
||||
from Crypto.Cipher._mode_openpgp import OpenPgpMode
|
||||
from Crypto.Cipher._mode_eax import EaxMode
|
||||
|
||||
ARC2Mode = int
|
||||
|
||||
MODE_ECB: ARC2Mode
|
||||
MODE_CBC: ARC2Mode
|
||||
MODE_CFB: ARC2Mode
|
||||
MODE_OFB: ARC2Mode
|
||||
MODE_CTR: ARC2Mode
|
||||
MODE_OPENPGP: ARC2Mode
|
||||
MODE_EAX: ARC2Mode
|
||||
|
||||
def new(key: Buffer,
|
||||
mode: ARC2Mode,
|
||||
iv : Optional[Buffer] = ...,
|
||||
IV : Optional[Buffer] = ...,
|
||||
nonce : Optional[Buffer] = ...,
|
||||
segment_size : int = ...,
|
||||
mac_len : int = ...,
|
||||
initial_value : Union[int, Buffer] = ...,
|
||||
counter : Dict = ...) -> \
|
||||
Union[EcbMode, CbcMode, CfbMode, OfbMode, CtrMode, OpenPgpMode]: ...
|
||||
|
||||
block_size: int
|
||||
key_size: Iterable[int]
|
||||
@@ -1,136 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Cipher/ARC4.py : ARC4
|
||||
#
|
||||
# ===================================================================
|
||||
# The contents of this file are dedicated to the public domain. To
|
||||
# the extent that dedication to the public domain is not available,
|
||||
# everyone is granted a worldwide, perpetual, royalty-free,
|
||||
# non-exclusive license to exercise all rights associated with the
|
||||
# contents of this file for any purpose whatsoever.
|
||||
# No rights are reserved.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
||||
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
||||
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
# ===================================================================
|
||||
|
||||
from Crypto.Util._raw_api import (load_pycryptodome_raw_lib, VoidPointer,
|
||||
create_string_buffer, get_raw_buffer,
|
||||
SmartPointer, c_size_t, c_uint8_ptr)
|
||||
|
||||
|
||||
_raw_arc4_lib = load_pycryptodome_raw_lib("Crypto.Cipher._ARC4", """
|
||||
int ARC4_stream_encrypt(void *rc4State, const uint8_t in[],
|
||||
uint8_t out[], size_t len);
|
||||
int ARC4_stream_init(uint8_t *key, size_t keylen,
|
||||
void **pRc4State);
|
||||
int ARC4_stream_destroy(void *rc4State);
|
||||
""")
|
||||
|
||||
|
||||
class ARC4Cipher:
|
||||
"""ARC4 cipher object. Do not create it directly. Use
|
||||
:func:`Crypto.Cipher.ARC4.new` instead.
|
||||
"""
|
||||
|
||||
def __init__(self, key, *args, **kwargs):
|
||||
"""Initialize an ARC4 cipher object
|
||||
|
||||
See also `new()` at the module level."""
|
||||
|
||||
if len(args) > 0:
|
||||
ndrop = args[0]
|
||||
args = args[1:]
|
||||
else:
|
||||
ndrop = kwargs.pop('drop', 0)
|
||||
|
||||
if len(key) not in key_size:
|
||||
raise ValueError("Incorrect ARC4 key length (%d bytes)" %
|
||||
len(key))
|
||||
|
||||
self._state = VoidPointer()
|
||||
result = _raw_arc4_lib.ARC4_stream_init(c_uint8_ptr(key),
|
||||
c_size_t(len(key)),
|
||||
self._state.address_of())
|
||||
if result != 0:
|
||||
raise ValueError("Error %d while creating the ARC4 cipher"
|
||||
% result)
|
||||
self._state = SmartPointer(self._state.get(),
|
||||
_raw_arc4_lib.ARC4_stream_destroy)
|
||||
|
||||
if ndrop > 0:
|
||||
# This is OK even if the cipher is used for decryption,
|
||||
# since encrypt and decrypt are actually the same thing
|
||||
# with ARC4.
|
||||
self.encrypt(b'\x00' * ndrop)
|
||||
|
||||
self.block_size = 1
|
||||
self.key_size = len(key)
|
||||
|
||||
def encrypt(self, plaintext):
|
||||
"""Encrypt a piece of data.
|
||||
|
||||
:param plaintext: The data to encrypt, of any size.
|
||||
:type plaintext: bytes, bytearray, memoryview
|
||||
:returns: the encrypted byte string, of equal length as the
|
||||
plaintext.
|
||||
"""
|
||||
|
||||
ciphertext = create_string_buffer(len(plaintext))
|
||||
result = _raw_arc4_lib.ARC4_stream_encrypt(self._state.get(),
|
||||
c_uint8_ptr(plaintext),
|
||||
ciphertext,
|
||||
c_size_t(len(plaintext)))
|
||||
if result:
|
||||
raise ValueError("Error %d while encrypting with RC4" % result)
|
||||
return get_raw_buffer(ciphertext)
|
||||
|
||||
def decrypt(self, ciphertext):
|
||||
"""Decrypt a piece of data.
|
||||
|
||||
:param ciphertext: The data to decrypt, of any size.
|
||||
:type ciphertext: bytes, bytearray, memoryview
|
||||
:returns: the decrypted byte string, of equal length as the
|
||||
ciphertext.
|
||||
"""
|
||||
|
||||
try:
|
||||
return self.encrypt(ciphertext)
|
||||
except ValueError as e:
|
||||
raise ValueError(str(e).replace("enc", "dec"))
|
||||
|
||||
|
||||
def new(key, *args, **kwargs):
|
||||
"""Create a new ARC4 cipher.
|
||||
|
||||
:param key:
|
||||
The secret key to use in the symmetric cipher.
|
||||
Its length must be in the range ``[1..256]``.
|
||||
The recommended length is 16 bytes.
|
||||
:type key: bytes, bytearray, memoryview
|
||||
|
||||
:Keyword Arguments:
|
||||
* *drop* (``integer``) --
|
||||
The amount of bytes to discard from the initial part of the keystream.
|
||||
In fact, such part has been found to be distinguishable from random
|
||||
data (while it shouldn't) and also correlated to key.
|
||||
|
||||
The recommended value is 3072_ bytes. The default value is 0.
|
||||
|
||||
:Return: an `ARC4Cipher` object
|
||||
|
||||
.. _3072: http://eprint.iacr.org/2002/067.pdf
|
||||
"""
|
||||
return ARC4Cipher(key, *args, **kwargs)
|
||||
|
||||
|
||||
# Size of a data block (in bytes)
|
||||
block_size = 1
|
||||
# Size of a key (in bytes)
|
||||
key_size = range(1, 256+1)
|
||||
@@ -1,16 +0,0 @@
|
||||
from typing import Any, Union, Iterable
|
||||
|
||||
Buffer = bytes|bytearray|memoryview
|
||||
|
||||
class ARC4Cipher:
|
||||
block_size: int
|
||||
key_size: int
|
||||
|
||||
def __init__(self, key: Buffer, *args: Any, **kwargs: Any) -> None: ...
|
||||
def encrypt(self, plaintext: Buffer) -> bytes: ...
|
||||
def decrypt(self, ciphertext: Buffer) -> bytes: ...
|
||||
|
||||
def new(key: Buffer, drop : int = ...) -> ARC4Cipher: ...
|
||||
|
||||
block_size: int
|
||||
key_size: Iterable[int]
|
||||
@@ -1,159 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Cipher/Blowfish.py : Blowfish
|
||||
#
|
||||
# ===================================================================
|
||||
# The contents of this file are dedicated to the public domain. To
|
||||
# the extent that dedication to the public domain is not available,
|
||||
# everyone is granted a worldwide, perpetual, royalty-free,
|
||||
# non-exclusive license to exercise all rights associated with the
|
||||
# contents of this file for any purpose whatsoever.
|
||||
# No rights are reserved.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
||||
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
||||
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
# ===================================================================
|
||||
"""
|
||||
Module's constants for the modes of operation supported with Blowfish:
|
||||
|
||||
:var MODE_ECB: :ref:`Electronic Code Book (ECB) <ecb_mode>`
|
||||
:var MODE_CBC: :ref:`Cipher-Block Chaining (CBC) <cbc_mode>`
|
||||
:var MODE_CFB: :ref:`Cipher FeedBack (CFB) <cfb_mode>`
|
||||
:var MODE_OFB: :ref:`Output FeedBack (OFB) <ofb_mode>`
|
||||
:var MODE_CTR: :ref:`CounTer Mode (CTR) <ctr_mode>`
|
||||
:var MODE_OPENPGP: :ref:`OpenPGP Mode <openpgp_mode>`
|
||||
:var MODE_EAX: :ref:`EAX Mode <eax_mode>`
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
from Crypto.Cipher import _create_cipher
|
||||
from Crypto.Util._raw_api import (load_pycryptodome_raw_lib,
|
||||
VoidPointer, SmartPointer, c_size_t,
|
||||
c_uint8_ptr)
|
||||
|
||||
_raw_blowfish_lib = load_pycryptodome_raw_lib(
|
||||
"Crypto.Cipher._raw_blowfish",
|
||||
"""
|
||||
int Blowfish_start_operation(const uint8_t key[],
|
||||
size_t key_len,
|
||||
void **pResult);
|
||||
int Blowfish_encrypt(const void *state,
|
||||
const uint8_t *in,
|
||||
uint8_t *out,
|
||||
size_t data_len);
|
||||
int Blowfish_decrypt(const void *state,
|
||||
const uint8_t *in,
|
||||
uint8_t *out,
|
||||
size_t data_len);
|
||||
int Blowfish_stop_operation(void *state);
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def _create_base_cipher(dict_parameters):
|
||||
"""This method instantiates and returns a smart pointer to
|
||||
a low-level base cipher. It will absorb named parameters in
|
||||
the process."""
|
||||
|
||||
try:
|
||||
key = dict_parameters.pop("key")
|
||||
except KeyError:
|
||||
raise TypeError("Missing 'key' parameter")
|
||||
|
||||
if len(key) not in key_size:
|
||||
raise ValueError("Incorrect Blowfish key length (%d bytes)" % len(key))
|
||||
|
||||
start_operation = _raw_blowfish_lib.Blowfish_start_operation
|
||||
stop_operation = _raw_blowfish_lib.Blowfish_stop_operation
|
||||
|
||||
void_p = VoidPointer()
|
||||
result = start_operation(c_uint8_ptr(key),
|
||||
c_size_t(len(key)),
|
||||
void_p.address_of())
|
||||
if result:
|
||||
raise ValueError("Error %X while instantiating the Blowfish cipher"
|
||||
% result)
|
||||
return SmartPointer(void_p.get(), stop_operation)
|
||||
|
||||
|
||||
def new(key, mode, *args, **kwargs):
|
||||
"""Create a new Blowfish cipher
|
||||
|
||||
:param key:
|
||||
The secret key to use in the symmetric cipher.
|
||||
Its length can vary from 5 to 56 bytes.
|
||||
:type key: bytes, bytearray, memoryview
|
||||
|
||||
:param mode:
|
||||
The chaining mode to use for encryption or decryption.
|
||||
:type mode: One of the supported ``MODE_*`` constants
|
||||
|
||||
:Keyword Arguments:
|
||||
* **iv** (*bytes*, *bytearray*, *memoryview*) --
|
||||
(Only applicable for ``MODE_CBC``, ``MODE_CFB``, ``MODE_OFB``,
|
||||
and ``MODE_OPENPGP`` modes).
|
||||
|
||||
The initialization vector to use for encryption or decryption.
|
||||
|
||||
For ``MODE_CBC``, ``MODE_CFB``, and ``MODE_OFB`` it must be 8 bytes long.
|
||||
|
||||
For ``MODE_OPENPGP`` mode only,
|
||||
it must be 8 bytes long for encryption
|
||||
and 10 bytes for decryption (in the latter case, it is
|
||||
actually the *encrypted* IV which was prefixed to the ciphertext).
|
||||
|
||||
If not provided, a random byte string is generated (you must then
|
||||
read its value with the :attr:`iv` attribute).
|
||||
|
||||
* **nonce** (*bytes*, *bytearray*, *memoryview*) --
|
||||
(Only applicable for ``MODE_EAX`` and ``MODE_CTR``).
|
||||
|
||||
A value that must never be reused for any other encryption done
|
||||
with this key.
|
||||
|
||||
For ``MODE_EAX`` there are no
|
||||
restrictions on its length (recommended: **16** bytes).
|
||||
|
||||
For ``MODE_CTR``, its length must be in the range **[0..7]**.
|
||||
|
||||
If not provided for ``MODE_EAX``, a random byte string is generated (you
|
||||
can read it back via the ``nonce`` attribute).
|
||||
|
||||
* **segment_size** (*integer*) --
|
||||
(Only ``MODE_CFB``).The number of **bits** the plaintext and ciphertext
|
||||
are segmented in. It must be a multiple of 8.
|
||||
If not specified, it will be assumed to be 8.
|
||||
|
||||
* **mac_len** : (*integer*) --
|
||||
(Only ``MODE_EAX``)
|
||||
Length of the authentication tag, in bytes.
|
||||
It must be no longer than 8 (default).
|
||||
|
||||
* **initial_value** : (*integer*) --
|
||||
(Only ``MODE_CTR``). The initial value for the counter within
|
||||
the counter block. By default it is **0**.
|
||||
|
||||
:Return: a Blowfish object, of the applicable mode.
|
||||
"""
|
||||
|
||||
return _create_cipher(sys.modules[__name__], key, mode, *args, **kwargs)
|
||||
|
||||
MODE_ECB = 1
|
||||
MODE_CBC = 2
|
||||
MODE_CFB = 3
|
||||
MODE_OFB = 5
|
||||
MODE_CTR = 6
|
||||
MODE_OPENPGP = 7
|
||||
MODE_EAX = 9
|
||||
|
||||
# Size of a data block (in bytes)
|
||||
block_size = 8
|
||||
# Size of a key (in bytes)
|
||||
key_size = range(4, 56 + 1)
|
||||
@@ -1,35 +0,0 @@
|
||||
from typing import Union, Dict, Iterable, Optional
|
||||
|
||||
Buffer = bytes|bytearray|memoryview
|
||||
|
||||
from Crypto.Cipher._mode_ecb import EcbMode
|
||||
from Crypto.Cipher._mode_cbc import CbcMode
|
||||
from Crypto.Cipher._mode_cfb import CfbMode
|
||||
from Crypto.Cipher._mode_ofb import OfbMode
|
||||
from Crypto.Cipher._mode_ctr import CtrMode
|
||||
from Crypto.Cipher._mode_openpgp import OpenPgpMode
|
||||
from Crypto.Cipher._mode_eax import EaxMode
|
||||
|
||||
BlowfishMode = int
|
||||
|
||||
MODE_ECB: BlowfishMode
|
||||
MODE_CBC: BlowfishMode
|
||||
MODE_CFB: BlowfishMode
|
||||
MODE_OFB: BlowfishMode
|
||||
MODE_CTR: BlowfishMode
|
||||
MODE_OPENPGP: BlowfishMode
|
||||
MODE_EAX: BlowfishMode
|
||||
|
||||
def new(key: Buffer,
|
||||
mode: BlowfishMode,
|
||||
iv : Optional[Buffer] = ...,
|
||||
IV : Optional[Buffer] = ...,
|
||||
nonce : Optional[Buffer] = ...,
|
||||
segment_size : int = ...,
|
||||
mac_len : int = ...,
|
||||
initial_value : Union[int, Buffer] = ...,
|
||||
counter : Dict = ...) -> \
|
||||
Union[EcbMode, CbcMode, CfbMode, OfbMode, CtrMode, OpenPgpMode]: ...
|
||||
|
||||
block_size: int
|
||||
key_size: Iterable[int]
|
||||
@@ -1,159 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Cipher/CAST.py : CAST
|
||||
#
|
||||
# ===================================================================
|
||||
# The contents of this file are dedicated to the public domain. To
|
||||
# the extent that dedication to the public domain is not available,
|
||||
# everyone is granted a worldwide, perpetual, royalty-free,
|
||||
# non-exclusive license to exercise all rights associated with the
|
||||
# contents of this file for any purpose whatsoever.
|
||||
# No rights are reserved.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
||||
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
||||
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
# ===================================================================
|
||||
"""
|
||||
Module's constants for the modes of operation supported with CAST:
|
||||
|
||||
:var MODE_ECB: :ref:`Electronic Code Book (ECB) <ecb_mode>`
|
||||
:var MODE_CBC: :ref:`Cipher-Block Chaining (CBC) <cbc_mode>`
|
||||
:var MODE_CFB: :ref:`Cipher FeedBack (CFB) <cfb_mode>`
|
||||
:var MODE_OFB: :ref:`Output FeedBack (OFB) <ofb_mode>`
|
||||
:var MODE_CTR: :ref:`CounTer Mode (CTR) <ctr_mode>`
|
||||
:var MODE_OPENPGP: :ref:`OpenPGP Mode <openpgp_mode>`
|
||||
:var MODE_EAX: :ref:`EAX Mode <eax_mode>`
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
from Crypto.Cipher import _create_cipher
|
||||
from Crypto.Util.py3compat import byte_string
|
||||
from Crypto.Util._raw_api import (load_pycryptodome_raw_lib,
|
||||
VoidPointer, SmartPointer,
|
||||
c_size_t, c_uint8_ptr)
|
||||
|
||||
_raw_cast_lib = load_pycryptodome_raw_lib(
|
||||
"Crypto.Cipher._raw_cast",
|
||||
"""
|
||||
int CAST_start_operation(const uint8_t key[],
|
||||
size_t key_len,
|
||||
void **pResult);
|
||||
int CAST_encrypt(const void *state,
|
||||
const uint8_t *in,
|
||||
uint8_t *out,
|
||||
size_t data_len);
|
||||
int CAST_decrypt(const void *state,
|
||||
const uint8_t *in,
|
||||
uint8_t *out,
|
||||
size_t data_len);
|
||||
int CAST_stop_operation(void *state);
|
||||
""")
|
||||
|
||||
|
||||
def _create_base_cipher(dict_parameters):
|
||||
"""This method instantiates and returns a handle to a low-level
|
||||
base cipher. It will absorb named parameters in the process."""
|
||||
|
||||
try:
|
||||
key = dict_parameters.pop("key")
|
||||
except KeyError:
|
||||
raise TypeError("Missing 'key' parameter")
|
||||
|
||||
if len(key) not in key_size:
|
||||
raise ValueError("Incorrect CAST key length (%d bytes)" % len(key))
|
||||
|
||||
start_operation = _raw_cast_lib.CAST_start_operation
|
||||
stop_operation = _raw_cast_lib.CAST_stop_operation
|
||||
|
||||
cipher = VoidPointer()
|
||||
result = start_operation(c_uint8_ptr(key),
|
||||
c_size_t(len(key)),
|
||||
cipher.address_of())
|
||||
if result:
|
||||
raise ValueError("Error %X while instantiating the CAST cipher"
|
||||
% result)
|
||||
|
||||
return SmartPointer(cipher.get(), stop_operation)
|
||||
|
||||
|
||||
def new(key, mode, *args, **kwargs):
|
||||
"""Create a new CAST cipher
|
||||
|
||||
:param key:
|
||||
The secret key to use in the symmetric cipher.
|
||||
Its length can vary from 5 to 16 bytes.
|
||||
:type key: bytes, bytearray, memoryview
|
||||
|
||||
:param mode:
|
||||
The chaining mode to use for encryption or decryption.
|
||||
:type mode: One of the supported ``MODE_*`` constants
|
||||
|
||||
:Keyword Arguments:
|
||||
* **iv** (*bytes*, *bytearray*, *memoryview*) --
|
||||
(Only applicable for ``MODE_CBC``, ``MODE_CFB``, ``MODE_OFB``,
|
||||
and ``MODE_OPENPGP`` modes).
|
||||
|
||||
The initialization vector to use for encryption or decryption.
|
||||
|
||||
For ``MODE_CBC``, ``MODE_CFB``, and ``MODE_OFB`` it must be 8 bytes long.
|
||||
|
||||
For ``MODE_OPENPGP`` mode only,
|
||||
it must be 8 bytes long for encryption
|
||||
and 10 bytes for decryption (in the latter case, it is
|
||||
actually the *encrypted* IV which was prefixed to the ciphertext).
|
||||
|
||||
If not provided, a random byte string is generated (you must then
|
||||
read its value with the :attr:`iv` attribute).
|
||||
|
||||
* **nonce** (*bytes*, *bytearray*, *memoryview*) --
|
||||
(Only applicable for ``MODE_EAX`` and ``MODE_CTR``).
|
||||
|
||||
A value that must never be reused for any other encryption done
|
||||
with this key.
|
||||
|
||||
For ``MODE_EAX`` there are no
|
||||
restrictions on its length (recommended: **16** bytes).
|
||||
|
||||
For ``MODE_CTR``, its length must be in the range **[0..7]**.
|
||||
|
||||
If not provided for ``MODE_EAX``, a random byte string is generated (you
|
||||
can read it back via the ``nonce`` attribute).
|
||||
|
||||
* **segment_size** (*integer*) --
|
||||
(Only ``MODE_CFB``).The number of **bits** the plaintext and ciphertext
|
||||
are segmented in. It must be a multiple of 8.
|
||||
If not specified, it will be assumed to be 8.
|
||||
|
||||
* **mac_len** : (*integer*) --
|
||||
(Only ``MODE_EAX``)
|
||||
Length of the authentication tag, in bytes.
|
||||
It must be no longer than 8 (default).
|
||||
|
||||
* **initial_value** : (*integer*) --
|
||||
(Only ``MODE_CTR``). The initial value for the counter within
|
||||
the counter block. By default it is **0**.
|
||||
|
||||
:Return: a CAST object, of the applicable mode.
|
||||
"""
|
||||
|
||||
return _create_cipher(sys.modules[__name__], key, mode, *args, **kwargs)
|
||||
|
||||
MODE_ECB = 1
|
||||
MODE_CBC = 2
|
||||
MODE_CFB = 3
|
||||
MODE_OFB = 5
|
||||
MODE_CTR = 6
|
||||
MODE_OPENPGP = 7
|
||||
MODE_EAX = 9
|
||||
|
||||
# Size of a data block (in bytes)
|
||||
block_size = 8
|
||||
# Size of a key (in bytes)
|
||||
key_size = range(5, 16 + 1)
|
||||
@@ -1,35 +0,0 @@
|
||||
from typing import Union, Dict, Iterable, Optional
|
||||
|
||||
Buffer = bytes|bytearray|memoryview
|
||||
|
||||
from Crypto.Cipher._mode_ecb import EcbMode
|
||||
from Crypto.Cipher._mode_cbc import CbcMode
|
||||
from Crypto.Cipher._mode_cfb import CfbMode
|
||||
from Crypto.Cipher._mode_ofb import OfbMode
|
||||
from Crypto.Cipher._mode_ctr import CtrMode
|
||||
from Crypto.Cipher._mode_openpgp import OpenPgpMode
|
||||
from Crypto.Cipher._mode_eax import EaxMode
|
||||
|
||||
CASTMode = int
|
||||
|
||||
MODE_ECB: CASTMode
|
||||
MODE_CBC: CASTMode
|
||||
MODE_CFB: CASTMode
|
||||
MODE_OFB: CASTMode
|
||||
MODE_CTR: CASTMode
|
||||
MODE_OPENPGP: CASTMode
|
||||
MODE_EAX: CASTMode
|
||||
|
||||
def new(key: Buffer,
|
||||
mode: CASTMode,
|
||||
iv : Optional[Buffer] = ...,
|
||||
IV : Optional[Buffer] = ...,
|
||||
nonce : Optional[Buffer] = ...,
|
||||
segment_size : int = ...,
|
||||
mac_len : int = ...,
|
||||
initial_value : Union[int, Buffer] = ...,
|
||||
counter : Dict = ...) -> \
|
||||
Union[EcbMode, CbcMode, CfbMode, OfbMode, CtrMode, OpenPgpMode]: ...
|
||||
|
||||
block_size: int
|
||||
key_size : Iterable[int]
|
||||
@@ -1,291 +0,0 @@
|
||||
# ===================================================================
|
||||
#
|
||||
# Copyright (c) 2014, Legrandin <helderijs@gmail.com>
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions
|
||||
# are met:
|
||||
#
|
||||
# 1. Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# 2. Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in
|
||||
# the documentation and/or other materials provided with the
|
||||
# distribution.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
||||
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||
# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
|
||||
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
# ===================================================================
|
||||
|
||||
from Crypto.Random import get_random_bytes
|
||||
|
||||
from Crypto.Util.py3compat import _copy_bytes
|
||||
from Crypto.Util._raw_api import (load_pycryptodome_raw_lib,
|
||||
create_string_buffer,
|
||||
get_raw_buffer, VoidPointer,
|
||||
SmartPointer, c_size_t,
|
||||
c_uint8_ptr, c_ulong,
|
||||
is_writeable_buffer)
|
||||
|
||||
_raw_chacha20_lib = load_pycryptodome_raw_lib("Crypto.Cipher._chacha20",
|
||||
"""
|
||||
int chacha20_init(void **pState,
|
||||
const uint8_t *key,
|
||||
size_t keySize,
|
||||
const uint8_t *nonce,
|
||||
size_t nonceSize);
|
||||
|
||||
int chacha20_destroy(void *state);
|
||||
|
||||
int chacha20_encrypt(void *state,
|
||||
const uint8_t in[],
|
||||
uint8_t out[],
|
||||
size_t len);
|
||||
|
||||
int chacha20_seek(void *state,
|
||||
unsigned long block_high,
|
||||
unsigned long block_low,
|
||||
unsigned offset);
|
||||
|
||||
int hchacha20( const uint8_t key[32],
|
||||
const uint8_t nonce16[16],
|
||||
uint8_t subkey[32]);
|
||||
""")
|
||||
|
||||
|
||||
def _HChaCha20(key, nonce):
|
||||
|
||||
assert(len(key) == 32)
|
||||
assert(len(nonce) == 16)
|
||||
|
||||
subkey = bytearray(32)
|
||||
result = _raw_chacha20_lib.hchacha20(
|
||||
c_uint8_ptr(key),
|
||||
c_uint8_ptr(nonce),
|
||||
c_uint8_ptr(subkey))
|
||||
if result:
|
||||
raise ValueError("Error %d when deriving subkey with HChaCha20" % result)
|
||||
|
||||
return subkey
|
||||
|
||||
|
||||
class ChaCha20Cipher(object):
|
||||
"""ChaCha20 (or XChaCha20) cipher object.
|
||||
Do not create it directly. Use :py:func:`new` instead.
|
||||
|
||||
:var nonce: The nonce with length 8, 12 or 24 bytes
|
||||
:vartype nonce: bytes
|
||||
"""
|
||||
|
||||
block_size = 1
|
||||
|
||||
def __init__(self, key, nonce):
|
||||
"""Initialize a ChaCha20/XChaCha20 cipher object
|
||||
|
||||
See also `new()` at the module level."""
|
||||
|
||||
self.nonce = _copy_bytes(None, None, nonce)
|
||||
|
||||
# XChaCha20 requires a key derivation with HChaCha20
|
||||
# See 2.3 in https://tools.ietf.org/html/draft-arciszewski-xchacha-03
|
||||
if len(nonce) == 24:
|
||||
key = _HChaCha20(key, nonce[:16])
|
||||
nonce = b'\x00' * 4 + nonce[16:]
|
||||
self._name = "XChaCha20"
|
||||
else:
|
||||
self._name = "ChaCha20"
|
||||
nonce = self.nonce
|
||||
|
||||
self._next = ("encrypt", "decrypt")
|
||||
|
||||
self._state = VoidPointer()
|
||||
result = _raw_chacha20_lib.chacha20_init(
|
||||
self._state.address_of(),
|
||||
c_uint8_ptr(key),
|
||||
c_size_t(len(key)),
|
||||
nonce,
|
||||
c_size_t(len(nonce)))
|
||||
if result:
|
||||
raise ValueError("Error %d instantiating a %s cipher" % (result,
|
||||
self._name))
|
||||
self._state = SmartPointer(self._state.get(),
|
||||
_raw_chacha20_lib.chacha20_destroy)
|
||||
|
||||
def encrypt(self, plaintext, output=None):
|
||||
"""Encrypt a piece of data.
|
||||
|
||||
Args:
|
||||
plaintext(bytes/bytearray/memoryview): The data to encrypt, of any size.
|
||||
Keyword Args:
|
||||
output(bytes/bytearray/memoryview): The location where the ciphertext
|
||||
is written to. If ``None``, the ciphertext is returned.
|
||||
Returns:
|
||||
If ``output`` is ``None``, the ciphertext is returned as ``bytes``.
|
||||
Otherwise, ``None``.
|
||||
"""
|
||||
|
||||
if "encrypt" not in self._next:
|
||||
raise TypeError("Cipher object can only be used for decryption")
|
||||
self._next = ("encrypt",)
|
||||
return self._encrypt(plaintext, output)
|
||||
|
||||
def _encrypt(self, plaintext, output):
|
||||
"""Encrypt without FSM checks"""
|
||||
|
||||
if output is None:
|
||||
ciphertext = create_string_buffer(len(plaintext))
|
||||
else:
|
||||
ciphertext = output
|
||||
|
||||
if not is_writeable_buffer(output):
|
||||
raise TypeError("output must be a bytearray or a writeable memoryview")
|
||||
|
||||
if len(plaintext) != len(output):
|
||||
raise ValueError("output must have the same length as the input"
|
||||
" (%d bytes)" % len(plaintext))
|
||||
|
||||
result = _raw_chacha20_lib.chacha20_encrypt(
|
||||
self._state.get(),
|
||||
c_uint8_ptr(plaintext),
|
||||
c_uint8_ptr(ciphertext),
|
||||
c_size_t(len(plaintext)))
|
||||
if result:
|
||||
raise ValueError("Error %d while encrypting with %s" % (result, self._name))
|
||||
|
||||
if output is None:
|
||||
return get_raw_buffer(ciphertext)
|
||||
else:
|
||||
return None
|
||||
|
||||
def decrypt(self, ciphertext, output=None):
|
||||
"""Decrypt a piece of data.
|
||||
|
||||
Args:
|
||||
ciphertext(bytes/bytearray/memoryview): The data to decrypt, of any size.
|
||||
Keyword Args:
|
||||
output(bytes/bytearray/memoryview): The location where the plaintext
|
||||
is written to. If ``None``, the plaintext is returned.
|
||||
Returns:
|
||||
If ``output`` is ``None``, the plaintext is returned as ``bytes``.
|
||||
Otherwise, ``None``.
|
||||
"""
|
||||
|
||||
if "decrypt" not in self._next:
|
||||
raise TypeError("Cipher object can only be used for encryption")
|
||||
self._next = ("decrypt",)
|
||||
|
||||
try:
|
||||
return self._encrypt(ciphertext, output)
|
||||
except ValueError as e:
|
||||
raise ValueError(str(e).replace("enc", "dec"))
|
||||
|
||||
def seek(self, position):
|
||||
"""Seek to a certain position in the key stream.
|
||||
|
||||
If you want to seek to a certain block,
|
||||
use ``seek(block_number * 64)``.
|
||||
|
||||
Args:
|
||||
position (integer):
|
||||
The absolute position within the key stream, in bytes.
|
||||
"""
|
||||
|
||||
block_number, offset = divmod(position, 64)
|
||||
block_low = block_number & 0xFFFFFFFF
|
||||
block_high = block_number >> 32
|
||||
|
||||
result = _raw_chacha20_lib.chacha20_seek(
|
||||
self._state.get(),
|
||||
c_ulong(block_high),
|
||||
c_ulong(block_low),
|
||||
offset
|
||||
)
|
||||
if result:
|
||||
raise ValueError("Error %d while seeking with %s" % (result, self._name))
|
||||
|
||||
|
||||
def _derive_Poly1305_key_pair(key, nonce):
|
||||
"""Derive a tuple (r, s, nonce) for a Poly1305 MAC.
|
||||
|
||||
If nonce is ``None``, a new 12-byte nonce is generated.
|
||||
"""
|
||||
|
||||
if len(key) != 32:
|
||||
raise ValueError("Poly1305 with ChaCha20 requires a 32-byte key")
|
||||
|
||||
if nonce is None:
|
||||
padded_nonce = nonce = get_random_bytes(12)
|
||||
elif len(nonce) == 8:
|
||||
# See RFC7538, 2.6: [...] ChaCha20 as specified here requires a 96-bit
|
||||
# nonce. So if the provided nonce is only 64-bit, then the first 32
|
||||
# bits of the nonce will be set to a constant number.
|
||||
# This will usually be zero, but for protocols with multiple senders it may be
|
||||
# different for each sender, but should be the same for all
|
||||
# invocations of the function with the same key by a particular
|
||||
# sender.
|
||||
padded_nonce = b'\x00\x00\x00\x00' + nonce
|
||||
elif len(nonce) == 12:
|
||||
padded_nonce = nonce
|
||||
else:
|
||||
raise ValueError("Poly1305 with ChaCha20 requires an 8- or 12-byte nonce")
|
||||
|
||||
rs = new(key=key, nonce=padded_nonce).encrypt(b'\x00' * 32)
|
||||
return rs[:16], rs[16:], nonce
|
||||
|
||||
|
||||
def new(**kwargs):
|
||||
"""Create a new ChaCha20 or XChaCha20 cipher
|
||||
|
||||
Keyword Args:
|
||||
key (bytes/bytearray/memoryview): The secret key to use.
|
||||
It must be 32 bytes long.
|
||||
nonce (bytes/bytearray/memoryview): A mandatory value that
|
||||
must never be reused for any other encryption
|
||||
done with this key.
|
||||
|
||||
For ChaCha20, it must be 8 or 12 bytes long.
|
||||
|
||||
For XChaCha20, it must be 24 bytes long.
|
||||
|
||||
If not provided, 8 bytes will be randomly generated
|
||||
(you can find them back in the ``nonce`` attribute).
|
||||
|
||||
:Return: a :class:`Crypto.Cipher.ChaCha20.ChaCha20Cipher` object
|
||||
"""
|
||||
|
||||
try:
|
||||
key = kwargs.pop("key")
|
||||
except KeyError as e:
|
||||
raise TypeError("Missing parameter %s" % e)
|
||||
|
||||
nonce = kwargs.pop("nonce", None)
|
||||
if nonce is None:
|
||||
nonce = get_random_bytes(8)
|
||||
|
||||
if len(key) != 32:
|
||||
raise ValueError("ChaCha20/XChaCha20 key must be 32 bytes long")
|
||||
|
||||
if len(nonce) not in (8, 12, 24):
|
||||
raise ValueError("Nonce must be 8/12 bytes(ChaCha20) or 24 bytes (XChaCha20)")
|
||||
|
||||
if kwargs:
|
||||
raise TypeError("Unknown parameters: " + str(kwargs))
|
||||
|
||||
return ChaCha20Cipher(key, nonce)
|
||||
|
||||
# Size of a data block (in bytes)
|
||||
block_size = 1
|
||||
|
||||
# Size of a key (in bytes)
|
||||
key_size = 32
|
||||
@@ -1,25 +0,0 @@
|
||||
from typing import Union, overload, Optional
|
||||
|
||||
Buffer = bytes|bytearray|memoryview
|
||||
|
||||
def _HChaCha20(key: Buffer, nonce: Buffer) -> bytearray: ...
|
||||
|
||||
class ChaCha20Cipher:
|
||||
block_size: int
|
||||
nonce: bytes
|
||||
|
||||
def __init__(self, key: Buffer, nonce: Buffer) -> None: ...
|
||||
@overload
|
||||
def encrypt(self, plaintext: Buffer) -> bytes: ...
|
||||
@overload
|
||||
def encrypt(self, plaintext: Buffer, output: Union[bytearray, memoryview]) -> None: ...
|
||||
@overload
|
||||
def decrypt(self, plaintext: Buffer) -> bytes: ...
|
||||
@overload
|
||||
def decrypt(self, plaintext: Buffer, output: Union[bytearray, memoryview]) -> None: ...
|
||||
def seek(self, position: int) -> None: ...
|
||||
|
||||
def new(key: Buffer, nonce: Optional[Buffer] = ...) -> ChaCha20Cipher: ...
|
||||
|
||||
block_size: int
|
||||
key_size: int
|
||||
@@ -1,334 +0,0 @@
|
||||
# ===================================================================
|
||||
#
|
||||
# Copyright (c) 2018, Helder Eijs <helderijs@gmail.com>
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions
|
||||
# are met:
|
||||
#
|
||||
# 1. Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# 2. Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in
|
||||
# the documentation and/or other materials provided with the
|
||||
# distribution.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
||||
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||
# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
|
||||
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
# ===================================================================
|
||||
|
||||
from binascii import unhexlify
|
||||
|
||||
from Crypto.Cipher import ChaCha20
|
||||
from Crypto.Cipher.ChaCha20 import _HChaCha20
|
||||
from Crypto.Hash import Poly1305, BLAKE2s
|
||||
|
||||
from Crypto.Random import get_random_bytes
|
||||
|
||||
from Crypto.Util.number import long_to_bytes
|
||||
from Crypto.Util.py3compat import _copy_bytes, bord
|
||||
from Crypto.Util._raw_api import is_buffer
|
||||
|
||||
|
||||
def _enum(**enums):
|
||||
return type('Enum', (), enums)
|
||||
|
||||
|
||||
_CipherStatus = _enum(PROCESSING_AUTH_DATA=1,
|
||||
PROCESSING_CIPHERTEXT=2,
|
||||
PROCESSING_DONE=3)
|
||||
|
||||
|
||||
class ChaCha20Poly1305Cipher(object):
|
||||
"""ChaCha20-Poly1305 and XChaCha20-Poly1305 cipher object.
|
||||
Do not create it directly. Use :py:func:`new` instead.
|
||||
|
||||
:var nonce: The nonce with length 8, 12 or 24 bytes
|
||||
:vartype nonce: byte string
|
||||
"""
|
||||
|
||||
def __init__(self, key, nonce):
|
||||
"""Initialize a ChaCha20-Poly1305 AEAD cipher object
|
||||
|
||||
See also `new()` at the module level."""
|
||||
|
||||
self._next = ("update", "encrypt", "decrypt", "digest",
|
||||
"verify")
|
||||
|
||||
self._authenticator = Poly1305.new(key=key, nonce=nonce, cipher=ChaCha20)
|
||||
|
||||
self._cipher = ChaCha20.new(key=key, nonce=nonce)
|
||||
self._cipher.seek(64) # Block counter starts at 1
|
||||
|
||||
self._len_aad = 0
|
||||
self._len_ct = 0
|
||||
self._mac_tag = None
|
||||
self._status = _CipherStatus.PROCESSING_AUTH_DATA
|
||||
|
||||
def update(self, data):
|
||||
"""Protect the associated data.
|
||||
|
||||
Associated data (also known as *additional authenticated data* - AAD)
|
||||
is the piece of the message that must stay in the clear, while
|
||||
still allowing the receiver to verify its integrity.
|
||||
An example is packet headers.
|
||||
|
||||
The associated data (possibly split into multiple segments) is
|
||||
fed into :meth:`update` before any call to :meth:`decrypt` or :meth:`encrypt`.
|
||||
If there is no associated data, :meth:`update` is not called.
|
||||
|
||||
:param bytes/bytearray/memoryview assoc_data:
|
||||
A piece of associated data. There are no restrictions on its size.
|
||||
"""
|
||||
|
||||
if "update" not in self._next:
|
||||
raise TypeError("update() method cannot be called")
|
||||
|
||||
self._len_aad += len(data)
|
||||
self._authenticator.update(data)
|
||||
|
||||
def _pad_aad(self):
|
||||
|
||||
assert(self._status == _CipherStatus.PROCESSING_AUTH_DATA)
|
||||
if self._len_aad & 0x0F:
|
||||
self._authenticator.update(b'\x00' * (16 - (self._len_aad & 0x0F)))
|
||||
self._status = _CipherStatus.PROCESSING_CIPHERTEXT
|
||||
|
||||
def encrypt(self, plaintext, output=None):
|
||||
"""Encrypt a piece of data.
|
||||
|
||||
Args:
|
||||
plaintext(bytes/bytearray/memoryview): The data to encrypt, of any size.
|
||||
Keyword Args:
|
||||
output(bytes/bytearray/memoryview): The location where the ciphertext
|
||||
is written to. If ``None``, the ciphertext is returned.
|
||||
Returns:
|
||||
If ``output`` is ``None``, the ciphertext is returned as ``bytes``.
|
||||
Otherwise, ``None``.
|
||||
"""
|
||||
|
||||
if "encrypt" not in self._next:
|
||||
raise TypeError("encrypt() method cannot be called")
|
||||
|
||||
if self._status == _CipherStatus.PROCESSING_AUTH_DATA:
|
||||
self._pad_aad()
|
||||
|
||||
self._next = ("encrypt", "digest")
|
||||
|
||||
result = self._cipher.encrypt(plaintext, output=output)
|
||||
self._len_ct += len(plaintext)
|
||||
if output is None:
|
||||
self._authenticator.update(result)
|
||||
else:
|
||||
self._authenticator.update(output)
|
||||
return result
|
||||
|
||||
def decrypt(self, ciphertext, output=None):
|
||||
"""Decrypt a piece of data.
|
||||
|
||||
Args:
|
||||
ciphertext(bytes/bytearray/memoryview): The data to decrypt, of any size.
|
||||
Keyword Args:
|
||||
output(bytes/bytearray/memoryview): The location where the plaintext
|
||||
is written to. If ``None``, the plaintext is returned.
|
||||
Returns:
|
||||
If ``output`` is ``None``, the plaintext is returned as ``bytes``.
|
||||
Otherwise, ``None``.
|
||||
"""
|
||||
|
||||
if "decrypt" not in self._next:
|
||||
raise TypeError("decrypt() method cannot be called")
|
||||
|
||||
if self._status == _CipherStatus.PROCESSING_AUTH_DATA:
|
||||
self._pad_aad()
|
||||
|
||||
self._next = ("decrypt", "verify")
|
||||
|
||||
self._len_ct += len(ciphertext)
|
||||
self._authenticator.update(ciphertext)
|
||||
return self._cipher.decrypt(ciphertext, output=output)
|
||||
|
||||
def _compute_mac(self):
|
||||
"""Finalize the cipher (if not done already) and return the MAC."""
|
||||
|
||||
if self._mac_tag:
|
||||
assert(self._status == _CipherStatus.PROCESSING_DONE)
|
||||
return self._mac_tag
|
||||
|
||||
assert(self._status != _CipherStatus.PROCESSING_DONE)
|
||||
|
||||
if self._status == _CipherStatus.PROCESSING_AUTH_DATA:
|
||||
self._pad_aad()
|
||||
|
||||
if self._len_ct & 0x0F:
|
||||
self._authenticator.update(b'\x00' * (16 - (self._len_ct & 0x0F)))
|
||||
|
||||
self._status = _CipherStatus.PROCESSING_DONE
|
||||
|
||||
self._authenticator.update(long_to_bytes(self._len_aad, 8)[::-1])
|
||||
self._authenticator.update(long_to_bytes(self._len_ct, 8)[::-1])
|
||||
self._mac_tag = self._authenticator.digest()
|
||||
return self._mac_tag
|
||||
|
||||
def digest(self):
|
||||
"""Compute the *binary* authentication tag (MAC).
|
||||
|
||||
:Return: the MAC tag, as 16 ``bytes``.
|
||||
"""
|
||||
|
||||
if "digest" not in self._next:
|
||||
raise TypeError("digest() method cannot be called")
|
||||
self._next = ("digest",)
|
||||
|
||||
return self._compute_mac()
|
||||
|
||||
def hexdigest(self):
|
||||
"""Compute the *printable* authentication tag (MAC).
|
||||
|
||||
This method is like :meth:`digest`.
|
||||
|
||||
:Return: the MAC tag, as a hexadecimal string.
|
||||
"""
|
||||
return "".join(["%02x" % bord(x) for x in self.digest()])
|
||||
|
||||
def verify(self, received_mac_tag):
|
||||
"""Validate the *binary* authentication tag (MAC).
|
||||
|
||||
The receiver invokes this method at the very end, to
|
||||
check if the associated data (if any) and the decrypted
|
||||
messages are valid.
|
||||
|
||||
:param bytes/bytearray/memoryview received_mac_tag:
|
||||
This is the 16-byte *binary* MAC, as received from the sender.
|
||||
:Raises ValueError:
|
||||
if the MAC does not match. The message has been tampered with
|
||||
or the key is incorrect.
|
||||
"""
|
||||
|
||||
if "verify" not in self._next:
|
||||
raise TypeError("verify() cannot be called"
|
||||
" when encrypting a message")
|
||||
self._next = ("verify",)
|
||||
|
||||
secret = get_random_bytes(16)
|
||||
|
||||
self._compute_mac()
|
||||
|
||||
mac1 = BLAKE2s.new(digest_bits=160, key=secret,
|
||||
data=self._mac_tag)
|
||||
mac2 = BLAKE2s.new(digest_bits=160, key=secret,
|
||||
data=received_mac_tag)
|
||||
|
||||
if mac1.digest() != mac2.digest():
|
||||
raise ValueError("MAC check failed")
|
||||
|
||||
def hexverify(self, hex_mac_tag):
|
||||
"""Validate the *printable* authentication tag (MAC).
|
||||
|
||||
This method is like :meth:`verify`.
|
||||
|
||||
:param string hex_mac_tag:
|
||||
This is the *printable* MAC.
|
||||
:Raises ValueError:
|
||||
if the MAC does not match. The message has been tampered with
|
||||
or the key is incorrect.
|
||||
"""
|
||||
|
||||
self.verify(unhexlify(hex_mac_tag))
|
||||
|
||||
def encrypt_and_digest(self, plaintext):
|
||||
"""Perform :meth:`encrypt` and :meth:`digest` in one step.
|
||||
|
||||
:param plaintext: The data to encrypt, of any size.
|
||||
:type plaintext: bytes/bytearray/memoryview
|
||||
:return: a tuple with two ``bytes`` objects:
|
||||
|
||||
- the ciphertext, of equal length as the plaintext
|
||||
- the 16-byte MAC tag
|
||||
"""
|
||||
|
||||
return self.encrypt(plaintext), self.digest()
|
||||
|
||||
def decrypt_and_verify(self, ciphertext, received_mac_tag):
|
||||
"""Perform :meth:`decrypt` and :meth:`verify` in one step.
|
||||
|
||||
:param ciphertext: The piece of data to decrypt.
|
||||
:type ciphertext: bytes/bytearray/memoryview
|
||||
:param bytes received_mac_tag:
|
||||
This is the 16-byte *binary* MAC, as received from the sender.
|
||||
:return: the decrypted data (as ``bytes``)
|
||||
:raises ValueError:
|
||||
if the MAC does not match. The message has been tampered with
|
||||
or the key is incorrect.
|
||||
"""
|
||||
|
||||
plaintext = self.decrypt(ciphertext)
|
||||
self.verify(received_mac_tag)
|
||||
return plaintext
|
||||
|
||||
|
||||
def new(**kwargs):
|
||||
"""Create a new ChaCha20-Poly1305 or XChaCha20-Poly1305 AEAD cipher.
|
||||
|
||||
:keyword key: The secret key to use. It must be 32 bytes long.
|
||||
:type key: byte string
|
||||
|
||||
:keyword nonce:
|
||||
A value that must never be reused for any other encryption
|
||||
done with this key.
|
||||
|
||||
For ChaCha20-Poly1305, it must be 8 or 12 bytes long.
|
||||
|
||||
For XChaCha20-Poly1305, it must be 24 bytes long.
|
||||
|
||||
If not provided, 12 ``bytes`` will be generated randomly
|
||||
(you can find them back in the ``nonce`` attribute).
|
||||
:type nonce: bytes, bytearray, memoryview
|
||||
|
||||
:Return: a :class:`Crypto.Cipher.ChaCha20.ChaCha20Poly1305Cipher` object
|
||||
"""
|
||||
|
||||
try:
|
||||
key = kwargs.pop("key")
|
||||
except KeyError as e:
|
||||
raise TypeError("Missing parameter %s" % e)
|
||||
|
||||
if len(key) != 32:
|
||||
raise ValueError("Key must be 32 bytes long")
|
||||
|
||||
nonce = kwargs.pop("nonce", None)
|
||||
if nonce is None:
|
||||
nonce = get_random_bytes(12)
|
||||
|
||||
if len(nonce) in (8, 12):
|
||||
chacha20_poly1305_nonce = nonce
|
||||
elif len(nonce) == 24:
|
||||
key = _HChaCha20(key, nonce[:16])
|
||||
chacha20_poly1305_nonce = b'\x00\x00\x00\x00' + nonce[16:]
|
||||
else:
|
||||
raise ValueError("Nonce must be 8, 12 or 24 bytes long")
|
||||
|
||||
if not is_buffer(nonce):
|
||||
raise TypeError("nonce must be bytes, bytearray or memoryview")
|
||||
|
||||
if kwargs:
|
||||
raise TypeError("Unknown parameters: " + str(kwargs))
|
||||
|
||||
cipher = ChaCha20Poly1305Cipher(key, chacha20_poly1305_nonce)
|
||||
cipher.nonce = _copy_bytes(None, None, nonce)
|
||||
return cipher
|
||||
|
||||
|
||||
# Size of a key (in bytes)
|
||||
key_size = 32
|
||||
@@ -1,28 +0,0 @@
|
||||
from typing import Union, Tuple, overload, Optional
|
||||
|
||||
Buffer = bytes|bytearray|memoryview
|
||||
|
||||
class ChaCha20Poly1305Cipher:
|
||||
nonce: bytes
|
||||
|
||||
def __init__(self, key: Buffer, nonce: Buffer) -> None: ...
|
||||
def update(self, data: Buffer) -> None: ...
|
||||
@overload
|
||||
def encrypt(self, plaintext: Buffer) -> bytes: ...
|
||||
@overload
|
||||
def encrypt(self, plaintext: Buffer, output: Union[bytearray, memoryview]) -> None: ...
|
||||
@overload
|
||||
def decrypt(self, plaintext: Buffer) -> bytes: ...
|
||||
@overload
|
||||
def decrypt(self, plaintext: Buffer, output: Union[bytearray, memoryview]) -> None: ...
|
||||
def digest(self) -> bytes: ...
|
||||
def hexdigest(self) -> str: ...
|
||||
def verify(self, received_mac_tag: Buffer) -> None: ...
|
||||
def hexverify(self, received_mac_tag: str) -> None: ...
|
||||
def encrypt_and_digest(self, plaintext: Buffer) -> Tuple[bytes, bytes]: ...
|
||||
def decrypt_and_verify(self, ciphertext: Buffer, received_mac_tag: Buffer) -> bytes: ...
|
||||
|
||||
def new(key: Buffer, nonce: Optional[Buffer] = ...) -> ChaCha20Poly1305Cipher: ...
|
||||
|
||||
block_size: int
|
||||
key_size: int
|
||||
@@ -1,158 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Cipher/DES.py : DES
|
||||
#
|
||||
# ===================================================================
|
||||
# The contents of this file are dedicated to the public domain. To
|
||||
# the extent that dedication to the public domain is not available,
|
||||
# everyone is granted a worldwide, perpetual, royalty-free,
|
||||
# non-exclusive license to exercise all rights associated with the
|
||||
# contents of this file for any purpose whatsoever.
|
||||
# No rights are reserved.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
||||
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
||||
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
# ===================================================================
|
||||
"""
|
||||
Module's constants for the modes of operation supported with Single DES:
|
||||
|
||||
:var MODE_ECB: :ref:`Electronic Code Book (ECB) <ecb_mode>`
|
||||
:var MODE_CBC: :ref:`Cipher-Block Chaining (CBC) <cbc_mode>`
|
||||
:var MODE_CFB: :ref:`Cipher FeedBack (CFB) <cfb_mode>`
|
||||
:var MODE_OFB: :ref:`Output FeedBack (OFB) <ofb_mode>`
|
||||
:var MODE_CTR: :ref:`CounTer Mode (CTR) <ctr_mode>`
|
||||
:var MODE_OPENPGP: :ref:`OpenPGP Mode <openpgp_mode>`
|
||||
:var MODE_EAX: :ref:`EAX Mode <eax_mode>`
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
from Crypto.Cipher import _create_cipher
|
||||
from Crypto.Util.py3compat import byte_string
|
||||
from Crypto.Util._raw_api import (load_pycryptodome_raw_lib,
|
||||
VoidPointer, SmartPointer,
|
||||
c_size_t, c_uint8_ptr)
|
||||
|
||||
_raw_des_lib = load_pycryptodome_raw_lib(
|
||||
"Crypto.Cipher._raw_des",
|
||||
"""
|
||||
int DES_start_operation(const uint8_t key[],
|
||||
size_t key_len,
|
||||
void **pResult);
|
||||
int DES_encrypt(const void *state,
|
||||
const uint8_t *in,
|
||||
uint8_t *out,
|
||||
size_t data_len);
|
||||
int DES_decrypt(const void *state,
|
||||
const uint8_t *in,
|
||||
uint8_t *out,
|
||||
size_t data_len);
|
||||
int DES_stop_operation(void *state);
|
||||
""")
|
||||
|
||||
|
||||
def _create_base_cipher(dict_parameters):
|
||||
"""This method instantiates and returns a handle to a low-level
|
||||
base cipher. It will absorb named parameters in the process."""
|
||||
|
||||
try:
|
||||
key = dict_parameters.pop("key")
|
||||
except KeyError:
|
||||
raise TypeError("Missing 'key' parameter")
|
||||
|
||||
if len(key) != key_size:
|
||||
raise ValueError("Incorrect DES key length (%d bytes)" % len(key))
|
||||
|
||||
start_operation = _raw_des_lib.DES_start_operation
|
||||
stop_operation = _raw_des_lib.DES_stop_operation
|
||||
|
||||
cipher = VoidPointer()
|
||||
result = start_operation(c_uint8_ptr(key),
|
||||
c_size_t(len(key)),
|
||||
cipher.address_of())
|
||||
if result:
|
||||
raise ValueError("Error %X while instantiating the DES cipher"
|
||||
% result)
|
||||
return SmartPointer(cipher.get(), stop_operation)
|
||||
|
||||
|
||||
def new(key, mode, *args, **kwargs):
|
||||
"""Create a new DES cipher.
|
||||
|
||||
:param key:
|
||||
The secret key to use in the symmetric cipher.
|
||||
It must be 8 byte long. The parity bits will be ignored.
|
||||
:type key: bytes/bytearray/memoryview
|
||||
|
||||
:param mode:
|
||||
The chaining mode to use for encryption or decryption.
|
||||
:type mode: One of the supported ``MODE_*`` constants
|
||||
|
||||
:Keyword Arguments:
|
||||
* **iv** (*byte string*) --
|
||||
(Only applicable for ``MODE_CBC``, ``MODE_CFB``, ``MODE_OFB``,
|
||||
and ``MODE_OPENPGP`` modes).
|
||||
|
||||
The initialization vector to use for encryption or decryption.
|
||||
|
||||
For ``MODE_CBC``, ``MODE_CFB``, and ``MODE_OFB`` it must be 8 bytes long.
|
||||
|
||||
For ``MODE_OPENPGP`` mode only,
|
||||
it must be 8 bytes long for encryption
|
||||
and 10 bytes for decryption (in the latter case, it is
|
||||
actually the *encrypted* IV which was prefixed to the ciphertext).
|
||||
|
||||
If not provided, a random byte string is generated (you must then
|
||||
read its value with the :attr:`iv` attribute).
|
||||
|
||||
* **nonce** (*byte string*) --
|
||||
(Only applicable for ``MODE_EAX`` and ``MODE_CTR``).
|
||||
|
||||
A value that must never be reused for any other encryption done
|
||||
with this key.
|
||||
|
||||
For ``MODE_EAX`` there are no
|
||||
restrictions on its length (recommended: **16** bytes).
|
||||
|
||||
For ``MODE_CTR``, its length must be in the range **[0..7]**.
|
||||
|
||||
If not provided for ``MODE_EAX``, a random byte string is generated (you
|
||||
can read it back via the ``nonce`` attribute).
|
||||
|
||||
* **segment_size** (*integer*) --
|
||||
(Only ``MODE_CFB``).The number of **bits** the plaintext and ciphertext
|
||||
are segmented in. It must be a multiple of 8.
|
||||
If not specified, it will be assumed to be 8.
|
||||
|
||||
* **mac_len** : (*integer*) --
|
||||
(Only ``MODE_EAX``)
|
||||
Length of the authentication tag, in bytes.
|
||||
It must be no longer than 8 (default).
|
||||
|
||||
* **initial_value** : (*integer*) --
|
||||
(Only ``MODE_CTR``). The initial value for the counter within
|
||||
the counter block. By default it is **0**.
|
||||
|
||||
:Return: a DES object, of the applicable mode.
|
||||
"""
|
||||
|
||||
return _create_cipher(sys.modules[__name__], key, mode, *args, **kwargs)
|
||||
|
||||
MODE_ECB = 1
|
||||
MODE_CBC = 2
|
||||
MODE_CFB = 3
|
||||
MODE_OFB = 5
|
||||
MODE_CTR = 6
|
||||
MODE_OPENPGP = 7
|
||||
MODE_EAX = 9
|
||||
|
||||
# Size of a data block (in bytes)
|
||||
block_size = 8
|
||||
# Size of a key (in bytes)
|
||||
key_size = 8
|
||||
@@ -1,35 +0,0 @@
|
||||
from typing import Union, Dict, Iterable, Optional
|
||||
|
||||
Buffer = bytes|bytearray|memoryview
|
||||
|
||||
from Crypto.Cipher._mode_ecb import EcbMode
|
||||
from Crypto.Cipher._mode_cbc import CbcMode
|
||||
from Crypto.Cipher._mode_cfb import CfbMode
|
||||
from Crypto.Cipher._mode_ofb import OfbMode
|
||||
from Crypto.Cipher._mode_ctr import CtrMode
|
||||
from Crypto.Cipher._mode_openpgp import OpenPgpMode
|
||||
from Crypto.Cipher._mode_eax import EaxMode
|
||||
|
||||
DESMode = int
|
||||
|
||||
MODE_ECB: DESMode
|
||||
MODE_CBC: DESMode
|
||||
MODE_CFB: DESMode
|
||||
MODE_OFB: DESMode
|
||||
MODE_CTR: DESMode
|
||||
MODE_OPENPGP: DESMode
|
||||
MODE_EAX: DESMode
|
||||
|
||||
def new(key: Buffer,
|
||||
mode: DESMode,
|
||||
iv : Optional[Buffer] = ...,
|
||||
IV : Optional[Buffer] = ...,
|
||||
nonce : Optional[Buffer] = ...,
|
||||
segment_size : int = ...,
|
||||
mac_len : int = ...,
|
||||
initial_value : Union[int, Buffer] = ...,
|
||||
counter : Dict = ...) -> \
|
||||
Union[EcbMode, CbcMode, CfbMode, OfbMode, CtrMode, OpenPgpMode]: ...
|
||||
|
||||
block_size: int
|
||||
key_size: int
|
||||
@@ -1,187 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Cipher/DES3.py : DES3
|
||||
#
|
||||
# ===================================================================
|
||||
# The contents of this file are dedicated to the public domain. To
|
||||
# the extent that dedication to the public domain is not available,
|
||||
# everyone is granted a worldwide, perpetual, royalty-free,
|
||||
# non-exclusive license to exercise all rights associated with the
|
||||
# contents of this file for any purpose whatsoever.
|
||||
# No rights are reserved.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
||||
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
||||
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
# ===================================================================
|
||||
"""
|
||||
Module's constants for the modes of operation supported with Triple DES:
|
||||
|
||||
:var MODE_ECB: :ref:`Electronic Code Book (ECB) <ecb_mode>`
|
||||
:var MODE_CBC: :ref:`Cipher-Block Chaining (CBC) <cbc_mode>`
|
||||
:var MODE_CFB: :ref:`Cipher FeedBack (CFB) <cfb_mode>`
|
||||
:var MODE_OFB: :ref:`Output FeedBack (OFB) <ofb_mode>`
|
||||
:var MODE_CTR: :ref:`CounTer Mode (CTR) <ctr_mode>`
|
||||
:var MODE_OPENPGP: :ref:`OpenPGP Mode <openpgp_mode>`
|
||||
:var MODE_EAX: :ref:`EAX Mode <eax_mode>`
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
from Crypto.Cipher import _create_cipher
|
||||
from Crypto.Util.py3compat import byte_string, bchr, bord, bstr
|
||||
from Crypto.Util._raw_api import (load_pycryptodome_raw_lib,
|
||||
VoidPointer, SmartPointer,
|
||||
c_size_t)
|
||||
|
||||
_raw_des3_lib = load_pycryptodome_raw_lib(
|
||||
"Crypto.Cipher._raw_des3",
|
||||
"""
|
||||
int DES3_start_operation(const uint8_t key[],
|
||||
size_t key_len,
|
||||
void **pResult);
|
||||
int DES3_encrypt(const void *state,
|
||||
const uint8_t *in,
|
||||
uint8_t *out,
|
||||
size_t data_len);
|
||||
int DES3_decrypt(const void *state,
|
||||
const uint8_t *in,
|
||||
uint8_t *out,
|
||||
size_t data_len);
|
||||
int DES3_stop_operation(void *state);
|
||||
""")
|
||||
|
||||
|
||||
def adjust_key_parity(key_in):
|
||||
"""Set the parity bits in a TDES key.
|
||||
|
||||
:param key_in: the TDES key whose bits need to be adjusted
|
||||
:type key_in: byte string
|
||||
|
||||
:returns: a copy of ``key_in``, with the parity bits correctly set
|
||||
:rtype: byte string
|
||||
|
||||
:raises ValueError: if the TDES key is not 16 or 24 bytes long
|
||||
:raises ValueError: if the TDES key degenerates into Single DES
|
||||
"""
|
||||
|
||||
def parity_byte(key_byte):
|
||||
parity = 1
|
||||
for i in range(1, 8):
|
||||
parity ^= (key_byte >> i) & 1
|
||||
return (key_byte & 0xFE) | parity
|
||||
|
||||
if len(key_in) not in key_size:
|
||||
raise ValueError("Not a valid TDES key")
|
||||
|
||||
key_out = b"".join([ bchr(parity_byte(bord(x))) for x in key_in ])
|
||||
|
||||
if key_out[:8] == key_out[8:16] or key_out[-16:-8] == key_out[-8:]:
|
||||
raise ValueError("Triple DES key degenerates to single DES")
|
||||
|
||||
return key_out
|
||||
|
||||
|
||||
def _create_base_cipher(dict_parameters):
|
||||
"""This method instantiates and returns a handle to a low-level base cipher.
|
||||
It will absorb named parameters in the process."""
|
||||
|
||||
try:
|
||||
key_in = dict_parameters.pop("key")
|
||||
except KeyError:
|
||||
raise TypeError("Missing 'key' parameter")
|
||||
|
||||
key = adjust_key_parity(bstr(key_in))
|
||||
|
||||
start_operation = _raw_des3_lib.DES3_start_operation
|
||||
stop_operation = _raw_des3_lib.DES3_stop_operation
|
||||
|
||||
cipher = VoidPointer()
|
||||
result = start_operation(key,
|
||||
c_size_t(len(key)),
|
||||
cipher.address_of())
|
||||
if result:
|
||||
raise ValueError("Error %X while instantiating the TDES cipher"
|
||||
% result)
|
||||
return SmartPointer(cipher.get(), stop_operation)
|
||||
|
||||
|
||||
def new(key, mode, *args, **kwargs):
|
||||
"""Create a new Triple DES cipher.
|
||||
|
||||
:param key:
|
||||
The secret key to use in the symmetric cipher.
|
||||
It must be 16 or 24 byte long. The parity bits will be ignored.
|
||||
:type key: bytes/bytearray/memoryview
|
||||
|
||||
:param mode:
|
||||
The chaining mode to use for encryption or decryption.
|
||||
:type mode: One of the supported ``MODE_*`` constants
|
||||
|
||||
:Keyword Arguments:
|
||||
* **iv** (*bytes*, *bytearray*, *memoryview*) --
|
||||
(Only applicable for ``MODE_CBC``, ``MODE_CFB``, ``MODE_OFB``,
|
||||
and ``MODE_OPENPGP`` modes).
|
||||
|
||||
The initialization vector to use for encryption or decryption.
|
||||
|
||||
For ``MODE_CBC``, ``MODE_CFB``, and ``MODE_OFB`` it must be 8 bytes long.
|
||||
|
||||
For ``MODE_OPENPGP`` mode only,
|
||||
it must be 8 bytes long for encryption
|
||||
and 10 bytes for decryption (in the latter case, it is
|
||||
actually the *encrypted* IV which was prefixed to the ciphertext).
|
||||
|
||||
If not provided, a random byte string is generated (you must then
|
||||
read its value with the :attr:`iv` attribute).
|
||||
|
||||
* **nonce** (*bytes*, *bytearray*, *memoryview*) --
|
||||
(Only applicable for ``MODE_EAX`` and ``MODE_CTR``).
|
||||
|
||||
A value that must never be reused for any other encryption done
|
||||
with this key.
|
||||
|
||||
For ``MODE_EAX`` there are no
|
||||
restrictions on its length (recommended: **16** bytes).
|
||||
|
||||
For ``MODE_CTR``, its length must be in the range **[0..7]**.
|
||||
|
||||
If not provided for ``MODE_EAX``, a random byte string is generated (you
|
||||
can read it back via the ``nonce`` attribute).
|
||||
|
||||
* **segment_size** (*integer*) --
|
||||
(Only ``MODE_CFB``).The number of **bits** the plaintext and ciphertext
|
||||
are segmented in. It must be a multiple of 8.
|
||||
If not specified, it will be assumed to be 8.
|
||||
|
||||
* **mac_len** : (*integer*) --
|
||||
(Only ``MODE_EAX``)
|
||||
Length of the authentication tag, in bytes.
|
||||
It must be no longer than 8 (default).
|
||||
|
||||
* **initial_value** : (*integer*) --
|
||||
(Only ``MODE_CTR``). The initial value for the counter within
|
||||
the counter block. By default it is **0**.
|
||||
|
||||
:Return: a Triple DES object, of the applicable mode.
|
||||
"""
|
||||
|
||||
return _create_cipher(sys.modules[__name__], key, mode, *args, **kwargs)
|
||||
|
||||
MODE_ECB = 1
|
||||
MODE_CBC = 2
|
||||
MODE_CFB = 3
|
||||
MODE_OFB = 5
|
||||
MODE_CTR = 6
|
||||
MODE_OPENPGP = 7
|
||||
MODE_EAX = 9
|
||||
|
||||
# Size of a data block (in bytes)
|
||||
block_size = 8
|
||||
# Size of a key (in bytes)
|
||||
key_size = (16, 24)
|
||||
@@ -1,37 +0,0 @@
|
||||
from typing import Union, Dict, Tuple, Optional
|
||||
|
||||
Buffer = bytes|bytearray|memoryview
|
||||
|
||||
from Crypto.Cipher._mode_ecb import EcbMode
|
||||
from Crypto.Cipher._mode_cbc import CbcMode
|
||||
from Crypto.Cipher._mode_cfb import CfbMode
|
||||
from Crypto.Cipher._mode_ofb import OfbMode
|
||||
from Crypto.Cipher._mode_ctr import CtrMode
|
||||
from Crypto.Cipher._mode_openpgp import OpenPgpMode
|
||||
from Crypto.Cipher._mode_eax import EaxMode
|
||||
|
||||
def adjust_key_parity(key_in: bytes) -> bytes: ...
|
||||
|
||||
DES3Mode = int
|
||||
|
||||
MODE_ECB: DES3Mode
|
||||
MODE_CBC: DES3Mode
|
||||
MODE_CFB: DES3Mode
|
||||
MODE_OFB: DES3Mode
|
||||
MODE_CTR: DES3Mode
|
||||
MODE_OPENPGP: DES3Mode
|
||||
MODE_EAX: DES3Mode
|
||||
|
||||
def new(key: Buffer,
|
||||
mode: DES3Mode,
|
||||
iv : Optional[Buffer] = ...,
|
||||
IV : Optional[Buffer] = ...,
|
||||
nonce : Optional[Buffer] = ...,
|
||||
segment_size : int = ...,
|
||||
mac_len : int = ...,
|
||||
initial_value : Union[int, Buffer] = ...,
|
||||
counter : Dict = ...) -> \
|
||||
Union[EcbMode, CbcMode, CfbMode, OfbMode, CtrMode, OpenPgpMode]: ...
|
||||
|
||||
block_size: int
|
||||
key_size: Tuple[int, int]
|
||||
@@ -1,231 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Cipher/PKCS1_OAEP.py : PKCS#1 OAEP
|
||||
#
|
||||
# ===================================================================
|
||||
# The contents of this file are dedicated to the public domain. To
|
||||
# the extent that dedication to the public domain is not available,
|
||||
# everyone is granted a worldwide, perpetual, royalty-free,
|
||||
# non-exclusive license to exercise all rights associated with the
|
||||
# contents of this file for any purpose whatsoever.
|
||||
# No rights are reserved.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
||||
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
||||
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
# ===================================================================
|
||||
|
||||
from Crypto.Signature.pss import MGF1
|
||||
import Crypto.Hash.SHA1
|
||||
|
||||
from Crypto.Util.py3compat import _copy_bytes
|
||||
import Crypto.Util.number
|
||||
from Crypto.Util.number import ceil_div, bytes_to_long, long_to_bytes
|
||||
from Crypto.Util.strxor import strxor
|
||||
from Crypto import Random
|
||||
from ._pkcs1_oaep_decode import oaep_decode
|
||||
|
||||
|
||||
class PKCS1OAEP_Cipher:
|
||||
"""Cipher object for PKCS#1 v1.5 OAEP.
|
||||
Do not create directly: use :func:`new` instead."""
|
||||
|
||||
def __init__(self, key, hashAlgo, mgfunc, label, randfunc):
|
||||
"""Initialize this PKCS#1 OAEP cipher object.
|
||||
|
||||
:Parameters:
|
||||
key : an RSA key object
|
||||
If a private half is given, both encryption and decryption are possible.
|
||||
If a public half is given, only encryption is possible.
|
||||
hashAlgo : hash object
|
||||
The hash function to use. This can be a module under `Crypto.Hash`
|
||||
or an existing hash object created from any of such modules. If not specified,
|
||||
`Crypto.Hash.SHA1` is used.
|
||||
mgfunc : callable
|
||||
A mask generation function that accepts two parameters: a string to
|
||||
use as seed, and the lenth of the mask to generate, in bytes.
|
||||
If not specified, the standard MGF1 consistent with ``hashAlgo`` is used (a safe choice).
|
||||
label : bytes/bytearray/memoryview
|
||||
A label to apply to this particular encryption. If not specified,
|
||||
an empty string is used. Specifying a label does not improve
|
||||
security.
|
||||
randfunc : callable
|
||||
A function that returns random bytes.
|
||||
|
||||
:attention: Modify the mask generation function only if you know what you are doing.
|
||||
Sender and receiver must use the same one.
|
||||
"""
|
||||
self._key = key
|
||||
|
||||
if hashAlgo:
|
||||
self._hashObj = hashAlgo
|
||||
else:
|
||||
self._hashObj = Crypto.Hash.SHA1
|
||||
|
||||
if mgfunc:
|
||||
self._mgf = mgfunc
|
||||
else:
|
||||
self._mgf = lambda x, y: MGF1(x, y, self._hashObj)
|
||||
|
||||
self._label = _copy_bytes(None, None, label)
|
||||
self._randfunc = randfunc
|
||||
|
||||
def can_encrypt(self):
|
||||
"""Legacy function to check if you can call :meth:`encrypt`.
|
||||
|
||||
.. deprecated:: 3.0"""
|
||||
return self._key.can_encrypt()
|
||||
|
||||
def can_decrypt(self):
|
||||
"""Legacy function to check if you can call :meth:`decrypt`.
|
||||
|
||||
.. deprecated:: 3.0"""
|
||||
return self._key.can_decrypt()
|
||||
|
||||
def encrypt(self, message):
|
||||
"""Encrypt a message with PKCS#1 OAEP.
|
||||
|
||||
:param message:
|
||||
The message to encrypt, also known as plaintext. It can be of
|
||||
variable length, but not longer than the RSA modulus (in bytes)
|
||||
minus 2, minus twice the hash output size.
|
||||
For instance, if you use RSA 2048 and SHA-256, the longest message
|
||||
you can encrypt is 190 byte long.
|
||||
:type message: bytes/bytearray/memoryview
|
||||
|
||||
:returns: The ciphertext, as large as the RSA modulus.
|
||||
:rtype: bytes
|
||||
|
||||
:raises ValueError:
|
||||
if the message is too long.
|
||||
"""
|
||||
|
||||
# See 7.1.1 in RFC3447
|
||||
modBits = Crypto.Util.number.size(self._key.n)
|
||||
k = ceil_div(modBits, 8) # Convert from bits to bytes
|
||||
hLen = self._hashObj.digest_size
|
||||
mLen = len(message)
|
||||
|
||||
# Step 1b
|
||||
ps_len = k - mLen - 2 * hLen - 2
|
||||
if ps_len < 0:
|
||||
raise ValueError("Plaintext is too long.")
|
||||
# Step 2a
|
||||
lHash = self._hashObj.new(self._label).digest()
|
||||
# Step 2b
|
||||
ps = b'\x00' * ps_len
|
||||
# Step 2c
|
||||
db = lHash + ps + b'\x01' + _copy_bytes(None, None, message)
|
||||
# Step 2d
|
||||
ros = self._randfunc(hLen)
|
||||
# Step 2e
|
||||
dbMask = self._mgf(ros, k-hLen-1)
|
||||
# Step 2f
|
||||
maskedDB = strxor(db, dbMask)
|
||||
# Step 2g
|
||||
seedMask = self._mgf(maskedDB, hLen)
|
||||
# Step 2h
|
||||
maskedSeed = strxor(ros, seedMask)
|
||||
# Step 2i
|
||||
em = b'\x00' + maskedSeed + maskedDB
|
||||
# Step 3a (OS2IP)
|
||||
em_int = bytes_to_long(em)
|
||||
# Step 3b (RSAEP)
|
||||
m_int = self._key._encrypt(em_int)
|
||||
# Step 3c (I2OSP)
|
||||
c = long_to_bytes(m_int, k)
|
||||
return c
|
||||
|
||||
def decrypt(self, ciphertext):
|
||||
"""Decrypt a message with PKCS#1 OAEP.
|
||||
|
||||
:param ciphertext: The encrypted message.
|
||||
:type ciphertext: bytes/bytearray/memoryview
|
||||
|
||||
:returns: The original message (plaintext).
|
||||
:rtype: bytes
|
||||
|
||||
:raises ValueError:
|
||||
if the ciphertext has the wrong length, or if decryption
|
||||
fails the integrity check (in which case, the decryption
|
||||
key is probably wrong).
|
||||
:raises TypeError:
|
||||
if the RSA key has no private half (i.e. you are trying
|
||||
to decrypt using a public key).
|
||||
"""
|
||||
|
||||
# See 7.1.2 in RFC3447
|
||||
modBits = Crypto.Util.number.size(self._key.n)
|
||||
k = ceil_div(modBits, 8) # Convert from bits to bytes
|
||||
hLen = self._hashObj.digest_size
|
||||
|
||||
# Step 1b and 1c
|
||||
if len(ciphertext) != k or k < hLen+2:
|
||||
raise ValueError("Ciphertext with incorrect length.")
|
||||
# Step 2a (O2SIP)
|
||||
ct_int = bytes_to_long(ciphertext)
|
||||
# Step 2b (RSADP) and step 2c (I2OSP)
|
||||
em = self._key._decrypt_to_bytes(ct_int)
|
||||
# Step 3a
|
||||
lHash = self._hashObj.new(self._label).digest()
|
||||
# y must be 0, but we MUST NOT check it here in order not to
|
||||
# allow attacks like Manger's (http://dl.acm.org/citation.cfm?id=704143)
|
||||
maskedSeed = em[1:hLen+1]
|
||||
maskedDB = em[hLen+1:]
|
||||
# Step 3c
|
||||
seedMask = self._mgf(maskedDB, hLen)
|
||||
# Step 3d
|
||||
seed = strxor(maskedSeed, seedMask)
|
||||
# Step 3e
|
||||
dbMask = self._mgf(seed, k-hLen-1)
|
||||
# Step 3f
|
||||
db = strxor(maskedDB, dbMask)
|
||||
# Step 3b + 3g
|
||||
res = oaep_decode(em, lHash, db)
|
||||
if res <= 0:
|
||||
raise ValueError("Incorrect decryption.")
|
||||
# Step 4
|
||||
return db[res:]
|
||||
|
||||
|
||||
def new(key, hashAlgo=None, mgfunc=None, label=b'', randfunc=None):
|
||||
"""Return a cipher object :class:`PKCS1OAEP_Cipher`
|
||||
that can be used to perform PKCS#1 OAEP encryption or decryption.
|
||||
|
||||
:param key:
|
||||
The key object to use to encrypt or decrypt the message.
|
||||
Decryption is only possible with a private RSA key.
|
||||
:type key: RSA key object
|
||||
|
||||
:param hashAlgo:
|
||||
The hash function to use. This can be a module under `Crypto.Hash`
|
||||
or an existing hash object created from any of such modules.
|
||||
If not specified, `Crypto.Hash.SHA1` is used.
|
||||
:type hashAlgo: hash object
|
||||
|
||||
:param mgfunc:
|
||||
A mask generation function that accepts two parameters: a string to
|
||||
use as seed, and the lenth of the mask to generate, in bytes.
|
||||
If not specified, the standard MGF1 consistent with ``hashAlgo`` is used (a safe choice).
|
||||
:type mgfunc: callable
|
||||
|
||||
:param label:
|
||||
A label to apply to this particular encryption. If not specified,
|
||||
an empty string is used. Specifying a label does not improve
|
||||
security.
|
||||
:type label: bytes/bytearray/memoryview
|
||||
|
||||
:param randfunc:
|
||||
A function that returns random bytes.
|
||||
The default is `Random.get_random_bytes`.
|
||||
:type randfunc: callable
|
||||
"""
|
||||
|
||||
if randfunc is None:
|
||||
randfunc = Random.get_random_bytes
|
||||
return PKCS1OAEP_Cipher(key, hashAlgo, mgfunc, label, randfunc)
|
||||
@@ -1,35 +0,0 @@
|
||||
from typing import Optional, Union, Callable, Any, overload
|
||||
from typing_extensions import Protocol
|
||||
|
||||
from Crypto.PublicKey.RSA import RsaKey
|
||||
|
||||
class HashLikeClass(Protocol):
|
||||
digest_size : int
|
||||
def new(self, data: Optional[bytes] = ...) -> Any: ...
|
||||
|
||||
class HashLikeModule(Protocol):
|
||||
digest_size : int
|
||||
@staticmethod
|
||||
def new(data: Optional[bytes] = ...) -> Any: ...
|
||||
|
||||
HashLike = Union[HashLikeClass, HashLikeModule]
|
||||
|
||||
Buffer = Union[bytes, bytearray, memoryview]
|
||||
|
||||
class PKCS1OAEP_Cipher:
|
||||
def __init__(self,
|
||||
key: RsaKey,
|
||||
hashAlgo: HashLike,
|
||||
mgfunc: Callable[[bytes, int], bytes],
|
||||
label: Buffer,
|
||||
randfunc: Callable[[int], bytes]) -> None: ...
|
||||
def can_encrypt(self) -> bool: ...
|
||||
def can_decrypt(self) -> bool: ...
|
||||
def encrypt(self, message: Buffer) -> bytes: ...
|
||||
def decrypt(self, ciphertext: Buffer) -> bytes: ...
|
||||
|
||||
def new(key: RsaKey,
|
||||
hashAlgo: Optional[HashLike] = ...,
|
||||
mgfunc: Optional[Callable[[bytes, int], bytes]] = ...,
|
||||
label: Optional[Buffer] = ...,
|
||||
randfunc: Optional[Callable[[int], bytes]] = ...) -> PKCS1OAEP_Cipher: ...
|
||||
@@ -1,189 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Cipher/PKCS1-v1_5.py : PKCS#1 v1.5
|
||||
#
|
||||
# ===================================================================
|
||||
# The contents of this file are dedicated to the public domain. To
|
||||
# the extent that dedication to the public domain is not available,
|
||||
# everyone is granted a worldwide, perpetual, royalty-free,
|
||||
# non-exclusive license to exercise all rights associated with the
|
||||
# contents of this file for any purpose whatsoever.
|
||||
# No rights are reserved.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
||||
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
||||
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
# ===================================================================
|
||||
|
||||
__all__ = ['new', 'PKCS115_Cipher']
|
||||
|
||||
from Crypto import Random
|
||||
from Crypto.Util.number import bytes_to_long, long_to_bytes
|
||||
from Crypto.Util.py3compat import bord, is_bytes, _copy_bytes
|
||||
from ._pkcs1_oaep_decode import pkcs1_decode
|
||||
|
||||
|
||||
class PKCS115_Cipher:
|
||||
"""This cipher can perform PKCS#1 v1.5 RSA encryption or decryption.
|
||||
Do not instantiate directly. Use :func:`Crypto.Cipher.PKCS1_v1_5.new` instead."""
|
||||
|
||||
def __init__(self, key, randfunc):
|
||||
"""Initialize this PKCS#1 v1.5 cipher object.
|
||||
|
||||
:Parameters:
|
||||
key : an RSA key object
|
||||
If a private half is given, both encryption and decryption are possible.
|
||||
If a public half is given, only encryption is possible.
|
||||
randfunc : callable
|
||||
Function that returns random bytes.
|
||||
"""
|
||||
|
||||
self._key = key
|
||||
self._randfunc = randfunc
|
||||
|
||||
def can_encrypt(self):
|
||||
"""Return True if this cipher object can be used for encryption."""
|
||||
return self._key.can_encrypt()
|
||||
|
||||
def can_decrypt(self):
|
||||
"""Return True if this cipher object can be used for decryption."""
|
||||
return self._key.can_decrypt()
|
||||
|
||||
def encrypt(self, message):
|
||||
"""Produce the PKCS#1 v1.5 encryption of a message.
|
||||
|
||||
This function is named ``RSAES-PKCS1-V1_5-ENCRYPT``, and it is specified in
|
||||
`section 7.2.1 of RFC8017
|
||||
<https://tools.ietf.org/html/rfc8017#page-28>`_.
|
||||
|
||||
:param message:
|
||||
The message to encrypt, also known as plaintext. It can be of
|
||||
variable length, but not longer than the RSA modulus (in bytes) minus 11.
|
||||
:type message: bytes/bytearray/memoryview
|
||||
|
||||
:Returns: A byte string, the ciphertext in which the message is encrypted.
|
||||
It is as long as the RSA modulus (in bytes).
|
||||
|
||||
:Raises ValueError:
|
||||
If the RSA key length is not sufficiently long to deal with the given
|
||||
message.
|
||||
"""
|
||||
|
||||
# See 7.2.1 in RFC8017
|
||||
k = self._key.size_in_bytes()
|
||||
mLen = len(message)
|
||||
|
||||
# Step 1
|
||||
if mLen > k - 11:
|
||||
raise ValueError("Plaintext is too long.")
|
||||
# Step 2a
|
||||
ps = []
|
||||
while len(ps) != k - mLen - 3:
|
||||
new_byte = self._randfunc(1)
|
||||
if bord(new_byte[0]) == 0x00:
|
||||
continue
|
||||
ps.append(new_byte)
|
||||
ps = b"".join(ps)
|
||||
# Step 2b
|
||||
em = b'\x00\x02' + ps + b'\x00' + _copy_bytes(None, None, message)
|
||||
# Step 3a (OS2IP)
|
||||
em_int = bytes_to_long(em)
|
||||
# Step 3b (RSAEP)
|
||||
m_int = self._key._encrypt(em_int)
|
||||
# Step 3c (I2OSP)
|
||||
c = long_to_bytes(m_int, k)
|
||||
return c
|
||||
|
||||
def decrypt(self, ciphertext, sentinel, expected_pt_len=0):
|
||||
r"""Decrypt a PKCS#1 v1.5 ciphertext.
|
||||
|
||||
This is the function ``RSAES-PKCS1-V1_5-DECRYPT`` specified in
|
||||
`section 7.2.2 of RFC8017
|
||||
<https://tools.ietf.org/html/rfc8017#page-29>`_.
|
||||
|
||||
Args:
|
||||
ciphertext (bytes/bytearray/memoryview):
|
||||
The ciphertext that contains the message to recover.
|
||||
sentinel (any type):
|
||||
The object to return whenever an error is detected.
|
||||
expected_pt_len (integer):
|
||||
The length the plaintext is known to have, or 0 if unknown.
|
||||
|
||||
Returns (byte string):
|
||||
It is either the original message or the ``sentinel`` (in case of an error).
|
||||
|
||||
.. warning::
|
||||
PKCS#1 v1.5 decryption is intrinsically vulnerable to timing
|
||||
attacks (see `Bleichenbacher's`__ attack).
|
||||
**Use PKCS#1 OAEP instead**.
|
||||
|
||||
This implementation attempts to mitigate the risk
|
||||
with some constant-time constructs.
|
||||
However, they are not sufficient by themselves: the type of protocol you
|
||||
implement and the way you handle errors make a big difference.
|
||||
|
||||
Specifically, you should make it very hard for the (malicious)
|
||||
party that submitted the ciphertext to quickly understand if decryption
|
||||
succeeded or not.
|
||||
|
||||
To this end, it is recommended that your protocol only encrypts
|
||||
plaintexts of fixed length (``expected_pt_len``),
|
||||
that ``sentinel`` is a random byte string of the same length,
|
||||
and that processing continues for as long
|
||||
as possible even if ``sentinel`` is returned (i.e. in case of
|
||||
incorrect decryption).
|
||||
|
||||
.. __: https://dx.doi.org/10.1007/BFb0055716
|
||||
"""
|
||||
|
||||
# See 7.2.2 in RFC8017
|
||||
k = self._key.size_in_bytes()
|
||||
|
||||
# Step 1
|
||||
if len(ciphertext) != k:
|
||||
raise ValueError("Ciphertext with incorrect length (not %d bytes)" % k)
|
||||
|
||||
# Step 2a (O2SIP)
|
||||
ct_int = bytes_to_long(ciphertext)
|
||||
|
||||
# Step 2b (RSADP) and Step 2c (I2OSP)
|
||||
em = self._key._decrypt_to_bytes(ct_int)
|
||||
|
||||
# Step 3 (not constant time when the sentinel is not a byte string)
|
||||
output = bytes(bytearray(k))
|
||||
if not is_bytes(sentinel) or len(sentinel) > k:
|
||||
size = pkcs1_decode(em, b'', expected_pt_len, output)
|
||||
if size < 0:
|
||||
return sentinel
|
||||
else:
|
||||
return output[size:]
|
||||
|
||||
# Step 3 (somewhat constant time)
|
||||
size = pkcs1_decode(em, sentinel, expected_pt_len, output)
|
||||
return output[size:]
|
||||
|
||||
|
||||
def new(key, randfunc=None):
|
||||
"""Create a cipher for performing PKCS#1 v1.5 encryption or decryption.
|
||||
|
||||
:param key:
|
||||
The key to use to encrypt or decrypt the message. This is a `Crypto.PublicKey.RSA` object.
|
||||
Decryption is only possible if *key* is a private RSA key.
|
||||
:type key: RSA key object
|
||||
|
||||
:param randfunc:
|
||||
Function that return random bytes.
|
||||
The default is :func:`Crypto.Random.get_random_bytes`.
|
||||
:type randfunc: callable
|
||||
|
||||
:returns: A cipher object `PKCS115_Cipher`.
|
||||
"""
|
||||
|
||||
if randfunc is None:
|
||||
randfunc = Random.get_random_bytes
|
||||
return PKCS115_Cipher(key, randfunc)
|
||||
@@ -1,20 +0,0 @@
|
||||
from typing import Callable, Union, Any, Optional, TypeVar
|
||||
|
||||
from Crypto.PublicKey.RSA import RsaKey
|
||||
|
||||
Buffer = Union[bytes, bytearray, memoryview]
|
||||
T = TypeVar('T')
|
||||
|
||||
class PKCS115_Cipher:
|
||||
def __init__(self,
|
||||
key: RsaKey,
|
||||
randfunc: Callable[[int], bytes]) -> None: ...
|
||||
def can_encrypt(self) -> bool: ...
|
||||
def can_decrypt(self) -> bool: ...
|
||||
def encrypt(self, message: Buffer) -> bytes: ...
|
||||
def decrypt(self, ciphertext: Buffer,
|
||||
sentinel: T,
|
||||
expected_pt_len: Optional[int] = ...) -> Union[bytes, T]: ...
|
||||
|
||||
def new(key: RsaKey,
|
||||
randfunc: Optional[Callable[[int], bytes]] = ...) -> PKCS115_Cipher: ...
|
||||
@@ -1,167 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Cipher/Salsa20.py : Salsa20 stream cipher (http://cr.yp.to/snuffle.html)
|
||||
#
|
||||
# Contributed by Fabrizio Tarizzo <fabrizio@fabriziotarizzo.org>.
|
||||
#
|
||||
# ===================================================================
|
||||
# The contents of this file are dedicated to the public domain. To
|
||||
# the extent that dedication to the public domain is not available,
|
||||
# everyone is granted a worldwide, perpetual, royalty-free,
|
||||
# non-exclusive license to exercise all rights associated with the
|
||||
# contents of this file for any purpose whatsoever.
|
||||
# No rights are reserved.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
||||
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
||||
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
# ===================================================================
|
||||
|
||||
from Crypto.Util.py3compat import _copy_bytes
|
||||
from Crypto.Util._raw_api import (load_pycryptodome_raw_lib,
|
||||
create_string_buffer,
|
||||
get_raw_buffer, VoidPointer,
|
||||
SmartPointer, c_size_t,
|
||||
c_uint8_ptr, is_writeable_buffer)
|
||||
|
||||
from Crypto.Random import get_random_bytes
|
||||
|
||||
_raw_salsa20_lib = load_pycryptodome_raw_lib("Crypto.Cipher._Salsa20",
|
||||
"""
|
||||
int Salsa20_stream_init(uint8_t *key, size_t keylen,
|
||||
uint8_t *nonce, size_t nonce_len,
|
||||
void **pSalsaState);
|
||||
int Salsa20_stream_destroy(void *salsaState);
|
||||
int Salsa20_stream_encrypt(void *salsaState,
|
||||
const uint8_t in[],
|
||||
uint8_t out[], size_t len);
|
||||
""")
|
||||
|
||||
|
||||
class Salsa20Cipher:
|
||||
"""Salsa20 cipher object. Do not create it directly. Use :py:func:`new`
|
||||
instead.
|
||||
|
||||
:var nonce: The nonce with length 8
|
||||
:vartype nonce: byte string
|
||||
"""
|
||||
|
||||
def __init__(self, key, nonce):
|
||||
"""Initialize a Salsa20 cipher object
|
||||
|
||||
See also `new()` at the module level."""
|
||||
|
||||
if len(key) not in key_size:
|
||||
raise ValueError("Incorrect key length for Salsa20 (%d bytes)" % len(key))
|
||||
|
||||
if len(nonce) != 8:
|
||||
raise ValueError("Incorrect nonce length for Salsa20 (%d bytes)" %
|
||||
len(nonce))
|
||||
|
||||
self.nonce = _copy_bytes(None, None, nonce)
|
||||
|
||||
self._state = VoidPointer()
|
||||
result = _raw_salsa20_lib.Salsa20_stream_init(
|
||||
c_uint8_ptr(key),
|
||||
c_size_t(len(key)),
|
||||
c_uint8_ptr(nonce),
|
||||
c_size_t(len(nonce)),
|
||||
self._state.address_of())
|
||||
if result:
|
||||
raise ValueError("Error %d instantiating a Salsa20 cipher")
|
||||
self._state = SmartPointer(self._state.get(),
|
||||
_raw_salsa20_lib.Salsa20_stream_destroy)
|
||||
|
||||
self.block_size = 1
|
||||
self.key_size = len(key)
|
||||
|
||||
def encrypt(self, plaintext, output=None):
|
||||
"""Encrypt a piece of data.
|
||||
|
||||
Args:
|
||||
plaintext(bytes/bytearray/memoryview): The data to encrypt, of any size.
|
||||
Keyword Args:
|
||||
output(bytes/bytearray/memoryview): The location where the ciphertext
|
||||
is written to. If ``None``, the ciphertext is returned.
|
||||
Returns:
|
||||
If ``output`` is ``None``, the ciphertext is returned as ``bytes``.
|
||||
Otherwise, ``None``.
|
||||
"""
|
||||
|
||||
if output is None:
|
||||
ciphertext = create_string_buffer(len(plaintext))
|
||||
else:
|
||||
ciphertext = output
|
||||
|
||||
if not is_writeable_buffer(output):
|
||||
raise TypeError("output must be a bytearray or a writeable memoryview")
|
||||
|
||||
if len(plaintext) != len(output):
|
||||
raise ValueError("output must have the same length as the input"
|
||||
" (%d bytes)" % len(plaintext))
|
||||
|
||||
result = _raw_salsa20_lib.Salsa20_stream_encrypt(
|
||||
self._state.get(),
|
||||
c_uint8_ptr(plaintext),
|
||||
c_uint8_ptr(ciphertext),
|
||||
c_size_t(len(plaintext)))
|
||||
if result:
|
||||
raise ValueError("Error %d while encrypting with Salsa20" % result)
|
||||
|
||||
if output is None:
|
||||
return get_raw_buffer(ciphertext)
|
||||
else:
|
||||
return None
|
||||
|
||||
def decrypt(self, ciphertext, output=None):
|
||||
"""Decrypt a piece of data.
|
||||
|
||||
Args:
|
||||
ciphertext(bytes/bytearray/memoryview): The data to decrypt, of any size.
|
||||
Keyword Args:
|
||||
output(bytes/bytearray/memoryview): The location where the plaintext
|
||||
is written to. If ``None``, the plaintext is returned.
|
||||
Returns:
|
||||
If ``output`` is ``None``, the plaintext is returned as ``bytes``.
|
||||
Otherwise, ``None``.
|
||||
"""
|
||||
|
||||
try:
|
||||
return self.encrypt(ciphertext, output=output)
|
||||
except ValueError as e:
|
||||
raise ValueError(str(e).replace("enc", "dec"))
|
||||
|
||||
|
||||
def new(key, nonce=None):
|
||||
"""Create a new Salsa20 cipher
|
||||
|
||||
:keyword key: The secret key to use. It must be 16 or 32 bytes long.
|
||||
:type key: bytes/bytearray/memoryview
|
||||
|
||||
:keyword nonce:
|
||||
A value that must never be reused for any other encryption
|
||||
done with this key. It must be 8 bytes long.
|
||||
|
||||
If not provided, a random byte string will be generated (you can read
|
||||
it back via the ``nonce`` attribute of the returned object).
|
||||
:type nonce: bytes/bytearray/memoryview
|
||||
|
||||
:Return: a :class:`Crypto.Cipher.Salsa20.Salsa20Cipher` object
|
||||
"""
|
||||
|
||||
if nonce is None:
|
||||
nonce = get_random_bytes(8)
|
||||
|
||||
return Salsa20Cipher(key, nonce)
|
||||
|
||||
# Size of a data block (in bytes)
|
||||
block_size = 1
|
||||
|
||||
# Size of a key (in bytes)
|
||||
key_size = (16, 32)
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
from typing import Union, Tuple, Optional, overload, Optional
|
||||
|
||||
Buffer = bytes|bytearray|memoryview
|
||||
|
||||
class Salsa20Cipher:
|
||||
nonce: bytes
|
||||
block_size: int
|
||||
key_size: int
|
||||
|
||||
def __init__(self,
|
||||
key: Buffer,
|
||||
nonce: Buffer) -> None: ...
|
||||
@overload
|
||||
def encrypt(self, plaintext: Buffer) -> bytes: ...
|
||||
@overload
|
||||
def encrypt(self, plaintext: Buffer, output: Union[bytearray, memoryview]) -> None: ...
|
||||
@overload
|
||||
def decrypt(self, plaintext: Buffer) -> bytes: ...
|
||||
@overload
|
||||
def decrypt(self, plaintext: Buffer, output: Union[bytearray, memoryview]) -> None: ...
|
||||
|
||||
def new(key: Buffer, nonce: Optional[Buffer] = ...) -> Salsa20Cipher: ...
|
||||
|
||||
block_size: int
|
||||
key_size: Tuple[int, int]
|
||||
|
||||
Binary file not shown.
@@ -1,131 +0,0 @@
|
||||
# ===================================================================
|
||||
#
|
||||
# Copyright (c) 2019, Legrandin <helderijs@gmail.com>
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions
|
||||
# are met:
|
||||
#
|
||||
# 1. Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# 2. Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in
|
||||
# the documentation and/or other materials provided with the
|
||||
# distribution.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
||||
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||
# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
|
||||
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
# ===================================================================
|
||||
|
||||
import sys
|
||||
|
||||
from Crypto.Cipher import _create_cipher
|
||||
from Crypto.Util._raw_api import (load_pycryptodome_raw_lib,
|
||||
VoidPointer, SmartPointer, c_size_t,
|
||||
c_uint8_ptr, c_uint)
|
||||
|
||||
_raw_blowfish_lib = load_pycryptodome_raw_lib(
|
||||
"Crypto.Cipher._raw_eksblowfish",
|
||||
"""
|
||||
int EKSBlowfish_start_operation(const uint8_t key[],
|
||||
size_t key_len,
|
||||
const uint8_t salt[16],
|
||||
size_t salt_len,
|
||||
unsigned cost,
|
||||
unsigned invert,
|
||||
void **pResult);
|
||||
int EKSBlowfish_encrypt(const void *state,
|
||||
const uint8_t *in,
|
||||
uint8_t *out,
|
||||
size_t data_len);
|
||||
int EKSBlowfish_decrypt(const void *state,
|
||||
const uint8_t *in,
|
||||
uint8_t *out,
|
||||
size_t data_len);
|
||||
int EKSBlowfish_stop_operation(void *state);
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def _create_base_cipher(dict_parameters):
|
||||
"""This method instantiates and returns a smart pointer to
|
||||
a low-level base cipher. It will absorb named parameters in
|
||||
the process."""
|
||||
|
||||
try:
|
||||
key = dict_parameters.pop("key")
|
||||
salt = dict_parameters.pop("salt")
|
||||
cost = dict_parameters.pop("cost")
|
||||
except KeyError as e:
|
||||
raise TypeError("Missing EKSBlowfish parameter: " + str(e))
|
||||
invert = dict_parameters.pop("invert", True)
|
||||
|
||||
if len(key) not in key_size:
|
||||
raise ValueError("Incorrect EKSBlowfish key length (%d bytes)" % len(key))
|
||||
|
||||
start_operation = _raw_blowfish_lib.EKSBlowfish_start_operation
|
||||
stop_operation = _raw_blowfish_lib.EKSBlowfish_stop_operation
|
||||
|
||||
void_p = VoidPointer()
|
||||
result = start_operation(c_uint8_ptr(key),
|
||||
c_size_t(len(key)),
|
||||
c_uint8_ptr(salt),
|
||||
c_size_t(len(salt)),
|
||||
c_uint(cost),
|
||||
c_uint(int(invert)),
|
||||
void_p.address_of())
|
||||
if result:
|
||||
raise ValueError("Error %X while instantiating the EKSBlowfish cipher"
|
||||
% result)
|
||||
return SmartPointer(void_p.get(), stop_operation)
|
||||
|
||||
|
||||
def new(key, mode, salt, cost, invert):
|
||||
"""Create a new EKSBlowfish cipher
|
||||
|
||||
Args:
|
||||
|
||||
key (bytes, bytearray, memoryview):
|
||||
The secret key to use in the symmetric cipher.
|
||||
Its length can vary from 0 to 72 bytes.
|
||||
|
||||
mode (one of the supported ``MODE_*`` constants):
|
||||
The chaining mode to use for encryption or decryption.
|
||||
|
||||
salt (bytes, bytearray, memoryview):
|
||||
The salt that bcrypt uses to thwart rainbow table attacks
|
||||
|
||||
cost (integer):
|
||||
The complexity factor in bcrypt
|
||||
|
||||
invert (bool):
|
||||
If ``False``, in the inner loop use ``ExpandKey`` first over the salt
|
||||
and then over the key, as defined in
|
||||
the `original bcrypt specification <https://www.usenix.org/legacy/events/usenix99/provos/provos_html/node4.html>`_.
|
||||
If ``True``, reverse the order, as in the first implementation of
|
||||
`bcrypt` in OpenBSD.
|
||||
|
||||
:Return: an EKSBlowfish object
|
||||
"""
|
||||
|
||||
kwargs = { 'salt':salt, 'cost':cost, 'invert':invert }
|
||||
return _create_cipher(sys.modules[__name__], key, mode, **kwargs)
|
||||
|
||||
|
||||
MODE_ECB = 1
|
||||
|
||||
# Size of a data block (in bytes)
|
||||
block_size = 8
|
||||
# Size of a key (in bytes)
|
||||
key_size = range(0, 72 + 1)
|
||||
@@ -1,15 +0,0 @@
|
||||
from typing import Union, Iterable
|
||||
|
||||
from Crypto.Cipher._mode_ecb import EcbMode
|
||||
|
||||
MODE_ECB: int
|
||||
|
||||
Buffer = Union[bytes, bytearray, memoryview]
|
||||
|
||||
def new(key: Buffer,
|
||||
mode: int,
|
||||
salt: Buffer,
|
||||
cost: int) -> EcbMode: ...
|
||||
|
||||
block_size: int
|
||||
key_size: Iterable[int]
|
||||
Binary file not shown.
@@ -1,91 +0,0 @@
|
||||
#
|
||||
# A block cipher is instantiated as a combination of:
|
||||
# 1. A base cipher (such as AES)
|
||||
# 2. A mode of operation (such as CBC)
|
||||
#
|
||||
# Both items are implemented as C modules.
|
||||
#
|
||||
# The API of #1 is (replace "AES" with the name of the actual cipher):
|
||||
# - AES_start_operaion(key) --> base_cipher_state
|
||||
# - AES_encrypt(base_cipher_state, in, out, length)
|
||||
# - AES_decrypt(base_cipher_state, in, out, length)
|
||||
# - AES_stop_operation(base_cipher_state)
|
||||
#
|
||||
# Where base_cipher_state is AES_State, a struct with BlockBase (set of
|
||||
# pointers to encrypt/decrypt/stop) followed by cipher-specific data.
|
||||
#
|
||||
# The API of #2 is (replace "CBC" with the name of the actual mode):
|
||||
# - CBC_start_operation(base_cipher_state) --> mode_state
|
||||
# - CBC_encrypt(mode_state, in, out, length)
|
||||
# - CBC_decrypt(mode_state, in, out, length)
|
||||
# - CBC_stop_operation(mode_state)
|
||||
#
|
||||
# where mode_state is a a pointer to base_cipher_state plus mode-specific data.
|
||||
|
||||
def _create_cipher(factory, key, mode, *args, **kwargs):
|
||||
|
||||
kwargs["key"] = key
|
||||
|
||||
if args:
|
||||
if mode in (8, 9, 10, 11, 12):
|
||||
if len(args) > 1:
|
||||
raise TypeError("Too many arguments for this mode")
|
||||
kwargs["nonce"] = args[0]
|
||||
elif mode in (2, 3, 5, 7):
|
||||
if len(args) > 1:
|
||||
raise TypeError("Too many arguments for this mode")
|
||||
kwargs["IV"] = args[0]
|
||||
elif mode == 6:
|
||||
if len(args) > 0:
|
||||
raise TypeError("Too many arguments for this mode")
|
||||
elif mode == 1:
|
||||
raise TypeError("IV is not meaningful for the ECB mode")
|
||||
|
||||
res = None
|
||||
extra_modes = kwargs.pop("add_aes_modes", False)
|
||||
|
||||
if mode == 1:
|
||||
from Crypto.Cipher._mode_ecb import _create_ecb_cipher
|
||||
res = _create_ecb_cipher(factory, **kwargs)
|
||||
elif mode == 2:
|
||||
from Crypto.Cipher._mode_cbc import _create_cbc_cipher
|
||||
res = _create_cbc_cipher(factory, **kwargs)
|
||||
elif mode == 3:
|
||||
from Crypto.Cipher._mode_cfb import _create_cfb_cipher
|
||||
res = _create_cfb_cipher(factory, **kwargs)
|
||||
elif mode == 5:
|
||||
from Crypto.Cipher._mode_ofb import _create_ofb_cipher
|
||||
res = _create_ofb_cipher(factory, **kwargs)
|
||||
elif mode == 6:
|
||||
from Crypto.Cipher._mode_ctr import _create_ctr_cipher
|
||||
res = _create_ctr_cipher(factory, **kwargs)
|
||||
elif mode == 7:
|
||||
from Crypto.Cipher._mode_openpgp import _create_openpgp_cipher
|
||||
res = _create_openpgp_cipher(factory, **kwargs)
|
||||
elif mode == 9:
|
||||
from Crypto.Cipher._mode_eax import _create_eax_cipher
|
||||
res = _create_eax_cipher(factory, **kwargs)
|
||||
elif extra_modes:
|
||||
if mode == 8:
|
||||
from Crypto.Cipher._mode_ccm import _create_ccm_cipher
|
||||
res = _create_ccm_cipher(factory, **kwargs)
|
||||
elif mode == 10:
|
||||
from Crypto.Cipher._mode_siv import _create_siv_cipher
|
||||
res = _create_siv_cipher(factory, **kwargs)
|
||||
elif mode == 11:
|
||||
from Crypto.Cipher._mode_gcm import _create_gcm_cipher
|
||||
res = _create_gcm_cipher(factory, **kwargs)
|
||||
elif mode == 12:
|
||||
from Crypto.Cipher._mode_ocb import _create_ocb_cipher
|
||||
res = _create_ocb_cipher(factory, **kwargs)
|
||||
elif mode == 13:
|
||||
from Crypto.Cipher._mode_kw import _create_kw_cipher
|
||||
res = _create_kw_cipher(factory, **kwargs)
|
||||
elif mode == 14:
|
||||
from Crypto.Cipher._mode_kwp import _create_kwp_cipher
|
||||
res = _create_kwp_cipher(factory, **kwargs)
|
||||
|
||||
if res is None:
|
||||
raise ValueError("Mode not supported")
|
||||
|
||||
return res
|
||||
Whitespace-only changes.
Binary file not shown.
Loaded 100 of 4824 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user