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/config.py
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"""BAUCLI — Configuration manager.
|
|
2
|
+
|
|
3
|
+
Manages ~/.baucli/config.json with multi-database support.
|
|
4
|
+
Passwords: keyring (macOS/Windows/Linux) → fallback to ~/.baucli/.credentials (mode 600).
|
|
5
|
+
|
|
6
|
+
Env vars:
|
|
7
|
+
BAUCLI_HOME — override the config dir (default ~/.baucli). Lets one machine
|
|
8
|
+
hold multiple isolated BAUCLI profiles (one per tenant), e.g.
|
|
9
|
+
`BAUCLI_HOME=~/.baucli-beg baucli sync` and
|
|
10
|
+
`BAUCLI_HOME=~/.baucli-ucbs baucli sync`.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
import sys
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Optional
|
|
18
|
+
|
|
19
|
+
# Per-profile root: env override → ~/.baucli fallback. Resolved once at import.
|
|
20
|
+
BAUCLI_DIR = Path(os.environ["BAUCLI_HOME"]).expanduser() if os.environ.get("BAUCLI_HOME") \
|
|
21
|
+
else Path.home() / ".baucli"
|
|
22
|
+
# Backwards-compat alias so any external code/tests referencing BAU_DIR still works.
|
|
23
|
+
BAU_DIR = BAUCLI_DIR
|
|
24
|
+
CONFIG_FILE = BAUCLI_DIR / "config.json"
|
|
25
|
+
CREDS_FILE = BAUCLI_DIR / ".credentials"
|
|
26
|
+
|
|
27
|
+
# Keyring service name is suffixed with the profile dir so credentials saved
|
|
28
|
+
# under different BAUCLI_HOME values don't collide in a shared OS keychain.
|
|
29
|
+
KEYRING_SERVICE = "baucli" if BAUCLI_DIR == Path.home() / ".baucli" \
|
|
30
|
+
else f"baucli:{BAUCLI_DIR.name}"
|
|
31
|
+
|
|
32
|
+
# Detect keyring availability at import time
|
|
33
|
+
_keyring = None
|
|
34
|
+
try:
|
|
35
|
+
import keyring as _keyring
|
|
36
|
+
# Test if backend is functional (headless Linux may have no backend)
|
|
37
|
+
_keyring.get_password(KEYRING_SERVICE, "__test__")
|
|
38
|
+
except Exception:
|
|
39
|
+
_keyring = None
|
|
40
|
+
|
|
41
|
+
DEFAULT_CONFIG = {
|
|
42
|
+
"license_key": None,
|
|
43
|
+
"api_url": "https://begbrokerdev.begcloud.com/ords/bauctx/api/v1",
|
|
44
|
+
"api_key": None,
|
|
45
|
+
"tenant_code": None,
|
|
46
|
+
"default_db": None,
|
|
47
|
+
"databases": {},
|
|
48
|
+
"sync_interval_min": 30,
|
|
49
|
+
"request_timeout": 30,
|
|
50
|
+
"context_output": "CLAUDE.md",
|
|
51
|
+
"session_token": None,
|
|
52
|
+
"session_expires_at": None,
|
|
53
|
+
"user_email": None,
|
|
54
|
+
"language": "en_US",
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def ensure_dir():
|
|
59
|
+
"""Create ~/.baucli/ if it doesn't exist."""
|
|
60
|
+
BAU_DIR.mkdir(parents=True, exist_ok=True)
|
|
61
|
+
# Restrict permissions on Unix
|
|
62
|
+
if sys.platform != "win32":
|
|
63
|
+
os.chmod(BAU_DIR, 0o700)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def load_config() -> dict:
|
|
67
|
+
"""Load config from ~/.baucli/config.json."""
|
|
68
|
+
if not CONFIG_FILE.exists():
|
|
69
|
+
return DEFAULT_CONFIG.copy()
|
|
70
|
+
with open(CONFIG_FILE) as f:
|
|
71
|
+
cfg = json.load(f)
|
|
72
|
+
# Merge with defaults for missing keys
|
|
73
|
+
merged = DEFAULT_CONFIG.copy()
|
|
74
|
+
merged.update(cfg)
|
|
75
|
+
return merged
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def save_config(cfg: dict):
|
|
79
|
+
"""Save config to ~/.baucli/config.json."""
|
|
80
|
+
ensure_dir()
|
|
81
|
+
with open(CONFIG_FILE, "w") as f:
|
|
82
|
+
json.dump(cfg, f, indent=2, ensure_ascii=False)
|
|
83
|
+
if sys.platform != "win32":
|
|
84
|
+
os.chmod(CONFIG_FILE, 0o600)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def load_credentials() -> dict:
|
|
88
|
+
"""Load credentials from ~/.baucli/.credentials."""
|
|
89
|
+
if not CREDS_FILE.exists():
|
|
90
|
+
return {}
|
|
91
|
+
with open(CREDS_FILE) as f:
|
|
92
|
+
return json.load(f)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def save_credentials(creds: dict):
|
|
96
|
+
"""Save credentials to ~/.baucli/.credentials (restricted permissions)."""
|
|
97
|
+
ensure_dir()
|
|
98
|
+
with open(CREDS_FILE, "w") as f:
|
|
99
|
+
json.dump(creds, f, indent=2)
|
|
100
|
+
if sys.platform != "win32":
|
|
101
|
+
os.chmod(CREDS_FILE, 0o600)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def get_db_password(db_name: str) -> Optional[str]:
|
|
105
|
+
"""Get password for a database. Tries keyring first, then file."""
|
|
106
|
+
# Try keyring
|
|
107
|
+
if _keyring:
|
|
108
|
+
try:
|
|
109
|
+
pwd = _keyring.get_password(KEYRING_SERVICE, db_name)
|
|
110
|
+
if pwd:
|
|
111
|
+
return pwd
|
|
112
|
+
except Exception:
|
|
113
|
+
pass
|
|
114
|
+
# Fallback to file
|
|
115
|
+
creds = load_credentials()
|
|
116
|
+
return creds.get(db_name, {}).get("password")
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def set_db_password(db_name: str, password: str):
|
|
120
|
+
"""Store password for a database. Uses keyring if available, else file."""
|
|
121
|
+
stored_in_keyring = False
|
|
122
|
+
if _keyring:
|
|
123
|
+
try:
|
|
124
|
+
_keyring.set_password(KEYRING_SERVICE, db_name, password)
|
|
125
|
+
stored_in_keyring = True
|
|
126
|
+
except Exception:
|
|
127
|
+
pass
|
|
128
|
+
if not stored_in_keyring:
|
|
129
|
+
# Fallback to file
|
|
130
|
+
creds = load_credentials()
|
|
131
|
+
if db_name not in creds:
|
|
132
|
+
creds[db_name] = {}
|
|
133
|
+
creds[db_name]["password"] = password
|
|
134
|
+
save_credentials(creds)
|
|
135
|
+
return stored_in_keyring
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def delete_db_password(db_name: str):
|
|
139
|
+
"""Remove a database password from keyring + credentials file. Idempotent."""
|
|
140
|
+
if _keyring:
|
|
141
|
+
try:
|
|
142
|
+
_keyring.delete_password(KEYRING_SERVICE, db_name)
|
|
143
|
+
except Exception:
|
|
144
|
+
pass
|
|
145
|
+
creds = load_credentials()
|
|
146
|
+
if db_name in creds:
|
|
147
|
+
creds.pop(db_name, None)
|
|
148
|
+
save_credentials(creds)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def get_database(cfg: dict, db_name: Optional[str] = None) -> tuple:
|
|
152
|
+
"""Get database config by name or default.
|
|
153
|
+
|
|
154
|
+
Returns (db_name, db_config) or (None, None) if not found.
|
|
155
|
+
"""
|
|
156
|
+
dbs = cfg.get("databases", {})
|
|
157
|
+
if not dbs:
|
|
158
|
+
return None, None
|
|
159
|
+
|
|
160
|
+
name = db_name or cfg.get("default_db")
|
|
161
|
+
if name and name in dbs:
|
|
162
|
+
return name, dbs[name]
|
|
163
|
+
|
|
164
|
+
# If no default, return first
|
|
165
|
+
if not name and dbs:
|
|
166
|
+
first = next(iter(dbs))
|
|
167
|
+
return first, dbs[first]
|
|
168
|
+
|
|
169
|
+
return None, None
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def list_databases(cfg: dict) -> list:
|
|
173
|
+
"""List all configured databases."""
|
|
174
|
+
result = []
|
|
175
|
+
default = cfg.get("default_db")
|
|
176
|
+
for name, db in cfg.get("databases", {}).items():
|
|
177
|
+
result.append({
|
|
178
|
+
"name": name,
|
|
179
|
+
"engine": db.get("engine", "?"),
|
|
180
|
+
"host": db.get("host", "?"),
|
|
181
|
+
"port": db.get("port", "?"),
|
|
182
|
+
"service": db.get("service"),
|
|
183
|
+
"wallet_path": db.get("wallet_path"),
|
|
184
|
+
"schemas": db.get("schemas", []),
|
|
185
|
+
"is_default": name == default,
|
|
186
|
+
})
|
|
187
|
+
return result
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def add_database(cfg: dict, name: str, engine: str, host: str, port: int,
|
|
191
|
+
service: str = None, database: str = None,
|
|
192
|
+
user: str = None, schemas: list = None,
|
|
193
|
+
wallet_path: str = None) -> dict:
|
|
194
|
+
"""Add a database to config.
|
|
195
|
+
|
|
196
|
+
wallet_path (Oracle only): directory of an unzipped Oracle Wallet used to
|
|
197
|
+
reach an Autonomous Database over mTLS TLS. When set, `service` is the TNS
|
|
198
|
+
alias from the wallet's tnsnames.ora (e.g. mydb_high) and host/port are
|
|
199
|
+
optional (the alias encodes the endpoint). Non-secret — the wallet password
|
|
200
|
+
(if any) lives in the keyring, never in config.json.
|
|
201
|
+
"""
|
|
202
|
+
db = {
|
|
203
|
+
"engine": engine.lower(),
|
|
204
|
+
"host": host,
|
|
205
|
+
"port": port,
|
|
206
|
+
"user": user,
|
|
207
|
+
"schemas": schemas or [],
|
|
208
|
+
}
|
|
209
|
+
if engine.lower() == "oracle":
|
|
210
|
+
db["service"] = service or database
|
|
211
|
+
if wallet_path:
|
|
212
|
+
db["wallet_path"] = wallet_path
|
|
213
|
+
else:
|
|
214
|
+
db["database"] = database or service
|
|
215
|
+
|
|
216
|
+
if "databases" not in cfg:
|
|
217
|
+
cfg["databases"] = {}
|
|
218
|
+
cfg["databases"][name] = db
|
|
219
|
+
|
|
220
|
+
# Set as default if first database
|
|
221
|
+
if not cfg.get("default_db"):
|
|
222
|
+
cfg["default_db"] = name
|
|
223
|
+
|
|
224
|
+
return cfg
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def remove_database(cfg: dict, name: str) -> dict:
|
|
228
|
+
"""Remove a database from config."""
|
|
229
|
+
cfg.get("databases", {}).pop(name, None)
|
|
230
|
+
# Remove credentials too
|
|
231
|
+
creds = load_credentials()
|
|
232
|
+
creds.pop(name, None)
|
|
233
|
+
save_credentials(creds)
|
|
234
|
+
# Reset default if needed
|
|
235
|
+
if cfg.get("default_db") == name:
|
|
236
|
+
dbs = cfg.get("databases", {})
|
|
237
|
+
cfg["default_db"] = next(iter(dbs)) if dbs else None
|
|
238
|
+
return cfg
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def is_configured() -> bool:
|
|
242
|
+
"""Check if BAUCLI is minimally configured."""
|
|
243
|
+
cfg = load_config()
|
|
244
|
+
return bool(cfg.get("api_key") and cfg.get("license_key"))
|
baucli/db/__init__.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""BAUCLI — Database extractors base class."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class BauConnectionError(Exception):
|
|
8
|
+
"""Raised when the BAUCLI cannot reach the source database.
|
|
9
|
+
|
|
10
|
+
Carries a short, actionable message (the friendly text shown to the user)
|
|
11
|
+
plus an optional raw cause for `--verbose` debugging. Callers should catch
|
|
12
|
+
this without printing a traceback — the user just needs to know whether
|
|
13
|
+
to fix the host, the credentials, or the database state.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, message: str, hint: str = None, cause: Exception = None):
|
|
17
|
+
super().__init__(message)
|
|
18
|
+
self.message = message
|
|
19
|
+
self.hint = hint
|
|
20
|
+
self.cause = cause
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class BaseExtractor(ABC):
|
|
24
|
+
"""Abstract base for database extractors."""
|
|
25
|
+
|
|
26
|
+
engine: str = "UNKNOWN"
|
|
27
|
+
|
|
28
|
+
def __init__(self, host: str = None, port: int = None, user: str = None,
|
|
29
|
+
password: str = None, service: str = None, database: str = None,
|
|
30
|
+
wallet_path: str = None, wallet_password: str = None):
|
|
31
|
+
self.host = host
|
|
32
|
+
self.port = port
|
|
33
|
+
self.user = user
|
|
34
|
+
self.password = password
|
|
35
|
+
self.service = service
|
|
36
|
+
self.database = database
|
|
37
|
+
# Oracle Wallet (Autonomous DB over mTLS). wallet_path = unzipped wallet
|
|
38
|
+
# dir; wallet_password decrypts ewallet.pem in thin mode (may be None for
|
|
39
|
+
# auto-login SSO wallets used via thick mode).
|
|
40
|
+
self.wallet_path = wallet_path
|
|
41
|
+
self.wallet_password = wallet_password
|
|
42
|
+
self._conn = None
|
|
43
|
+
|
|
44
|
+
@abstractmethod
|
|
45
|
+
def connect(self):
|
|
46
|
+
"""Establish database connection."""
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
@abstractmethod
|
|
50
|
+
def disconnect(self):
|
|
51
|
+
"""Close database connection."""
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
@abstractmethod
|
|
55
|
+
def test_connection(self) -> dict:
|
|
56
|
+
"""Test connection and return version info."""
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
@abstractmethod
|
|
60
|
+
def get_native_identifier(self) -> Optional[str]:
|
|
61
|
+
"""Get native database identifier (DBID, system_identifier, etc.)."""
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
@abstractmethod
|
|
65
|
+
def extract_objects(self, schema: str, since_date: str = None, on_progress=None,
|
|
66
|
+
name_filter: str = None) -> list:
|
|
67
|
+
"""Extract PL/SQL objects (packages, functions, procedures, triggers).
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
schema: Schema/owner name
|
|
71
|
+
since_date: ISO date string — if set, only modified since this date
|
|
72
|
+
on_progress: callable(current, total, name) — called after each object
|
|
73
|
+
name_filter: case-insensitive exact OBJECT_NAME — when set, only that
|
|
74
|
+
object is extracted (used by `baucli sync --object NAME`).
|
|
75
|
+
|
|
76
|
+
Returns list of dicts:
|
|
77
|
+
[{"name": "PKG_X", "type": "PACKAGE", "source": "CREATE...", "last_ddl": "2026-..."}]
|
|
78
|
+
"""
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
@abstractmethod
|
|
82
|
+
def extract_tables(self, schema: str, since_date: str = None, on_progress=None,
|
|
83
|
+
name_filter: str = None) -> list:
|
|
84
|
+
"""Extract table metadata (columns, types, constraints).
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
schema: Schema/owner name
|
|
88
|
+
since_date: ISO date string — if set, only modified since this date
|
|
89
|
+
on_progress: callable(current, total, name) — called after each table
|
|
90
|
+
name_filter: case-insensitive exact TABLE_NAME — when set, only that
|
|
91
|
+
table is extracted (used by `baucli sync --table NAME`).
|
|
92
|
+
|
|
93
|
+
Returns list of dicts:
|
|
94
|
+
[{"name": "EMPLOYEES", "type": "TABLE", "columns": [...], "last_ddl": "2026-..."}]
|
|
95
|
+
"""
|
|
96
|
+
pass
|
|
97
|
+
|
|
98
|
+
def count_objects(self, schema: str) -> int:
|
|
99
|
+
"""Quick COUNT(*) of PL/SQL objects in schema. Override in subclass."""
|
|
100
|
+
return -1 # -1 = not supported
|
|
101
|
+
|
|
102
|
+
def count_tables(self, schema: str) -> int:
|
|
103
|
+
"""Quick COUNT(*) of tables/views in schema. Override in subclass."""
|
|
104
|
+
return -1
|
|
105
|
+
|
|
106
|
+
def extract_all(self, schema: str, since_date: str = None) -> list:
|
|
107
|
+
"""Extract both PL/SQL objects and tables."""
|
|
108
|
+
objects = self.extract_objects(schema, since_date=since_date)
|
|
109
|
+
tables = self.extract_tables(schema, since_date=since_date)
|
|
110
|
+
return objects + tables
|
|
111
|
+
|
|
112
|
+
def __enter__(self):
|
|
113
|
+
self.connect()
|
|
114
|
+
return self
|
|
115
|
+
|
|
116
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
117
|
+
self.disconnect()
|