labull-framework 0.1.4.2__tar.gz → 0.1.4.3__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.
Files changed (47) hide show
  1. {labull_framework-0.1.4.2/labull_framework.egg-info → labull_framework-0.1.4.3}/PKG-INFO +1 -1
  2. {labull_framework-0.1.4.2 → labull_framework-0.1.4.3}/labull_framework/canvas/data.py +93 -8
  3. {labull_framework-0.1.4.2 → labull_framework-0.1.4.3}/labull_framework/canvas/static/canvas.js +9 -5
  4. {labull_framework-0.1.4.2 → labull_framework-0.1.4.3}/labull_framework/canvas/static/page.html +2 -2
  5. {labull_framework-0.1.4.2 → labull_framework-0.1.4.3}/labull_framework/magic_block/devkit.py +71 -6
  6. {labull_framework-0.1.4.2 → labull_framework-0.1.4.3/labull_framework.egg-info}/PKG-INFO +1 -1
  7. {labull_framework-0.1.4.2 → labull_framework-0.1.4.3}/pyproject.toml +1 -1
  8. {labull_framework-0.1.4.2 → labull_framework-0.1.4.3}/LICENSE +0 -0
  9. {labull_framework-0.1.4.2 → labull_framework-0.1.4.3}/MANIFEST.in +0 -0
  10. {labull_framework-0.1.4.2 → labull_framework-0.1.4.3}/README-pypi.md +0 -0
  11. {labull_framework-0.1.4.2 → labull_framework-0.1.4.3}/labull_framework/__init__.py +0 -0
  12. {labull_framework-0.1.4.2 → labull_framework-0.1.4.3}/labull_framework/admin.py +0 -0
  13. {labull_framework-0.1.4.2 → labull_framework-0.1.4.3}/labull_framework/apps.py +0 -0
  14. {labull_framework-0.1.4.2 → labull_framework-0.1.4.3}/labull_framework/auth/__init__.py +0 -0
  15. {labull_framework-0.1.4.2 → labull_framework-0.1.4.3}/labull_framework/auth/backend.py +0 -0
  16. {labull_framework-0.1.4.2 → labull_framework-0.1.4.3}/labull_framework/auth/bootstrap.py +0 -0
  17. {labull_framework-0.1.4.2 → labull_framework-0.1.4.3}/labull_framework/auth/sso.py +0 -0
  18. {labull_framework-0.1.4.2 → labull_framework-0.1.4.3}/labull_framework/auth/views.py +0 -0
  19. {labull_framework-0.1.4.2 → labull_framework-0.1.4.3}/labull_framework/canvas/__init__.py +0 -0
  20. {labull_framework-0.1.4.2 → labull_framework-0.1.4.3}/labull_framework/canvas/gitinfo.py +0 -0
  21. {labull_framework-0.1.4.2 → labull_framework-0.1.4.3}/labull_framework/canvas/static/canvas.css +0 -0
  22. {labull_framework-0.1.4.2 → labull_framework-0.1.4.3}/labull_framework/canvas/views.py +0 -0
  23. {labull_framework-0.1.4.2 → labull_framework-0.1.4.3}/labull_framework/env.py +0 -0
  24. {labull_framework-0.1.4.2 → labull_framework-0.1.4.3}/labull_framework/jsonio.py +0 -0
  25. {labull_framework-0.1.4.2 → labull_framework-0.1.4.3}/labull_framework/magic_block/__init__.py +0 -0
  26. {labull_framework-0.1.4.2 → labull_framework-0.1.4.3}/labull_framework/magic_block/deepcheck.py +0 -0
  27. {labull_framework-0.1.4.2 → labull_framework-0.1.4.3}/labull_framework/magic_block/dispatch.py +0 -0
  28. {labull_framework-0.1.4.2 → labull_framework-0.1.4.3}/labull_framework/magic_block/export.py +0 -0
  29. {labull_framework-0.1.4.2 → labull_framework-0.1.4.3}/labull_framework/magic_block/health.py +0 -0
  30. {labull_framework-0.1.4.2 → labull_framework-0.1.4.3}/labull_framework/magic_block/registry.py +0 -0
  31. {labull_framework-0.1.4.2 → labull_framework-0.1.4.3}/labull_framework/magic_block/sdk.py +0 -0
  32. {labull_framework-0.1.4.2 → labull_framework-0.1.4.3}/labull_framework/magic_block/service.py +0 -0
  33. {labull_framework-0.1.4.2 → labull_framework-0.1.4.3}/labull_framework/magic_block/validate.py +0 -0
  34. {labull_framework-0.1.4.2 → labull_framework-0.1.4.3}/labull_framework/magic_block/views.py +0 -0
  35. {labull_framework-0.1.4.2 → labull_framework-0.1.4.3}/labull_framework/management/__init__.py +0 -0
  36. {labull_framework-0.1.4.2 → labull_framework-0.1.4.3}/labull_framework/management/commands/__init__.py +0 -0
  37. {labull_framework-0.1.4.2 → labull_framework-0.1.4.3}/labull_framework/management/commands/labull_contract.py +0 -0
  38. {labull_framework-0.1.4.2 → labull_framework-0.1.4.3}/labull_framework/migrations/0001_initial.py +0 -0
  39. {labull_framework-0.1.4.2 → labull_framework-0.1.4.3}/labull_framework/migrations/__init__.py +0 -0
  40. {labull_framework-0.1.4.2 → labull_framework-0.1.4.3}/labull_framework/models.py +0 -0
  41. {labull_framework-0.1.4.2 → labull_framework-0.1.4.3}/labull_framework/settings.py +0 -0
  42. {labull_framework-0.1.4.2 → labull_framework-0.1.4.3}/labull_framework/urls.py +0 -0
  43. {labull_framework-0.1.4.2 → labull_framework-0.1.4.3}/labull_framework.egg-info/SOURCES.txt +0 -0
  44. {labull_framework-0.1.4.2 → labull_framework-0.1.4.3}/labull_framework.egg-info/dependency_links.txt +0 -0
  45. {labull_framework-0.1.4.2 → labull_framework-0.1.4.3}/labull_framework.egg-info/requires.txt +0 -0
  46. {labull_framework-0.1.4.2 → labull_framework-0.1.4.3}/labull_framework.egg-info/top_level.txt +0 -0
  47. {labull_framework-0.1.4.2 → labull_framework-0.1.4.3}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: labull-framework
3
- Version: 0.1.4.2
3
+ Version: 0.1.4.3
4
4
  Summary: Labull Framework: a pluggable Django app with Authing OIDC login and magic blocks
5
5
  Author: Labull Framework
6
6
  License-Expression: MIT
@@ -156,8 +156,11 @@ def _file_of(field: Any) -> dict[str, Any] | None:
156
156
  }
157
157
 
158
158
 
159
- def _shape_field(field: Any, model: Any) -> dict[str, Any]:
160
- """一个字段在画布上的全部信息(**唯一一处**拼这个字典)。"""
159
+ def _shape_field(field: Any, model: Any, enums: dict[str, dict[str, Any]] | None = None) -> dict[str, Any]:
160
+ """一个字段在画布上的全部信息(**唯一一处**拼这个字典)。
161
+
162
+ :param enums: `_enums_of(model)` 那一份(类名 → 中文名 / 成员);不给就只有取值,没有类名。
163
+ """
161
164
  kind = _kind_of(field)
162
165
  item: dict[str, Any] = {
163
166
  'name': field.name,
@@ -171,6 +174,10 @@ def _shape_field(field: Any, model: Any) -> dict[str, Any]:
171
174
  'brief': _brief(field),
172
175
  'briefLine': _brief_line(field),
173
176
  'enum': _enum_of(field),
177
+ # 🔴 这个字段用的是**哪个枚举类**(`Order.Status`),认不出就是 None。
178
+ # 取值那一半在 `enum` 里,类名这一半只能这么补:字段上没有任何回指(见 `_choice_items`)。
179
+ # 导出(`devkit._field_lines`)靠它写出 `Order.Status.OPEN` 那种真能用的写法。
180
+ 'enumRef': _enum_ref(field, enums or {}),
174
181
  'file': _file_of(field),
175
182
  # 默认值也要能看(`default=Status.OPEN` 这类信息在页面上很有用)。
176
183
  'default': _default_of(field),
@@ -248,6 +255,9 @@ def shape() -> dict[str, list[dict[str, Any]]]:
248
255
  label = str(model._meta.app_label)
249
256
  fields: list[dict[str, Any]] = []
250
257
  relations: list[dict[str, Any]] = []
258
+ # 🔴 枚举类**先扫一遍**:每个带 choices 的字段要在这一步认出"它用的是哪个类"(`enumRef`)。
259
+ # 放在字段循环之前是判据:`_shape_field` 拿它当参数,顺序反了就只能再扫一遍模型。
260
+ enums = _enums_of(model)
251
261
  for field in model._meta.get_fields():
252
262
  if getattr(field, 'many_to_many', False):
253
263
  target = field.related_model
@@ -286,7 +296,7 @@ def shape() -> dict[str, list[dict[str, Any]]]:
286
296
  relations.append(
287
297
  _relation_of(field, kind, '1', target, column=str(field.attname))
288
298
  )
289
- fields.append(_shape_field(field, model))
299
+ fields.append(_shape_field(field, model, enums))
290
300
  tables.append(
291
301
  {
292
302
  'name': model.__name__,
@@ -295,6 +305,10 @@ def shape() -> dict[str, list[dict[str, Any]]]:
295
305
  'verbose': _verbose(model),
296
306
  'brief': _brief(model),
297
307
  'briefLine': _brief_line(model),
308
+ # 🔴 这张表**自己定义**的那几个枚举类(类名 → 中文名 / 成员)。
309
+ # 它是**只增不减**的信息:画布不读它,导出(`devkit`)靠它写出 `Order.Status.OPEN`
310
+ # 那种写法。见 `_enums_of()` 顶上那段(为什么必须扫模型类体)。
311
+ 'enums': enums,
298
312
  'fields': fields,
299
313
  'relations': relations,
300
314
  }
@@ -358,16 +372,87 @@ def _type_of(field: Any) -> str:
358
372
  return 'text'
359
373
 
360
374
 
375
+ def _choice_items(field: Any) -> list[tuple[Any, Any]]:
376
+ """一个带 choices 的字段:`[(取值, 中文名)]`(**只有这一处**做那个归一化)。
377
+
378
+ 🔴 Django 已经把 choices 归一化过了:`TextChoices` 那份「取值 → 中文名」在 `field.choices` 上
379
+ 只剩一个**普通列表**(实测 `[('open', '进行中'), …]`,连元素都不是 `ChoiceItem`)——
380
+ **回指枚举类的那条线索在字段上根本不存在**,类名与成员名只能回到模型类体上去扫(见 `_enums_of`)。
381
+ 🔴 归一化收成这一份:字段那一层与类那一层要比"取值是不是同一批",两边各做一遍就会出现
382
+ "看着一样、比不相等"(`'1'` 与 `1`、`ChoiceItem` 与元组)—— 而那种漂没人会立刻发现。
383
+ """
384
+ out: list[tuple[Any, Any]] = []
385
+ for item in list(getattr(field, 'choices', None) or []):
386
+ if isinstance(item, (tuple, list)) and len(item) == 2:
387
+ out.append((item[0], item[1]))
388
+ return out
389
+
390
+
361
391
  def _enum_of(field: Any) -> dict[str, Any] | None:
362
- """枚举列:`TextChoices` 在 `_meta` 里只剩「取值 → 中文名」这两样。"""
363
- choices = list(getattr(field, 'choices', None) or [])
364
- if not choices:
392
+ """枚举列:取值与它们的中文名(`TextChoices` 在 `_meta` 里只剩这两样)。"""
393
+ items = _choice_items(field)
394
+ if not items:
365
395
  return None
366
- values = [str(value) for value, _ in choices]
367
- labels = {str(value): str(label) for value, label in choices}
396
+ values = [str(value) for value, _label in items]
397
+ labels = {str(value): str(label) for value, label in items}
368
398
  return {'values': values, 'labels': labels}
369
399
 
370
400
 
401
+ def _enum_ref(field: Any, enums: dict[str, dict[str, Any]]) -> str | None:
402
+ """这个字段用的是模型上哪个枚举类(`Order.Status` ⇒ 回 `'Order.Status'`);认不出就 `None`。
403
+
404
+ 🔴 为什么要它:作者在块里写的是 `Order.Status.OPEN`(类属性),而字段那一层只剩「取值 → 中文名」——
405
+ 导出里不写类名,作者就只能照值写 `Order.Status.open`,而那种写法在**保存时的引用深检**里
406
+ 会被判 `missing`(`deepcheck._resolve()` 是按类属性往下走的)—— 导出与深检因此对不上。
407
+ 🔴 匹配只按**取值那一批**(`_choice_items` 那一份归一化):字段可以只取枚举的一个子集,
408
+ 所以"字段取值 ⊆ 枚举成员"也算命中 —— 但**两个类都能容下时必须都不认**(回 `None`):
409
+ 挑一个猜错的类比不写更坏(作者会照着一个假的类名写)。
410
+ """
411
+ values = [str(value) for value, _label in _choice_items(field)]
412
+ if not values:
413
+ return None
414
+ wanted = set(values)
415
+ hits = [
416
+ name
417
+ for name, item in enums.items()
418
+ if wanted <= {str(member['value']) for member in item['members']}
419
+ ]
420
+ return hits[0] if len(hits) == 1 else None
421
+
422
+
423
+ def _enum_name(value: Any) -> str:
424
+ """枚举类的名字(Django 的 `QualifiedName` 是 `str` 的子类 ⇒ 这里给的就是 `Order.Status`)。"""
425
+ return str(getattr(value, '__qualname__', None) or getattr(value, '__name__', '') or '').strip()
426
+
427
+
428
+ def _enums_of(model: Any) -> dict[str, dict[str, Any]]:
429
+ """这张表的类体里定义的那几个枚举类:`{'Order.Status': {'labels', 'members'}}`。
430
+
431
+ 🔴 **必须扫模型类体**(`vars(model)`):`field` 上没有任何属性回指那个枚举类(见 `_choice_items`),
432
+ 而块作者在 `backend_open.py` 里用的正是类属性(`Order.Status.OPEN`)—— 导出要写的就是这个名字。
433
+ 🔴 判据用 `models.Choices`(基类),**不是**猜 `__members__`:`TextChoices` / `IntegerChoices`
434
+ 都是它的子类,而写死的 `choices` 列表(`[('a', '甲')]`)**不是** —— 那种字段照旧只有取值。
435
+ 🔴 用 `vars()` 而不是 `dir()`:`dir()` 会把 django 塞给类的那些东西一起列出来,而 `vars()` 就是
436
+ 这个模型**自己**写了什么(继承来的枚举类不属于这张表)。
437
+ 🔴 **全部收进来**,包括一个字段都没用到的:导出是"清单",不是"用到了才给"(少写一格信息
438
+ 比多写一格更坏 —— 作者是拿它当字典查的)。
439
+ """
440
+ from django.db import models
441
+
442
+ out: dict[str, dict[str, Any]] = {}
443
+ for value in vars(model).values():
444
+ if not isinstance(value, type) or not issubclass(value, models.Choices) or value is models.Choices:
445
+ continue
446
+ name = _enum_name(value)
447
+ if not name:
448
+ continue
449
+ out[name] = {
450
+ 'labels': {str(member.value): str(member.label) for member in value},
451
+ 'members': [{'name': str(member.name), 'value': str(member.value)} for member in value],
452
+ }
453
+ return out
454
+
455
+
371
456
  def payload() -> dict[str, Any]:
372
457
  """画布整包:当前形状 + 与 git 那份比出来的三态 + 头部那行 git 信息 + 布局。
373
458
 
@@ -387,9 +387,13 @@ function svgEl(tag, attrs = {}) {
387
387
  */
388
388
  function boundaryLayer(bounds) {
389
389
  const group = svgEl('g', { class: 'boundary-layer' });
390
- // 🔴 边界与总线要**向上下无限延展**(用户明确要的:「左侧边界应该是向上也是无限延展」):
391
- // 上下各多加 `BOUNDARY_SPAN`,拖到哪、缩到多小,它们都还在 —— 不跟着内容高度走。
392
- const top = -BOUNDARY_SPAN;
390
+ /* 🔴 边界 / 总线 / 那块淡色底:**只向下延展,不向上**(用户明确要的:
391
+ * 「向上其实不需要无限延展……只要向下和向右延展即可」)。
392
+ * 上端钉在**内容顶端之上一点点**(`bounds.top` 里已经留了标签的位置)——
393
+ * 上面本来就没有卡片,再画上去只是一条没意义的虚线加一大块灰底。
394
+ * 下端仍留 `BOUNDARY_SPAN`:往下是真会继续长的(表一张张往下排),
395
+ * 拖下去、缩下去时它不能"中途断掉"。 */
396
+ const top = bounds ? bounds.top : 0;
393
397
  const bottom = (bounds ? bounds.top + bounds.height : canvas.clientHeight || 900) + BOUNDARY_SPAN;
394
398
  // 边界**左边**那一块:没有内容、也过不去,铺一层很淡的底把它标出来。
395
399
  group.append(svgEl('rect', {
@@ -401,8 +405,8 @@ function boundaryLayer(bounds) {
401
405
  group.append(svgEl('line', {
402
406
  x1: RAIL_X, y1: top, x2: RAIL_X, y2: bottom, class: 'boundary-rail',
403
407
  }));
404
- // ⚠️ 标签**不能**跟着 `top` 跑到 -20000 去:它们钉在内容最上面那张卡片之上(一眼能找到边界)。
405
- const labelY = (bounds ? bounds.top : 0) + 16;
408
+ // 两个标签钉在**上端**(也就是内容最上面那张卡片之上),一眼能看到边界是什么。
409
+ const labelY = top + 16;
406
410
  const edge = svgEl('text', { x: BOUNDARY_X + 8, y: labelY, class: 'boundary-label' });
407
411
  edge.textContent = '边界(左边没有内容)';
408
412
  const rail = svgEl('text', { x: RAIL_X + 8, y: labelY + 16, class: 'boundary-label is-rail' });
@@ -7,7 +7,7 @@
7
7
  <!-- ⚠️ `?v=` 是**给浏览器的缓存换钥匙**:这两个静态件在"这一页是公开的"之后尤其容易被缓存住,
8
8
  改了正文而人不刷新(或刷新了也还是旧的),症状是"代码明明改了、页面却没变"(真踩过)。
9
9
  改这两个文件时把版本号往上加一位。 -->
10
- <link rel="stylesheet" href="canvas/asset/canvas.css?v=48">
10
+ <link rel="stylesheet" href="canvas/asset/canvas.css?v=49">
11
11
  </head>
12
12
  <body>
13
13
  <header class="bar">
@@ -52,6 +52,6 @@
52
52
  拖动表可排版(拖完自动存);空白处按住拖 = 平移,Ctrl + 滚轮 = 缩放;
53
53
  点表标题展开备注与关联,点字段行展开枚举取值 / 文件去向 / 关联目标,点线上的关联键名看它的备注。
54
54
  </p>
55
- <script src="canvas/asset/canvas.js?v=48"></script>
55
+ <script src="canvas/asset/canvas.js?v=49"></script>
56
56
  </body>
57
57
  </html>
@@ -158,8 +158,54 @@ def _api_lines() -> list[str]:
158
158
  ]
159
159
 
160
160
 
161
+ def _enum_short(text: Any) -> str:
162
+ """枚举引用的**最后一段**(`Order.Status` ⇒ `Status`)。
163
+
164
+ 🔴 类名要写在 `class Order:` 的类体里(`Status = Status`),写成 `Order.Status = Order.Status`
165
+ 的话那个类属性就变成一个字符串注解了 —— 而作者真正用的是 `Order.Status.OPEN`。
166
+ """
167
+ return str(text or '').split('.')[-1]
168
+
169
+
170
+ def _enum_classes(table: dict[str, Any], used: set[str]) -> list[str]:
171
+ """这张表自己定义的那几个枚举类(**类体**写法,作者照它写 `Order.Status.OPEN`)。
172
+
173
+ 🔴 为什么非要有这一块:字段那一行只剩「取值 → 中文名」,**类名与成员名一个都没有** ——
174
+ 作者照值写 `Order.Status.open`,而保存时的引用深检(`deepcheck._resolve()`)是按类属性
175
+ 往下走的,当场判 `missing`。导出与深检必须说同一件事。
176
+ 🔴 `used` 是"真的有字段在用它"的那几个:一个都没用到的也照写(导出是清单,不是用到了才给),
177
+ 但那几行挂一句说明,免得人以为漏了。
178
+ """
179
+ out: list[str] = []
180
+ for name, item in (table.get('enums') or {}).items():
181
+ short = _enum_short(name)
182
+ members = item.get('members') or []
183
+ labels = item.get('labels') or {}
184
+ if not short or not members:
185
+ continue
186
+ out.append(f' class {short}:')
187
+ if name not in used:
188
+ out.append(' # ⚠️ 这张表里现在没有字段在用它(照旧写出来:它是可以用到的取值集合)')
189
+ for member in members:
190
+ member_name = str(member.get('name') or '')
191
+ if not member_name:
192
+ continue
193
+ value = str(member.get('value'))
194
+ label = _flat(labels.get(value)) or value
195
+ out.append(f' {member_name} = {value!r} # {label}')
196
+ out.append('')
197
+ if out:
198
+ out.insert(0, ' # ── 枚举:这些取值按**类属性**用(`Order.Status.OPEN`),不是 `Order.Status.open` ──')
199
+ return out
200
+
201
+
161
202
  def _field_lines(field: dict[str, Any]) -> str:
162
- """一个模型字段那一行(类型 + 一行备注)。关联字段的类型是目标模型的名字。"""
203
+ """一个模型字段那一行(类型 + 一行备注)。关联字段的类型是目标模型的名字。
204
+
205
+ 🔴 枚举字段这一行的判据:**类型写 `str`(库里就是一列 varchar),可用取值写成类属性**。
206
+ 注释里给全名(`Order.Status`,有的话)与「成员名=取值=中文名」—— 作者照着写就同时过
207
+ 编辑器的补全与保存时的引用深检。
208
+ """
163
209
  kind = str(field.get('kind') or '')
164
210
  name = str(field.get('name') or '')
165
211
  if not name:
@@ -188,7 +234,19 @@ def _field_lines(field: dict[str, Any]) -> str:
188
234
  values = enum.get('values') or []
189
235
  if values:
190
236
  labels = enum.get('labels') or {}
191
- notes.append('取值:' + ' / '.join(f'{value}={_flat(labels.get(value)) or value}' for value in values))
237
+ # 🔴 成员名(`OPEN`)与取值(`open`)是**两件事**:值能当字符串存,而代码里要用的是成员名。
238
+ # 形状里有 `enumRef`(类名)与那张表的 `enums`(成员表)时,就把成员名一起摆出来。
239
+ enum_ref = str(field.get('enumRef') or '')
240
+ members = ((field.get('_enums') or {}).get(enum_ref) or {}).get('members') or []
241
+ by_value = {str(member.get('value')): str(member.get('name') or '') for member in members}
242
+ short = _enum_short(enum_ref)
243
+ where = f'{short}.' if short else ''
244
+ notes.append(
245
+ '取值:'
246
+ + ' / '.join(
247
+ f'{where}{by_value.get(value) or value}={_flat(labels.get(value)) or value}' for value in values
248
+ )
249
+ )
192
250
  comment = f' # {" · ".join(notes)}' if notes else ''
193
251
  return f' {name}: {annotation}{comment}'
194
252
 
@@ -214,7 +272,7 @@ def _relation_lines(table: dict[str, Any]) -> list[str]:
214
272
 
215
273
 
216
274
  def _table_block(table: dict[str, Any]) -> list[str]:
217
- """一张表那一块(类名 / 备注 / 真实 import / 字段 / 关联)。"""
275
+ """一张表那一块(类名 / 备注 / 真实 import / 枚举类 / 字段 / 关联)。"""
218
276
  name = str(table.get('name') or '')
219
277
  module = str(table.get('module') or '')
220
278
  if not name:
@@ -224,8 +282,14 @@ def _table_block(table: dict[str, Any]) -> list[str]:
224
282
  out = [f'class {name}:', f' """{brief}({where})"""' if brief else f' """{where}"""']
225
283
  if module:
226
284
  out.append(f' # 一般这样 import:from {module}.models import {name}')
227
- for field in table.get('fields') or []:
228
- line = _field_lines(field if isinstance(field, dict) else {})
285
+ enums = table.get('enums') or {}
286
+ fields = [field for field in (table.get('fields') or []) if isinstance(field, dict)]
287
+ # 🔴 枚举类与字段那一半**必须对上**:`_field_lines` 要拿成员表把「取值 → 成员名」写全,
288
+ # 而成员表在 table 那一层。传进去比让每个字段自己去找一份可靠(也就不会有第二份匹配逻辑)。
289
+ used = {str(field.get('enumRef') or '') for field in fields if field.get('enumRef')}
290
+ out.extend(_enum_classes(table, used))
291
+ for field in fields:
292
+ line = _field_lines({**field, '_enums': enums})
229
293
  if line:
230
294
  out.append(line)
231
295
  out.extend(_relation_lines(table))
@@ -246,7 +310,7 @@ def backend_lock_pyi(tables: list[dict[str, Any]] | None = None, items: tuple[En
246
310
  ' ② 能抛的六个 HTTP 语义错误;',
247
311
  ' ③ 服务端交给 handler 的 ctx(就那 4 个键);',
248
312
  ' ④ 平台读哪些环境变量(**只有名字与说明,没有值**);',
249
- ' ⑤ 项目里现在有哪些模型、每个字段是什么、枚举有哪些取值。',
313
+ ' ⑤ 项目里现在有哪些模型、每个字段是什么、枚举有哪些**成员与取值**。',
250
314
  '',
251
315
  f'🔴 它不参与运行:块后端真正 import 的是 {sdk.LIB_SDK},模型走每张表下面那行注释的路径。',
252
316
  '🔴 块包必备件:' + ' / '.join((BLOCK_ENTRY, BLOCK_MANIFEST)) + f';块自己的后端是 {BLOCK_BACKEND_FILE}(可选)。',
@@ -296,6 +360,7 @@ def backend_lock_pyi(tables: list[dict[str, Any]] | None = None, items: tuple[En
296
360
  '# 没配的键**不存在**(先判 `"OCR_KEY" in ctx.config`),别的变量一个都不给块。',
297
361
  '',
298
362
  '# ── ⑤ 项目里的模型(照 .labull/contract.json 那份模型形状生成) ──',
363
+ '# 枚举按**类属性**用(`Order.Status.OPEN`):成员名不是取值 —— `Order.Status.open` 不存在。',
299
364
  ]
300
365
  )
301
366
  if not tables:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: labull-framework
3
- Version: 0.1.4.2
3
+ Version: 0.1.4.3
4
4
  Summary: Labull Framework: a pluggable Django app with Authing OIDC login and magic blocks
5
5
  Author: Labull Framework
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "labull-framework"
7
- version = "0.1.4.2"
7
+ version = "0.1.4.3"
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"