labull-framework 1.0.2__tar.gz → 1.0.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {labull_framework-1.0.2/labull_framework.egg-info → labull_framework-1.0.4}/PKG-INFO +1 -1
- {labull_framework-1.0.2 → labull_framework-1.0.4}/labull_framework/magic_block/export.py +76 -13
- {labull_framework-1.0.2 → labull_framework-1.0.4}/labull_framework/magic_block/parallel.py +35 -4
- {labull_framework-1.0.2 → labull_framework-1.0.4}/labull_framework/magic_block/views.py +95 -0
- {labull_framework-1.0.2 → labull_framework-1.0.4/labull_framework.egg-info}/PKG-INFO +1 -1
- {labull_framework-1.0.2 → labull_framework-1.0.4}/pyproject.toml +1 -1
- {labull_framework-1.0.2 → labull_framework-1.0.4}/LICENSE +0 -0
- {labull_framework-1.0.2 → labull_framework-1.0.4}/MANIFEST.in +0 -0
- {labull_framework-1.0.2 → labull_framework-1.0.4}/README-pypi.md +0 -0
- {labull_framework-1.0.2 → labull_framework-1.0.4}/labull_framework/__init__.py +0 -0
- {labull_framework-1.0.2 → labull_framework-1.0.4}/labull_framework/admin.py +0 -0
- {labull_framework-1.0.2 → labull_framework-1.0.4}/labull_framework/apps.py +0 -0
- {labull_framework-1.0.2 → labull_framework-1.0.4}/labull_framework/auth/__init__.py +0 -0
- {labull_framework-1.0.2 → labull_framework-1.0.4}/labull_framework/auth/backend.py +0 -0
- {labull_framework-1.0.2 → labull_framework-1.0.4}/labull_framework/auth/bootstrap.py +0 -0
- {labull_framework-1.0.2 → labull_framework-1.0.4}/labull_framework/auth/sso.py +0 -0
- {labull_framework-1.0.2 → labull_framework-1.0.4}/labull_framework/auth/views.py +0 -0
- {labull_framework-1.0.2 → labull_framework-1.0.4}/labull_framework/canvas/__init__.py +0 -0
- {labull_framework-1.0.2 → labull_framework-1.0.4}/labull_framework/canvas/data.py +0 -0
- {labull_framework-1.0.2 → labull_framework-1.0.4}/labull_framework/canvas/gitinfo.py +0 -0
- {labull_framework-1.0.2 → labull_framework-1.0.4}/labull_framework/canvas/shape_ast.py +0 -0
- {labull_framework-1.0.2 → labull_framework-1.0.4}/labull_framework/canvas/static/canvas.css +0 -0
- {labull_framework-1.0.2 → labull_framework-1.0.4}/labull_framework/canvas/static/canvas.js +0 -0
- {labull_framework-1.0.2 → labull_framework-1.0.4}/labull_framework/canvas/static/page.html +0 -0
- {labull_framework-1.0.2 → labull_framework-1.0.4}/labull_framework/canvas/views.py +0 -0
- {labull_framework-1.0.2 → labull_framework-1.0.4}/labull_framework/env.py +0 -0
- {labull_framework-1.0.2 → labull_framework-1.0.4}/labull_framework/jsonio.py +0 -0
- {labull_framework-1.0.2 → labull_framework-1.0.4}/labull_framework/magic_block/__init__.py +0 -0
- {labull_framework-1.0.2 → labull_framework-1.0.4}/labull_framework/magic_block/blockpackage.py +0 -0
- {labull_framework-1.0.2 → labull_framework-1.0.4}/labull_framework/magic_block/deepcheck.py +0 -0
- {labull_framework-1.0.2 → labull_framework-1.0.4}/labull_framework/magic_block/devkit.py +0 -0
- {labull_framework-1.0.2 → labull_framework-1.0.4}/labull_framework/magic_block/dispatch.py +0 -0
- {labull_framework-1.0.2 → labull_framework-1.0.4}/labull_framework/magic_block/health.py +0 -0
- {labull_framework-1.0.2 → labull_framework-1.0.4}/labull_framework/magic_block/registry.py +0 -0
- {labull_framework-1.0.2 → labull_framework-1.0.4}/labull_framework/magic_block/sdk.py +0 -0
- {labull_framework-1.0.2 → labull_framework-1.0.4}/labull_framework/magic_block/service.py +0 -0
- {labull_framework-1.0.2 → labull_framework-1.0.4}/labull_framework/magic_block/subproc.py +0 -0
- {labull_framework-1.0.2 → labull_framework-1.0.4}/labull_framework/magic_block/validate.py +0 -0
- {labull_framework-1.0.2 → labull_framework-1.0.4}/labull_framework/management/__init__.py +0 -0
- {labull_framework-1.0.2 → labull_framework-1.0.4}/labull_framework/management/commands/__init__.py +0 -0
- {labull_framework-1.0.2 → labull_framework-1.0.4}/labull_framework/management/commands/labull_contract.py +0 -0
- {labull_framework-1.0.2 → labull_framework-1.0.4}/labull_framework/management/commands/labull_requires.py +0 -0
- {labull_framework-1.0.2 → labull_framework-1.0.4}/labull_framework/migrations/0001_initial.py +0 -0
- {labull_framework-1.0.2 → labull_framework-1.0.4}/labull_framework/migrations/0002_remove_magicblock_prev_files_and_more.py +0 -0
- {labull_framework-1.0.2 → labull_framework-1.0.4}/labull_framework/migrations/0003_magicblock_tags.py +0 -0
- {labull_framework-1.0.2 → labull_framework-1.0.4}/labull_framework/migrations/0004_magicblock_detail.py +0 -0
- {labull_framework-1.0.2 → labull_framework-1.0.4}/labull_framework/migrations/__init__.py +0 -0
- {labull_framework-1.0.2 → labull_framework-1.0.4}/labull_framework/models.py +0 -0
- {labull_framework-1.0.2 → labull_framework-1.0.4}/labull_framework/settings.py +0 -0
- {labull_framework-1.0.2 → labull_framework-1.0.4}/labull_framework/urls.py +0 -0
- {labull_framework-1.0.2 → labull_framework-1.0.4}/labull_framework/virtual.py +0 -0
- {labull_framework-1.0.2 → labull_framework-1.0.4}/labull_framework.egg-info/SOURCES.txt +0 -0
- {labull_framework-1.0.2 → labull_framework-1.0.4}/labull_framework.egg-info/dependency_links.txt +0 -0
- {labull_framework-1.0.2 → labull_framework-1.0.4}/labull_framework.egg-info/requires.txt +0 -0
- {labull_framework-1.0.2 → labull_framework-1.0.4}/labull_framework.egg-info/top_level.txt +0 -0
- {labull_framework-1.0.2 → labull_framework-1.0.4}/setup.cfg +0 -0
|
@@ -24,8 +24,9 @@ from __future__ import annotations
|
|
|
24
24
|
|
|
25
25
|
import os
|
|
26
26
|
import re
|
|
27
|
+
import threading
|
|
27
28
|
from pathlib import Path
|
|
28
|
-
from typing import Any, Iterable
|
|
29
|
+
from typing import Any, Callable, Iterable
|
|
29
30
|
|
|
30
31
|
from . import devkit, parallel, validate
|
|
31
32
|
from .sdk import (
|
|
@@ -611,6 +612,12 @@ def _starter_manifest(row: Any) -> str:
|
|
|
611
612
|
# ──────────────────────────── 回:目录 → 逐块对比(两种入口,一套判据) ────────────────────────────
|
|
612
613
|
|
|
613
614
|
|
|
615
|
+
#: 值得跑一遍"保存那套判据"的两种状态:**磁盘上那份与库里不一样**(`modified`),或者库里还没有
|
|
616
|
+
#: 这一行(`not-in-db`)。`unchanged` 的**不跑** —— 库里那份就是它,而它当初保存时已经判过;
|
|
617
|
+
#: 再判一遍纯白付,而这一步正是读回最贵的地方(见 `_judge()` 里那段说明)。
|
|
618
|
+
CHECKED_STATUSES = ('modified', 'not-in-db')
|
|
619
|
+
|
|
620
|
+
|
|
614
621
|
class _Collect:
|
|
615
622
|
"""一次扫描攒下来的东西。
|
|
616
623
|
|
|
@@ -628,7 +635,11 @@ class _Collect:
|
|
|
628
635
|
self.skipped_folders: list[str] = []
|
|
629
636
|
|
|
630
637
|
|
|
631
|
-
def scan_folders(
|
|
638
|
+
def scan_folders(
|
|
639
|
+
upload: dict,
|
|
640
|
+
include_missing: bool = False,
|
|
641
|
+
progress: Callable[[dict], None] | None = None,
|
|
642
|
+
) -> dict:
|
|
632
643
|
"""**浏览器扫到的**那个目录,与库里的块逐块比一遍(只读,不写库)。
|
|
633
644
|
|
|
634
645
|
:param upload: `{'root'?, 'folders': [{'where', 'files'}], 'failures'?, 'truncated'?}`
|
|
@@ -666,16 +677,21 @@ def scan_folders(upload: dict, include_missing: bool = False) -> dict:
|
|
|
666
677
|
files = {str(name): str(body) for name, body in raw.items()} if isinstance(raw, dict) else {}
|
|
667
678
|
collect.files += len(files)
|
|
668
679
|
collect.folders.append({'abs': where, 'where': where, 'segments': segments, 'files': files})
|
|
669
|
-
return _judge(str(given.get('root') or ''), collect, include_missing)
|
|
680
|
+
return _judge(str(given.get('root') or ''), collect, include_missing, progress)
|
|
670
681
|
|
|
671
682
|
|
|
672
|
-
def read_dir(
|
|
683
|
+
def read_dir(
|
|
684
|
+
path: str,
|
|
685
|
+
include_missing: bool = False,
|
|
686
|
+
progress: Callable[[dict], None] | None = None,
|
|
687
|
+
) -> dict:
|
|
673
688
|
"""读一个**服务器上的**目录,与库里的块**逐块比一遍**(只读,不写库)。
|
|
674
689
|
|
|
675
690
|
⚠️ 控制台那条路走 `scan_folders()`(开发目录在用户自己机器上);这一条留给
|
|
676
691
|
"源码本来就在服务器上"的用法(CLI / 脚本)。两条入口的判断完全一样。
|
|
677
692
|
:param include_missing: 默认 `False` —— 路径往往只指到一个块或一个模块的文件夹,
|
|
678
693
|
"没扫到"不等于"要删掉",默认列出来只会误导。
|
|
694
|
+
:param progress: 进度回调(可选),形状见 `_judge()`。
|
|
679
695
|
"""
|
|
680
696
|
given = str(path or '').strip()
|
|
681
697
|
collect = _Collect()
|
|
@@ -693,7 +709,7 @@ def read_dir(path: str, include_missing: bool = False) -> dict:
|
|
|
693
709
|
return _result(str(root), collect, [], [])
|
|
694
710
|
|
|
695
711
|
_discover(root, collect, [], 0)
|
|
696
|
-
return _judge(_posix(str(root)), collect, include_missing)
|
|
712
|
+
return _judge(_posix(str(root)), collect, include_missing, progress)
|
|
697
713
|
|
|
698
714
|
|
|
699
715
|
def _new_block(disk: dict) -> dict:
|
|
@@ -738,8 +754,21 @@ def _new_block(disk: dict) -> dict:
|
|
|
738
754
|
}
|
|
739
755
|
|
|
740
756
|
|
|
741
|
-
def _judge(
|
|
742
|
-
|
|
757
|
+
def _judge(
|
|
758
|
+
root: str,
|
|
759
|
+
collect: _Collect,
|
|
760
|
+
include_missing: bool,
|
|
761
|
+
progress: Callable[[dict], None] | None = None,
|
|
762
|
+
) -> dict:
|
|
763
|
+
"""配对 + 逐文件比 + 提前算"能不能存"(**两种读入口共用这一处**)。
|
|
764
|
+
|
|
765
|
+
:param progress: 进度回调(可选)。**只在流式读回那条口上有值**(见 `views.read_view`):
|
|
766
|
+
它会收到 `{'phase': 'judged', 'blocks', 'checks'}` 与每个块算完时的
|
|
767
|
+
`{'phase': 'check', 'done', 'total', 'id', 'name', 'ok'}` —— 前端据此画进度条,
|
|
768
|
+
也因为**字节一直在往回走**,网关不会因为"半天没有回话"把整条请求掐成 504。
|
|
769
|
+
🔴 回调在**池子的线程里**被调用(见 `parallel.map_blocks` 那条边界):
|
|
770
|
+
只许做"记下来"这种线程安全的事,**不许碰数据库**。所以这里传出去的是**纯数据**。
|
|
771
|
+
"""
|
|
743
772
|
pool = list(_model().objects.all())
|
|
744
773
|
matched, orphan = _assign(collect.folders, pool)
|
|
745
774
|
used = {pool[index].id: collect.folders[disk]['where'] for disk, index in matched.items()}
|
|
@@ -783,13 +812,47 @@ def _judge(root: str, collect: _Collect, include_missing: bool) -> dict:
|
|
|
783
812
|
)
|
|
784
813
|
|
|
785
814
|
# 🔴 每条 diff 上用**保存那套判据**提前算结论("扫描说能存、真存又不过"是同一件事的两个说法)。
|
|
786
|
-
# 🔴 这一段是整条读回里**最贵的一步**(每块要起两个子进程:试加载 + 引用深检,实测 0.83s
|
|
787
|
-
#
|
|
815
|
+
# 🔴 这一段是整条读回里**最贵的一步**(每块要起两个子进程:试加载 + 引用深检,实测 0.83s/块;
|
|
816
|
+
# 真实项目里块大一些,5 秒/块也常见)⇒ 并发算、再按原顺序贴回那几条 diff 上。
|
|
788
817
|
# ⚠️ 池子里跑的是**纯函数**(`validate.check_block` 不碰数据库),写库/改对象全在这条线程上。
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
818
|
+
#
|
|
819
|
+
# 🔴 **只校验"值得校验"的那两种状态**(`modified` / `not-in-db`),`unchanged` 的一律跳过。
|
|
820
|
+
# 为什么(这条是 504 那个问题的正路,量出来的):
|
|
821
|
+
# · `unchanged` = 磁盘上那一包与库里那一份**逐字节相同**。库里那一份是**当初保存时**
|
|
822
|
+
# 过过同一套判据的(保存那条路一个字都没松),再跑一遍不会得出别的结论 —— 纯白付;
|
|
823
|
+
# · 而这一段的代价并不小:8 个块(全是没改动的)量到 **5.8 秒**、每次都真起 16 个子进程
|
|
824
|
+
# (`scratch/probe/read-bench.py` 那一趟)。一个真实的开发目录 23 个块里通常只有 8 个改过
|
|
825
|
+
# ⇒ 过去有 **2/3 的时间在重复校验没动过的东西**,而线上 gunicorn/nginx/ALB 都在 60 秒那道线上。
|
|
826
|
+
# · 跳过它**不会**让错误溜进库:真有人手动勾上一个 `unchanged` 的块去存,`write_dir()` →
|
|
827
|
+
# `service.save()` 会**再判一次**(判据只有那一处),失败照旧逐条报出来。
|
|
828
|
+
# ⚠️ 代价是有意接受的:读回的清单里 `unchanged` 那几条不再显示"不能存:…" —— 没改动的块
|
|
829
|
+
# 本来就不该在这一趟里替它做体检(它上一次保存时已经做过)。
|
|
830
|
+
targets = [
|
|
831
|
+
item for item in blocks
|
|
832
|
+
if item.get('canApply', False) and item['files'] and item['status'] in CHECKED_STATUSES
|
|
833
|
+
]
|
|
834
|
+
if progress is not None:
|
|
835
|
+
# 配对与逐文件比已经做完(这一段是便宜的):先把"要校验几个"告诉调用方,
|
|
836
|
+
# 后面的进度才有分母。⚠️ 这一步在**调用线程**里跑,可以放心用。
|
|
837
|
+
progress({'phase': 'judged', 'blocks': len(blocks), 'checks': len(targets)})
|
|
838
|
+
done = {'n': 0}
|
|
839
|
+
done_lock = threading.Lock()
|
|
840
|
+
|
|
841
|
+
def checked(item: dict) -> list[str]:
|
|
842
|
+
messages = [message for _stage, message in validate.check_block(item['files'])]
|
|
843
|
+
if progress is not None:
|
|
844
|
+
# 🔴 只报"第几个算完了",结论仍由调用线程按原顺序贴回(顺序是契约)。
|
|
845
|
+
with done_lock:
|
|
846
|
+
done['n'] += 1
|
|
847
|
+
sofar = done['n']
|
|
848
|
+
progress({
|
|
849
|
+
'phase': 'check', 'done': sofar, 'total': len(targets),
|
|
850
|
+
'id': str(item.get('id') or ''), 'name': str(item.get('name') or ''),
|
|
851
|
+
'ok': not messages,
|
|
852
|
+
})
|
|
853
|
+
return messages
|
|
854
|
+
|
|
855
|
+
messages_of = parallel.map_blocks(targets, checked)
|
|
793
856
|
for item, messages in zip(targets, messages_of):
|
|
794
857
|
if messages:
|
|
795
858
|
item['canApply'] = False
|
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
· `export._judge()` —— 并发算逐块校验结论,再顺序贴回那几条 diff;
|
|
14
14
|
· `health.check_all_blocks()` —— 并发算结论,再顺序写那四列;
|
|
15
15
|
· `export.write_dir()` —— 只并发"预热"校验结果,保存本身仍在调用线程里(见那里的说明)。
|
|
16
|
+
⚠️ `map_blocks(..., on_done=...)` 那个**进度回调**也在池子的线程里跑(`export._judge()` 用它
|
|
17
|
+
给"流式读回"报进度)—— 所以它同样只许做"记下来"这种线程安全的事,**不许碰数据库**。
|
|
16
18
|
|
|
17
19
|
🔴 **为什么要有上限**:一次全量体检 = N 个块 × 2 个子进程,每个子进程都要跑一遍 `django.setup()`。
|
|
18
20
|
不限并发的话,20 个块就能同时拉起几十个 Python 进程:内存与 CPU 一起打满,
|
|
@@ -89,7 +91,11 @@ def _settings_count() -> Any:
|
|
|
89
91
|
return None
|
|
90
92
|
|
|
91
93
|
|
|
92
|
-
def map_blocks(
|
|
94
|
+
def map_blocks(
|
|
95
|
+
items: Iterable[T],
|
|
96
|
+
worker: Callable[[T], R],
|
|
97
|
+
on_done: Callable[[int, T, R], None] | None = None,
|
|
98
|
+
) -> list[R]:
|
|
93
99
|
"""把 `worker` 铺到每一项上,**结果与入参一一对应(原顺序)**。
|
|
94
100
|
|
|
95
101
|
🔴 **顺序是契约**:调用方拿回去是"第几个块 → 哪一条结论",错位就是把 A 的结论贴到 B 上。
|
|
@@ -99,13 +105,38 @@ def map_blocks(items: Iterable[T], worker: Callable[[T], R]) -> list[R]:
|
|
|
99
105
|
而控制台里一大半操作都是单个块),顺便也让"串行那一套"仍有一条现成的退路。
|
|
100
106
|
🔴 `worker` 抛异常就**照原样抛**(与串行循环同一种行为:一个块炸了不该被悄悄吞掉)。
|
|
101
107
|
要"一个块失败不拖累别的块",由 worker 自己接住 —— `health.evaluate()` 就是这么写的。
|
|
108
|
+
🔴 `on_done(index, item, result)`:**每算完一个就报一次**(进度条 / 流式回话用它)。
|
|
109
|
+
它在**池子的线程里**被调用(串行那一路就在调用线程里),所以只能做"把一件事记下来"
|
|
110
|
+
这种线程安全的动作(`queue.Queue.put` / 计数)—— 照样**不许碰数据库**,与 worker 同一条边界。
|
|
111
|
+
传了它就一定会在**每一项**上都调用一次(异常那一路不调用:那一项没有结果可报)。
|
|
102
112
|
"""
|
|
103
113
|
rows = list(items)
|
|
104
114
|
if len(rows) <= 1:
|
|
105
|
-
|
|
115
|
+
out: list[R] = []
|
|
116
|
+
for index, item in enumerate(rows):
|
|
117
|
+
result = worker(item)
|
|
118
|
+
out.append(result)
|
|
119
|
+
if on_done is not None:
|
|
120
|
+
on_done(index, item, result)
|
|
121
|
+
return out
|
|
106
122
|
size = min(worker_count(), len(rows))
|
|
107
123
|
if size <= 1:
|
|
108
|
-
|
|
124
|
+
out = []
|
|
125
|
+
for index, item in enumerate(rows):
|
|
126
|
+
result = worker(item)
|
|
127
|
+
out.append(result)
|
|
128
|
+
if on_done is not None:
|
|
129
|
+
on_done(index, item, result)
|
|
130
|
+
return out
|
|
131
|
+
numbered = list(enumerate(rows))
|
|
132
|
+
|
|
133
|
+
def run_task(pair: tuple[int, T]) -> R:
|
|
134
|
+
index, item = pair
|
|
135
|
+
result = worker(item)
|
|
136
|
+
if on_done is not None:
|
|
137
|
+
on_done(index, item, result)
|
|
138
|
+
return result
|
|
139
|
+
|
|
109
140
|
# ⚠️ `list(...)` 要写在 `with` 里面:`map()` 是惰性的,出了 `with` 池子已经关了。
|
|
110
141
|
with ThreadPoolExecutor(max_workers=size, thread_name_prefix='labull-block') as pool:
|
|
111
|
-
return list(pool.map(
|
|
142
|
+
return list(pool.map(run_task, numbered))
|
|
@@ -523,6 +523,16 @@ def read_view(request: HttpRequest) -> HttpResponse:
|
|
|
523
523
|
开发目录在用户自己机器上(导出就铺在那里),后端读不到它:由浏览器照导出报文里的
|
|
524
524
|
`rules` 扫一遍,把"我扫到了什么"递上来(形状见 `frontend/magic-block/src/localfs.ts`)。
|
|
525
525
|
· `{'path': '<服务器上的绝对路径>'}` —— 后端自己读那个目录(源码本来就在服务器上时用)。
|
|
526
|
+
|
|
527
|
+
🔴 **`stream: true` ⇒ 一行一个 JSON(NDJSON),边算边回**(1.0.3 起):
|
|
528
|
+
这一条口一次要把整个目录收下来、再**逐块**做保存前那套判据(试加载 + 引用深检,
|
|
529
|
+
实测 0.8~3.7 秒/块)。块一多,整条请求就要几十秒"一声不响" —— 前置网关(nginx
|
|
530
|
+
`proxy_read_timeout` 默认 60 秒,很多云网关只给 30 秒)等不到任何字节就把它掐成
|
|
531
|
+
**504**,前端只看到一句 `请求失败:504 /magic-blocks/read`,而真正的原因
|
|
532
|
+
("我在算第 7 个块")一个字都传不出来。流式之后**字节一直在往回走**,网关不会掐,
|
|
533
|
+
前端也能画出进度条。
|
|
534
|
+
⚠️ 老前端不发这个键 ⇒ 走原来的整份 JSON(那条路一个字都没改)。
|
|
535
|
+
⚠️ 这一层只加"怎么回话",判据仍然是 `_judge()` 那一处。
|
|
526
536
|
"""
|
|
527
537
|
refused = _method(request, 'POST')
|
|
528
538
|
if refused is not None:
|
|
@@ -530,11 +540,96 @@ def read_view(request: HttpRequest) -> HttpResponse:
|
|
|
530
540
|
from .export import read_dir, scan_folders
|
|
531
541
|
|
|
532
542
|
body = _body(request)
|
|
543
|
+
if body.get('stream') is True:
|
|
544
|
+
return _read_stream(body)
|
|
533
545
|
if isinstance(body.get('folders'), list):
|
|
534
546
|
return _json(scan_folders(body, include_missing=body.get('includeMissing') is True))
|
|
535
547
|
return _json(read_dir(str(body.get('path') or ''), include_missing=body.get('includeMissing') is True))
|
|
536
548
|
|
|
537
549
|
|
|
550
|
+
def _line(event: dict) -> bytes:
|
|
551
|
+
"""一行 NDJSON(`ensure_ascii=False`:中文原样过去,前端不用再解一层)。"""
|
|
552
|
+
return (json.dumps(event, ensure_ascii=False) + '\n').encode('utf-8')
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
def _read_stream(body: dict) -> HttpResponse:
|
|
556
|
+
"""读回那一趟的**流式**回话(形状见 `read_view` 那段说明)。
|
|
557
|
+
|
|
558
|
+
事件(一行一个 JSON 对象):
|
|
559
|
+
· `{'type': 'start', 'folders', 'files'}` —— 立刻发,先说清"这一趟要处理多少";
|
|
560
|
+
· `{'type': 'judged', 'blocks', 'checks'}` —— 配对与逐文件比完,后面要校验几个;
|
|
561
|
+
· `{'type': 'check', 'done', 'total', 'id', 'name', 'ok'}` —— 每校验完一个块就发一条;
|
|
562
|
+
· `{'type': 'ping', 'seconds'}` —— 10 秒没有别的事就发一次心跳(**保活**用);
|
|
563
|
+
· `{'type': 'result', 'result': {...}}` —— 整份结果(形状与整份 JSON 那条口**逐字相同**);
|
|
564
|
+
· `{'type': 'error', 'message'}` —— 后端炸了(这一趟没有结果)。
|
|
565
|
+
🔴 干活的那一半在**后台线程**里跑:主线程(响应的生成器)只负责把队列里的东西写出去。
|
|
566
|
+
不这么写的话,`_judge()` 要等全部算完才返回,那"边算边回"就无从谈起。
|
|
567
|
+
⚠️ 后台线程会自己开一条数据库连接(Django 的连接是每线程一份),所以 `finally` 里
|
|
568
|
+
`connections.close_all()` 收掉它 —— 不然每读回一次就漏一条连接。
|
|
569
|
+
"""
|
|
570
|
+
import queue
|
|
571
|
+
import threading
|
|
572
|
+
import time
|
|
573
|
+
|
|
574
|
+
from django.db import connections
|
|
575
|
+
from django.http import StreamingHttpResponse
|
|
576
|
+
|
|
577
|
+
from .export import read_dir, scan_folders
|
|
578
|
+
|
|
579
|
+
events: queue.Queue = queue.Queue()
|
|
580
|
+
started = time.monotonic()
|
|
581
|
+
folders = body.get('folders')
|
|
582
|
+
folder_count = len(folders) if isinstance(folders, list) else 0
|
|
583
|
+
file_count = 0
|
|
584
|
+
if isinstance(folders, list):
|
|
585
|
+
for item in folders:
|
|
586
|
+
raw = item.get('files') if isinstance(item, dict) else None
|
|
587
|
+
file_count += len(raw) if isinstance(raw, dict) else 0
|
|
588
|
+
|
|
589
|
+
def work() -> None:
|
|
590
|
+
try:
|
|
591
|
+
if isinstance(folders, list):
|
|
592
|
+
result = scan_folders(
|
|
593
|
+
body, include_missing=body.get('includeMissing') is True, progress=events.put)
|
|
594
|
+
else:
|
|
595
|
+
result = read_dir(
|
|
596
|
+
str(body.get('path') or ''), include_missing=body.get('includeMissing') is True, progress=events.put)
|
|
597
|
+
events.put({'type': 'result', 'result': result})
|
|
598
|
+
except BaseException as exc: # noqa: BLE001 —— 后台线程里炸了也只能从这条口说出去
|
|
599
|
+
events.put({'type': 'error', 'message': f'{type(exc).__name__}: {exc}'})
|
|
600
|
+
finally:
|
|
601
|
+
connections.close_all()
|
|
602
|
+
events.put(None)
|
|
603
|
+
|
|
604
|
+
def generate():
|
|
605
|
+
yield _line({'type': 'start', 'folders': folder_count, 'files': file_count})
|
|
606
|
+
thread = threading.Thread(target=work, name='labull-read', daemon=True)
|
|
607
|
+
thread.start()
|
|
608
|
+
while True:
|
|
609
|
+
try:
|
|
610
|
+
event = events.get(timeout=10)
|
|
611
|
+
except queue.Empty:
|
|
612
|
+
# 🔴 心跳不是"假装有进度":它的唯一作用是**让字节继续流动**(见 `read_view` 那段)。
|
|
613
|
+
yield _line({'type': 'ping', 'seconds': round(time.monotonic() - started, 1)})
|
|
614
|
+
continue
|
|
615
|
+
if event is None:
|
|
616
|
+
break
|
|
617
|
+
if isinstance(event, dict) and event.get('phase') == 'judged':
|
|
618
|
+
yield _line({'type': 'judged', 'blocks': event.get('blocks', 0), 'checks': event.get('checks', 0)})
|
|
619
|
+
continue
|
|
620
|
+
if isinstance(event, dict) and event.get('phase') == 'check':
|
|
621
|
+
yield _line({'type': 'check', **{key: value for key, value in event.items() if key != 'phase'}})
|
|
622
|
+
continue
|
|
623
|
+
yield _line(event if isinstance(event, dict) else {'type': 'event', 'event': str(event)})
|
|
624
|
+
|
|
625
|
+
response = StreamingHttpResponse(generate(), content_type='application/x-ndjson; charset=utf-8')
|
|
626
|
+
# 🔴 让 nginx **别缓冲**:缓冲会把这一串攒起来一起发,"边算边回"就等于没有(进度条不动、
|
|
627
|
+
# 网关那边也可能照样等到超时)。这条头是 nginx 认的约定(不是标准头,浏览器忽略它)。
|
|
628
|
+
response['X-Accel-Buffering'] = 'no'
|
|
629
|
+
response['Cache-Control'] = 'no-store'
|
|
630
|
+
return response
|
|
631
|
+
|
|
632
|
+
|
|
538
633
|
@staff_only
|
|
539
634
|
def check_view(request: HttpRequest) -> HttpResponse:
|
|
540
635
|
"""`POST /magic-blocks/check` —— 重新体检(默认全量,也可以点名几块),然后**统一写库**。
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "labull-framework"
|
|
7
|
-
version = "1.0.
|
|
7
|
+
version = "1.0.4"
|
|
8
8
|
description = "Labull Framework: a pluggable Django app with Authing OIDC login and magic blocks"
|
|
9
9
|
readme = "README-pypi.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{labull_framework-1.0.2 → labull_framework-1.0.4}/labull_framework/magic_block/blockpackage.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{labull_framework-1.0.2 → labull_framework-1.0.4}/labull_framework/management/commands/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{labull_framework-1.0.2 → labull_framework-1.0.4}/labull_framework/migrations/0001_initial.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{labull_framework-1.0.2 → labull_framework-1.0.4}/labull_framework.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|