pucit 0.1.0__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.
- pucit/__about__.py +1 -0
- pucit/__init__.py +3 -0
- pucit/__main__.py +4 -0
- pucit/cli.py +258 -0
- pucit/commands/__init__.py +1 -0
- pucit/commands/bypass.py +55 -0
- pucit/commands/doctor.py +80 -0
- pucit/commands/install.py +83 -0
- pucit/commands/oracle.py +176 -0
- pucit/commands/scaffold.py +71 -0
- pucit/config.py +45 -0
- pucit/cpp_build.py +214 -0
- pucit/docker_util.py +146 -0
- pucit/platform/__init__.py +5 -0
- pucit/platform/packages.py +89 -0
- pucit/util.py +114 -0
- pucit-0.1.0.dist-info/METADATA +134 -0
- pucit-0.1.0.dist-info/RECORD +21 -0
- pucit-0.1.0.dist-info/WHEEL +4 -0
- pucit-0.1.0.dist-info/entry_points.txt +2 -0
- pucit-0.1.0.dist-info/licenses/LICENSE.txt +21 -0
pucit/__about__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
pucit/__init__.py
ADDED
pucit/__main__.py
ADDED
pucit/cli.py
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"""pucit — PUCIT student toolkit CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import List, Optional
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
from pucit import __version__
|
|
11
|
+
from pucit import cpp_build
|
|
12
|
+
from pucit import docker_util as d
|
|
13
|
+
from pucit.commands.bypass import bypass_app
|
|
14
|
+
from pucit.commands.doctor import run_doctor, run_list
|
|
15
|
+
from pucit.commands.install import install_app
|
|
16
|
+
from pucit.commands.oracle import oracle_app
|
|
17
|
+
from pucit.commands.scaffold import init_project, new_file
|
|
18
|
+
from pucit.util import fail, first_existing, info, ok, run_cmd, run_streaming, which
|
|
19
|
+
|
|
20
|
+
app = typer.Typer(
|
|
21
|
+
name="pucit",
|
|
22
|
+
help="PUCIT student toolkit — Oracle, PF/C++, bypass, and lab ease commands.",
|
|
23
|
+
no_args_is_help=True,
|
|
24
|
+
add_completion=False,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
app.add_typer(install_app, name="install")
|
|
28
|
+
app.add_typer(bypass_app, name="bypass")
|
|
29
|
+
app.add_typer(oracle_app, name="oracle")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _oracle_target(target: str) -> None:
|
|
33
|
+
if target.lower() not in {"oracle", "db", "oracledb"}:
|
|
34
|
+
fail(f"Unknown target '{target}'. Try: oracle")
|
|
35
|
+
raise typer.Exit(1)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@app.command("start")
|
|
39
|
+
def start_cmd(target: str = typer.Argument(..., help="Service to start (oracle)")) -> None:
|
|
40
|
+
"""Start a managed service: pucit start oracle"""
|
|
41
|
+
_oracle_target(target)
|
|
42
|
+
from pucit.commands.oracle import oracle_start
|
|
43
|
+
|
|
44
|
+
oracle_start()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@app.command("stop")
|
|
48
|
+
def stop_cmd(target: str = typer.Argument(..., help="Service to stop (oracle)")) -> None:
|
|
49
|
+
"""Stop a managed service: pucit stop oracle"""
|
|
50
|
+
_oracle_target(target)
|
|
51
|
+
from pucit.commands.oracle import oracle_stop
|
|
52
|
+
|
|
53
|
+
oracle_stop()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@app.command("status")
|
|
57
|
+
def status_cmd(target: str = typer.Argument("oracle", help="Service (oracle)")) -> None:
|
|
58
|
+
"""Show service status: pucit status oracle"""
|
|
59
|
+
_oracle_target(target)
|
|
60
|
+
from pucit.commands.oracle import oracle_status
|
|
61
|
+
|
|
62
|
+
oracle_status()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@app.command("logs")
|
|
66
|
+
def logs_cmd(
|
|
67
|
+
target: str = typer.Argument("oracle"),
|
|
68
|
+
follow: bool = typer.Option(False, "--follow", "-f"),
|
|
69
|
+
tail: int = typer.Option(100, "--tail"),
|
|
70
|
+
) -> None:
|
|
71
|
+
"""Show service logs: pucit logs oracle"""
|
|
72
|
+
_oracle_target(target)
|
|
73
|
+
from pucit.commands.oracle import oracle_logs
|
|
74
|
+
|
|
75
|
+
oracle_logs(follow=follow, tail=tail)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@app.command("run")
|
|
79
|
+
def run_cmd(
|
|
80
|
+
sources: Optional[List[str]] = typer.Argument(None, help="C++ sources (default: main.cpp)"),
|
|
81
|
+
flags: Optional[str] = typer.Option(None, "--flags", "-f", help='Extra flags e.g. "-O2"'),
|
|
82
|
+
std: Optional[str] = typer.Option(None, "--std", help="Language standard"),
|
|
83
|
+
out: Optional[str] = typer.Option(None, "--out", "-o", help="Output binary"),
|
|
84
|
+
input_file: Optional[str] = typer.Option(None, "--input", "-i", help="stdin from file"),
|
|
85
|
+
) -> None:
|
|
86
|
+
"""Compile and run: pucit run main.cpp"""
|
|
87
|
+
try:
|
|
88
|
+
code = cpp_build.execute_run(
|
|
89
|
+
list(sources or []),
|
|
90
|
+
flags=flags,
|
|
91
|
+
std=std,
|
|
92
|
+
out=out,
|
|
93
|
+
input_file=input_file,
|
|
94
|
+
)
|
|
95
|
+
except (FileNotFoundError, RuntimeError) as exc:
|
|
96
|
+
fail(str(exc))
|
|
97
|
+
raise typer.Exit(1) from exc
|
|
98
|
+
raise typer.Exit(code)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@app.command("compile")
|
|
102
|
+
def compile_cmd(
|
|
103
|
+
sources: Optional[List[str]] = typer.Argument(None),
|
|
104
|
+
flags: Optional[str] = typer.Option(None, "--flags", "-f"),
|
|
105
|
+
std: Optional[str] = typer.Option(None, "--std"),
|
|
106
|
+
out: Optional[str] = typer.Option(None, "--out", "-o"),
|
|
107
|
+
) -> None:
|
|
108
|
+
"""Compile only: pucit compile main.cpp"""
|
|
109
|
+
try:
|
|
110
|
+
code = cpp_build.execute_run(
|
|
111
|
+
list(sources or []),
|
|
112
|
+
flags=flags,
|
|
113
|
+
std=std,
|
|
114
|
+
out=out,
|
|
115
|
+
compile_only=True,
|
|
116
|
+
)
|
|
117
|
+
except (FileNotFoundError, RuntimeError) as exc:
|
|
118
|
+
fail(str(exc))
|
|
119
|
+
raise typer.Exit(1) from exc
|
|
120
|
+
raise typer.Exit(code)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@app.command("debug")
|
|
124
|
+
def debug_cmd(
|
|
125
|
+
sources: Optional[List[str]] = typer.Argument(None),
|
|
126
|
+
flags: Optional[str] = typer.Option(None, "--flags", "-f"),
|
|
127
|
+
std: Optional[str] = typer.Option(None, "--std"),
|
|
128
|
+
out: Optional[str] = typer.Option(None, "--out", "-o"),
|
|
129
|
+
) -> None:
|
|
130
|
+
"""Compile with -g and open gdb."""
|
|
131
|
+
try:
|
|
132
|
+
resolved = cpp_build.resolve_sources(list(sources or []))
|
|
133
|
+
code, binary, _ = cpp_build.compile_sources(
|
|
134
|
+
resolved, out=out, flags=flags, std=std, debug=True
|
|
135
|
+
)
|
|
136
|
+
except (FileNotFoundError, RuntimeError) as exc:
|
|
137
|
+
fail(str(exc))
|
|
138
|
+
raise typer.Exit(1) from exc
|
|
139
|
+
if code != 0:
|
|
140
|
+
raise typer.Exit(code)
|
|
141
|
+
gdb = which("gdb")
|
|
142
|
+
if not gdb:
|
|
143
|
+
fail("gdb not found. Run: pucit install pf")
|
|
144
|
+
raise typer.Exit(1)
|
|
145
|
+
info(f"Starting gdb on {binary}")
|
|
146
|
+
raise typer.Exit(run_streaming([gdb, str(binary)]))
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@app.command("watch")
|
|
150
|
+
def watch_cmd(
|
|
151
|
+
sources: Optional[List[str]] = typer.Argument(None),
|
|
152
|
+
flags: Optional[str] = typer.Option(None, "--flags", "-f"),
|
|
153
|
+
std: Optional[str] = typer.Option(None, "--std"),
|
|
154
|
+
out: Optional[str] = typer.Option(None, "--out", "-o"),
|
|
155
|
+
input_file: Optional[str] = typer.Option(None, "--input", "-i"),
|
|
156
|
+
) -> None:
|
|
157
|
+
"""Recompile + run on file change."""
|
|
158
|
+
try:
|
|
159
|
+
code = cpp_build.watch_run(
|
|
160
|
+
list(sources or []),
|
|
161
|
+
flags=flags,
|
|
162
|
+
std=std,
|
|
163
|
+
out=out,
|
|
164
|
+
input_file=input_file,
|
|
165
|
+
)
|
|
166
|
+
except (FileNotFoundError, RuntimeError) as exc:
|
|
167
|
+
fail(str(exc))
|
|
168
|
+
raise typer.Exit(1) from exc
|
|
169
|
+
raise typer.Exit(code)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@app.command("clean")
|
|
173
|
+
def clean_cmd() -> None:
|
|
174
|
+
"""Remove build/ and a.out in the current directory."""
|
|
175
|
+
removed = cpp_build.clean_artifacts()
|
|
176
|
+
if not removed:
|
|
177
|
+
info("Nothing to clean")
|
|
178
|
+
else:
|
|
179
|
+
for path in removed:
|
|
180
|
+
ok(f"Removed {path}")
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
@app.command("new")
|
|
184
|
+
def new_cmd(
|
|
185
|
+
name: str = typer.Argument(..., help="File or stem, e.g. hello or hello.cpp"),
|
|
186
|
+
force: bool = typer.Option(False, "--force", "-f"),
|
|
187
|
+
) -> None:
|
|
188
|
+
"""Create a new C++ file from template."""
|
|
189
|
+
new_file(name, force=force)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
@app.command("init")
|
|
193
|
+
def init_cmd(force: bool = typer.Option(False, "--force", "-f")) -> None:
|
|
194
|
+
"""Scaffold a PF lab folder (main.cpp, Makefile, .gitignore)."""
|
|
195
|
+
init_project(force=force)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
@app.command("doctor")
|
|
199
|
+
def doctor_cmd() -> None:
|
|
200
|
+
"""Check compilers, Docker, Oracle, bypass, and friends."""
|
|
201
|
+
raise typer.Exit(run_doctor())
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
@app.command("list")
|
|
205
|
+
def list_cmd() -> None:
|
|
206
|
+
"""List managed components and their status."""
|
|
207
|
+
raise typer.Exit(run_list())
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
@app.command("version")
|
|
211
|
+
def version_cmd() -> None:
|
|
212
|
+
"""Show pucit version."""
|
|
213
|
+
typer.echo(__version__)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
@app.command("which")
|
|
217
|
+
def which_cmd(tool: str = typer.Argument(..., help="Tool name, e.g. g++ or docker")) -> None:
|
|
218
|
+
"""Show resolved path for a tool."""
|
|
219
|
+
# allow g++ style
|
|
220
|
+
path = which(tool) or first_existing((tool,))
|
|
221
|
+
if not path and tool in {"g++", "cxx", "compiler"}:
|
|
222
|
+
path = cpp_build.find_compiler()
|
|
223
|
+
if not path and tool == "docker":
|
|
224
|
+
path = d.docker_bin()
|
|
225
|
+
if path:
|
|
226
|
+
typer.echo(path)
|
|
227
|
+
else:
|
|
228
|
+
fail(f"{tool} not found")
|
|
229
|
+
raise typer.Exit(1)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
@app.command("open")
|
|
233
|
+
def open_cmd(path: str = typer.Argument(".", help="Path to open")) -> None:
|
|
234
|
+
"""Open a folder/file in VS Code, Cursor, or the file manager."""
|
|
235
|
+
target = Path(path).resolve()
|
|
236
|
+
editors = ("cursor", "code", "codium")
|
|
237
|
+
for editor in editors:
|
|
238
|
+
binary = which(editor)
|
|
239
|
+
if binary:
|
|
240
|
+
raise typer.Exit(run_cmd([binary, str(target)]).returncode)
|
|
241
|
+
# fallback file manager / start
|
|
242
|
+
import sys
|
|
243
|
+
|
|
244
|
+
if sys.platform.startswith("win"):
|
|
245
|
+
raise typer.Exit(run_cmd(["explorer", str(target)]).returncode)
|
|
246
|
+
opener = which("xdg-open")
|
|
247
|
+
if opener:
|
|
248
|
+
raise typer.Exit(run_cmd([opener, str(target)]).returncode)
|
|
249
|
+
fail("No editor or file manager found")
|
|
250
|
+
raise typer.Exit(1)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def main() -> None:
|
|
254
|
+
app()
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
if __name__ == "__main__":
|
|
258
|
+
main()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Command modules for the pucit CLI."""
|
pucit/commands/bypass.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Campus proxy via bypass-pucit."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
|
|
11
|
+
from pucit import config as cfg
|
|
12
|
+
from pucit.util import fail, info, ok, which
|
|
13
|
+
|
|
14
|
+
bypass_app = typer.Typer(help="Campus internet bypass (wraps bypass-pucit).")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _bypass_argv() -> list:
|
|
18
|
+
binary = which("bypass_pucit")
|
|
19
|
+
if binary:
|
|
20
|
+
return [binary]
|
|
21
|
+
# fallback: python -m bypass_pucit
|
|
22
|
+
return [shutil.which("python3") or shutil.which("python") or "python3", "-m", "bypass_pucit"]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@bypass_app.command("set")
|
|
26
|
+
def bypass_set(
|
|
27
|
+
proxy: Optional[str] = typer.Option(
|
|
28
|
+
None, "--proxy", "-p", help="Proxy URL (default from config / bypass-pucit)"
|
|
29
|
+
),
|
|
30
|
+
) -> None:
|
|
31
|
+
"""Apply campus proxy settings via bypass_pucit."""
|
|
32
|
+
argv = _bypass_argv() + ["set"]
|
|
33
|
+
proxy_url = proxy or str(cfg.get("proxy"))
|
|
34
|
+
if proxy_url:
|
|
35
|
+
argv.extend(["--proxy", proxy_url])
|
|
36
|
+
info(" ".join(argv))
|
|
37
|
+
result = subprocess.run(argv)
|
|
38
|
+
if result.returncode == 0:
|
|
39
|
+
ok("Proxy applied")
|
|
40
|
+
else:
|
|
41
|
+
fail("bypass_pucit set failed")
|
|
42
|
+
raise typer.Exit(result.returncode)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@bypass_app.command("unset")
|
|
46
|
+
def bypass_unset() -> None:
|
|
47
|
+
"""Remove campus proxy settings."""
|
|
48
|
+
argv = _bypass_argv() + ["unset"]
|
|
49
|
+
info(" ".join(argv))
|
|
50
|
+
result = subprocess.run(argv)
|
|
51
|
+
if result.returncode == 0:
|
|
52
|
+
ok("Proxy removed")
|
|
53
|
+
else:
|
|
54
|
+
fail("bypass_pucit unset failed")
|
|
55
|
+
raise typer.Exit(result.returncode)
|
pucit/commands/doctor.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Environment doctor and component listing."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import platform
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
|
|
11
|
+
from pucit import __version__
|
|
12
|
+
from pucit import docker_util as d
|
|
13
|
+
from pucit.cpp_build import find_compiler
|
|
14
|
+
from pucit.util import console, which
|
|
15
|
+
|
|
16
|
+
doctor_app = typer.Typer(help="Health checks (also available as top-level pucit doctor).")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _row(table: Table, name: str, ok: bool, detail: str) -> None:
|
|
20
|
+
mark = "[green]ok[/green]" if ok else "[red]missing[/red]"
|
|
21
|
+
table.add_row(name, mark, detail)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def run_doctor() -> int:
|
|
25
|
+
table = Table(title=f"pucit doctor (v{__version__})")
|
|
26
|
+
table.add_column("Check")
|
|
27
|
+
table.add_column("Status")
|
|
28
|
+
table.add_column("Detail")
|
|
29
|
+
|
|
30
|
+
_row(table, "OS", True, f"{platform.system()} {platform.release()} ({platform.machine()})")
|
|
31
|
+
_row(table, "Python", True, sys.version.split()[0])
|
|
32
|
+
|
|
33
|
+
compiler = find_compiler()
|
|
34
|
+
_row(table, "C++ compiler", bool(compiler), compiler or "run: pucit install pf")
|
|
35
|
+
_row(table, "make", bool(which("make")), which("make") or "—")
|
|
36
|
+
_row(table, "gdb", bool(which("gdb")), which("gdb") or "—")
|
|
37
|
+
_row(table, "cmake", bool(which("cmake")), which("cmake") or "—")
|
|
38
|
+
|
|
39
|
+
docker = d.docker_bin()
|
|
40
|
+
docker_ok = bool(docker) and d.docker_available()
|
|
41
|
+
detail = docker or "run: pucit install docker"
|
|
42
|
+
if docker and not d.docker_available():
|
|
43
|
+
detail = f"{docker} found but daemon not usable"
|
|
44
|
+
_row(table, "Docker", docker_ok, detail)
|
|
45
|
+
|
|
46
|
+
name = d.container_name()
|
|
47
|
+
if docker_ok and d.container_exists(name):
|
|
48
|
+
state = "running" if d.container_running(name) else "stopped"
|
|
49
|
+
_row(table, "Oracle container", True, f"{name} ({state})")
|
|
50
|
+
else:
|
|
51
|
+
_row(table, "Oracle container", False, "run: pucit install oracle")
|
|
52
|
+
|
|
53
|
+
bypass = which("bypass_pucit")
|
|
54
|
+
_row(table, "bypass_pucit", bool(bypass), bypass or "pip install bypass-pucit")
|
|
55
|
+
|
|
56
|
+
sqlplus = which("sqlplus")
|
|
57
|
+
_row(table, "sqlplus", bool(sqlplus), sqlplus or "optional: pucit install sqlclient")
|
|
58
|
+
|
|
59
|
+
console.print(table)
|
|
60
|
+
return 0
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def run_list() -> int:
|
|
64
|
+
table = Table(title="pucit managed components")
|
|
65
|
+
table.add_column("Component")
|
|
66
|
+
table.add_column("Status")
|
|
67
|
+
|
|
68
|
+
compiler = find_compiler()
|
|
69
|
+
table.add_row("pf / C++", "ready" if compiler else "not installed")
|
|
70
|
+
docker_ok = bool(d.docker_bin()) and d.docker_available()
|
|
71
|
+
table.add_row("docker", "ready" if docker_ok else "not ready")
|
|
72
|
+
name = d.container_name()
|
|
73
|
+
if docker_ok and d.container_exists(name):
|
|
74
|
+
table.add_row("oracle", "running" if d.container_running(name) else "stopped")
|
|
75
|
+
else:
|
|
76
|
+
table.add_row("oracle", "not installed")
|
|
77
|
+
table.add_row("bypass_pucit", "ready" if which("bypass_pucit") else "not installed")
|
|
78
|
+
table.add_row("sqlplus", "ready" if which("sqlplus") else "not installed")
|
|
79
|
+
console.print(table)
|
|
80
|
+
return 0
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Install subcommands: oracle, pf, docker, sqlclient."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from pucit.commands.oracle import oracle_install
|
|
10
|
+
from pucit.platform import PackageError, docker_packages, install_packages, pf_packages
|
|
11
|
+
from pucit.util import detect_pkg_manager, fail, info, is_windows, ok, run_cmd, which
|
|
12
|
+
|
|
13
|
+
install_app = typer.Typer(help="Install toolchains and services.")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@install_app.command("oracle")
|
|
17
|
+
def install_oracle(
|
|
18
|
+
password: Optional[str] = typer.Option(None, "--password", "-p"),
|
|
19
|
+
pull_only: bool = typer.Option(False, "--pull-only"),
|
|
20
|
+
) -> None:
|
|
21
|
+
"""Install Oracle Free via Docker."""
|
|
22
|
+
oracle_install(password=password, pull_only=pull_only)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@install_app.command("pf")
|
|
26
|
+
def install_pf() -> None:
|
|
27
|
+
"""Install Programming Fundamentals C++ essentials."""
|
|
28
|
+
try:
|
|
29
|
+
code = install_packages(pf_packages(), title="PF / C++ essentials")
|
|
30
|
+
except PackageError as exc:
|
|
31
|
+
fail(str(exc))
|
|
32
|
+
raise typer.Exit(1) from exc
|
|
33
|
+
if which("g++") or which("clang++"):
|
|
34
|
+
ok(f"Compiler: {which('g++') or which('clang++')}")
|
|
35
|
+
else:
|
|
36
|
+
info("Packages installed; open a new shell if g++ is still not on PATH.")
|
|
37
|
+
raise typer.Exit(code)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@install_app.command("docker")
|
|
41
|
+
def install_docker() -> None:
|
|
42
|
+
"""Install Docker Engine / Docker Desktop."""
|
|
43
|
+
try:
|
|
44
|
+
code = install_packages(docker_packages(), title="Docker")
|
|
45
|
+
except PackageError as exc:
|
|
46
|
+
fail(str(exc))
|
|
47
|
+
raise typer.Exit(1) from exc
|
|
48
|
+
if is_windows():
|
|
49
|
+
info("Start Docker Desktop from the Start menu, then re-open your terminal.")
|
|
50
|
+
else:
|
|
51
|
+
if which("systemctl"):
|
|
52
|
+
run_cmd(["sudo", "systemctl", "enable", "--now", "docker"], capture=True)
|
|
53
|
+
info("If needed, add yourself to the docker group: sudo usermod -aG docker $USER")
|
|
54
|
+
raise typer.Exit(code)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@install_app.command("sqlclient")
|
|
58
|
+
def install_sqlclient() -> None:
|
|
59
|
+
"""Install or guide Oracle Instant Client / sqlplus."""
|
|
60
|
+
if which("sqlplus"):
|
|
61
|
+
ok(f"sqlplus already present: {which('sqlplus')}")
|
|
62
|
+
return
|
|
63
|
+
|
|
64
|
+
if is_windows():
|
|
65
|
+
info("Windows: download Instant Client (Basic + SQL*Plus) from Oracle:")
|
|
66
|
+
info(" https://www.oracle.com/database/technologies/instant-client/downloads.html")
|
|
67
|
+
info("Add the unzipped folder to PATH, then reopen the terminal.")
|
|
68
|
+
raise typer.Exit(0)
|
|
69
|
+
|
|
70
|
+
manager = detect_pkg_manager()
|
|
71
|
+
pkgs = ["oracle-instantclient-sqlplus"] if manager == "dnf" else []
|
|
72
|
+
if pkgs:
|
|
73
|
+
try:
|
|
74
|
+
code = install_packages(pkgs, title="Oracle SQL*Plus")
|
|
75
|
+
raise typer.Exit(code)
|
|
76
|
+
except PackageError:
|
|
77
|
+
pass
|
|
78
|
+
|
|
79
|
+
info("sqlplus is not in your distro repos by default.")
|
|
80
|
+
info("Download Instant Client Basic + SQL*Plus RPMs/ZIPs from:")
|
|
81
|
+
info(" https://www.oracle.com/database/technologies/instant-client/linux-x86-64-downloads.html")
|
|
82
|
+
info("Then: sudo alien/rpm install or unzip and export LD_LIBRARY_PATH + PATH.")
|
|
83
|
+
raise typer.Exit(0)
|
pucit/commands/oracle.py
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""Oracle Database Free via Docker."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
from pucit import docker_util as d
|
|
11
|
+
from pucit.platform import PackageError, docker_packages, install_packages
|
|
12
|
+
from pucit.util import fail, info, ok, run_cmd, warn
|
|
13
|
+
|
|
14
|
+
oracle_app = typer.Typer(help="Manage the local Oracle Free container.")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@oracle_app.command("install")
|
|
18
|
+
def oracle_install(
|
|
19
|
+
password: Optional[str] = typer.Option(
|
|
20
|
+
None, "--password", "-p", help="ORACLE_PWD (generated if omitted)"
|
|
21
|
+
),
|
|
22
|
+
pull_only: bool = typer.Option(False, "--pull-only", help="Only pull the image"),
|
|
23
|
+
) -> None:
|
|
24
|
+
"""Install (pull + create) Oracle Free Docker container."""
|
|
25
|
+
if not d.docker_bin():
|
|
26
|
+
warn("Docker not found — trying to install it first")
|
|
27
|
+
try:
|
|
28
|
+
code = install_packages(docker_packages(), title="docker")
|
|
29
|
+
except PackageError as exc:
|
|
30
|
+
fail(str(exc))
|
|
31
|
+
raise typer.Exit(1) from exc
|
|
32
|
+
if code != 0:
|
|
33
|
+
raise typer.Exit(code)
|
|
34
|
+
|
|
35
|
+
if not d.docker_available():
|
|
36
|
+
fail("Docker is installed but not usable. Start the Docker daemon/Desktop and retry.")
|
|
37
|
+
raise typer.Exit(1)
|
|
38
|
+
|
|
39
|
+
info(f"Pulling {d.oracle_image()} …")
|
|
40
|
+
success, output = d.pull_image()
|
|
41
|
+
if not success:
|
|
42
|
+
fail("Image pull failed.")
|
|
43
|
+
if "unauthorized" in output.lower() or "denied" in output.lower() or "login" in output.lower():
|
|
44
|
+
info("Oracle Container Registry may require login / license accept:")
|
|
45
|
+
info(" 1) Visit https://container-registry.oracle.com/ and accept the Database Free terms")
|
|
46
|
+
info(" 2) docker login container-registry.oracle.com")
|
|
47
|
+
info(" 3) pucit install oracle")
|
|
48
|
+
else:
|
|
49
|
+
print(output)
|
|
50
|
+
raise typer.Exit(1)
|
|
51
|
+
ok("Image ready")
|
|
52
|
+
|
|
53
|
+
if pull_only:
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
name = d.container_name()
|
|
57
|
+
if d.container_exists(name):
|
|
58
|
+
ok(f"Container '{name}' already exists. Use: pucit start oracle")
|
|
59
|
+
d.print_connect_info()
|
|
60
|
+
return
|
|
61
|
+
|
|
62
|
+
pwd = password or os.environ.get("PUCIT_ORACLE_PWD") or d.load_oracle_password() or d.generate_password()
|
|
63
|
+
d.save_oracle_password(pwd)
|
|
64
|
+
argv = d.build_run_argv(pwd, name)
|
|
65
|
+
display = [a.replace(f"ORACLE_PWD={pwd}", "ORACLE_PWD=***") for a in argv]
|
|
66
|
+
info(" ".join(display))
|
|
67
|
+
result = run_cmd(argv, capture=True)
|
|
68
|
+
if result.returncode != 0:
|
|
69
|
+
fail((result.stderr or result.stdout or "docker run failed").strip())
|
|
70
|
+
raise typer.Exit(result.returncode)
|
|
71
|
+
ok(f"Created container '{name}'")
|
|
72
|
+
info("Oracle may take 1–2 minutes to become ready. Check: pucit status oracle")
|
|
73
|
+
d.print_connect_info(pwd)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@oracle_app.command("start")
|
|
77
|
+
def oracle_start() -> None:
|
|
78
|
+
"""Start the Oracle container."""
|
|
79
|
+
name = d.container_name()
|
|
80
|
+
if not d.container_exists(name):
|
|
81
|
+
fail(f"Container '{name}' not found. Run: pucit install oracle")
|
|
82
|
+
raise typer.Exit(1)
|
|
83
|
+
if d.container_running(name):
|
|
84
|
+
ok(f"'{name}' is already running")
|
|
85
|
+
return
|
|
86
|
+
result = run_cmd(d.docker_argv("start", name), capture=True)
|
|
87
|
+
if result.returncode != 0:
|
|
88
|
+
fail((result.stderr or "start failed").strip())
|
|
89
|
+
raise typer.Exit(result.returncode)
|
|
90
|
+
ok(f"Started '{name}'")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@oracle_app.command("stop")
|
|
94
|
+
def oracle_stop() -> None:
|
|
95
|
+
"""Stop the Oracle container."""
|
|
96
|
+
name = d.container_name()
|
|
97
|
+
if not d.container_exists(name):
|
|
98
|
+
fail(f"Container '{name}' not found")
|
|
99
|
+
raise typer.Exit(1)
|
|
100
|
+
result = run_cmd(d.docker_argv("stop", name), capture=True)
|
|
101
|
+
if result.returncode != 0:
|
|
102
|
+
fail((result.stderr or "stop failed").strip())
|
|
103
|
+
raise typer.Exit(result.returncode)
|
|
104
|
+
ok(f"Stopped '{name}'")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@oracle_app.command("status")
|
|
108
|
+
def oracle_status() -> None:
|
|
109
|
+
"""Show Oracle container status."""
|
|
110
|
+
name = d.container_name()
|
|
111
|
+
if not d.docker_bin():
|
|
112
|
+
fail("Docker/Podman not found")
|
|
113
|
+
raise typer.Exit(1)
|
|
114
|
+
if not d.container_exists(name):
|
|
115
|
+
warn(f"Container '{name}' is not installed")
|
|
116
|
+
raise typer.Exit(1)
|
|
117
|
+
state = "running" if d.container_running(name) else "stopped"
|
|
118
|
+
ok(f"{name}: {state}")
|
|
119
|
+
d.print_connect_info()
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@oracle_app.command("logs")
|
|
123
|
+
def oracle_logs(
|
|
124
|
+
follow: bool = typer.Option(False, "--follow", "-f", help="Follow log output"),
|
|
125
|
+
tail: int = typer.Option(100, "--tail", help="Lines to show"),
|
|
126
|
+
) -> None:
|
|
127
|
+
"""Show Oracle container logs."""
|
|
128
|
+
name = d.container_name()
|
|
129
|
+
argv = d.docker_argv("logs", "--tail", str(tail))
|
|
130
|
+
if follow:
|
|
131
|
+
argv.append("-f")
|
|
132
|
+
argv.append(name)
|
|
133
|
+
raise typer.Exit(run_cmd(argv).returncode)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@oracle_app.command("rm")
|
|
137
|
+
def oracle_rm(
|
|
138
|
+
volumes: bool = typer.Option(False, "--volumes", "-v", help="Also remove data volume"),
|
|
139
|
+
force: bool = typer.Option(False, "--force", "-f", help="Force remove"),
|
|
140
|
+
) -> None:
|
|
141
|
+
"""Remove the Oracle container."""
|
|
142
|
+
name = d.container_name()
|
|
143
|
+
if not d.container_exists(name):
|
|
144
|
+
warn(f"Container '{name}' not found")
|
|
145
|
+
return
|
|
146
|
+
argv = d.docker_argv("rm")
|
|
147
|
+
if force:
|
|
148
|
+
argv.append("-f")
|
|
149
|
+
if volumes:
|
|
150
|
+
argv.append("-v")
|
|
151
|
+
argv.append(name)
|
|
152
|
+
result = run_cmd(argv, capture=True)
|
|
153
|
+
if result.returncode != 0:
|
|
154
|
+
fail((result.stderr or "rm failed").strip())
|
|
155
|
+
raise typer.Exit(result.returncode)
|
|
156
|
+
ok(f"Removed '{name}'")
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@oracle_app.command("connect")
|
|
160
|
+
def oracle_connect(
|
|
161
|
+
launch: bool = typer.Option(False, "--launch", "-l", help="Launch sqlplus if available"),
|
|
162
|
+
) -> None:
|
|
163
|
+
"""Print (or launch) a sqlplus connection."""
|
|
164
|
+
from pucit.util import which
|
|
165
|
+
|
|
166
|
+
d.print_connect_info()
|
|
167
|
+
if not launch:
|
|
168
|
+
return
|
|
169
|
+
sqlplus = which("sqlplus")
|
|
170
|
+
if not sqlplus:
|
|
171
|
+
fail("sqlplus not found. Try: pucit install sqlclient")
|
|
172
|
+
raise typer.Exit(1)
|
|
173
|
+
pwd = d.load_oracle_password() or "oracle"
|
|
174
|
+
port = d.oracle_port()
|
|
175
|
+
conn = f"system/{pwd}@//localhost:{port}/FREEPDB1"
|
|
176
|
+
raise typer.Exit(run_cmd([sqlplus, conn]).returncode)
|