baucli 1.0.1__py3-none-any.whl
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.
- baucli/__init__.py +8 -0
- baucli/apex_rebuild.py +1458 -0
- baucli/apexlang.py +317 -0
- baucli/api.py +914 -0
- baucli/cli.py +7754 -0
- baucli/config.py +244 -0
- baucli/db/__init__.py +117 -0
- baucli/db/mssql.py +443 -0
- baucli/db/mysql.py +388 -0
- baucli/db/oracle.py +1294 -0
- baucli/db/postgresql.py +482 -0
- baucli/docmig.py +149 -0
- baucli/git.py +214 -0
- baucli/legacy/__init__.py +9 -0
- baucli/legacy/delphi.py +365 -0
- baucli/mcp_server.py +657 -0
- baucli/project.py +360 -0
- baucli/rag_embed.py +26 -0
- baucli/sample_scan.py +105 -0
- baucli/scope_scan.py +272 -0
- baucli/screenshot.py +316 -0
- baucli/seed_check.py +190 -0
- baucli/stress.py +409 -0
- baucli/sync.py +846 -0
- baucli-1.0.1.dist-info/METADATA +179 -0
- baucli-1.0.1.dist-info/RECORD +29 -0
- baucli-1.0.1.dist-info/WHEEL +5 -0
- baucli-1.0.1.dist-info/entry_points.txt +2 -0
- baucli-1.0.1.dist-info/top_level.txt +1 -0
baucli/apexlang.py
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
"""APEXLang split-export reader for DOCAPEX ingestion.
|
|
2
|
+
|
|
3
|
+
Builds the `POST /apex/sync` payload (contract of
|
|
4
|
+
PKG_BAU_DOCAPEX_CORE.SYNC_APP) by PARSING THE APP DIRECTORY of a SQLcl
|
|
5
|
+
split export (`application.apx` + `pages/p*.apx`) — no APEX dictionary
|
|
6
|
+
access required. This is the canonical ingestion path: the split export
|
|
7
|
+
is what teams version and what `apex-rebuild` operates on.
|
|
8
|
+
|
|
9
|
+
Component ids
|
|
10
|
+
=============
|
|
11
|
+
The server upserts regions/items/processes/DAs keyed by numeric component
|
|
12
|
+
ids. APEXLang blocks have names, not ids, so we derive a STABLE numeric id
|
|
13
|
+
from sha256(page|kind|name|ordinal): the same block maps to the same id on
|
|
14
|
+
every sync, which keeps the server's per-row hash change-detection working.
|
|
15
|
+
|
|
16
|
+
Parsing model
|
|
17
|
+
=============
|
|
18
|
+
Same machinery proven by apex-rebuild: paren-balanced block scanning that
|
|
19
|
+
skips ```fences``` and quoted strings, plus property lookups by indent
|
|
20
|
+
depth. Only depth-1 blocks of the page are scanned (region / pageItem /
|
|
21
|
+
process / dynamicAction are siblings in the MMD grammar).
|
|
22
|
+
"""
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import hashlib
|
|
26
|
+
import json
|
|
27
|
+
import re
|
|
28
|
+
from datetime import datetime, timezone
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
|
|
31
|
+
from baucli.apex_rebuild import find_matching_paren, _resolve_root_and_appdir
|
|
32
|
+
|
|
33
|
+
_PAGE_HEAD = re.compile(r"^page\s+(\d+)\s*\(", re.MULTILINE)
|
|
34
|
+
_APP_HEAD = re.compile(r"^app\s+([A-Za-z0-9_-]+)\s*\(", re.MULTILINE)
|
|
35
|
+
_BLOCK = re.compile(
|
|
36
|
+
r"^([ \t]*)(region|pageItem|process|dynamicAction)[ \t]+"
|
|
37
|
+
r"([A-Za-z_][A-Za-z0-9_-]*)[ \t]*\(",
|
|
38
|
+
re.MULTILINE,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _stable_id(*parts) -> int:
|
|
43
|
+
"""Deterministic 48-bit id from the block identity — stable across
|
|
44
|
+
syncs so the server's hash diffing sees the same row."""
|
|
45
|
+
key = "|".join(str(p) for p in parts)
|
|
46
|
+
return int(hashlib.sha256(key.encode("utf-8")).hexdigest()[:12], 16)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _prop(body: str, key: str, indent: str) -> str | None:
|
|
50
|
+
"""Scalar `key: value` at exactly the given indent."""
|
|
51
|
+
m = re.search(rf"^{re.escape(indent)}{re.escape(key)}\s*:\s*(.*?)\s*$",
|
|
52
|
+
body, re.MULTILINE)
|
|
53
|
+
return m.group(1) if m else None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _nested(body: str, parent: str, key: str) -> str | None:
|
|
57
|
+
"""`parent { ... key: value ... }` anywhere in the block body."""
|
|
58
|
+
m = re.search(rf"^([ \t]*){re.escape(parent)}\s*\{{(.*?)^\1\}}",
|
|
59
|
+
body, re.MULTILINE | re.DOTALL)
|
|
60
|
+
if not m:
|
|
61
|
+
return None
|
|
62
|
+
kv = re.search(rf"^[ \t]*{re.escape(key)}\s*:\s*(.*?)\s*$",
|
|
63
|
+
m.group(2), re.MULTILINE)
|
|
64
|
+
return kv.group(1) if kv else None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _fence(body: str) -> str | None:
|
|
68
|
+
"""First ```fenced``` content in the block body (sql/plsql/html)."""
|
|
69
|
+
m = re.search(r"```[a-z]*\n(.*?)```", body, re.DOTALL)
|
|
70
|
+
return m.group(1).strip() if m else None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _blocks(page_body: str, page_indent: str = " "):
|
|
74
|
+
"""Depth-1 blocks (kind, name, body) of a page body."""
|
|
75
|
+
for m in _BLOCK.finditer(page_body):
|
|
76
|
+
if m.group(1) != page_indent:
|
|
77
|
+
continue # nested (e.g. `action` inside dynamicAction)
|
|
78
|
+
op = page_body.find("(", m.end() - 1)
|
|
79
|
+
try:
|
|
80
|
+
end = find_matching_paren(page_body, op)
|
|
81
|
+
except ValueError:
|
|
82
|
+
continue
|
|
83
|
+
yield m.group(2), m.group(3), page_body[op + 1:end - 1]
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _parse_page(path: Path, payload: dict) -> None:
|
|
87
|
+
src = path.read_text(encoding="utf-8", errors="replace")
|
|
88
|
+
hm = _PAGE_HEAD.search(src)
|
|
89
|
+
if not hm:
|
|
90
|
+
return
|
|
91
|
+
page_id = int(hm.group(1))
|
|
92
|
+
op = src.find("(", hm.end() - 1)
|
|
93
|
+
body = src[op + 1:find_matching_paren(src, op) - 1]
|
|
94
|
+
|
|
95
|
+
mode = _nested(body, "appearance", "pageMode") or "normal"
|
|
96
|
+
# Alvos `page: N` (colunas-link, botões redirect, branches) → usados por
|
|
97
|
+
# _infer_modal_launchers p/ descobrir qual página lança cada modal.
|
|
98
|
+
_targets = sorted({int(n) for n in re.findall(r"\bpage\s*:\s*(\d+)", body)})
|
|
99
|
+
payload["pages"].append({
|
|
100
|
+
"page_id": page_id,
|
|
101
|
+
"page_name": _prop(body, "name", " "),
|
|
102
|
+
"page_title": _prop(body, "title", " "),
|
|
103
|
+
"page_alias": _prop(body, "alias", " "),
|
|
104
|
+
"page_mode": "Modal Dialog" if mode == "modalDialog" else "Normal",
|
|
105
|
+
"page_type": None,
|
|
106
|
+
"parent_page_id": None,
|
|
107
|
+
"authorization_scheme":
|
|
108
|
+
(_nested(body, "security", "authorizationScheme") or "")
|
|
109
|
+
.lstrip("@") or None,
|
|
110
|
+
"help_text": _nested(body, "help", "helpText"),
|
|
111
|
+
"_targets": _targets,
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
for kind, name, blk in _blocks(body):
|
|
115
|
+
if kind == "region":
|
|
116
|
+
# Source: fenced SQL/PLSQL, or the form/table reference.
|
|
117
|
+
source = _fence(blk)
|
|
118
|
+
if source is None:
|
|
119
|
+
source = (_nested(blk, "source", "tableName")
|
|
120
|
+
or _nested(blk, "source", "table"))
|
|
121
|
+
payload["regions"].append({
|
|
122
|
+
"page_id": page_id,
|
|
123
|
+
"region_id": _stable_id(page_id, "region", name),
|
|
124
|
+
"region_name": _prop(blk, "name", " ") or name,
|
|
125
|
+
"region_type": _prop(blk, "type", " "),
|
|
126
|
+
"region_template":
|
|
127
|
+
(_nested(blk, "appearance", "template") or "").lstrip("@/")
|
|
128
|
+
or None,
|
|
129
|
+
"display_position": _nested(blk, "layout", "slot"),
|
|
130
|
+
"display_sequence":
|
|
131
|
+
_num(_nested(blk, "layout", "sequence")),
|
|
132
|
+
"parent_region_id": None,
|
|
133
|
+
"region_source": source,
|
|
134
|
+
})
|
|
135
|
+
elif kind == "pageItem":
|
|
136
|
+
label = (_nested(blk, "label", "label")
|
|
137
|
+
or _prop(blk, "label", " "))
|
|
138
|
+
template = _nested(blk, "appearance", "template") or ""
|
|
139
|
+
required = ("Y" if ("required" in template
|
|
140
|
+
or (_nested(blk, "validation",
|
|
141
|
+
"valueRequired") == "true"))
|
|
142
|
+
else "N")
|
|
143
|
+
src_type = _nested(blk, "source", "type")
|
|
144
|
+
src_value = (_nested(blk, "source", "dbColumn")
|
|
145
|
+
or _nested(blk, "source", "column")
|
|
146
|
+
or _nested(blk, "source", "sqlQuery"))
|
|
147
|
+
if src_type is None and _nested(blk, "source", "formRegion"):
|
|
148
|
+
src_type = "formRegion"
|
|
149
|
+
# Vínculo item→região: layout { region: @nome } aponta o bloco da
|
|
150
|
+
# região; o id estável é o mesmo gerado para o bloco region, então
|
|
151
|
+
# o servidor resolve o FK BAU_DOCAPEX_REGIONS_ID no upsert.
|
|
152
|
+
region_ref = _nested(blk, "layout", "region")
|
|
153
|
+
payload["items"].append({
|
|
154
|
+
"page_id": page_id,
|
|
155
|
+
"region_id": (_stable_id(page_id, "region", region_ref.lstrip("@"))
|
|
156
|
+
if region_ref else None),
|
|
157
|
+
"item_id": _stable_id(page_id, "item", name),
|
|
158
|
+
"item_name": name,
|
|
159
|
+
"item_type": _prop(blk, "type", " "),
|
|
160
|
+
"label": (label or "")[:450] or None,
|
|
161
|
+
"display_sequence": _num(_nested(blk, "layout", "sequence")),
|
|
162
|
+
"data_type": _nested(blk, "source", "dataType"),
|
|
163
|
+
"is_required": required,
|
|
164
|
+
# VARCHAR2(4000) on the server is BYTES — UTF-8 text must
|
|
165
|
+
# stay well under it (4000 chars of accented text overflow).
|
|
166
|
+
"default_value": (_nested(blk, "default", "staticValue")
|
|
167
|
+
or "")[:1000] or None,
|
|
168
|
+
"source_type": src_type,
|
|
169
|
+
"source_value": (src_value or "")[:2000] or None,
|
|
170
|
+
"lov_name": None,
|
|
171
|
+
"help_text": _nested(blk, "help", "helpText"),
|
|
172
|
+
})
|
|
173
|
+
elif kind == "process":
|
|
174
|
+
payload["proc"].append({
|
|
175
|
+
"page_id": page_id,
|
|
176
|
+
"process_id": _stable_id(page_id, "process", name),
|
|
177
|
+
"process_name": _prop(blk, "name", " ") or name,
|
|
178
|
+
"process_type": _prop(blk, "type", " "),
|
|
179
|
+
"process_point":
|
|
180
|
+
_nested(blk, "execution", "point") or "afterSubmit",
|
|
181
|
+
"display_sequence":
|
|
182
|
+
_num(_nested(blk, "execution", "sequence")),
|
|
183
|
+
# Native form processes (autoRowFetch/autoRowProcessing)
|
|
184
|
+
# carry the table in source{tableName}, not a code fence.
|
|
185
|
+
"process_source": (_fence(blk)
|
|
186
|
+
or _nested(blk, "source", "tableName")),
|
|
187
|
+
"when_button_pressed":
|
|
188
|
+
_nested(blk, "serverSideCondition", "value"),
|
|
189
|
+
})
|
|
190
|
+
elif kind == "dynamicAction":
|
|
191
|
+
actions = re.findall(
|
|
192
|
+
r"^[ \t]*action[ \t]+[A-Za-z_][\w-]*[ \t]*\(\s*\n"
|
|
193
|
+
r"[ \t]*action\s*:\s*(\S+)",
|
|
194
|
+
blk, re.MULTILINE)
|
|
195
|
+
payload["da"].append({
|
|
196
|
+
"page_id": page_id,
|
|
197
|
+
"region_id": None,
|
|
198
|
+
"da_id": _stable_id(page_id, "da", name),
|
|
199
|
+
"da_name": _prop(blk, "name", " ") or name,
|
|
200
|
+
"event_name": _nested(blk, "when", "event"),
|
|
201
|
+
"selection_type": _nested(blk, "when", "selectionType"),
|
|
202
|
+
"selection_value":
|
|
203
|
+
(_nested(blk, "when", "button")
|
|
204
|
+
or _nested(blk, "when", "items")
|
|
205
|
+
or "").lstrip("@") or None,
|
|
206
|
+
"condition_type": None,
|
|
207
|
+
"condition_value": None,
|
|
208
|
+
"actions_json": json.dumps(actions) if actions else None,
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _num(v: str | None):
|
|
213
|
+
try:
|
|
214
|
+
return int(v) if v is not None else None
|
|
215
|
+
except (TypeError, ValueError):
|
|
216
|
+
return None
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _application_id(app_root: Path, override: int | None) -> int | None:
|
|
220
|
+
"""From --app-id, else the deployments/*.json manifest."""
|
|
221
|
+
if override:
|
|
222
|
+
return override
|
|
223
|
+
dep_dir = app_root / "deployments"
|
|
224
|
+
if dep_dir.is_dir():
|
|
225
|
+
for f in sorted(dep_dir.glob("*.json")):
|
|
226
|
+
try:
|
|
227
|
+
data = json.loads(f.read_text(encoding="utf-8"))
|
|
228
|
+
app_id = (data.get("app") or {}).get("id")
|
|
229
|
+
if app_id:
|
|
230
|
+
return int(app_id)
|
|
231
|
+
except (OSError, ValueError):
|
|
232
|
+
continue
|
|
233
|
+
return None
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def extract_apexlang(project_root: Path, app_dir: str, schema: str,
|
|
237
|
+
application_id: int | None = None,
|
|
238
|
+
workspace: str | None = None) -> dict:
|
|
239
|
+
"""Parse an APEXLang split export into the /apex/sync payload."""
|
|
240
|
+
root, eff_app_dir = _resolve_root_and_appdir(project_root, app_dir)
|
|
241
|
+
if root is None:
|
|
242
|
+
raise FileNotFoundError(
|
|
243
|
+
f"App directory '{app_dir}' has no pages/ folder under "
|
|
244
|
+
f"{project_root} or its parents.")
|
|
245
|
+
app_root = root / "apex" / eff_app_dir
|
|
246
|
+
|
|
247
|
+
app_id = _application_id(app_root, application_id)
|
|
248
|
+
if app_id is None:
|
|
249
|
+
raise ValueError(
|
|
250
|
+
"Could not determine the APEX application id: pass --app-id or "
|
|
251
|
+
"keep a deployments/*.json with {\"app\":{\"id\":N}} in the "
|
|
252
|
+
"export.")
|
|
253
|
+
|
|
254
|
+
app_name = None
|
|
255
|
+
app_alias = None
|
|
256
|
+
app_file = app_root / "application.apx"
|
|
257
|
+
if app_file.is_file():
|
|
258
|
+
src = app_file.read_text(encoding="utf-8", errors="replace")
|
|
259
|
+
am = _APP_HEAD.search(src)
|
|
260
|
+
if am:
|
|
261
|
+
app_alias = am.group(1)
|
|
262
|
+
op = src.find("(", am.end() - 1)
|
|
263
|
+
body = src[op + 1:find_matching_paren(src, op) - 1]
|
|
264
|
+
app_name = _prop(body, "name", " ")
|
|
265
|
+
|
|
266
|
+
page_files = sorted((app_root / "pages").glob("*.apx"))
|
|
267
|
+
last_upd = max((f.stat().st_mtime for f in page_files), default=None)
|
|
268
|
+
|
|
269
|
+
payload = {
|
|
270
|
+
"schema": schema.upper(),
|
|
271
|
+
"app": {
|
|
272
|
+
"application_id": app_id,
|
|
273
|
+
"app_name": app_name or eff_app_dir,
|
|
274
|
+
"app_alias": app_alias,
|
|
275
|
+
"workspace_name": workspace,
|
|
276
|
+
"authentication_scheme": None,
|
|
277
|
+
"theme_name": None,
|
|
278
|
+
"build_status": None,
|
|
279
|
+
"app_version": None,
|
|
280
|
+
"page_count": len(page_files),
|
|
281
|
+
"last_updated_by": "APEXLANG",
|
|
282
|
+
"last_updated_on": (
|
|
283
|
+
datetime.fromtimestamp(last_upd, tz=timezone.utc)
|
|
284
|
+
.strftime("%Y-%m-%dT%H:%M:%S") if last_upd else None),
|
|
285
|
+
},
|
|
286
|
+
"pages": [], "regions": [], "items": [], "proc": [], "da": [],
|
|
287
|
+
}
|
|
288
|
+
for f in page_files:
|
|
289
|
+
_parse_page(f, payload)
|
|
290
|
+
_infer_modal_launchers(payload)
|
|
291
|
+
return payload
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _infer_modal_launchers(payload: dict) -> None:
|
|
295
|
+
"""Infere, p/ cada página modal (Modal Dialog), a página LANÇADORA — a
|
|
296
|
+
página Normal que a referencia via `page: N` (coluna-link / botão / branch).
|
|
297
|
+
Grava `launch_page` no dict da modal p/ o servidor registrar
|
|
298
|
+
`BAU_DOCAPEX_PAGES.SS_LAUNCH_PAGE` no sync (só preenche quando a coluna está
|
|
299
|
+
NULL — edições da p42 vencem). Não descobre modais abertas só por JS
|
|
300
|
+
(`apex.navigation.dialog`) sem `page: N` no export — essas seguem pela
|
|
301
|
+
auto-descoberta em runtime (harvest do link f?p). Limpa o campo transitório
|
|
302
|
+
`_targets`."""
|
|
303
|
+
pages = payload.get("pages", [])
|
|
304
|
+
by_id = {p["page_id"]: p for p in pages}
|
|
305
|
+
cands: dict[int, list[int]] = {}
|
|
306
|
+
for p in pages:
|
|
307
|
+
for t in (p.get("_targets") or []):
|
|
308
|
+
if t and t != p["page_id"]:
|
|
309
|
+
cands.setdefault(t, []).append(p["page_id"])
|
|
310
|
+
for p in pages:
|
|
311
|
+
if p.get("page_mode") == "Modal Dialog":
|
|
312
|
+
ls = cands.get(p["page_id"], [])
|
|
313
|
+
# prefere lançador Normal (evita modal→modal); desempate determinístico
|
|
314
|
+
normal = sorted(l for l in ls
|
|
315
|
+
if by_id.get(l, {}).get("page_mode") != "Modal Dialog")
|
|
316
|
+
p["launch_page"] = normal[0] if normal else (sorted(ls)[0] if ls else None)
|
|
317
|
+
p.pop("_targets", None)
|