labull-framework 3.0.4__tar.gz → 3.0.5__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-3.0.4/labull_framework.egg-info → labull_framework-3.0.5}/PKG-INFO +1 -1
- {labull_framework-3.0.4 → labull_framework-3.0.5}/labull_framework/access.py +266 -184
- {labull_framework-3.0.4 → labull_framework-3.0.5}/labull_framework/env.py +8 -13
- {labull_framework-3.0.4 → labull_framework-3.0.5}/labull_framework/magic_block/devkit.py +8 -8
- {labull_framework-3.0.4 → labull_framework-3.0.5}/labull_framework/magic_block/requires.py +5 -5
- {labull_framework-3.0.4 → labull_framework-3.0.5/labull_framework.egg-info}/PKG-INFO +1 -1
- {labull_framework-3.0.4 → labull_framework-3.0.5}/pyproject.toml +1 -1
- {labull_framework-3.0.4 → labull_framework-3.0.5}/LICENSE +0 -0
- {labull_framework-3.0.4 → labull_framework-3.0.5}/MANIFEST.in +0 -0
- {labull_framework-3.0.4 → labull_framework-3.0.5}/README-pypi.md +0 -0
- {labull_framework-3.0.4 → labull_framework-3.0.5}/labull_framework/__init__.py +0 -0
- {labull_framework-3.0.4 → labull_framework-3.0.5}/labull_framework/admin.py +0 -0
- {labull_framework-3.0.4 → labull_framework-3.0.5}/labull_framework/apps.py +0 -0
- {labull_framework-3.0.4 → labull_framework-3.0.5}/labull_framework/auth/__init__.py +0 -0
- {labull_framework-3.0.4 → labull_framework-3.0.5}/labull_framework/auth/backend.py +0 -0
- {labull_framework-3.0.4 → labull_framework-3.0.5}/labull_framework/auth/bootstrap.py +0 -0
- {labull_framework-3.0.4 → labull_framework-3.0.5}/labull_framework/auth/sso.py +0 -0
- {labull_framework-3.0.4 → labull_framework-3.0.5}/labull_framework/auth/views.py +0 -0
- {labull_framework-3.0.4 → labull_framework-3.0.5}/labull_framework/canvas/__init__.py +0 -0
- {labull_framework-3.0.4 → labull_framework-3.0.5}/labull_framework/canvas/data.py +0 -0
- {labull_framework-3.0.4 → labull_framework-3.0.5}/labull_framework/canvas/gitinfo.py +0 -0
- {labull_framework-3.0.4 → labull_framework-3.0.5}/labull_framework/canvas/shape_ast.py +0 -0
- {labull_framework-3.0.4 → labull_framework-3.0.5}/labull_framework/canvas/static/canvas.css +0 -0
- {labull_framework-3.0.4 → labull_framework-3.0.5}/labull_framework/canvas/static/canvas.js +0 -0
- {labull_framework-3.0.4 → labull_framework-3.0.5}/labull_framework/canvas/static/page.html +0 -0
- {labull_framework-3.0.4 → labull_framework-3.0.5}/labull_framework/canvas/views.py +0 -0
- {labull_framework-3.0.4 → labull_framework-3.0.5}/labull_framework/checks.py +0 -0
- {labull_framework-3.0.4 → labull_framework-3.0.5}/labull_framework/flow.py +0 -0
- {labull_framework-3.0.4 → labull_framework-3.0.5}/labull_framework/jsonio.py +0 -0
- {labull_framework-3.0.4 → labull_framework-3.0.5}/labull_framework/magic_block/__init__.py +0 -0
- {labull_framework-3.0.4 → labull_framework-3.0.5}/labull_framework/magic_block/blockcheck.py +0 -0
- {labull_framework-3.0.4 → labull_framework-3.0.5}/labull_framework/magic_block/blockpackage.py +0 -0
- {labull_framework-3.0.4 → labull_framework-3.0.5}/labull_framework/magic_block/blocktypes.py +0 -0
- {labull_framework-3.0.4 → labull_framework-3.0.5}/labull_framework/magic_block/deepcheck.py +0 -0
- {labull_framework-3.0.4 → labull_framework-3.0.5}/labull_framework/magic_block/dispatch.py +0 -0
- {labull_framework-3.0.4 → labull_framework-3.0.5}/labull_framework/magic_block/export.py +0 -0
- {labull_framework-3.0.4 → labull_framework-3.0.5}/labull_framework/magic_block/health.py +0 -0
- {labull_framework-3.0.4 → labull_framework-3.0.5}/labull_framework/magic_block/manifest.py +0 -0
- {labull_framework-3.0.4 → labull_framework-3.0.5}/labull_framework/magic_block/parallel.py +0 -0
- {labull_framework-3.0.4 → labull_framework-3.0.5}/labull_framework/magic_block/registry.py +0 -0
- {labull_framework-3.0.4 → labull_framework-3.0.5}/labull_framework/magic_block/sdk.py +0 -0
- {labull_framework-3.0.4 → labull_framework-3.0.5}/labull_framework/magic_block/service.py +0 -0
- {labull_framework-3.0.4 → labull_framework-3.0.5}/labull_framework/magic_block/subproc.py +0 -0
- {labull_framework-3.0.4 → labull_framework-3.0.5}/labull_framework/magic_block/types/__init__.py +0 -0
- {labull_framework-3.0.4 → labull_framework-3.0.5}/labull_framework/magic_block/types/plain.py +0 -0
- {labull_framework-3.0.4 → labull_framework-3.0.5}/labull_framework/magic_block/types/workflow.py +0 -0
- {labull_framework-3.0.4 → labull_framework-3.0.5}/labull_framework/magic_block/validate.py +0 -0
- {labull_framework-3.0.4 → labull_framework-3.0.5}/labull_framework/magic_block/views.py +0 -0
- {labull_framework-3.0.4 → labull_framework-3.0.5}/labull_framework/management/__init__.py +0 -0
- {labull_framework-3.0.4 → labull_framework-3.0.5}/labull_framework/management/commands/__init__.py +0 -0
- {labull_framework-3.0.4 → labull_framework-3.0.5}/labull_framework/management/commands/labull_access.py +0 -0
- {labull_framework-3.0.4 → labull_framework-3.0.5}/labull_framework/management/commands/labull_blocktype.py +0 -0
- {labull_framework-3.0.4 → labull_framework-3.0.5}/labull_framework/management/commands/labull_contract.py +0 -0
- {labull_framework-3.0.4 → labull_framework-3.0.5}/labull_framework/management/commands/labull_requires.py +0 -0
- {labull_framework-3.0.4 → labull_framework-3.0.5}/labull_framework/migrations/0001_initial.py +0 -0
- {labull_framework-3.0.4 → labull_framework-3.0.5}/labull_framework/migrations/0002_remove_magicblock_prev_files_and_more.py +0 -0
- {labull_framework-3.0.4 → labull_framework-3.0.5}/labull_framework/migrations/0003_magicblock_tags.py +0 -0
- {labull_framework-3.0.4 → labull_framework-3.0.5}/labull_framework/migrations/0004_magicblock_detail.py +0 -0
- {labull_framework-3.0.4 → labull_framework-3.0.5}/labull_framework/migrations/0005_labullrole.py +0 -0
- {labull_framework-3.0.4 → labull_framework-3.0.5}/labull_framework/migrations/0006_default_groups_permissions.py +0 -0
- {labull_framework-3.0.4 → labull_framework-3.0.5}/labull_framework/migrations/0007_no_builtin_permissions.py +0 -0
- {labull_framework-3.0.4 → labull_framework-3.0.5}/labull_framework/migrations/__init__.py +0 -0
- {labull_framework-3.0.4 → labull_framework-3.0.5}/labull_framework/models.py +0 -0
- {labull_framework-3.0.4 → labull_framework-3.0.5}/labull_framework/permissions.py +0 -0
- {labull_framework-3.0.4 → labull_framework-3.0.5}/labull_framework/settings.py +0 -0
- {labull_framework-3.0.4 → labull_framework-3.0.5}/labull_framework/urls.py +0 -0
- {labull_framework-3.0.4 → labull_framework-3.0.5}/labull_framework/virtual.py +0 -0
- {labull_framework-3.0.4 → labull_framework-3.0.5}/labull_framework.egg-info/SOURCES.txt +0 -0
- {labull_framework-3.0.4 → labull_framework-3.0.5}/labull_framework.egg-info/dependency_links.txt +0 -0
- {labull_framework-3.0.4 → labull_framework-3.0.5}/labull_framework.egg-info/requires.txt +0 -0
- {labull_framework-3.0.4 → labull_framework-3.0.5}/labull_framework.egg-info/top_level.txt +0 -0
- {labull_framework-3.0.4 → labull_framework-3.0.5}/setup.cfg +0 -0
|
@@ -1,24 +1,32 @@
|
|
|
1
|
-
"""权限码与权限包(组)的**声明 → 落库**那一半 ——
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
1
|
+
"""权限码与权限包(组)的**声明 → 落库**那一半 —— 声明住在项目里的一份 YAML 文件。
|
|
2
|
+
|
|
3
|
+
默认位置:`<BASE_DIR>/labull-access.yaml`(可以用 `LABULL_ACCESS_FILE` 指到别的路径)。
|
|
4
|
+
文件长这样(一份普通 YAML,**不是**环境变量里那种一行塞完的串):
|
|
5
|
+
|
|
6
|
+
```yaml
|
|
7
|
+
# 权限(可选;模型 Meta.permissions 里已经声明过的不必重复)
|
|
8
|
+
permissions:
|
|
9
|
+
canvasdemo.Reimbursement:
|
|
10
|
+
review_reimbursement: 可以审批报销单
|
|
11
|
+
pay_reimbursement: 可以对报销单付款
|
|
12
|
+
|
|
13
|
+
# 权限包(组):组名 → 权限码
|
|
14
|
+
groups:
|
|
15
|
+
财务:
|
|
16
|
+
- canvasdemo.review_reimbursement
|
|
17
|
+
- canvasdemo.pay_reimbursement
|
|
18
|
+
出纳:
|
|
19
|
+
- canvasdemo.pay_reimbursement
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
🔴 **为什么 permissions 里要写模型**:`Permission` 的行主键是 `(content_type, codename)`,
|
|
23
|
+
而 content_type 只能从一个**真实模型**拿到。写模型名换来的是 content_type 正确、
|
|
24
|
+
与 `Meta.permissions` 指向同一行;对外暴露的码仍是 `app_label.codename`。
|
|
25
|
+
🔴 **不引 PyYAML**:这个仓库不引第三方运行时依赖,所以这里只认上面这一种子集
|
|
26
|
+
(`键:` / `键: 值` / 缩进两格的映射或 `- 项` 列表),不认识的写法**当场报错**。
|
|
27
|
+
🔴 同步**只增不删**:少一个组 / 少一个权限**不会**从库里删掉 —— 删组会级联掉
|
|
28
|
+
`auth_user_groups` 里人的授权,那是人手动配的数据。要删只能人自己在库里删。
|
|
29
|
+
🔴 `sync()` **永不抛**:它是启动路径上的一件事(与 `auth/bootstrap.py` 同一个模式)。
|
|
22
30
|
🔴 组名只是**标签**:代码里任何判据都不许读组名(判权永远看 permission)。
|
|
23
31
|
"""
|
|
24
32
|
|
|
@@ -27,6 +35,7 @@ from __future__ import annotations
|
|
|
27
35
|
import logging
|
|
28
36
|
import re
|
|
29
37
|
from dataclasses import dataclass, field
|
|
38
|
+
from pathlib import Path
|
|
30
39
|
from typing import Any
|
|
31
40
|
|
|
32
41
|
from .env import read_env
|
|
@@ -34,28 +43,28 @@ from .permissions import PERMISSION_PATTERN
|
|
|
34
43
|
|
|
35
44
|
logger = logging.getLogger(__name__)
|
|
36
45
|
|
|
37
|
-
#:
|
|
38
|
-
|
|
46
|
+
#: 声明文件的名字(默认落在项目的 `BASE_DIR` 下)。
|
|
47
|
+
ACCESS_FILE_NAME = 'labull-access.yaml'
|
|
39
48
|
|
|
40
|
-
#:
|
|
41
|
-
|
|
49
|
+
#: 把声明文件指到别处(路径;相对路径按 `BASE_DIR` 解析)。
|
|
50
|
+
ACCESS_FILE_ENV = 'LABULL_ACCESS_FILE'
|
|
42
51
|
|
|
43
52
|
#: 一个标识符(app_label / 模型名 / codename 共用这一份判据)。
|
|
44
53
|
_IDENT = re.compile(r'^[A-Za-z_][A-Za-z0-9_]*$')
|
|
45
54
|
|
|
46
|
-
#: Django `Permission.codename` / `Group.name`
|
|
55
|
+
#: Django `Permission.codename` / `Permission.name` / `Group.name` 的上限(照抄模型那三列)。
|
|
47
56
|
CODENAME_MAX = 100
|
|
48
57
|
NAME_MAX = 255
|
|
49
58
|
GROUP_NAME_MAX = 150
|
|
50
59
|
|
|
51
60
|
|
|
52
61
|
class AccessError(ValueError):
|
|
53
|
-
"""
|
|
62
|
+
"""声明文件写得不对 —— 报文是中文原话,直接给人看(带行号)。"""
|
|
54
63
|
|
|
55
64
|
|
|
56
65
|
@dataclass(frozen=True)
|
|
57
66
|
class PermissionSpec:
|
|
58
|
-
"""一条权限声明:`app_label.Model
|
|
67
|
+
"""一条权限声明:`app_label.Model` → `codename: 中文名`。"""
|
|
59
68
|
|
|
60
69
|
app_label: str
|
|
61
70
|
model: str
|
|
@@ -76,6 +85,14 @@ class GroupSpec:
|
|
|
76
85
|
permissions: tuple[str, ...] = ()
|
|
77
86
|
|
|
78
87
|
|
|
88
|
+
@dataclass(frozen=True)
|
|
89
|
+
class Declaration:
|
|
90
|
+
"""一份声明文件的全部内容(`permissions:` / `groups:` 都可以整块不写)。"""
|
|
91
|
+
|
|
92
|
+
permissions: tuple[PermissionSpec, ...] = ()
|
|
93
|
+
groups: tuple[GroupSpec, ...] = ()
|
|
94
|
+
|
|
95
|
+
|
|
79
96
|
@dataclass
|
|
80
97
|
class Report:
|
|
81
98
|
"""一次同步的结论(命令打印它、启动日志挑着打;**空报告 = 什么都没发生**)。"""
|
|
@@ -117,154 +134,227 @@ class Report:
|
|
|
117
134
|
for item in self.added:
|
|
118
135
|
out.append(f'{prefix}把 {item} 放进权限包')
|
|
119
136
|
for item in self.missing:
|
|
120
|
-
out.append(f'{prefix}✗ 权限包要的码不存在:{item}
|
|
121
|
-
f'{
|
|
137
|
+
out.append(f'{prefix}✗ 权限包要的码不存在:{item}(先在模型的 Meta.permissions / '
|
|
138
|
+
f'{ACCESS_FILE_NAME} 的 permissions 里声明,再 migrate)')
|
|
122
139
|
return out
|
|
123
140
|
|
|
124
141
|
|
|
125
|
-
|
|
126
|
-
"""按 `;` 切条目(空条目忽略 —— 结尾多一个 `;` 是手滑,不是错误)。"""
|
|
127
|
-
return [item.strip() for item in str(raw or '').split(';') if item.strip()]
|
|
142
|
+
# ──────────────────────────── 读那一份 YAML(只认这一种子集) ────────────────────────────
|
|
128
143
|
|
|
129
144
|
|
|
130
|
-
def
|
|
131
|
-
"""
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
145
|
+
def _strip_comment(line: str) -> str:
|
|
146
|
+
"""去掉行尾注释(`#` 在引号里不算注释 —— 中文名里带 `#` 是合法的)。"""
|
|
147
|
+
quote = ''
|
|
148
|
+
for index, char in enumerate(line):
|
|
149
|
+
if quote:
|
|
150
|
+
if char == quote:
|
|
151
|
+
quote = ''
|
|
152
|
+
elif char in ('"', "'"):
|
|
153
|
+
quote = char
|
|
154
|
+
elif char == '#':
|
|
155
|
+
return line[:index]
|
|
156
|
+
return line
|
|
136
157
|
|
|
137
158
|
|
|
138
|
-
def
|
|
139
|
-
"""`
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
if
|
|
191
|
-
raise
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
159
|
+
def _value(text: str) -> str:
|
|
160
|
+
"""一个标量值(去掉一对引号;`''` / `""` 是合法的空串)。"""
|
|
161
|
+
value = text.strip()
|
|
162
|
+
if len(value) >= 2 and value[0] == value[-1] and value[0] in ('"', "'"):
|
|
163
|
+
return value[1:-1]
|
|
164
|
+
return value
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _error(line_no: int, message: str) -> AccessError:
|
|
168
|
+
return AccessError(f'{ACCESS_FILE_NAME} 第 {line_no} 行:{message}')
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def parse_access(text: str) -> Declaration:
|
|
172
|
+
"""把声明文件的正文读成 `Declaration`(格式不对抛 `AccessError`,带行号)。
|
|
173
|
+
|
|
174
|
+
只认这一种子集(**不引 YAML 库**):
|
|
175
|
+
|
|
176
|
+
```yaml
|
|
177
|
+
permissions:
|
|
178
|
+
app.Model:
|
|
179
|
+
codename: 中文名
|
|
180
|
+
groups:
|
|
181
|
+
组名:
|
|
182
|
+
- app_label.codename
|
|
183
|
+
```
|
|
184
|
+
"""
|
|
185
|
+
permissions: list[PermissionSpec] = []
|
|
186
|
+
groups: list[GroupSpec] = []
|
|
187
|
+
seen_codes: set[str] = set()
|
|
188
|
+
seen_groups: set[str] = set()
|
|
189
|
+
section = ''
|
|
190
|
+
model: tuple[str, str] | None = None # (app_label, model)
|
|
191
|
+
group: str = '' # 当前正在收权限码的组名
|
|
192
|
+
|
|
193
|
+
for index, raw in enumerate(str(text or '').splitlines(), start=1):
|
|
194
|
+
line = _strip_comment(raw).rstrip()
|
|
195
|
+
if not line.strip():
|
|
196
|
+
continue
|
|
197
|
+
indent = len(line) - len(line.lstrip())
|
|
198
|
+
content = line.strip()
|
|
199
|
+
|
|
200
|
+
if indent == 0:
|
|
201
|
+
if not content.endswith(':'):
|
|
202
|
+
raise _error(index, f'顶层只许写 `permissions:` 或 `groups:`(读到 {content!r})')
|
|
203
|
+
key = content[:-1].strip()
|
|
204
|
+
if key not in ('permissions', 'groups'):
|
|
205
|
+
raise _error(index, f'顶层不认识的键 `{key}`:只有 permissions / groups 两块')
|
|
206
|
+
section = key
|
|
207
|
+
model = None
|
|
208
|
+
group = ''
|
|
209
|
+
continue
|
|
210
|
+
|
|
211
|
+
if not section:
|
|
212
|
+
raise _error(index, '这一行缩进了,但上面还没有 `permissions:` / `groups:` 那一行')
|
|
213
|
+
|
|
214
|
+
if section == 'permissions':
|
|
215
|
+
if indent == 2:
|
|
216
|
+
target = content[:-1].strip() if content.endswith(':') else ''
|
|
217
|
+
parts = target.split('.')
|
|
218
|
+
if len(parts) != 2 or not all(_IDENT.match(part) for part in parts):
|
|
219
|
+
raise _error(
|
|
220
|
+
index,
|
|
221
|
+
f'{content!r} 不是 `app_label.Model:` 的形状 —— 要写**真实的模型**'
|
|
222
|
+
f'(权限行必须挂在一个 content_type 上),例如 `canvasdemo.Reimbursement:`',
|
|
223
|
+
)
|
|
224
|
+
model = (parts[0], parts[1])
|
|
225
|
+
continue
|
|
226
|
+
if indent == 4:
|
|
227
|
+
if model is None:
|
|
228
|
+
raise _error(index, '这一行上面少了 `app_label.Model:` 那一行')
|
|
229
|
+
if ':' not in content:
|
|
230
|
+
raise _error(index, f'`{content}` 少了中文名(写法是 `codename: 中文名`)')
|
|
231
|
+
codename, name = content.split(':', 1)
|
|
232
|
+
codename, name = codename.strip(), _value(name)
|
|
233
|
+
if not _IDENT.match(codename) or len(codename) > CODENAME_MAX:
|
|
234
|
+
raise _error(
|
|
235
|
+
index,
|
|
236
|
+
f'codename `{codename}` 不是标识符(字母/数字/下划线,≤{CODENAME_MAX})',
|
|
214
237
|
)
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
238
|
+
if not name or len(name) > NAME_MAX:
|
|
239
|
+
raise _error(index, f'`{codename}` 的中文名要写、且 ≤{NAME_MAX} 字')
|
|
240
|
+
code = f'{model[0]}.{codename}'
|
|
241
|
+
if code in seen_codes:
|
|
242
|
+
raise _error(index, f'权限码 `{code}` 写了两次(同一个码只能出现一次)')
|
|
243
|
+
seen_codes.add(code)
|
|
244
|
+
permissions.append(PermissionSpec(
|
|
245
|
+
app_label=model[0], model=model[1], codename=codename, name=name,
|
|
246
|
+
))
|
|
247
|
+
continue
|
|
248
|
+
raise _error(index, f'缩进不对(读到 {indent} 格):permissions 下只许两格 / 四格')
|
|
249
|
+
|
|
250
|
+
# section == 'groups'
|
|
251
|
+
if indent == 2:
|
|
252
|
+
if not content.endswith(':'):
|
|
253
|
+
raise _error(index, f'{content!r} 不是 `组名:` 的形状(组名后面要冒号、值缩进在下面)')
|
|
254
|
+
name = content[:-1].strip()
|
|
255
|
+
if not name or len(name) > GROUP_NAME_MAX:
|
|
256
|
+
raise _error(index, f'组名要写、且 ≤{GROUP_NAME_MAX} 字(Django `Group.name` 的上限)')
|
|
257
|
+
if name in seen_groups:
|
|
258
|
+
raise _error(index, f'权限包「{name}」写了两次')
|
|
259
|
+
seen_groups.add(name)
|
|
260
|
+
group = name
|
|
261
|
+
groups.append(GroupSpec(name=name, permissions=()))
|
|
262
|
+
continue
|
|
263
|
+
if indent == 4:
|
|
264
|
+
if not group:
|
|
265
|
+
raise _error(index, '这一行上面少了 `组名:` 那一行')
|
|
266
|
+
if not content.startswith('- '):
|
|
267
|
+
raise _error(index, f'{content!r} 要写成 `- app_label.codename`(列表项)')
|
|
268
|
+
code = content[2:].strip()
|
|
269
|
+
if not PERMISSION_PATTERN.match(code):
|
|
270
|
+
raise _error(
|
|
271
|
+
index,
|
|
272
|
+
f'`{code}` 不是 `app_label.codename` 的形状 —— 要写全'
|
|
273
|
+
f'(例如 canvasdemo.review_reimbursement)',
|
|
274
|
+
)
|
|
275
|
+
current = groups[-1]
|
|
276
|
+
groups[-1] = GroupSpec(name=current.name, permissions=(*current.permissions, code))
|
|
277
|
+
continue
|
|
278
|
+
raise _error(index, f'缩进不对(读到 {indent} 格):groups 下只许两格 / 四格')
|
|
279
|
+
|
|
280
|
+
return Declaration(permissions=tuple(permissions), groups=tuple(groups))
|
|
220
281
|
|
|
221
282
|
|
|
222
|
-
|
|
223
|
-
"""环境里声明的权限(格式错抛 `AccessError`;没配就是空)。"""
|
|
224
|
-
return parse_permissions(read_env(PERMISSIONS_ENV))
|
|
283
|
+
# ──────────────────────────── 文件在哪 ────────────────────────────
|
|
225
284
|
|
|
226
285
|
|
|
227
|
-
def
|
|
228
|
-
"""
|
|
229
|
-
|
|
286
|
+
def _base_dir() -> Path | None:
|
|
287
|
+
"""项目的 `BASE_DIR`(没设就退回当前工作目录 —— `manage.py` 通常就在项目根跑)。"""
|
|
288
|
+
try:
|
|
289
|
+
from django.conf import settings
|
|
290
|
+
|
|
291
|
+
base = getattr(settings, 'BASE_DIR', None)
|
|
292
|
+
if base:
|
|
293
|
+
return Path(base)
|
|
294
|
+
except Exception: # noqa: BLE001 —— 没配 Django 时退回 cwd
|
|
295
|
+
pass
|
|
296
|
+
try:
|
|
297
|
+
return Path.cwd()
|
|
298
|
+
except Exception: # noqa: BLE001
|
|
299
|
+
return None
|
|
230
300
|
|
|
231
301
|
|
|
232
|
-
def
|
|
233
|
-
"""
|
|
302
|
+
def access_file() -> tuple[Path | None, bool]:
|
|
303
|
+
"""声明文件在哪:`(路径, 是不是显式配的)`。
|
|
304
|
+
|
|
305
|
+
`LABULL_ACCESS_FILE` 配了就用它(相对路径按 `BASE_DIR` 解析);没配就用
|
|
306
|
+
`<BASE_DIR>/labull-access.yaml`。取不到任何基准目录 ⇒ `(None, False)`。
|
|
307
|
+
"""
|
|
308
|
+
raw = read_env(ACCESS_FILE_ENV).strip()
|
|
309
|
+
base = _base_dir()
|
|
310
|
+
if raw:
|
|
311
|
+
path = Path(raw)
|
|
312
|
+
if not path.is_absolute() and base is not None:
|
|
313
|
+
path = base / path
|
|
314
|
+
return path, True
|
|
315
|
+
if base is not None:
|
|
316
|
+
return base / ACCESS_FILE_NAME, False
|
|
317
|
+
return None, False
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def load() -> tuple[Declaration, list[str]]:
|
|
321
|
+
"""读声明文件:`(声明, 错误清单)`。文件不在 ⇒ 空声明(默认位置)或一条错误(显式配的)。"""
|
|
322
|
+
path, explicit = access_file()
|
|
323
|
+
if path is None:
|
|
324
|
+
return Declaration(), []
|
|
325
|
+
if not path.is_file():
|
|
326
|
+
if explicit:
|
|
327
|
+
return Declaration(), [
|
|
328
|
+
f'{ACCESS_FILE_ENV} 指向 `{path}`,但那个文件不存在 —— 改对路径、或把这一项删掉'
|
|
329
|
+
]
|
|
330
|
+
return Declaration(), []
|
|
234
331
|
try:
|
|
235
|
-
return
|
|
236
|
-
except AccessError:
|
|
237
|
-
return
|
|
332
|
+
return parse_access(path.read_text(encoding='utf-8-sig')), []
|
|
333
|
+
except AccessError as exc:
|
|
334
|
+
return Declaration(), [str(exc)]
|
|
335
|
+
except Exception as exc: # noqa: BLE001 —— 读文件失败也只进错误清单(启动路径不抛)
|
|
336
|
+
return Declaration(), [f'读不了 `{path}`:{type(exc).__name__}: {exc}']
|
|
238
337
|
|
|
239
338
|
|
|
240
339
|
def declaration_errors() -> list[str]:
|
|
241
|
-
"""
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
parser(raw)
|
|
249
|
-
except AccessError as exc:
|
|
250
|
-
errors.append(str(exc))
|
|
251
|
-
return errors
|
|
340
|
+
"""声明文件的**格式**问题(不碰数据库:系统检查用,`migrate` / `runserver` 就能看见)。"""
|
|
341
|
+
return load()[1]
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def declared_permission_codes() -> set[str]:
|
|
345
|
+
"""声明文件里的权限码(**不碰数据库**;读不出来就当空 —— 判据那一层另有地方报它)。"""
|
|
346
|
+
return {spec.code for spec in load()[0].permissions}
|
|
252
347
|
|
|
253
348
|
|
|
254
349
|
def known_group_names() -> set[str] | None:
|
|
255
|
-
"""项目里"有哪些组"
|
|
350
|
+
"""项目里"有哪些组"(声明文件里的 ∪ 库里现有的);取不到任何一处 ⇒ `None`(调用方跳过判据)。
|
|
256
351
|
|
|
257
|
-
🔴 为什么要并上库里的:组是**只增不删**的 ——
|
|
258
|
-
|
|
352
|
+
🔴 为什么要并上库里的:组是**只增不删**的 —— 没在文件里声明的组(管理员手建的)照样合法;
|
|
353
|
+
把"没声明"当成"不存在"会把它误判掉。
|
|
259
354
|
"""
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
try:
|
|
264
|
-
names.update(spec.name for spec in declared_groups())
|
|
265
|
-
known = True
|
|
266
|
-
except AccessError:
|
|
267
|
-
pass
|
|
355
|
+
declaration, errors = load()
|
|
356
|
+
names = {spec.name for spec in declaration.groups}
|
|
357
|
+
known = bool(names) or not errors
|
|
268
358
|
try:
|
|
269
359
|
from django.contrib.auth.models import Group
|
|
270
360
|
|
|
@@ -276,13 +366,13 @@ def known_group_names() -> set[str] | None:
|
|
|
276
366
|
|
|
277
367
|
|
|
278
368
|
def permissions_catalog() -> list[dict[str, str]]:
|
|
279
|
-
"""项目里**声明过的业务权限**(按码排序):`Meta.permissions` ∪
|
|
369
|
+
"""项目里**声明过的业务权限**(按码排序):`Meta.permissions` ∪ 声明文件的 `permissions:`。
|
|
280
370
|
|
|
281
371
|
每条:`{'code', 'app_label', 'model', 'name', 'source'}`。
|
|
282
372
|
|
|
283
373
|
🔴 Django 自动生成的 `add_/change_/delete_/view_` **不在里面**:那是模型级 CRUD,
|
|
284
374
|
不是业务权限(拿它当业务权限目录是越界,见 `devkit` 的硬规矩第 1 条)。
|
|
285
|
-
🔴 不读数据库(导出不该依赖"这台机器上库迁移到哪一步了"
|
|
375
|
+
🔴 不读数据库(导出不该依赖"这台机器上库迁移到哪一步了"):真源是代码与声明文件。
|
|
286
376
|
"""
|
|
287
377
|
rows: dict[str, dict[str, str]] = {}
|
|
288
378
|
try:
|
|
@@ -299,36 +389,30 @@ def permissions_catalog() -> list[dict[str, str]]:
|
|
|
299
389
|
'name': str(name),
|
|
300
390
|
'source': 'Meta.permissions',
|
|
301
391
|
})
|
|
302
|
-
except Exception: # noqa: BLE001 —— 没有 Django
|
|
303
|
-
pass
|
|
304
|
-
try:
|
|
305
|
-
for spec in declared_permissions():
|
|
306
|
-
# 环境变量那一份**覆盖**同名码的展示信息(它是显式声明的,人写的那一份为准)。
|
|
307
|
-
rows[spec.code] = {
|
|
308
|
-
'code': spec.code,
|
|
309
|
-
'app_label': spec.app_label,
|
|
310
|
-
'model': spec.model,
|
|
311
|
-
'name': spec.name,
|
|
312
|
-
'source': PERMISSIONS_ENV,
|
|
313
|
-
}
|
|
314
|
-
except AccessError:
|
|
392
|
+
except Exception: # noqa: BLE001 —— 没有 Django 时就只剩声明文件那一半
|
|
315
393
|
pass
|
|
394
|
+
for spec in load()[0].permissions:
|
|
395
|
+
# 声明文件那一份**覆盖**同名码的展示信息(它是显式声明的,人写的那一份为准)。
|
|
396
|
+
rows[spec.code] = {
|
|
397
|
+
'code': spec.code,
|
|
398
|
+
'app_label': spec.app_label,
|
|
399
|
+
'model': spec.model,
|
|
400
|
+
'name': spec.name,
|
|
401
|
+
'source': ACCESS_FILE_NAME,
|
|
402
|
+
}
|
|
316
403
|
return [rows[code] for code in sorted(rows)]
|
|
317
404
|
|
|
318
405
|
|
|
319
406
|
def groups_catalog() -> list[dict[str, Any]]:
|
|
320
|
-
"""
|
|
407
|
+
"""声明文件里的权限包:`[{'name': 组名, 'permissions': [码, …]}, …]`(没配就是空)。
|
|
321
408
|
|
|
322
409
|
🔴 只读**声明**、不读库:导出那一刻铺给块作者的是"这个项目声明了哪些包";
|
|
323
410
|
库里手建的组不进来(它们是操作动作,不是契约)。
|
|
324
411
|
"""
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
]
|
|
330
|
-
except AccessError:
|
|
331
|
-
return []
|
|
412
|
+
return [
|
|
413
|
+
{'name': spec.name, 'permissions': list(spec.permissions)}
|
|
414
|
+
for spec in load()[0].groups
|
|
415
|
+
]
|
|
332
416
|
|
|
333
417
|
|
|
334
418
|
def _model_class(app_label: str, model: str) -> Any:
|
|
@@ -348,13 +432,11 @@ def sync(*, dry_run: bool = False) -> Report:
|
|
|
348
432
|
🔴 库还没迁移 / 连不上 ⇒ 结论进 `errors`,不抛:这是启动路径上的一件事。
|
|
349
433
|
"""
|
|
350
434
|
report = Report(dry_run=dry_run)
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
except AccessError as exc:
|
|
355
|
-
report.errors.append(str(exc))
|
|
435
|
+
declaration, errors = load()
|
|
436
|
+
report.errors.extend(errors)
|
|
437
|
+
if errors:
|
|
356
438
|
return report
|
|
357
|
-
if not
|
|
439
|
+
if not declaration.permissions and not declaration.groups:
|
|
358
440
|
return report
|
|
359
441
|
try:
|
|
360
442
|
from django.contrib.auth.models import Group, Permission
|
|
@@ -369,7 +451,7 @@ def sync(*, dry_run: bool = False) -> Report:
|
|
|
369
451
|
#: dry-run 下"这一次会建出来"的那些码:组那一段要把它们当成"可用",
|
|
370
452
|
#: 否则一次全新环境的 dry-run 会把每个码都报成 missing(那是假错)。
|
|
371
453
|
planned: set[str] = set()
|
|
372
|
-
for spec in
|
|
454
|
+
for spec in declaration.permissions:
|
|
373
455
|
existing = Permission.objects.filter(
|
|
374
456
|
content_type__app_label=spec.app_label, codename=spec.codename,
|
|
375
457
|
).first()
|
|
@@ -397,7 +479,7 @@ def sync(*, dry_run: bool = False) -> Report:
|
|
|
397
479
|
report.permission_name_differs.append(spec.code)
|
|
398
480
|
|
|
399
481
|
# ── ② 组:缺就建;权限只 add 缺的那些 ──
|
|
400
|
-
for spec in
|
|
482
|
+
for spec in declaration.groups:
|
|
401
483
|
group = Group.objects.filter(name=spec.name).first()
|
|
402
484
|
if group is None:
|
|
403
485
|
if dry_run:
|
|
@@ -224,22 +224,17 @@ ENV_ITEMS: tuple[EnvItem, ...] = (
|
|
|
224
224
|
hint='LABULL_BLOCK_WORKERS=2',
|
|
225
225
|
group='平台开关',
|
|
226
226
|
),
|
|
227
|
-
# ── 授权(权限码 /
|
|
228
|
-
# 🔴
|
|
227
|
+
# ── 授权(权限码 / 权限包):**声明在项目里的一份 YAML 文件**,落库由框架同步 ──
|
|
228
|
+
# 🔴 为什么不是迁移:迁移里的 `RunPython` 跑在 `create_permissions` **之前**,
|
|
229
229
|
# 要拿权限行去塞组就得自己再建一遍权限行(那是第二份真源)。这里只在启动时对一次账。
|
|
230
|
+
# 🔴 为什么不是环境变量:`group → 权限码` 是一张映射,塞进一行环境变量既难读、又容易写错
|
|
231
|
+
# (见 `labull-access.yaml` 的写法)。
|
|
230
232
|
# 🔴 同步**只增不删**:要删组 / 删权限,只能人自己在库里删(删组会级联掉人的授权)。
|
|
231
233
|
EnvItem(
|
|
232
|
-
name='
|
|
233
|
-
brief='
|
|
234
|
-
'
|
|
235
|
-
hint='
|
|
236
|
-
group='授权(权限码 / 权限包)',
|
|
237
|
-
),
|
|
238
|
-
EnvItem(
|
|
239
|
-
name='LABULL_GROUPS',
|
|
240
|
-
brief='权限包(组)声明:`组名=app_label.codename[,app_label.codename…]`,多个包用 `;` 分隔'
|
|
241
|
-
'(组名只是标签;启动时把组与它的权限同步进库,只增不删)',
|
|
242
|
-
hint='LABULL_GROUPS=财务=canvasdemo.review_reimbursement,canvasdemo.pay_reimbursement',
|
|
234
|
+
name='LABULL_ACCESS_FILE',
|
|
235
|
+
brief='授权声明文件的路径(默认就是 `BASE_DIR/labull-access.yaml`;相对路径按 `BASE_DIR` 解析)。'
|
|
236
|
+
'文件里写 `permissions:`(权限码 + 中文名)与 `groups:`(组名 → 权限码)',
|
|
237
|
+
hint='LABULL_ACCESS_FILE=config/labull-access.yaml',
|
|
243
238
|
group='授权(权限码 / 权限包)',
|
|
244
239
|
),
|
|
245
240
|
# ── 块的配置面 ──
|
|
@@ -1404,8 +1404,8 @@ def backend_lock_pyi(
|
|
|
1404
1404
|
for item in virtuals:
|
|
1405
1405
|
lines.extend(_table_block(item, virtual=True))
|
|
1406
1406
|
# ── ⑥ 授权(权限码 / 权限包):块里写 `permission:` 与 `requires.permissions` 用的就是它们 ──
|
|
1407
|
-
# 🔴 这一段**不读数据库**:权限码来自模型的 `Meta.permissions` 与
|
|
1408
|
-
#
|
|
1407
|
+
# 🔴 这一段**不读数据库**:权限码来自模型的 `Meta.permissions` 与 `labull-access.yaml` 的
|
|
1408
|
+
# `permissions:`,权限包来自同一份文件的 `groups:` —— 导出不该依赖"这台机器上库迁移到哪一步了"。
|
|
1409
1409
|
lines.append('')
|
|
1410
1410
|
lines.append('# ── ⑥ 权限码与权限包(块里 `permission:` / 清单的 `requires.permissions` 用它们)──')
|
|
1411
1411
|
lines.extend(_catalog_lines())
|
|
@@ -1417,7 +1417,7 @@ def _catalog_lines() -> list[str]:
|
|
|
1417
1417
|
out: list[str] = []
|
|
1418
1418
|
catalog = permissions_catalog()
|
|
1419
1419
|
if catalog:
|
|
1420
|
-
out.append('# 业务权限码(声明在模型的 `Meta.permissions` 或
|
|
1420
|
+
out.append('# 业务权限码(声明在模型的 `Meta.permissions` 或 labull-access.yaml 的 permissions:):')
|
|
1421
1421
|
for row in catalog:
|
|
1422
1422
|
out.append(f"# {row['code']} —— {row['name']}({row['app_label']}.{row['model']})")
|
|
1423
1423
|
else:
|
|
@@ -1428,14 +1428,14 @@ def _catalog_lines() -> list[str]:
|
|
|
1428
1428
|
groups = groups_catalog()
|
|
1429
1429
|
if groups:
|
|
1430
1430
|
out.append('#')
|
|
1431
|
-
out.append('# 权限包(组名 → 权限码;来自
|
|
1431
|
+
out.append('# 权限包(组名 → 权限码;来自 labull-access.yaml 的 groups:,启动时框架会同步进库、只增不删):')
|
|
1432
1432
|
for item in groups:
|
|
1433
1433
|
codes = '、'.join(item['permissions']) or '(空包)'
|
|
1434
1434
|
out.append(f"# {item['name']}:{codes}")
|
|
1435
1435
|
out.append('# 🔴 组名只是标签:块里**不许**按组名判人,判权只看 permission 码。')
|
|
1436
1436
|
else:
|
|
1437
1437
|
out.append('#')
|
|
1438
|
-
out.append('# (没有声明权限包:管理员只能逐个给人授权;要打包就在
|
|
1438
|
+
out.append('# (没有声明权限包:管理员只能逐个给人授权;要打包就在 labull-access.yaml 的 groups: 里写)')
|
|
1439
1439
|
return out
|
|
1440
1440
|
|
|
1441
1441
|
def _access_doc() -> str:
|
|
@@ -1466,14 +1466,14 @@ def _access_doc() -> str:
|
|
|
1466
1466
|
else:
|
|
1467
1467
|
out.append('')
|
|
1468
1468
|
out.append('(这个项目没有声明权限包:管理员只能逐个给人授权;'
|
|
1469
|
-
'要打包就在
|
|
1469
|
+
'要打包就在 `labull-access.yaml` 的 `groups:` 里写)')
|
|
1470
1470
|
out.append('')
|
|
1471
1471
|
out.append('**怎么引用它们**:`flow.py` 里写 `permission:<码>`(`confirm_permissions=(\'<码>\',)` 同理);'
|
|
1472
1472
|
'块的 `requires.permissions` 里**用到就必须写**,`requires.groups` 只报备'
|
|
1473
1473
|
'"这个块假定这些包存在"(它不参与任何授权判据)。')
|
|
1474
1474
|
out.append('')
|
|
1475
|
-
out.append('🔴 声明真源:权限码在模型的 `Meta.permissions` 或
|
|
1476
|
-
'
|
|
1475
|
+
out.append('🔴 声明真源:权限码在模型的 `Meta.permissions` 或 `labull-access.yaml` 的 `permissions:`;'
|
|
1476
|
+
'权限包在同一份文件的 `groups:`。框架启动时把它们**同步进库**(权限行 + 组),'
|
|
1477
1477
|
'**只增不删**(删只能人自己在库里删)。')
|
|
1478
1478
|
return '\n'.join(out)
|
|
1479
1479
|
|
|
@@ -21,7 +21,7 @@ from typing import Any
|
|
|
21
21
|
from . import blockpackage
|
|
22
22
|
from .blockpackage import line_of
|
|
23
23
|
from .manifest import read_manifest
|
|
24
|
-
from ..access import
|
|
24
|
+
from ..access import ACCESS_FILE_NAME
|
|
25
25
|
from ..permissions import PERMISSION_PATTERN, permission_list_text
|
|
26
26
|
from .sdk import (
|
|
27
27
|
BLOCK_CONFIG_PREFIX,
|
|
@@ -830,20 +830,20 @@ def check_requires(
|
|
|
830
830
|
if code not in table:
|
|
831
831
|
hard.append(
|
|
832
832
|
f'{prefix}requires.permissions 声明了 `{code}`,但这个项目里没有这个权限码 —— '
|
|
833
|
-
f'改法:在模型的 `Meta.permissions` 里声明它,或在
|
|
834
|
-
f'`
|
|
833
|
+
f'改法:在模型的 `Meta.permissions` 里声明它,或在 `{ACCESS_FILE_NAME}` 的 '
|
|
834
|
+
f'`permissions:` 里加一条(启动时框架会建出来);'
|
|
835
835
|
f'当前项目有的是 {permission_list_text(table)}'
|
|
836
836
|
)
|
|
837
837
|
|
|
838
838
|
# ⑦ **权限包**(`requires.groups`):只报备"这个块假定这些包存在",**不判权**(组名只是标签)。
|
|
839
|
-
# 🔴 存在性 = 声明的(`
|
|
839
|
+
# 🔴 存在性 = 声明的(`labull-access.yaml` 的 `groups:`)∪ 库里现有的(只增不删 ⇒ 手建的组也合法)。
|
|
840
840
|
names = _known_group_names()
|
|
841
841
|
if names is not None:
|
|
842
842
|
for name in _declared_list(declared, 'groups'):
|
|
843
843
|
if name not in names:
|
|
844
844
|
hard.append(
|
|
845
845
|
f'{prefix}requires.groups 声明了权限包「{name}」,但这个项目里没有这个包 —— '
|
|
846
|
-
f'改法:在
|
|
846
|
+
f'改法:在 `{ACCESS_FILE_NAME}` 的 `groups:` 里声明它(启动时框架会建出来),'
|
|
847
847
|
f'或把这一条删掉。组名只是标签,它不参与任何授权判据。'
|
|
848
848
|
)
|
|
849
849
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "labull-framework"
|
|
7
|
-
version = "3.0.
|
|
7
|
+
version = "3.0.5"
|
|
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
|
|
File without changes
|
{labull_framework-3.0.4 → labull_framework-3.0.5}/labull_framework/magic_block/blockcheck.py
RENAMED
|
File without changes
|
{labull_framework-3.0.4 → labull_framework-3.0.5}/labull_framework/magic_block/blockpackage.py
RENAMED
|
File without changes
|
{labull_framework-3.0.4 → labull_framework-3.0.5}/labull_framework/magic_block/blocktypes.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-3.0.4 → labull_framework-3.0.5}/labull_framework/magic_block/types/__init__.py
RENAMED
|
File without changes
|
{labull_framework-3.0.4 → labull_framework-3.0.5}/labull_framework/magic_block/types/plain.py
RENAMED
|
File without changes
|
{labull_framework-3.0.4 → labull_framework-3.0.5}/labull_framework/magic_block/types/workflow.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{labull_framework-3.0.4 → labull_framework-3.0.5}/labull_framework/management/commands/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{labull_framework-3.0.4 → labull_framework-3.0.5}/labull_framework/migrations/0001_initial.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{labull_framework-3.0.4 → labull_framework-3.0.5}/labull_framework/migrations/0005_labullrole.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-3.0.4 → labull_framework-3.0.5}/labull_framework.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|