pythia-plsql 0.1.1__tar.gz → 0.1.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 (39) hide show
  1. {pythia_plsql-0.1.1/scripts/pythia_plsql.egg-info → pythia_plsql-0.1.3}/PKG-INFO +11 -3
  2. {pythia_plsql-0.1.1 → pythia_plsql-0.1.3}/README.md +10 -2
  3. {pythia_plsql-0.1.1 → pythia_plsql-0.1.3}/pyproject.toml +1 -1
  4. {pythia_plsql-0.1.1 → pythia_plsql-0.1.3}/scripts/pythia.py +119 -2
  5. {pythia_plsql-0.1.1 → pythia_plsql-0.1.3/scripts/pythia_plsql.egg-info}/PKG-INFO +11 -3
  6. {pythia_plsql-0.1.1 → pythia_plsql-0.1.3}/skills/plsql-write/SKILL.md +3 -0
  7. {pythia_plsql-0.1.1 → pythia_plsql-0.1.3}/tests/test_phase1.py +52 -0
  8. {pythia_plsql-0.1.1 → pythia_plsql-0.1.3}/LICENSE +0 -0
  9. {pythia_plsql-0.1.1 → pythia_plsql-0.1.3}/queries/compile-errors.sql +0 -0
  10. {pythia_plsql-0.1.1 → pythia_plsql-0.1.3}/queries/dependencies.sql +0 -0
  11. {pythia_plsql-0.1.1 → pythia_plsql-0.1.3}/queries/impact.sql +0 -0
  12. {pythia_plsql-0.1.1 → pythia_plsql-0.1.3}/queries/invalid-objects.sql +0 -0
  13. {pythia_plsql-0.1.1 → pythia_plsql-0.1.3}/queries/name-occupants.sql +0 -0
  14. {pythia_plsql-0.1.1 → pythia_plsql-0.1.3}/queries/object-source.sql +0 -0
  15. {pythia_plsql-0.1.1 → pythia_plsql-0.1.3}/queries/plscope-enabled.sql +0 -0
  16. {pythia_plsql-0.1.1 → pythia_plsql-0.1.3}/queries/plscope-statements.sql +0 -0
  17. {pythia_plsql-0.1.1 → pythia_plsql-0.1.3}/queries/plscope-usages.sql +0 -0
  18. {pythia_plsql-0.1.1 → pythia_plsql-0.1.3}/queries/session-privileges.sql +0 -0
  19. {pythia_plsql-0.1.1 → pythia_plsql-0.1.3}/queries/similar-candidates.sql +0 -0
  20. {pythia_plsql-0.1.1 → pythia_plsql-0.1.3}/queries/source.sql +0 -0
  21. {pythia_plsql-0.1.1 → pythia_plsql-0.1.3}/scripts/pythia_plsql.egg-info/SOURCES.txt +0 -0
  22. {pythia_plsql-0.1.1 → pythia_plsql-0.1.3}/scripts/pythia_plsql.egg-info/dependency_links.txt +0 -0
  23. {pythia_plsql-0.1.1 → pythia_plsql-0.1.3}/scripts/pythia_plsql.egg-info/entry_points.txt +0 -0
  24. {pythia_plsql-0.1.1 → pythia_plsql-0.1.3}/scripts/pythia_plsql.egg-info/requires.txt +0 -0
  25. {pythia_plsql-0.1.1 → pythia_plsql-0.1.3}/scripts/pythia_plsql.egg-info/top_level.txt +0 -0
  26. {pythia_plsql-0.1.1 → pythia_plsql-0.1.3}/setup.cfg +0 -0
  27. {pythia_plsql-0.1.1 → pythia_plsql-0.1.3}/skills/plsql-apply/SKILL.md +0 -0
  28. {pythia_plsql-0.1.1 → pythia_plsql-0.1.3}/skills/plsql-explore/SKILL.md +0 -0
  29. {pythia_plsql-0.1.1 → pythia_plsql-0.1.3}/skills/plsql-explore/reference/data-dictionary.md +0 -0
  30. {pythia_plsql-0.1.1 → pythia_plsql-0.1.3}/skills/plsql-impact/SKILL.md +0 -0
  31. {pythia_plsql-0.1.1 → pythia_plsql-0.1.3}/skills/plsql-review/SKILL.md +0 -0
  32. {pythia_plsql-0.1.1 → pythia_plsql-0.1.3}/skills/plsql-review/reference/antipatterns.md +0 -0
  33. {pythia_plsql-0.1.1 → pythia_plsql-0.1.3}/skills/plsql-setup/SKILL.md +0 -0
  34. {pythia_plsql-0.1.1 → pythia_plsql-0.1.3}/skills/plsql-skill-author/SKILL.md +0 -0
  35. {pythia_plsql-0.1.1 → pythia_plsql-0.1.3}/skills/plsql-write/reference/patterns.md +0 -0
  36. {pythia_plsql-0.1.1 → pythia_plsql-0.1.3}/tests/test_install.py +0 -0
  37. {pythia_plsql-0.1.1 → pythia_plsql-0.1.3}/tests/test_phase2.py +0 -0
  38. {pythia_plsql-0.1.1 → pythia_plsql-0.1.3}/tests/test_phase3.py +0 -0
  39. {pythia_plsql-0.1.1 → pythia_plsql-0.1.3}/tests/test_phase5.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pythia-plsql
3
- Version: 0.1.1
3
+ Version: 0.1.3
4
4
  Summary: PL/SQL development for AI agents on Oracle Database - expert data-dictionary queries, impact analysis, and a snapshot-verified write path with honest rollback.
5
5
  License: MIT
6
6
  Project-URL: Homepage, https://github.com/thaildhe172591/pythia
@@ -20,6 +20,8 @@ Dynamic: license-file
20
20
 
21
21
  > Oracle's MCP gives your agent a connection. **pythia gives it the judgment to use it.**
22
22
 
23
+ **English** · [Tiếng Việt](README.vi.md)
24
+
23
25
  [![ci](https://github.com/thaildhe172591/pythia/actions/workflows/ci.yml/badge.svg)](https://github.com/thaildhe172591/pythia/actions/workflows/ci.yml)
24
26
  [![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
25
27
  ![python](https://img.shields.io/badge/python-3.9%2B-blue)
@@ -83,6 +85,12 @@ for internal mirrors) — without Node it copies the bundled skill pack into
83
85
  `.claude/skills/` and `.agents/skills/`. Skills alone: `npx skills add
84
86
  thaildhe172591/pythia`, or `/plugin marketplace add thaildhe172591/pythia`.
85
87
 
88
+ `pip install` is **once per machine**; `pythia install` is **once per
89
+ project** — run it in each repo's root to drop the skills and a fresh
90
+ `.pythia/connections.json` there. The CLI always reads the config of the
91
+ project you are standing in (searched upward from the current directory,
92
+ no global fallback), so one global CLI never mixes up databases.
93
+
86
94
  Running from a clone works too — `python scripts/pythia.py <command>`; every
87
95
  printed follow-up command matches how you invoked it. Windows, macOS, Linux
88
96
  and WSL are all CI-tested.
@@ -94,8 +102,8 @@ and WSL are all CI-tested.
94
102
  | `check` connectivity + counts | `deps` what it depends on | `apply` the six-step write |
95
103
  | `ls` find objects | `impact` what depends on it | `journal` list · diff · export · restore |
96
104
  | `src` source, compiler line numbers | `errors` compile errors, line:col | `policy` show · set |
97
- | `args` signatures | `invalid` everything broken | |
98
- | `ddl` via DBMS_METADATA | `plscope` exact identifier usages | |
105
+ | `args` signatures | `invalid` everything broken | `unistr` exact non-ASCII literals |
106
+ | `ddl` via DBMS_METADATA | `plscope` exact identifier usages | `agent-user` least-privilege setup |
99
107
  | `cols` columns + types | `similar` programs named like this | |
100
108
  | `grep` search all source | | |
101
109
  | `sql` free query (SELECT/WITH only) | | |
@@ -6,6 +6,8 @@
6
6
 
7
7
  > Oracle's MCP gives your agent a connection. **pythia gives it the judgment to use it.**
8
8
 
9
+ **English** · [Tiếng Việt](README.vi.md)
10
+
9
11
  [![ci](https://github.com/thaildhe172591/pythia/actions/workflows/ci.yml/badge.svg)](https://github.com/thaildhe172591/pythia/actions/workflows/ci.yml)
10
12
  [![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
11
13
  ![python](https://img.shields.io/badge/python-3.9%2B-blue)
@@ -69,6 +71,12 @@ for internal mirrors) — without Node it copies the bundled skill pack into
69
71
  `.claude/skills/` and `.agents/skills/`. Skills alone: `npx skills add
70
72
  thaildhe172591/pythia`, or `/plugin marketplace add thaildhe172591/pythia`.
71
73
 
74
+ `pip install` is **once per machine**; `pythia install` is **once per
75
+ project** — run it in each repo's root to drop the skills and a fresh
76
+ `.pythia/connections.json` there. The CLI always reads the config of the
77
+ project you are standing in (searched upward from the current directory,
78
+ no global fallback), so one global CLI never mixes up databases.
79
+
72
80
  Running from a clone works too — `python scripts/pythia.py <command>`; every
73
81
  printed follow-up command matches how you invoked it. Windows, macOS, Linux
74
82
  and WSL are all CI-tested.
@@ -80,8 +88,8 @@ and WSL are all CI-tested.
80
88
  | `check` connectivity + counts | `deps` what it depends on | `apply` the six-step write |
81
89
  | `ls` find objects | `impact` what depends on it | `journal` list · diff · export · restore |
82
90
  | `src` source, compiler line numbers | `errors` compile errors, line:col | `policy` show · set |
83
- | `args` signatures | `invalid` everything broken | |
84
- | `ddl` via DBMS_METADATA | `plscope` exact identifier usages | |
91
+ | `args` signatures | `invalid` everything broken | `unistr` exact non-ASCII literals |
92
+ | `ddl` via DBMS_METADATA | `plscope` exact identifier usages | `agent-user` least-privilege setup |
85
93
  | `cols` columns + types | `similar` programs named like this | |
86
94
  | `grep` search all source | | |
87
95
  | `sql` free query (SELECT/WITH only) | | |
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pythia-plsql"
7
- version = "0.1.1"
7
+ version = "0.1.3"
8
8
  description = "PL/SQL development for AI agents on Oracle Database - expert data-dictionary queries, impact analysis, and a snapshot-verified write path with honest rollback."
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -42,6 +42,8 @@ you always know whether you saw everything.
42
42
  pythia journal restore <id>
43
43
  pythia policy
44
44
  pythia install
45
+ pythia unistr "Nhóm không được để trống"
46
+ pythia agent-user --save
45
47
  """
46
48
  import argparse
47
49
  import json
@@ -1300,6 +1302,106 @@ def cmd_conventions(conn, schema, ns):
1300
1302
  print(f"\nProse rules for agents: {md}")
1301
1303
 
1302
1304
 
1305
+ AGENT_USER_SQL = """\
1306
+ -- Least-privilege agent credential for schema {owner} — run as a DBA.
1307
+ -- Proxy authentication: the agent logs in with its OWN password but works
1308
+ -- inside {owner}; it never learns the owner password, owns nothing, and
1309
+ -- revocation is one statement. Deliberately absent: DBA, RESOURCE, ANY
1310
+ -- privileges, utility grants — the agent needs none of them to develop
1311
+ -- PL/SQL, and every extra grant widens the blast radius.
1312
+
1313
+ CREATE USER {agent} IDENTIFIED BY "{password}";
1314
+ GRANT CREATE SESSION TO {agent};
1315
+ ALTER USER {owner} GRANT CONNECT THROUGH {agent};
1316
+
1317
+ -- Cut the agent off later (owner untouched):
1318
+ -- ALTER USER {owner} REVOKE CONNECT THROUGH {agent};
1319
+ -- Fresh owner schema instead? See examples/agent-user-setup.example.sql.
1320
+ """
1321
+
1322
+
1323
+ def agent_password():
1324
+ """Random password satisfying common Oracle verify functions: letter
1325
+ first, upper+lower+digit+special, 16+ chars. Always quoted in SQL."""
1326
+ import secrets
1327
+ import string
1328
+ body = "".join(secrets.choice(string.ascii_letters + string.digits)
1329
+ for _ in range(12))
1330
+ return "Ag" + body + "#7"
1331
+
1332
+
1333
+ def save_agent_connection(root, base_name, base, agent, password):
1334
+ """Add <base>_agent alongside the owner entry (never overwrite it) and
1335
+ make it the default. Returns the new entry name."""
1336
+ path = pathlib.Path(root) / CONFIG_DIR / CONFIG_NAME
1337
+ cfg = json.loads(path.read_text(encoding="utf-8"))
1338
+ owner = (base.get("schema") or base["user"]).upper()
1339
+ entry = dict(base)
1340
+ entry["user"] = f"{agent.lower()}[{owner.lower()}]"
1341
+ entry["password"] = password
1342
+ entry["schema"] = owner
1343
+ name = f"{base_name}_agent"
1344
+ cfg[name] = entry
1345
+ cfg["default"] = name
1346
+ path.write_text(json.dumps(cfg, indent=2, ensure_ascii=False) + "\n",
1347
+ encoding="utf-8")
1348
+ return name
1349
+
1350
+
1351
+ def cmd_agent_user(conn, schema, ns):
1352
+ cwd = pathlib.Path.cwd()
1353
+ cfg, root = find_config(cwd, os.environ)
1354
+ base_name, base = resolve_connection(cfg, ns.conn, os.environ, cwd, root)
1355
+ owner = (base.get("schema") or base["user"]).upper()
1356
+ if "[" in base["user"]:
1357
+ sys.exit(f"Connection {base_name} already uses a proxy user "
1358
+ f"({base['user']}) — nothing to set up.")
1359
+ agent = (ns.name or f"{owner}_AGENT").upper()
1360
+ password = agent_password()
1361
+ print(AGENT_USER_SQL.format(owner=owner, agent=agent, password=password))
1362
+ if ns.save:
1363
+ name = save_agent_connection(root, base_name, base, agent, password)
1364
+ print(f"Saved connection '{name}' (now the default) in "
1365
+ f"{pathlib.Path(root) / CONFIG_DIR / CONFIG_NAME};\n"
1366
+ f"the owner entry '{base_name}' is untouched — switch back "
1367
+ f"any time with --conn {base_name}.")
1368
+ print(f"\nNext: run the SQL above as a DBA, then: {invocation()} check")
1369
+ else:
1370
+ print(f"-- Rerun with --save to add this credential to "
1371
+ f"connections.json as '{base_name}_agent' and make it the "
1372
+ f"default once the DBA has run it.")
1373
+
1374
+
1375
+ # unistr escapes. Single quote doubles to '' per SQL (a \' is ORA-01756 for
1376
+ # Oracle); backslash doubles for unistr itself.
1377
+ UNISTR_ESC = {0: r"\0", 8: r"\b", 9: r"\t", 10: r"\n", 13: r"\r",
1378
+ 11: r"\v", 12: r"\f", 92: "\\\\", 39: "''"}
1379
+
1380
+
1381
+ def to_unistr(text):
1382
+ r"""A National-charset-safe Oracle literal for any text. Non-ASCII goes
1383
+ to \XXXX escapes, so the statement survives every client/DB charset on
1384
+ the way in — the standard way to keep Vietnamese messages exact."""
1385
+ out = []
1386
+ for ch in text:
1387
+ cp = ord(ch)
1388
+ if cp in UNISTR_ESC:
1389
+ out.append(UNISTR_ESC[cp])
1390
+ elif 0x1F < cp < 0x7F:
1391
+ out.append(ch)
1392
+ elif cp > 0xFFFF:
1393
+ out.append("\\U%08X" % cp) # beyond the BMP: \U + 8 hex
1394
+ else:
1395
+ out.append("\\%04X" % cp)
1396
+ return "unistr('%s')" % "".join(out)
1397
+
1398
+
1399
+ def cmd_unistr(conn, schema, ns):
1400
+ text = " ".join(ns.text) if ns.text else sys.stdin.read().rstrip("\n")
1401
+ u = to_unistr(text)
1402
+ print("'loi:'||%s||':loi'" % u if ns.loi else u)
1403
+
1404
+
1303
1405
  DEFAULT_SKILLS_SOURCE = "thaildhe172591/pythia"
1304
1406
 
1305
1407
  # Kept byte-identical to examples/connections.example.json —
@@ -1398,9 +1500,11 @@ COMMANDS = {"check": cmd_check, "ls": cmd_ls, "src": cmd_src, "args": cmd_args,
1398
1500
  "invalid": cmd_invalid, "errors": cmd_errors, "deps": cmd_deps,
1399
1501
  "impact": cmd_impact, "similar": cmd_similar, "plscope": cmd_plscope,
1400
1502
  "policy": cmd_policy, "journal": cmd_journal, "apply": cmd_apply,
1401
- "conventions": cmd_conventions, "install": cmd_install}
1503
+ "conventions": cmd_conventions, "install": cmd_install,
1504
+ "unistr": cmd_unistr, "agent-user": cmd_agent_user}
1402
1505
 
1403
- NO_DB_COMMANDS = {"policy", "journal", "conventions", "install"}
1506
+ NO_DB_COMMANDS = {"policy", "journal", "conventions", "install", "unistr",
1507
+ "agent-user"}
1404
1508
 
1405
1509
 
1406
1510
  # --- CLI ---------------------------------------------------------------------
@@ -1499,6 +1603,19 @@ def build_parser():
1499
1603
  help="impact depth for the preview (default 3)")
1500
1604
  sub.add_parser("conventions", parents=[common()],
1501
1605
  help="show the project's house-style naming patterns")
1606
+ s = sub.add_parser("agent-user", parents=[common()],
1607
+ help="SQL for a least-privilege proxy agent user; "
1608
+ "--save adds it to connections.json")
1609
+ s.add_argument("--name", help="agent user name (default <OWNER>_AGENT)")
1610
+ s.add_argument("--save", action="store_true",
1611
+ help="add the new credential as <conn>_agent and make it "
1612
+ "the default connection")
1613
+ s = sub.add_parser("unistr", parents=[common()],
1614
+ help="Oracle unistr('...') literal for non-ASCII text "
1615
+ "(Vietnamese messages stay exact)")
1616
+ s.add_argument("text", nargs="*", help="text; omit to read stdin")
1617
+ s.add_argument("--loi", action="store_true",
1618
+ help="wrap as 'loi:'||unistr(...)||':loi'")
1502
1619
  s = sub.add_parser("install", parents=[common()],
1503
1620
  help="install the skill pack and scaffold .pythia/ config")
1504
1621
  s.add_argument("--source", default=DEFAULT_SKILLS_SOURCE,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pythia-plsql
3
- Version: 0.1.1
3
+ Version: 0.1.3
4
4
  Summary: PL/SQL development for AI agents on Oracle Database - expert data-dictionary queries, impact analysis, and a snapshot-verified write path with honest rollback.
5
5
  License: MIT
6
6
  Project-URL: Homepage, https://github.com/thaildhe172591/pythia
@@ -20,6 +20,8 @@ Dynamic: license-file
20
20
 
21
21
  > Oracle's MCP gives your agent a connection. **pythia gives it the judgment to use it.**
22
22
 
23
+ **English** · [Tiếng Việt](README.vi.md)
24
+
23
25
  [![ci](https://github.com/thaildhe172591/pythia/actions/workflows/ci.yml/badge.svg)](https://github.com/thaildhe172591/pythia/actions/workflows/ci.yml)
24
26
  [![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
25
27
  ![python](https://img.shields.io/badge/python-3.9%2B-blue)
@@ -83,6 +85,12 @@ for internal mirrors) — without Node it copies the bundled skill pack into
83
85
  `.claude/skills/` and `.agents/skills/`. Skills alone: `npx skills add
84
86
  thaildhe172591/pythia`, or `/plugin marketplace add thaildhe172591/pythia`.
85
87
 
88
+ `pip install` is **once per machine**; `pythia install` is **once per
89
+ project** — run it in each repo's root to drop the skills and a fresh
90
+ `.pythia/connections.json` there. The CLI always reads the config of the
91
+ project you are standing in (searched upward from the current directory,
92
+ no global fallback), so one global CLI never mixes up databases.
93
+
86
94
  Running from a clone works too — `python scripts/pythia.py <command>`; every
87
95
  printed follow-up command matches how you invoked it. Windows, macOS, Linux
88
96
  and WSL are all CI-tested.
@@ -94,8 +102,8 @@ and WSL are all CI-tested.
94
102
  | `check` connectivity + counts | `deps` what it depends on | `apply` the six-step write |
95
103
  | `ls` find objects | `impact` what depends on it | `journal` list · diff · export · restore |
96
104
  | `src` source, compiler line numbers | `errors` compile errors, line:col | `policy` show · set |
97
- | `args` signatures | `invalid` everything broken | |
98
- | `ddl` via DBMS_METADATA | `plscope` exact identifier usages | |
105
+ | `args` signatures | `invalid` everything broken | `unistr` exact non-ASCII literals |
106
+ | `ddl` via DBMS_METADATA | `plscope` exact identifier usages | `agent-user` least-privilege setup |
99
107
  | `cols` columns + types | `similar` programs named like this | |
100
108
  | `grep` search all source | | |
101
109
  | `sql` free query (SELECT/WITH only) | | |
@@ -33,6 +33,9 @@ existing ones — not to introduce a better style.
33
33
  - End the file with the PL/SQL block's `;` and a final line holding `/`.
34
34
  - Name the object unqualified, or qualified with the exact schema the
35
35
  connection targets — a mismatch is refused at apply time.
36
+ - **Non-ASCII string literals** (Vietnamese messages, any accented text):
37
+ never paste raw — run `pythia unistr "<text>"` and use the printed
38
+ `unistr('...')`, so the text survives every client/DB charset exactly.
36
39
  5. **Check yourself before handing off.** Reread against
37
40
  `reference/patterns.md` — cursor and bulk patterns, exception discipline,
38
41
  bind variables, commit ownership.
@@ -240,6 +240,58 @@ def test_source_unit_filter():
240
240
  assert pythia.filter_units(rows, body=False, spec=False) == rows
241
241
 
242
242
 
243
+ def test_to_unistr():
244
+ r"""Ported from the field tool's self-check: Vietnamese exact, quote
245
+ doubled per SQL (never \'), backslash doubled, beyond-BMP \U form."""
246
+ u = pythia.to_unistr
247
+ assert u("Nhóm không được để trống") == \
248
+ r"unistr('Nh\00F3m kh\00F4ng \0111\01B0\1EE3c \0111\1EC3 tr\1ED1ng')"
249
+ assert u("Mã bệnh") == r"unistr('M\00E3 b\1EC7nh')"
250
+ assert u("O'Brien") == "unistr('O''Brien')"
251
+ assert u("a\\b") == r"unistr('a\\b')"
252
+ assert u("\U0001F600") == r"unistr('\U0001F600')"
253
+ assert u("ABC 123") == "unistr('ABC 123')"
254
+
255
+
256
+ def test_agent_user_sql_is_least_privilege():
257
+ sql = pythia.AGENT_USER_SQL.format(owner="APP", agent="APP_AGENT",
258
+ password="x")
259
+ assert "CREATE USER APP_AGENT" in sql
260
+ assert "GRANT CREATE SESSION TO APP_AGENT" in sql
261
+ assert "GRANT CONNECT THROUGH APP_AGENT" in sql
262
+ for bad in ("DBA TO", "RESOURCE TO", "ANY "): # the whole point
263
+ assert bad not in sql.upper().replace("-- ", ""), bad
264
+
265
+
266
+ def test_agent_password_shape():
267
+ for _ in range(20):
268
+ pw = pythia.agent_password()
269
+ assert len(pw) >= 14 and pw[0].isalpha() and "#" in pw
270
+ assert '"' not in pw and "'" not in pw # safe inside quotes
271
+
272
+
273
+ def test_save_agent_connection_keeps_owner_and_sets_default():
274
+ with tempfile.TemporaryDirectory() as td:
275
+ root = pathlib.Path(td)
276
+ (root / ".pythia").mkdir()
277
+ cfg = {"default": "dev",
278
+ "dev": {"host": "h", "port": 1521, "service_name": "svc",
279
+ "user": "PYTHIA", "password": "ownerpw",
280
+ "schema": "PYTHIA"}}
281
+ (root / ".pythia" / "connections.json").write_text(
282
+ json.dumps(cfg), encoding="utf-8")
283
+ name = pythia.save_agent_connection(root, "dev", cfg["dev"],
284
+ "PYTHIA_AGENT", "agpw")
285
+ out = json.loads((root / ".pythia" / "connections.json")
286
+ .read_text(encoding="utf-8"))
287
+ assert name == "dev_agent" and out["default"] == "dev_agent"
288
+ assert out["dev"]["user"] == "PYTHIA" # owner untouched
289
+ assert out["dev"]["password"] == "ownerpw"
290
+ assert out["dev_agent"]["user"] == "pythia_agent[pythia]"
291
+ assert out["dev_agent"]["password"] == "agpw"
292
+ assert out["dev_agent"]["host"] == "h" # connect details copied
293
+
294
+
243
295
  def test_json_envelope():
244
296
  s = pythia.json_envelope("ls", "DEV", "OWNER1", ["A", "B"],
245
297
  [(1, datetime.date(2026, 1, 2))], truncated=True)
File without changes
File without changes