apply_pl_data_scope.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. #!/usr/bin/env python3
  2. """为 PL API 批量接入数据权限(PR4 Phase 3)。"""
  3. from __future__ import annotations
  4. import re
  5. from pathlib import Path
  6. ROOT = Path(__file__).resolve().parents[1]
  7. IMPORT_LINE = "from app.services.pl_data_scope import apply_pl_scope, assert_pl_row_in_scope\n"
  8. def ensure_import(content: str) -> str:
  9. if "from app.services.pl_data_scope import" in content:
  10. return content
  11. marker = "from app.core.db_auth import DbUserContext, get_db_current_user\n"
  12. if marker in content:
  13. return content.replace(marker, marker + IMPORT_LINE)
  14. return content
  15. def patch_get_helper(content: str, fn_name: str, model_name: str) -> str:
  16. """_get_xxx(db, tenant_no, id) -> _get_xxx(db, tenant_no, id, user) + assert."""
  17. old_sig = f"def {fn_name}(db: Session, tenant_no: str, "
  18. if f"def {fn_name}(db: Session, tenant_no: str, " not in content:
  19. return content
  20. if f"def {fn_name}(db: Session, tenant_no: str, " in content and "user: DbUserContext" in content.split(f"def {fn_name}")[1].split("\n\n")[0]:
  21. return content
  22. pattern = rf"(def {fn_name}\(\n db: Session,\n tenant_no: str,\n \w+_id: str,\n\) -> \w+:\n)( \w+ = \([\s\S]*?\n if not \w+:\n raise HTTPException\(status_code=404[^\n]+\n return \w+\n)"
  23. repl = rf"\1\2".replace("\\2", "") # noqa - use explicit replacement below
  24. def replacer(m):
  25. body = m.group(0)
  26. if "assert_pl_row_in_scope" in body:
  27. return body
  28. var = re.search(r"if not (\w+):", body).group(1)
  29. return (
  30. body.replace(
  31. f"def {fn_name}(db: Session, tenant_no: str, ",
  32. f"def {fn_name}(db: Session, tenant_no: str, ",
  33. )
  34. .replace(
  35. f") -> ",
  36. f",\n user: DbUserContext,\n) -> ",
  37. 1,
  38. )
  39. if ",\n user: DbUserContext," not in body
  40. else body
  41. )
  42. # simpler manual replace per function
  43. old = f"""def {fn_name}(db: Session, tenant_no: str, """
  44. # find entity id param name
  45. m = re.search(rf"def {fn_name}\(db: Session, tenant_no: str, (\w+): str\)", content)
  46. if not m:
  47. return content
  48. id_param = m.group(1)
  49. old_block = f"""def {fn_name}(db: Session, tenant_no: str, {id_param}: str) ->"""
  50. if f"user: DbUserContext" in content[content.find(old_block) : content.find(old_block) + 200]:
  51. return content
  52. new_block = f"""def {fn_name}(db: Session, tenant_no: str, {id_param}: str, user: DbUserContext) ->"""
  53. content = content.replace(old_block, new_block, 1)
  54. # add assert before return
  55. needle = f"if not {id_param.split('_')[0] if False else ''}"
  56. # find variable name from query
  57. fn_body_start = content.find(new_block)
  58. fn_chunk = content[fn_body_start : fn_body_start + 600]
  59. var_m = re.search(r"(\w+) = \(\n db\.query", fn_chunk)
  60. if not var_m:
  61. var_m = re.search(r"(\w+) = \(\n db\.query", content[fn_body_start : fn_body_start + 800])
  62. var = var_m.group(1) if var_m else id_param.replace("_id", "").replace("carrier", "carrier")
  63. if var_m:
  64. var = var_m.group(1)
  65. else:
  66. # guess from id_param
  67. var = {"carrier_id": "carrier", "vehicle_id": "vehicle", "personnel_id": "personnel", "plan_id": "plan", "transport_id": "transport"}.get(id_param, "row")
  68. insert = f""" assert_pl_row_in_scope(db, {var}, {model_name}, user, tenant_no)
  69. return {var}
  70. """
  71. old_return = f" return {var}\n"
  72. if f"assert_pl_row_in_scope(db, {var}," not in content:
  73. content = content.replace(old_return, insert, 1)
  74. # fix _get calls: _get_xxx(db, tenant_no, id) -> _get_xxx(db, tenant_no, id, user)
  75. content = re.sub(
  76. rf"{fn_name}\(db, tenant_no, ([^,\)]+)\)",
  77. rf"{fn_name}(db, tenant_no, \1, user)",
  78. content,
  79. )
  80. return content
  81. def add_scope_before_order(content: str, model: str, anchor: str) -> str:
  82. """在 order_by 前插入 apply_pl_scope。"""
  83. if f"apply_pl_scope(query, {model}," in content:
  84. return content
  85. idx = content.find(anchor)
  86. if idx == -1:
  87. return content
  88. insert = f" query = apply_pl_scope(query, {model}, user, db, tenant_no)\n\n "
  89. # insert before anchor's line
  90. line_start = content.rfind("\n", 0, idx) + 1
  91. return content[:line_start] + insert + content[line_start:]
  92. def patch_base_data():
  93. path = ROOT / "app/api/pl/base_data.py"
  94. c = ensure_import(path.read_text())
  95. c = add_scope_before_order(c, "PlCarrier", " query = query.order_by(\n case((PlCarrier.order_num")
  96. c = add_scope_before_order(c, "PlVehicle", " vehicles = query.order_by(")
  97. if "vehicles = query.order_by(" not in c:
  98. c = add_scope_before_order(c, "PlVehicle", " query = query.order_by(")
  99. c = add_scope_before_order(c, "PlPersonnel", " query = query.order_by(\n case((PlPersonnel.order_num")
  100. # driver list - second PlPersonnel order_by
  101. if c.count("apply_pl_scope(query, PlPersonnel,") < 2:
  102. c = c.replace(
  103. " query = query.order_by(\n case((PlPersonnel.order_num.is_(None), 1), else_=0).asc(),\n PlPersonnel.create_time.desc(),\n )\n return [_personnel_to_driver_read(p) for p in query.all()]",
  104. " query = apply_pl_scope(query, PlPersonnel, user, db, tenant_no)\n\n query = query.order_by(\n case((PlPersonnel.order_num.is_(None), 1), else_=0).asc(),\n PlPersonnel.create_time.desc(),\n )\n return [_personnel_to_driver_read(p) for p in query.all()]",
  105. 1,
  106. )
  107. for fn, model in (
  108. ("_get_carrier", "PlCarrier"),
  109. ("_get_vehicle", "PlVehicle"),
  110. ("_get_personnel", "PlPersonnel"),
  111. ):
  112. c = patch_get_helper(c, fn, model)
  113. path.write_text(c)
  114. print("patched base_data.py")
  115. def patch_plan():
  116. path = ROOT / "app/api/pl/plan.py"
  117. c = ensure_import(path.read_text())
  118. if "apply_pl_scope(query, PlPlan," not in c:
  119. c = c.replace(
  120. " total = query.count()\n offset = (page - 1) * page_size\n plans = query.order_by(PlPlan.create_time.desc())",
  121. " query = apply_pl_scope(query, PlPlan, user, db, tenant_no)\n\n total = query.count()\n offset = (page - 1) * page_size\n plans = query.order_by(PlPlan.create_time.desc())",
  122. 1,
  123. )
  124. c = patch_get_helper(c, "_get_plan", "PlPlan")
  125. path.write_text(c)
  126. print("patched plan.py")
  127. def patch_transport():
  128. path = ROOT / "app/api/pl/transport.py"
  129. c = ensure_import(path.read_text())
  130. if "apply_pl_scope(query, PlTransport," not in c:
  131. c = c.replace(
  132. " total = query.count()\n offset = (page - 1) * page_size\n transports = query.order_by(PlTransport.create_time.desc())",
  133. " query = apply_pl_scope(query, PlTransport, user, db, tenant_no)\n\n total = query.count()\n offset = (page - 1) * page_size\n transports = query.order_by(PlTransport.create_time.desc())",
  134. 1,
  135. )
  136. c = patch_get_helper(c, "_get_transport", "PlTransport")
  137. path.write_text(c)
  138. print("patched transport.py")
  139. def patch_inventory():
  140. path = ROOT / "app/api/pl/inventory.py"
  141. c = ensure_import(path.read_text())
  142. replacements = [
  143. (
  144. " total = query.count()\n offset = (page - 1) * page_size\n items = query.order_by(PlInventory.product_name)",
  145. " query = apply_pl_scope(query, PlInventory, user, db, tenant_no)\n\n total = query.count()\n offset = (page - 1) * page_size\n items = query.order_by(PlInventory.product_name)",
  146. ),
  147. (
  148. " return query.order_by(PlInventoryWarning.create_time.desc()).all()",
  149. " query = apply_pl_scope(query, PlInventoryWarning, user, db, tenant_no)\n return query.order_by(PlInventoryWarning.create_time.desc()).all()",
  150. ),
  151. (
  152. " total = query.count()\n offset = (page - 1) * page_size\n items = query.order_by(PlInventoryTransaction.create_time.desc())",
  153. " query = apply_pl_scope(query, PlInventoryTransaction, user, db, tenant_no)\n\n total = query.count()\n offset = (page - 1) * page_size\n items = query.order_by(PlInventoryTransaction.create_time.desc())",
  154. ),
  155. (
  156. " query = db.query(PlInventoryTransaction).filter(\n PlInventoryTransaction.tenant_no == tenant_no,\n PlInventoryTransaction.del_flag == 0,\n PlInventoryTransaction.create_time >= date_from,\n PlInventoryTransaction.create_time <= date_to,\n )\n\n if product_id:",
  157. " query = db.query(PlInventoryTransaction).filter(\n PlInventoryTransaction.tenant_no == tenant_no,\n PlInventoryTransaction.del_flag == 0,\n PlInventoryTransaction.create_time >= date_from,\n PlInventoryTransaction.create_time <= date_to,\n )\n query = apply_pl_scope(query, PlInventoryTransaction, user, db, tenant_no)\n\n if product_id:",
  158. ),
  159. (
  160. " return query.order_by(PlDocumentReminder.create_time.desc()).all()",
  161. " query = apply_pl_scope(query, PlDocumentReminder, user, db, tenant_no)\n return query.order_by(PlDocumentReminder.create_time.desc()).all()",
  162. ),
  163. ]
  164. for old, new in replacements:
  165. if old in c and new not in c:
  166. c = c.replace(old, new, 1)
  167. # warning update assert
  168. if "assert_pl_row_in_scope(db, warning," not in c:
  169. c = c.replace(
  170. " if not warning:\n raise HTTPException(status_code=404, detail=\"预警不存在\")\n\n if payload.status == \"resolved\":",
  171. " if not warning:\n raise HTTPException(status_code=404, detail=\"预警不存在\")\n assert_pl_row_in_scope(db, warning, PlInventoryWarning, user, tenant_no)\n\n if payload.status == \"resolved\":",
  172. 1,
  173. )
  174. if "assert_pl_row_in_scope(db, reminder," not in c:
  175. c = c.replace(
  176. " if not reminder:\n raise HTTPException(status_code=404, detail=\"提醒不存在\")",
  177. " if not reminder:\n raise HTTPException(status_code=404, detail=\"提醒不存在\")\n assert_pl_row_in_scope(db, reminder, PlDocumentReminder, user, tenant_no)",
  178. 1,
  179. )
  180. if "assert_pl_row_in_scope(db, inventory," not in c:
  181. c = c.replace(
  182. " if inventory:\n inventory.low_stock_warning = payload.low_stock_warning",
  183. " if inventory:\n assert_pl_row_in_scope(db, inventory, PlInventory, user, tenant_no)\n inventory.low_stock_warning = payload.low_stock_warning",
  184. 1,
  185. )
  186. path.write_text(c)
  187. print("patched inventory.py")
  188. if __name__ == "__main__":
  189. patch_base_data()
  190. patch_plan()
  191. patch_transport()
  192. patch_inventory()