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/api.py
ADDED
|
@@ -0,0 +1,914 @@
|
|
|
1
|
+
"""BAUCLI — REST API client for BAU Server.
|
|
2
|
+
|
|
3
|
+
All communication with the BAU Server goes through this module.
|
|
4
|
+
Base URL: https://begbrokerdev.begcloud.com/ords/bauctx/api/v1
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
from typing import Optional
|
|
9
|
+
import requests
|
|
10
|
+
|
|
11
|
+
from baucli import __version__, __product__
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class BauApiError(Exception):
|
|
15
|
+
"""BAU API error with status and message."""
|
|
16
|
+
def __init__(self, message: str, status_code: int = 0, response: dict = None):
|
|
17
|
+
super().__init__(message)
|
|
18
|
+
self.status_code = status_code
|
|
19
|
+
self.response = response or {}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class BauSessionError(BauApiError):
|
|
23
|
+
"""Session token missing/invalid/expired/revoked. Caller should `baucli login`."""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
SESSION_ERROR_TAGS = {
|
|
27
|
+
"session_missing", "session_invalid", "session_revoked",
|
|
28
|
+
"session_expired", "session_tenant_mismatch",
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class BauApi:
|
|
33
|
+
"""REST API client for BAU Server."""
|
|
34
|
+
|
|
35
|
+
def __init__(self, api_url: str, api_key: str = None,
|
|
36
|
+
session_token: str = None, timeout: int = 30):
|
|
37
|
+
self.api_url = api_url.rstrip("/")
|
|
38
|
+
self.api_key = api_key
|
|
39
|
+
self.session_token = session_token
|
|
40
|
+
self.timeout = timeout
|
|
41
|
+
self.session = requests.Session()
|
|
42
|
+
self.session.headers.update({
|
|
43
|
+
"User-Agent": f"{__product__}/{__version__}",
|
|
44
|
+
"Accept": "application/json",
|
|
45
|
+
})
|
|
46
|
+
if api_key:
|
|
47
|
+
self.session.headers["Authorization"] = f"Bearer {api_key}"
|
|
48
|
+
if session_token:
|
|
49
|
+
self.session.headers["X-Bau-Session"] = session_token
|
|
50
|
+
|
|
51
|
+
def _request(self, method: str, path: str, json_data: dict = None,
|
|
52
|
+
params: dict = None, auth_required: bool = True,
|
|
53
|
+
timeout: int = None) -> dict:
|
|
54
|
+
"""Make an HTTP request to the BAU API.
|
|
55
|
+
|
|
56
|
+
timeout: per-call override in seconds. Falls back to self.timeout.
|
|
57
|
+
Long server-side endpoints pass a higher floor here (e.g. rag_index).
|
|
58
|
+
"""
|
|
59
|
+
url = f"{self.api_url}/{path.lstrip('/')}"
|
|
60
|
+
eff_timeout = timeout or self.timeout
|
|
61
|
+
|
|
62
|
+
headers = {}
|
|
63
|
+
if not auth_required and self.api_key:
|
|
64
|
+
# Remove Bearer for public endpoints
|
|
65
|
+
headers["Authorization"] = ""
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
if not auth_required:
|
|
69
|
+
# Use a clean session without Bearer for public endpoints
|
|
70
|
+
resp = requests.request(
|
|
71
|
+
method, url, json=json_data, params=params,
|
|
72
|
+
timeout=eff_timeout,
|
|
73
|
+
headers={"User-Agent": f"{__product__}/{__version__}",
|
|
74
|
+
"Accept": "application/json",
|
|
75
|
+
"Content-Type": "application/json"},
|
|
76
|
+
)
|
|
77
|
+
else:
|
|
78
|
+
resp = self.session.request(
|
|
79
|
+
method, url, json=json_data, params=params,
|
|
80
|
+
timeout=eff_timeout,
|
|
81
|
+
)
|
|
82
|
+
except requests.ConnectionError:
|
|
83
|
+
raise BauApiError(f"Cannot connect to BAU Server at {self.api_url}")
|
|
84
|
+
except requests.Timeout:
|
|
85
|
+
raise BauApiError(f"Request timeout ({eff_timeout}s) to {url}")
|
|
86
|
+
|
|
87
|
+
data = self._parse_body(resp)
|
|
88
|
+
|
|
89
|
+
if resp.status_code == 401:
|
|
90
|
+
msg = (data or {}).get("message", "Unauthorized") if isinstance(data, dict) else "Unauthorized"
|
|
91
|
+
if msg in SESSION_ERROR_TAGS:
|
|
92
|
+
raise BauSessionError(msg, 401, data or {})
|
|
93
|
+
raise BauApiError(msg, 401, data or {})
|
|
94
|
+
|
|
95
|
+
if data is None:
|
|
96
|
+
# Plain-text response (e.g. deploy DDL script).
|
|
97
|
+
return {"status": "ok", "text": resp.text}
|
|
98
|
+
|
|
99
|
+
if isinstance(data, dict) and data.get("status") == "error":
|
|
100
|
+
raise BauApiError(
|
|
101
|
+
data.get("message", "Unknown error"),
|
|
102
|
+
resp.status_code, data
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
return data
|
|
106
|
+
|
|
107
|
+
@staticmethod
|
|
108
|
+
def _parse_body(resp) -> Optional[dict]:
|
|
109
|
+
"""Parse response body as JSON. Handles the ORDS plsql/block quirk
|
|
110
|
+
where OWA_UTIL.MIME_HEADER emits `Content-type: ...\\n\\n{json}` into
|
|
111
|
+
the body instead of an HTTP header. Returns None for plain-text bodies.
|
|
112
|
+
"""
|
|
113
|
+
try:
|
|
114
|
+
return resp.json()
|
|
115
|
+
except (json.JSONDecodeError, ValueError):
|
|
116
|
+
pass
|
|
117
|
+
text = resp.text
|
|
118
|
+
if text.lstrip().lower().startswith("content-type:"):
|
|
119
|
+
sep = text.find("\n\n")
|
|
120
|
+
if sep > 0:
|
|
121
|
+
try:
|
|
122
|
+
return json.loads(text[sep + 2:])
|
|
123
|
+
except (json.JSONDecodeError, ValueError):
|
|
124
|
+
pass
|
|
125
|
+
return None
|
|
126
|
+
|
|
127
|
+
# ── Public endpoints (no auth) ──────────────────────────
|
|
128
|
+
|
|
129
|
+
def health(self) -> dict:
|
|
130
|
+
"""GET /health — Server health check."""
|
|
131
|
+
return self._request("GET", "health", auth_required=False)
|
|
132
|
+
|
|
133
|
+
def license_activate(self, license_key: str) -> dict:
|
|
134
|
+
"""POST /license/activate — Activate license, receive config."""
|
|
135
|
+
return self._request("POST", "license/activate",
|
|
136
|
+
json_data={"license_key": license_key},
|
|
137
|
+
auth_required=False)
|
|
138
|
+
|
|
139
|
+
def license_validate(self, license_key: str) -> dict:
|
|
140
|
+
"""POST /license/validate — Validate license (heartbeat)."""
|
|
141
|
+
return self._request("POST", "license/validate",
|
|
142
|
+
json_data={"license_key": license_key},
|
|
143
|
+
auth_required=False)
|
|
144
|
+
|
|
145
|
+
# ── Protected endpoints (Bearer token) ──────────────────
|
|
146
|
+
|
|
147
|
+
def environments(self) -> dict:
|
|
148
|
+
"""GET /environments — read-only tenant promotion ladder (no secrets).
|
|
149
|
+
|
|
150
|
+
Topology + connectivity (engine/host/service) + the SQLcl named
|
|
151
|
+
connection HINT per env. Lets a thin client discover the ladder without
|
|
152
|
+
a direct BAUCTX login; credentials stay local (wallet/keyring)."""
|
|
153
|
+
return self._request("GET", "environments")
|
|
154
|
+
|
|
155
|
+
# ── Agent verification (Phase 1A) — read-only fact-check ─
|
|
156
|
+
# Structural ground-truth from the catalog. Every response carries
|
|
157
|
+
# ok/source/confidence/stale/in_project + exists:false first-class.
|
|
158
|
+
|
|
159
|
+
def verify_object(self, schema: str, object: str) -> dict:
|
|
160
|
+
"""GET /verify/object — does the table/view/PL-SQL/sequence exist? Type,
|
|
161
|
+
status_doc, purpose, source/confidence, in_project."""
|
|
162
|
+
return self._request("GET", "verify/object",
|
|
163
|
+
params={"schema": schema, "object": object})
|
|
164
|
+
|
|
165
|
+
def verify_column(self, schema: str, table: str, column: str) -> dict:
|
|
166
|
+
"""GET /verify/column — a single column's ground-truth: data_type,
|
|
167
|
+
nullable, PK/FK/UK, fk_ref, semantic_domain, sensitive, label, default,
|
|
168
|
+
sample_values, description + source/confidence."""
|
|
169
|
+
return self._request("GET", "verify/column",
|
|
170
|
+
params={"schema": schema, "table": table, "column": column})
|
|
171
|
+
|
|
172
|
+
def agent_columns(self, schema: str, table: str) -> dict:
|
|
173
|
+
"""GET /columns — compact list of every column of a table (name/type/
|
|
174
|
+
nullable/pk/fk/uk/domain/label/default)."""
|
|
175
|
+
return self._request("GET", "columns",
|
|
176
|
+
params={"schema": schema, "table": table})
|
|
177
|
+
|
|
178
|
+
def agent_fk(self, schema: str, table: str) -> dict:
|
|
179
|
+
"""GET /fk — foreign keys: references (this→other) + referenced_by
|
|
180
|
+
(other→this). Kills invented joins."""
|
|
181
|
+
return self._request("GET", "fk",
|
|
182
|
+
params={"schema": schema, "table": table})
|
|
183
|
+
|
|
184
|
+
def agent_domain(self, schema: str, table: str, column: str) -> dict:
|
|
185
|
+
"""GET /domain — closed enum of a column (CHECK values + PT/EN labels).
|
|
186
|
+
Kills invented enum values."""
|
|
187
|
+
return self._request("GET", "domain",
|
|
188
|
+
params={"schema": schema, "table": table, "column": column})
|
|
189
|
+
|
|
190
|
+
def agent_constraints(self, schema: str, table: str) -> dict:
|
|
191
|
+
"""GET /constraints — PK/UK/FK/CHECK of a table (+ friendly DOCERRMSG
|
|
192
|
+
message per constraint)."""
|
|
193
|
+
return self._request("GET", "constraints",
|
|
194
|
+
params={"schema": schema, "table": table})
|
|
195
|
+
|
|
196
|
+
def conventions(self, schema: str) -> dict:
|
|
197
|
+
"""GET /conventions — house style DERIVED from the catalog (PK/FK naming,
|
|
198
|
+
audit columns, active flag + values, top semantic domains) + the schema's
|
|
199
|
+
AI_CONTEXT prose (form/report standard). For generating new code that
|
|
200
|
+
matches the rest of the schema."""
|
|
201
|
+
return self._request("GET", "conventions", params={"schema": schema})
|
|
202
|
+
|
|
203
|
+
def task_brief(self, schema: str, task: str = None, tables: str = None,
|
|
204
|
+
scope: str = None, budget: int = 8000) -> dict:
|
|
205
|
+
"""GET /task_brief — one curated context pack for a task: target tables
|
|
206
|
+
(resolved from `tables` CSV > `scope` code > keyword match on `task`),
|
|
207
|
+
each with compact columns/FKs/closed-domains, plus lean house conventions
|
|
208
|
+
and the business flows that touch them. Budget-aware (`budget` chars)."""
|
|
209
|
+
params = {"schema": schema, "budget": budget}
|
|
210
|
+
if task: params["task"] = task
|
|
211
|
+
if tables: params["tables"] = tables
|
|
212
|
+
if scope: params["scope"] = scope
|
|
213
|
+
return self._request("GET", "task_brief", params=params)
|
|
214
|
+
|
|
215
|
+
def validate_change(self, schema: str, intent: dict) -> dict:
|
|
216
|
+
"""POST /validate-change — validate a structured change intent (write-loop)
|
|
217
|
+
against structure, conventions, impact and data profile. Returns
|
|
218
|
+
verdict PASS/WARN/FAIL + checks + a house-style suggested_ddl. Read-only.
|
|
219
|
+
intent: {action: ADD_COLUMN|MODIFY_NOT_NULL|ADD_UNIQUE|ADD_INDEX|ADD_FK,
|
|
220
|
+
table, column, data_type?, nullable?, references?, ref_column?}."""
|
|
221
|
+
return self._request("POST", "validate-change", json_data=intent,
|
|
222
|
+
params={"schema": schema})
|
|
223
|
+
|
|
224
|
+
def propose(self, schema: str, intent: dict) -> dict:
|
|
225
|
+
"""POST /propose — validate + STORE a change proposal (status PROPOSED).
|
|
226
|
+
Returns {proposal_id, verdict, validation}."""
|
|
227
|
+
return self._request("POST", "propose", json_data=intent,
|
|
228
|
+
params={"schema": schema})
|
|
229
|
+
|
|
230
|
+
def proposals(self, schema: str = None, status: str = None) -> dict:
|
|
231
|
+
"""GET /proposals — list change proposals (optionally by schema/status)."""
|
|
232
|
+
p = {}
|
|
233
|
+
if schema: p["schema"] = schema
|
|
234
|
+
if status: p["status"] = status
|
|
235
|
+
return self._request("GET", "proposals", params=p)
|
|
236
|
+
|
|
237
|
+
def proposal(self, proposal_id: int) -> dict:
|
|
238
|
+
"""GET /proposals/{id} — full proposal detail (DDL + validation)."""
|
|
239
|
+
return self._request("GET", f"proposals/{proposal_id}")
|
|
240
|
+
|
|
241
|
+
def proposal_regen_purpose(self, proposal_id: int) -> dict:
|
|
242
|
+
"""POST /proposals/{id}/regen-purpose — (re)generate the AI motive
|
|
243
|
+
(purpose) for a proposal. Returns {ok, id, purpose}."""
|
|
244
|
+
return self._request("POST", f"proposals/{proposal_id}/regen-purpose")
|
|
245
|
+
|
|
246
|
+
def proposal_status(self, proposal_id: int, status: str, notes: str = None) -> dict:
|
|
247
|
+
"""POST /proposals/{id}/status — approve/reject/apply a proposal."""
|
|
248
|
+
body = {"status": status}
|
|
249
|
+
if notes: body["notes"] = notes
|
|
250
|
+
return self._request("POST", f"proposals/{proposal_id}/status", json_data=body)
|
|
251
|
+
|
|
252
|
+
def profile(self, schema: str, table: str) -> dict:
|
|
253
|
+
"""GET /profile — a table's DATA PROFILE (per column: num_distinct,
|
|
254
|
+
null_pct, histogram, top_values) from optimizer stats. Grounds the
|
|
255
|
+
agent in the real data distribution."""
|
|
256
|
+
return self._request("GET", "profile", params={"schema": schema, "table": table})
|
|
257
|
+
|
|
258
|
+
def profile_ingest(self, body: dict) -> dict:
|
|
259
|
+
"""POST /profile/ingest — upload a data profile collected locally from
|
|
260
|
+
the client DB optimizer stats. Body: {schema, tables:[{name, num_rows,
|
|
261
|
+
columns:[{name, num_distinct, num_nulls, null_pct, sample_size,
|
|
262
|
+
histogram, top_values}]}]}."""
|
|
263
|
+
return self._request("POST", "profile/ingest", json_data=body)
|
|
264
|
+
|
|
265
|
+
def golden(self, schema: str) -> dict:
|
|
266
|
+
"""GET /golden — the schema's GOLDEN entities (canonical/source-of-truth
|
|
267
|
+
core tables, PL/SQL objects and flows), each with rationale + source
|
|
268
|
+
(AI/HEURISTIC/MANUAL). The authoritative map to build on."""
|
|
269
|
+
return self._request("GET", "golden", params={"schema": schema})
|
|
270
|
+
|
|
271
|
+
def quality(self, schema: str = None, check: str = None,
|
|
272
|
+
only_doc: bool = False) -> dict:
|
|
273
|
+
"""GET /quality — open documentation-quality issues for the tenant
|
|
274
|
+
(optionally a schema). Each issue has check/severity/object/message.
|
|
275
|
+
only_doc=True narrows to the "uncertain documentation" checks
|
|
276
|
+
(DOCTAB.DOC_UNCERTAIN / DOCPL.DOC_UNCERTAIN)."""
|
|
277
|
+
params = {}
|
|
278
|
+
if schema:
|
|
279
|
+
params["schema"] = schema
|
|
280
|
+
if check:
|
|
281
|
+
params["check"] = check
|
|
282
|
+
if only_doc:
|
|
283
|
+
params["only_doc"] = "Y"
|
|
284
|
+
return self._request("GET", "quality", params=params)
|
|
285
|
+
|
|
286
|
+
def quality_scan(self, schema: str) -> dict:
|
|
287
|
+
"""POST /quality/scan — (re)run the quality detectors for a schema and
|
|
288
|
+
return open-issue counts by check. Cheap; safe to run any time."""
|
|
289
|
+
return self._request("POST", "quality/scan", params={"schema": schema})
|
|
290
|
+
|
|
291
|
+
def golden_classify(self, schema: str, force: bool = False) -> dict:
|
|
292
|
+
"""POST /golden/classify — enqueue the async GOLDEN classification job
|
|
293
|
+
(heuristic + AI). Server-gated: only runs when the schema is fully
|
|
294
|
+
documented AND golden is out of date (unless force). Returns
|
|
295
|
+
{queued, job_id} or {queued: false, message}."""
|
|
296
|
+
params = {"schema": schema}
|
|
297
|
+
if force:
|
|
298
|
+
params["force"] = "Y"
|
|
299
|
+
return self._request("POST", "golden/classify", params=params)
|
|
300
|
+
|
|
301
|
+
def golden_status(self, schema: str, timeout: int = None) -> dict:
|
|
302
|
+
"""GET /golden/status — lightweight signal for the dev nudge:
|
|
303
|
+
{golden_needed, fully_documented, golden_last_at}."""
|
|
304
|
+
return self._request("GET", "golden/status", params={"schema": schema},
|
|
305
|
+
timeout=timeout)
|
|
306
|
+
|
|
307
|
+
# ---- Semantic layer (Phase 6): verified query library ----
|
|
308
|
+
def semantic_queries(self, schema: str, kind: str = None,
|
|
309
|
+
entity: str = None, verified_only: bool = False) -> dict:
|
|
310
|
+
"""GET /semantic/queries — the schema's SEMANTIC query library
|
|
311
|
+
(metrics, golden queries, rule-checks). Each item: code, kind, name,
|
|
312
|
+
question, entity, verified, verified_at, rowcount. Use verified queries
|
|
313
|
+
instead of inventing SQL."""
|
|
314
|
+
params = {"schema": schema}
|
|
315
|
+
if kind:
|
|
316
|
+
params["kind"] = kind
|
|
317
|
+
if entity:
|
|
318
|
+
params["entity"] = entity
|
|
319
|
+
if verified_only:
|
|
320
|
+
params["verified"] = "Y"
|
|
321
|
+
return self._request("GET", "semantic/queries", params=params)
|
|
322
|
+
|
|
323
|
+
def semantic_query(self, schema: str, code: str) -> dict:
|
|
324
|
+
"""GET /semantic/query — one library entry with the FULL SQL text and
|
|
325
|
+
last verified sample."""
|
|
326
|
+
return self._request("GET", "semantic/query",
|
|
327
|
+
params={"schema": schema, "code": code})
|
|
328
|
+
|
|
329
|
+
def semantic_search(self, schema: str, q: str, k: int = 3) -> dict:
|
|
330
|
+
"""GET /semantic/search — resolve a query by CODE (exact) or by MEANING
|
|
331
|
+
(natural-language question, via RAG over kind='QUERY'). Returns
|
|
332
|
+
{match:'code', query} or {match:'semantic', results:[{code, question,
|
|
333
|
+
entity, verified, distance, sql}]} or {match:'none', note}.
|
|
334
|
+
|
|
335
|
+
NB: o param vai como `question` (NÃO `q`) — `q` é reservado pelo ORDS
|
|
336
|
+
(filtro ?q={JSON}) e derruba a request com 400 (ver server script 216)."""
|
|
337
|
+
return self._request("GET", "semantic/search",
|
|
338
|
+
params={"schema": schema, "question": q, "k": k})
|
|
339
|
+
|
|
340
|
+
def semantic_pending(self, schema: str) -> dict:
|
|
341
|
+
"""GET /semantic/pending — library entries not yet verified by
|
|
342
|
+
execution. `semantic verify` fetches these, runs them read-only against
|
|
343
|
+
the local DB, and posts the result back."""
|
|
344
|
+
return self._request("GET", "semantic/pending", params={"schema": schema})
|
|
345
|
+
|
|
346
|
+
def semantic_register(self, body: dict) -> dict:
|
|
347
|
+
"""POST /semantic/register — register/upsert a query by (schema, code).
|
|
348
|
+
Body: {schema, code, kind, name, question, description, grain, entity,
|
|
349
|
+
sql, result_kind}. Changing the SQL resets its verified flag."""
|
|
350
|
+
return self._request("POST", "semantic/register", json_data=body)
|
|
351
|
+
|
|
352
|
+
def semantic_suggest(self, schema: str, max_n: int = 15) -> dict:
|
|
353
|
+
"""POST /semantic/suggest — ASSÍNCRONO: enfileira um job SEMANTIC_SEED em
|
|
354
|
+
que a IA propõe métricas/rule-checks das tabelas/CHECKs/colunas de
|
|
355
|
+
domínio DOCUMENTADAS e grava as NOVAS como não-verificadas (source=AI)
|
|
356
|
+
para curadoria. Retorna {ok, queued, job_id, message} na hora — a IA roda
|
|
357
|
+
no drain do servidor (a chamada síncrona estourava o timeout do gateway).
|
|
358
|
+
As sugestões aparecem quando o job concluir (App 601 › Consultas
|
|
359
|
+
Semânticas / baucli semantic list)."""
|
|
360
|
+
return self._request("POST", "semantic/suggest",
|
|
361
|
+
params={"schema": schema, "max": max_n})
|
|
362
|
+
|
|
363
|
+
def semantic_verify(self, schema: str, code: str, result: dict) -> dict:
|
|
364
|
+
"""POST /semantic/verify — report the outcome of running a query
|
|
365
|
+
read-only. result: {source, rowcount, ms, sample, error}. error=None
|
|
366
|
+
marks it verified; a non-null error keeps it unverified."""
|
|
367
|
+
return self._request("POST", "semantic/verify",
|
|
368
|
+
params={"schema": schema, "code": code},
|
|
369
|
+
json_data=result)
|
|
370
|
+
|
|
371
|
+
def scope(self, code: str) -> dict:
|
|
372
|
+
"""GET /scopes/{code} — a project scope set (BAU_SCOPE_SETS) as JSON:
|
|
373
|
+
the named object subset a project needs. Used by `sync --scope` to send
|
|
374
|
+
only the listed objects. Returns {"error": ...} when the code is unknown."""
|
|
375
|
+
return self._request("GET", f"scopes/{code}")
|
|
376
|
+
|
|
377
|
+
def scopes_list(self) -> dict:
|
|
378
|
+
"""GET /scopes — all scope sets of the tenant."""
|
|
379
|
+
return self._request("GET", "scopes")
|
|
380
|
+
|
|
381
|
+
def scope_create(self, code: str, name: str, schema: str,
|
|
382
|
+
description: str = None) -> dict:
|
|
383
|
+
"""POST /scopes — create a scope set (schema must be registered)."""
|
|
384
|
+
body = {"code": code, "name": name, "schema": schema}
|
|
385
|
+
if description:
|
|
386
|
+
body["description"] = description
|
|
387
|
+
return self._request("POST", "scopes", json_data=body)
|
|
388
|
+
|
|
389
|
+
def scope_add_item(self, code: str, otype: str, owner: str, name: str,
|
|
390
|
+
subprogram: str = None, include_kind: str = None,
|
|
391
|
+
note: str = None) -> dict:
|
|
392
|
+
"""POST /scopes/{code}/items — add one object to a scope set."""
|
|
393
|
+
body = {"type": otype, "owner": owner, "name": name}
|
|
394
|
+
if subprogram:
|
|
395
|
+
body["subprogram"] = subprogram
|
|
396
|
+
if include_kind:
|
|
397
|
+
body["include_kind"] = include_kind
|
|
398
|
+
if note:
|
|
399
|
+
body["note"] = note
|
|
400
|
+
return self._request("POST", f"scopes/{code}/items", json_data=body)
|
|
401
|
+
|
|
402
|
+
def scope_remove_item(self, code: str, item_id: int) -> dict:
|
|
403
|
+
"""DELETE /scopes/{code}/items/{id} — remove one scope item."""
|
|
404
|
+
return self._request("DELETE", f"scopes/{code}/items/{item_id}")
|
|
405
|
+
|
|
406
|
+
def scope_resolve(self, code: str) -> dict:
|
|
407
|
+
"""POST /scopes/{code}/resolve — bind scope items to the synced catalog
|
|
408
|
+
rows (BAU_SCOPE_ITEMS.*_ID). Returns {ok,linked,total,unresolved}."""
|
|
409
|
+
return self._request("POST", f"scopes/{code}/resolve", json_data={})
|
|
410
|
+
|
|
411
|
+
# ---- DOCMIG (migração de legado -> APEX) --------------------------
|
|
412
|
+
def docmig_import(self, payload: dict) -> dict:
|
|
413
|
+
"""POST /docmig/import — upsert the legacy-screen inventory (common
|
|
414
|
+
model: project/screens/blocks/fields/actions/reports). The server
|
|
415
|
+
resolves blocks/fields to the documented DOCTAB tables/columns."""
|
|
416
|
+
return self._request("POST", "docmig/import", json_data=payload)
|
|
417
|
+
|
|
418
|
+
def docmig_projects(self) -> dict:
|
|
419
|
+
"""GET /docmig/projects — migration projects of the tenant."""
|
|
420
|
+
return self._request("GET", "docmig/projects")
|
|
421
|
+
|
|
422
|
+
def docmig_project(self, code: str) -> dict:
|
|
423
|
+
"""GET /docmig/projects/{code} — project overview + screens/reports."""
|
|
424
|
+
return self._request("GET", f"docmig/projects/{code}")
|
|
425
|
+
|
|
426
|
+
def docmig_screen(self, screen_id: int) -> dict:
|
|
427
|
+
"""GET /docmig/screens/{id} — full screen inventory with DOCTAB
|
|
428
|
+
bindings (labels, domains, form item types) — the AI blueprint."""
|
|
429
|
+
return self._request("GET", f"docmig/screens/{screen_id}")
|
|
430
|
+
|
|
431
|
+
def docmig_report(self, report_id: int) -> dict:
|
|
432
|
+
"""GET /docmig/reports/{id} — legacy report + columns."""
|
|
433
|
+
return self._request("GET", f"docmig/reports/{report_id}")
|
|
434
|
+
|
|
435
|
+
# ---- DOCAGENT (Advisor de Agentes de IA) --------------------------
|
|
436
|
+
def agents_list(self) -> dict:
|
|
437
|
+
"""GET /agents — agentes de IA sugeridos do tenant (DOCAGENT)."""
|
|
438
|
+
return self._request("GET", "agents")
|
|
439
|
+
|
|
440
|
+
def agent(self, code: str) -> dict:
|
|
441
|
+
"""Alias of agent_get (kept for the bau_agents MCP tool)."""
|
|
442
|
+
return self.agent_get(code)
|
|
443
|
+
|
|
444
|
+
def agent_get(self, code: str) -> dict:
|
|
445
|
+
"""GET /agents/{code} — um agente sugerido + refs de ancoragem."""
|
|
446
|
+
return self._request("GET", f"agents/{code}")
|
|
447
|
+
|
|
448
|
+
def agents_suggest(self, schema: str, max_agents: int = 6) -> dict:
|
|
449
|
+
"""POST /agents/suggest — enfileira análise assíncrona do negócio de um schema."""
|
|
450
|
+
return self._request("POST", "agents/suggest",
|
|
451
|
+
json_data={"schema": schema, "max": max_agents})
|
|
452
|
+
|
|
453
|
+
def agent_approve(self, code: str) -> dict:
|
|
454
|
+
"""POST /agents/{code}/approve — aprova um agente sugerido."""
|
|
455
|
+
return self._request("POST", f"agents/{code}/approve")
|
|
456
|
+
|
|
457
|
+
def impact(self, schema: str, obj: str, direction: str = "DOWN",
|
|
458
|
+
depth: int = 5, report: bool = False, lang: str = "pt_BR") -> dict:
|
|
459
|
+
"""GET /impact/:schema/:object[/report] — dependency-graph impact analysis.
|
|
460
|
+
|
|
461
|
+
report=False → grafo JSON (conjunto impactado); report=True → laudo de
|
|
462
|
+
negócio gerado por IA (Markdown, dentro de {"report": ...}).
|
|
463
|
+
"""
|
|
464
|
+
path = f"impact/{schema}/{obj}" + ("/report" if report else "")
|
|
465
|
+
params = {"dir": direction, "depth": depth}
|
|
466
|
+
if report:
|
|
467
|
+
params["lang"] = lang
|
|
468
|
+
# O laudo (/report) faz uma chamada de IA síncrona no servidor — pode
|
|
469
|
+
# passar dos 30s default. Piso maior só para o report; o grafo é rápido.
|
|
470
|
+
return self._request("GET", path, params=params,
|
|
471
|
+
timeout=(max(self.timeout, 180) if report else None))
|
|
472
|
+
|
|
473
|
+
def impact_report_queue(self, schema: str, obj: str, direction: str = "DOWN",
|
|
474
|
+
depth: int = 5, lang: str = "pt_BR") -> dict:
|
|
475
|
+
"""POST /impact/:schema/:object/report/queue — enfileira um job
|
|
476
|
+
assíncrono que gera o laudo de impacto por IA e persiste em BAU_REPORTS
|
|
477
|
+
(IMPACT/HTML). Retorna {queued, job_id, message}. O resultado aparece em
|
|
478
|
+
App 601 › Análise de Impacto (p71) / Relatórios. Substitui a chamada
|
|
479
|
+
síncrona que estourava o timeout do gateway."""
|
|
480
|
+
return self._request("POST", f"impact/{schema}/{obj}/report/queue",
|
|
481
|
+
params={"dir": direction, "depth": depth, "lang": lang})
|
|
482
|
+
|
|
483
|
+
def audit_enqueue(self, schema: str, tables: str = None, schedule: str = None,
|
|
484
|
+
provider_id: int = None, reason: str = None) -> dict:
|
|
485
|
+
"""POST /audit — enfileira o DA virtual (auditoria de modelagem por IA)
|
|
486
|
+
como job assíncrono e volta NA HORA. Audita o schema inteiro ou só
|
|
487
|
+
`tables` (CSV). Cada achado sério vira uma proposta no App 601 (validada
|
|
488
|
+
pelo write-loop). Retorna {ok, job_id, enqueued, status:'QUEUED',
|
|
489
|
+
custo_ia_est}. Acompanhe com audit_status / audit_findings."""
|
|
490
|
+
body = {"schema": schema}
|
|
491
|
+
if tables: body["tables"] = tables
|
|
492
|
+
if schedule: body["schedule"] = schedule
|
|
493
|
+
if provider_id: body["provider_id"] = provider_id
|
|
494
|
+
if reason: body["reason"] = reason
|
|
495
|
+
return self._request("POST", "audit", json_data=body)
|
|
496
|
+
|
|
497
|
+
def audit_status(self, job_id: int) -> dict:
|
|
498
|
+
"""GET /audit/{job}/status — progresso do run (itens_total/feitos/falha,
|
|
499
|
+
propostas_geradas, iniciado/concluido_em, status QUEUED|RUNNING|DONE|FAILED)."""
|
|
500
|
+
return self._request("GET", f"audit/{int(job_id)}/status")
|
|
501
|
+
|
|
502
|
+
def audit_findings(self, job_id: int) -> dict:
|
|
503
|
+
"""GET /audit/{job}/findings — as propostas geradas pelo run (App 601):
|
|
504
|
+
proposal_id, tabela, coluna, severidade, categoria, achado, action,
|
|
505
|
+
suggested_ddl, verdict."""
|
|
506
|
+
return self._request("GET", f"audit/{int(job_id)}/findings")
|
|
507
|
+
|
|
508
|
+
def flows_list(self, schema: str) -> dict:
|
|
509
|
+
"""GET /flows/{schema} — index of structured business flows (DOCFLOW)."""
|
|
510
|
+
return self._request("GET", f"flows/{schema}")
|
|
511
|
+
|
|
512
|
+
def flow(self, schema: str, obj: str, subprogram: str = None,
|
|
513
|
+
include_draft: bool = False) -> dict:
|
|
514
|
+
"""GET /flows/{schema}/{object} — structured business flow JSON
|
|
515
|
+
(steps / refs / rules). Only DOCUMENTED unless include_draft."""
|
|
516
|
+
params = {}
|
|
517
|
+
if subprogram:
|
|
518
|
+
params["subprogram"] = subprogram
|
|
519
|
+
if include_draft:
|
|
520
|
+
params["draft"] = "Y"
|
|
521
|
+
return self._request("GET", f"flows/{schema}/{obj}", params=params or None)
|
|
522
|
+
|
|
523
|
+
def rag_index(self, schema: str) -> dict:
|
|
524
|
+
"""POST /rag/index/{schema} — refresh chunks + embed IN-DB (server-side,
|
|
525
|
+
via VECTOR_EMBEDDING). Sem cálculo na borda. Retorna {chunks, embedded,
|
|
526
|
+
pending, model, ok}. Long server-side embed: floor the timeout at 300s
|
|
527
|
+
so a low configured default doesn't kill the call."""
|
|
528
|
+
return self._request("POST", f"rag/index/{schema}", json_data={},
|
|
529
|
+
timeout=max(self.timeout, 300))
|
|
530
|
+
|
|
531
|
+
def rag_search(self, schema: str, query: str, top_k: int = 8) -> dict:
|
|
532
|
+
"""POST /rag/search/{schema} — busca semântica por TEXTO; o servidor
|
|
533
|
+
embeda a pergunta IN-DB e devolve top-k chunks do catálogo aprovado."""
|
|
534
|
+
return self._request("POST", f"rag/search/{schema}",
|
|
535
|
+
json_data={"query": query, "top_k": top_k})
|
|
536
|
+
|
|
537
|
+
def docs_system(self, application_id: int, audience: str = "USER",
|
|
538
|
+
fmt: str = "HTML", language: str = "pt_BR") -> dict:
|
|
539
|
+
"""POST /docs/system — generate the System Documentation for an APEX app
|
|
540
|
+
and stream it back. Returns {content: bytes, report_id, content_type}.
|
|
541
|
+
|
|
542
|
+
Deterministic composer (no AI): reuses the approved DOCFLOW/DOCTAB/DOCPL
|
|
543
|
+
catalog. audience USER=user manual, TECH=+field tables & data model.
|
|
544
|
+
format HTML or DOCX (Word-compatible)."""
|
|
545
|
+
url = f"{self.api_url}/docs/system"
|
|
546
|
+
try:
|
|
547
|
+
resp = self.session.request(
|
|
548
|
+
"POST", url,
|
|
549
|
+
json={"application_id": application_id, "audience": audience,
|
|
550
|
+
"format": fmt, "language": language},
|
|
551
|
+
timeout=max(self.timeout, 120),
|
|
552
|
+
)
|
|
553
|
+
except requests.ConnectionError:
|
|
554
|
+
raise BauApiError(f"Cannot connect to BAU Server at {self.api_url}")
|
|
555
|
+
except requests.Timeout:
|
|
556
|
+
raise BauApiError(f"Request timeout to {url}")
|
|
557
|
+
if resp.status_code == 401:
|
|
558
|
+
raise BauApiError("Unauthorized", 401)
|
|
559
|
+
if resp.status_code == 404:
|
|
560
|
+
raise BauApiError("Aplicação não encontrada no BAU (rode 'baucli apex sync' antes)", 404)
|
|
561
|
+
if resp.status_code >= 400:
|
|
562
|
+
raise BauApiError(f"Server error {resp.status_code}", resp.status_code)
|
|
563
|
+
return {
|
|
564
|
+
"content": resp.content,
|
|
565
|
+
"report_id": resp.headers.get("X-Bau-Report-Id"),
|
|
566
|
+
"content_type": resp.headers.get("Content-Type", ""),
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
def ai_limits(self) -> dict:
|
|
570
|
+
"""GET /limits — per-analyst AI limits of the tenant + live month usage.
|
|
571
|
+
|
|
572
|
+
Read-only oversight (management is done in App 601). Returns
|
|
573
|
+
{"status":"ok","text": <json array>} or the parsed list."""
|
|
574
|
+
return self._request("GET", "limits")
|
|
575
|
+
|
|
576
|
+
def sync_status(self, schema: str) -> dict:
|
|
577
|
+
"""GET /sync/status?schema=X — Get sync recommendation."""
|
|
578
|
+
return self._request("GET", "sync/status", params={"schema": schema})
|
|
579
|
+
|
|
580
|
+
def sync_batch(self, schema: str, db_engine: str, objects: list,
|
|
581
|
+
db_version: str = None,
|
|
582
|
+
overwrite_undocumented: bool = False,
|
|
583
|
+
dependencies: list = None,
|
|
584
|
+
sequences: list = None,
|
|
585
|
+
db_name: str = None) -> dict:
|
|
586
|
+
"""POST /sync/batch — Send objects to BAU Server.
|
|
587
|
+
|
|
588
|
+
dependencies: array de deps do dicionario (grafo). Enviado numa chamada
|
|
589
|
+
dedicada (objects=[]); o servidor faz replace por schema em BAU_DEP_RAW.
|
|
590
|
+
db_name: nome do database registrado. Enviado para que um tenant com
|
|
591
|
+
MAIS DE UM database ativo resolva sem ambiguidade (senao o servidor
|
|
592
|
+
levanta ORA-20453 ao auto-selecionar).
|
|
593
|
+
"""
|
|
594
|
+
payload = {
|
|
595
|
+
"schema": schema,
|
|
596
|
+
"db_engine": db_engine,
|
|
597
|
+
"objects": objects,
|
|
598
|
+
}
|
|
599
|
+
if db_version:
|
|
600
|
+
payload["db_version"] = db_version
|
|
601
|
+
if overwrite_undocumented:
|
|
602
|
+
payload["overwrite_undocumented"] = "Y"
|
|
603
|
+
if dependencies is not None:
|
|
604
|
+
payload["dependencies"] = dependencies
|
|
605
|
+
if sequences is not None:
|
|
606
|
+
payload["sequences"] = sequences
|
|
607
|
+
if db_name:
|
|
608
|
+
payload["db_name"] = db_name
|
|
609
|
+
return self._request("POST", "sync/batch", json_data=payload)
|
|
610
|
+
|
|
611
|
+
def source_upload(self, schema: str, name: str, obj_type: str,
|
|
612
|
+
source: str, db_engine: str = "ORACLE") -> dict:
|
|
613
|
+
"""POST /source/upload — Upload single object source."""
|
|
614
|
+
return self._request("POST", "source/upload", json_data={
|
|
615
|
+
"schema": schema, "name": name, "type": obj_type,
|
|
616
|
+
"source": source, "db_engine": db_engine,
|
|
617
|
+
})
|
|
618
|
+
|
|
619
|
+
def get_source(self, schema: str, name: str) -> dict:
|
|
620
|
+
"""GET /source/{schema}/{name} — Get current source."""
|
|
621
|
+
return self._request("GET", f"source/{schema}/{name}")
|
|
622
|
+
|
|
623
|
+
def get_source_hashes(self, schema: str) -> dict:
|
|
624
|
+
"""GET /source/hashes/{schema} — Current source hash per object (drift)."""
|
|
625
|
+
return self._request("GET", f"source/hashes/{schema}")
|
|
626
|
+
|
|
627
|
+
def get_source_history(self, schema: str, name: str) -> dict:
|
|
628
|
+
"""GET /source/history/{schema}/{name} — All source revisions for one object."""
|
|
629
|
+
return self._request("GET", f"source/history/{schema}/{name}")
|
|
630
|
+
|
|
631
|
+
def get_table_hashes(self, schema: str) -> dict:
|
|
632
|
+
"""GET /tables/hashes/{schema} — Canonical column signature hash per table (drift)."""
|
|
633
|
+
return self._request("GET", f"tables/hashes/{schema}")
|
|
634
|
+
|
|
635
|
+
def get_table_columns_diff(self, schema: str, table: str) -> dict:
|
|
636
|
+
"""GET /tables/columns/{schema}/{table} — Column list shaped for per-column diff."""
|
|
637
|
+
return self._request("GET", f"tables/columns/{schema}/{table}")
|
|
638
|
+
|
|
639
|
+
def get_columns_ui(self, schema: str, table: str) -> dict:
|
|
640
|
+
"""GET /columns/{schema}/{table} — Rich UI metadata used by `apex-rebuild`.
|
|
641
|
+
|
|
642
|
+
Returns {"columns": [...]} where each entry carries label, align_form,
|
|
643
|
+
align_report, format_mask, text_case, hidden, read_only, show_in_forms,
|
|
644
|
+
show_in_reports, *_display_order, lov_display_order, description,
|
|
645
|
+
form_item_type (exact APEXlang token, e.g. "checkbox"/"selectList") and
|
|
646
|
+
domain_values ([{value,label}, ...] from the column's CHECK constraint)
|
|
647
|
+
— the last two let `apex-rebuild` retype an item and synthesize its
|
|
648
|
+
static LOV / single-checkbox checked&unchecked values from the domain.
|
|
649
|
+
"""
|
|
650
|
+
return self._request("GET", f"columns/{schema}/{table}")
|
|
651
|
+
|
|
652
|
+
def objects_changed(self, days: int = 30, schema_name: str = None) -> dict:
|
|
653
|
+
"""GET /objects/changed — Modified objects."""
|
|
654
|
+
params = {"days": days}
|
|
655
|
+
if schema_name:
|
|
656
|
+
params["schema_name"] = schema_name
|
|
657
|
+
return self._request("GET", "objects/changed", params=params)
|
|
658
|
+
|
|
659
|
+
def deploys_pending(self, schema: str = None, status: str = None) -> dict:
|
|
660
|
+
"""GET /deploys/pending — Pending deploy queue."""
|
|
661
|
+
params = {}
|
|
662
|
+
if schema:
|
|
663
|
+
params["schema"] = schema
|
|
664
|
+
if status:
|
|
665
|
+
params["status"] = status
|
|
666
|
+
query = "&".join(f"{k}={v}" for k, v in params.items())
|
|
667
|
+
endpoint = "deploys/pending"
|
|
668
|
+
if query:
|
|
669
|
+
endpoint += "?" + query
|
|
670
|
+
return self._request("GET", endpoint)
|
|
671
|
+
|
|
672
|
+
def deploy_review(self, deploy_id: int, action: str,
|
|
673
|
+
reviewer: str = None, notes: str = None) -> dict:
|
|
674
|
+
"""POST /deploys/{id}/review — Approve or reject."""
|
|
675
|
+
payload = {"action": action}
|
|
676
|
+
if reviewer:
|
|
677
|
+
payload["reviewer"] = reviewer
|
|
678
|
+
if notes:
|
|
679
|
+
payload["notes"] = notes
|
|
680
|
+
return self._request("POST", f"deploys/{deploy_id}/review", json_data=payload)
|
|
681
|
+
|
|
682
|
+
def deploy_script(self, deploy_id: int) -> dict:
|
|
683
|
+
"""GET /deploys/{id}/script — Download DDL script."""
|
|
684
|
+
return self._request("GET", f"deploys/{deploy_id}/script")
|
|
685
|
+
|
|
686
|
+
def deploy_confirm(self, deploy_id: int, success: bool, error: str = None) -> dict:
|
|
687
|
+
"""POST /deploys/{id}/confirm — Mark deploy APPLIED or FAILED after local execution."""
|
|
688
|
+
payload = {"success": "Y" if success else "N"}
|
|
689
|
+
if error:
|
|
690
|
+
payload["error"] = error[:4000]
|
|
691
|
+
return self._request("POST", f"deploys/{deploy_id}/confirm", json_data=payload)
|
|
692
|
+
|
|
693
|
+
# ── DOCTAB deploy queue (ANNOTATIONS / COMMENT ON per table) ──────────
|
|
694
|
+
def doctab_deploys_pending(self, schema: str = None, status: str = None,
|
|
695
|
+
limit: int = 100) -> dict:
|
|
696
|
+
"""GET /doctab/deploys/pending — Pending DOCTAB deploys."""
|
|
697
|
+
params = {"p_limit": limit}
|
|
698
|
+
if schema:
|
|
699
|
+
params["schema"] = schema
|
|
700
|
+
if status:
|
|
701
|
+
params["status"] = status
|
|
702
|
+
return self._request("GET", "doctab/deploys/pending", params=params)
|
|
703
|
+
|
|
704
|
+
def doctab_deploy_script(self, deploy_id: int) -> dict:
|
|
705
|
+
"""GET /doctab/deploys/{id}/script — Download DOCTAB DDL script."""
|
|
706
|
+
return self._request("GET", f"doctab/deploys/{deploy_id}/script")
|
|
707
|
+
|
|
708
|
+
def doctab_deploy_preview(self, deploy_id: int) -> dict:
|
|
709
|
+
"""GET /doctab/deploys/{id}/preview — Preview DOCTAB deploy metadata."""
|
|
710
|
+
return self._request("GET", f"doctab/deploys/{deploy_id}/preview")
|
|
711
|
+
|
|
712
|
+
def doctab_deploy_confirm(self, deploy_id: int, success: bool,
|
|
713
|
+
error: str = None) -> dict:
|
|
714
|
+
"""POST /doctab/deploys/{id}/confirm — Mark DOCTAB deploy APPLIED or FAILED."""
|
|
715
|
+
payload = {"success": "Y" if success else "N"}
|
|
716
|
+
if error:
|
|
717
|
+
payload["error"] = error[:4000]
|
|
718
|
+
return self._request("POST", f"doctab/deploys/{deploy_id}/confirm",
|
|
719
|
+
json_data=payload)
|
|
720
|
+
|
|
721
|
+
def deploy_preview(self, deploy_id: int) -> dict:
|
|
722
|
+
"""GET /deploys/{id}/preview — Preview header and metadata."""
|
|
723
|
+
return self._request("GET", f"deploys/{deploy_id}/preview")
|
|
724
|
+
|
|
725
|
+
def doctab_get_script(self, schema_name: str) -> dict:
|
|
726
|
+
"""GET /doctab/script/{schema_name} — Full COMMENT ON DDL script."""
|
|
727
|
+
return self._request("GET", f"doctab/script/{schema_name}")
|
|
728
|
+
|
|
729
|
+
def annotations_get_table(self, schema: str, table: str) -> dict:
|
|
730
|
+
"""GET /annotations/{schema}/{table}/script — Oracle 23ai+ DDL for one table."""
|
|
731
|
+
return self._request("GET", f"annotations/{schema}/{table}/script")
|
|
732
|
+
|
|
733
|
+
def annotations_get_file(self, schema: str) -> dict:
|
|
734
|
+
"""GET /annotations/{schema}/file — Oracle 23ai+ DDL for every table in the schema."""
|
|
735
|
+
return self._request("GET", f"annotations/{schema}/file")
|
|
736
|
+
|
|
737
|
+
def get_ui_kit(self, schema: str, engine: str = "ORACLE",
|
|
738
|
+
language: str = "EN", only: str = None) -> str:
|
|
739
|
+
"""GET /ui-kit/{schema} — UI Kit bundle (domains + errors + APEX handler).
|
|
740
|
+
|
|
741
|
+
Returns the script as plain text. `only` accepts a CSV combo of
|
|
742
|
+
DOMAINS, ERRORS, HANDLER (None or empty = all three).
|
|
743
|
+
"""
|
|
744
|
+
params = {"engine": engine, "lang": language}
|
|
745
|
+
if only:
|
|
746
|
+
params["only"] = only
|
|
747
|
+
result = self._request("GET", f"ui-kit/{schema}", params=params)
|
|
748
|
+
return result.get("text", "") if isinstance(result, dict) else str(result)
|
|
749
|
+
|
|
750
|
+
def context_languages(self, schema: str) -> dict:
|
|
751
|
+
"""GET /context/{schema}/languages — JSON {primary, languages[]} for a schema.
|
|
752
|
+
|
|
753
|
+
Drives the bilingual decision in `bau context`: when len(languages) > 1
|
|
754
|
+
and the user didn't pin a single `--language`, the agent fetches one
|
|
755
|
+
context per language and writes a file per locale.
|
|
756
|
+
"""
|
|
757
|
+
return self._request("GET", f"context/{schema}/languages")
|
|
758
|
+
|
|
759
|
+
def viewer_list(self, schema: str, status: str = None, obj_type: str = None) -> dict:
|
|
760
|
+
"""GET /viewer/{schema} — List objects + tables for viewer."""
|
|
761
|
+
params = {}
|
|
762
|
+
if status:
|
|
763
|
+
params["status"] = status
|
|
764
|
+
if obj_type:
|
|
765
|
+
params["type"] = obj_type
|
|
766
|
+
query = "&".join(f"{k}={v}" for k, v in params.items())
|
|
767
|
+
endpoint = f"viewer/{schema}"
|
|
768
|
+
if query:
|
|
769
|
+
endpoint += "?" + query
|
|
770
|
+
return self._request("GET", endpoint)
|
|
771
|
+
|
|
772
|
+
def viewer_detail(self, category: str, item_id: int, language: str = None) -> dict:
|
|
773
|
+
"""GET /viewer/detail/{OBJ|TBL}/{id} — Full documentation detail.
|
|
774
|
+
|
|
775
|
+
When `language` is set (e.g. 'pt_BR'), the server replaces EN canonical
|
|
776
|
+
text (purpose, description, prompt, business_domain) with the matching
|
|
777
|
+
translation from BAU_DOCTRANSLATE_VALUES. Falls back to EN when no
|
|
778
|
+
translation exists.
|
|
779
|
+
"""
|
|
780
|
+
params = {"language": language} if language else None
|
|
781
|
+
return self._request("GET", f"viewer/detail/{category}/{item_id}", params=params)
|
|
782
|
+
|
|
783
|
+
def get_context(self, schema: str, language: str = "EN",
|
|
784
|
+
tables: str = None, objects: str = None,
|
|
785
|
+
scope: str = None) -> dict:
|
|
786
|
+
"""GET /context/{schema} — AI context markdown.
|
|
787
|
+
|
|
788
|
+
`tables` and `objects` are optional comma-separated names that
|
|
789
|
+
narrow the markdown to just those tables / PL/SQL objects. When
|
|
790
|
+
both are NULL the full schema context is returned (legacy behavior).
|
|
791
|
+
"""
|
|
792
|
+
params = {"language": language}
|
|
793
|
+
if tables:
|
|
794
|
+
params["tables"] = tables
|
|
795
|
+
if objects:
|
|
796
|
+
params["objects"] = objects
|
|
797
|
+
if scope:
|
|
798
|
+
params["scope"] = scope # 'preamble' = business context only
|
|
799
|
+
return self._request("GET", f"context/{schema}", params=params)
|
|
800
|
+
|
|
801
|
+
def database_register(self, db_name: str, db_engine: str,
|
|
802
|
+
db_version: str = None, host: str = None,
|
|
803
|
+
port: int = None, native_identifier: str = None) -> dict:
|
|
804
|
+
"""POST /database/register — Register database UUID."""
|
|
805
|
+
payload = {"db_name": db_name, "db_engine": db_engine}
|
|
806
|
+
if db_version:
|
|
807
|
+
payload["db_version"] = db_version
|
|
808
|
+
if host:
|
|
809
|
+
payload["host"] = host
|
|
810
|
+
if port:
|
|
811
|
+
payload["port"] = port
|
|
812
|
+
if native_identifier:
|
|
813
|
+
payload["native_identifier"] = native_identifier
|
|
814
|
+
return self._request("POST", "database/register", json_data=payload)
|
|
815
|
+
|
|
816
|
+
def database_validate(self, bau_database_uuid: str) -> dict:
|
|
817
|
+
"""POST /database/validate — Validate database fingerprint."""
|
|
818
|
+
return self._request("POST", "database/validate",
|
|
819
|
+
json_data={"bau_database_uuid": bau_database_uuid})
|
|
820
|
+
|
|
821
|
+
def get_quota(self) -> dict:
|
|
822
|
+
"""GET /quota — Full quota status."""
|
|
823
|
+
return self._request("GET", "quota")
|
|
824
|
+
|
|
825
|
+
# ── DOCAPEX endpoints ──
|
|
826
|
+
|
|
827
|
+
def apex_sync(self, payload: dict) -> dict:
|
|
828
|
+
"""POST /apex/sync — Send full APEX app snapshot to the server.
|
|
829
|
+
|
|
830
|
+
`payload` must follow the SYNC_APEX_APP contract: {schema, app, pages,
|
|
831
|
+
regions, items, proc, da}. Use OracleExtractor.extract_apex to build it.
|
|
832
|
+
"""
|
|
833
|
+
return self._request("POST", "apex/sync", json_data=payload)
|
|
834
|
+
|
|
835
|
+
def apex_hashes(self, schema: str, application_id: int) -> dict:
|
|
836
|
+
"""GET /apex/hashes/{schema}/{app_id} — Per-row HASH_CURRENT for drift."""
|
|
837
|
+
return self._request("GET", f"apex/hashes/{schema}/{application_id}")
|
|
838
|
+
|
|
839
|
+
def apex_screenshot_plan(self, schema: str, application_id: int) -> dict:
|
|
840
|
+
"""GET /apex/screenshot-plan/{schema}/{app} — capture recipe per modal.
|
|
841
|
+
|
|
842
|
+
Returns {"status":"ok","modals":[{page_id, launch_page, launch_items,
|
|
843
|
+
launch_selector, dialog_selector, wait_ms}, ...]} documented by the
|
|
844
|
+
analyst in App 601 (p42). Used to open modal pages deterministically.
|
|
845
|
+
"""
|
|
846
|
+
return self._request(
|
|
847
|
+
"GET", f"apex/screenshot-plan/{schema}/{application_id}")
|
|
848
|
+
|
|
849
|
+
def apex_upload_screenshot(self, application_id: int, page_no: int,
|
|
850
|
+
image_bytes: bytes,
|
|
851
|
+
mime: str = "image/png") -> dict:
|
|
852
|
+
"""POST /apex/apps/{app}/pages/{page_no}/screenshot — raw image bytes.
|
|
853
|
+
|
|
854
|
+
Uploads a page screenshot by NATURAL KEY (APEX app id + page number);
|
|
855
|
+
the server resolves the BAU_DOCAPEX_PAGES row and routes to DB or the
|
|
856
|
+
OCI bucket per config. `image_bytes` is the raw PNG/JPEG/WEBP content.
|
|
857
|
+
"""
|
|
858
|
+
url = (f"{self.api_url}/apex/apps/{application_id}"
|
|
859
|
+
f"/pages/{page_no}/screenshot")
|
|
860
|
+
try:
|
|
861
|
+
resp = self.session.request(
|
|
862
|
+
"POST", url, data=image_bytes,
|
|
863
|
+
headers={"Content-Type": mime},
|
|
864
|
+
timeout=max(self.timeout, 60),
|
|
865
|
+
)
|
|
866
|
+
except requests.ConnectionError:
|
|
867
|
+
raise BauApiError(f"Cannot connect to BAU Server at {self.api_url}")
|
|
868
|
+
except requests.Timeout:
|
|
869
|
+
raise BauApiError(f"Request timeout ({max(self.timeout, 60)}s) to {url}")
|
|
870
|
+
|
|
871
|
+
data = self._parse_body(resp)
|
|
872
|
+
if resp.status_code == 401:
|
|
873
|
+
msg = (data or {}).get("message", "Unauthorized") if isinstance(data, dict) else "Unauthorized"
|
|
874
|
+
if msg in SESSION_ERROR_TAGS:
|
|
875
|
+
raise BauSessionError(msg, 401, data or {})
|
|
876
|
+
raise BauApiError(msg, 401, data or {})
|
|
877
|
+
if isinstance(data, dict) and data.get("status") == "error":
|
|
878
|
+
raise BauApiError(data.get("message", "Unknown error"),
|
|
879
|
+
resp.status_code, data)
|
|
880
|
+
if data is None:
|
|
881
|
+
return {"status": "ok", "text": resp.text}
|
|
882
|
+
return data
|
|
883
|
+
|
|
884
|
+
def apex_ai_generate(self, level: str, row_id: int,
|
|
885
|
+
provider_id: int = None) -> dict:
|
|
886
|
+
"""POST /apex/ai/generate/{level}/{id} — Trigger AI doc generation.
|
|
887
|
+
|
|
888
|
+
level: APP | PAGE | REGION | ITEM | PROC | DA
|
|
889
|
+
"""
|
|
890
|
+
body = {}
|
|
891
|
+
if provider_id is not None:
|
|
892
|
+
body["provider_id"] = provider_id
|
|
893
|
+
return self._request("POST", f"apex/ai/generate/{level}/{row_id}",
|
|
894
|
+
json_data=body)
|
|
895
|
+
|
|
896
|
+
def apex_ai_approve(self, level: str, row_id: int,
|
|
897
|
+
approver: str = None) -> dict:
|
|
898
|
+
"""POST /apex/ai/approve/{level}/{id} — Approve AI draft."""
|
|
899
|
+
body = {}
|
|
900
|
+
if approver:
|
|
901
|
+
body["approver"] = approver
|
|
902
|
+
return self._request("POST", f"apex/ai/approve/{level}/{row_id}",
|
|
903
|
+
json_data=body)
|
|
904
|
+
|
|
905
|
+
# ── Session auth (Bearer required, session NOT required) ──
|
|
906
|
+
|
|
907
|
+
def auth_login(self, email: str, password: str) -> dict:
|
|
908
|
+
"""POST /auth/login — Exchange email+password for a session token."""
|
|
909
|
+
return self._request("POST", "auth/login",
|
|
910
|
+
json_data={"email": email, "password": password})
|
|
911
|
+
|
|
912
|
+
def auth_logout(self) -> dict:
|
|
913
|
+
"""POST /auth/logout — Revoke the active session (X-Bau-Session header)."""
|
|
914
|
+
return self._request("POST", "auth/logout")
|