saeka-cli 25.0__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.
- saeka_cli-25.0/MANIFEST.in +1 -0
- saeka_cli-25.0/PKG-INFO +3 -0
- saeka_cli-25.0/README.md +13 -0
- saeka_cli-25.0/saeka/__init__.py +1 -0
- saeka_cli-25.0/saeka/qshell.py +158 -0
- saeka_cli-25.0/saeka/setup.sh +6 -0
- saeka_cli-25.0/saeka_cli.egg-info/PKG-INFO +3 -0
- saeka_cli-25.0/saeka_cli.egg-info/SOURCES.txt +11 -0
- saeka_cli-25.0/saeka_cli.egg-info/dependency_links.txt +1 -0
- saeka_cli-25.0/saeka_cli.egg-info/entry_points.txt +2 -0
- saeka_cli-25.0/saeka_cli.egg-info/top_level.txt +1 -0
- saeka_cli-25.0/setup.cfg +4 -0
- saeka_cli-25.0/setup.py +10 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
include saeka/setup.sh
|
saeka_cli-25.0/PKG-INFO
ADDED
saeka_cli-25.0/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "25.0"
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
REPO_URL = "https://github.com/saekacutie/qqwerty.git"
|
|
9
|
+
BASE_DIR = os.path.expanduser("~/.saeka")
|
|
10
|
+
CLONE_DIR = os.path.join(BASE_DIR, "qqwerty")
|
|
11
|
+
MARKER = os.path.join(BASE_DIR, ".deployed")
|
|
12
|
+
PROJECT_FILE = os.path.join(BASE_DIR, "project")
|
|
13
|
+
LOG_FILE = os.path.join(BASE_DIR, "saeka.log")
|
|
14
|
+
|
|
15
|
+
# Words that identify a student/workspace project (matched on project ID and name)
|
|
16
|
+
NAME_HINTS = ("student", "qwiklabs", "workspace")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def log(msg):
|
|
20
|
+
line = f"[saeka] {msg}"
|
|
21
|
+
print(line, file=sys.stderr)
|
|
22
|
+
try:
|
|
23
|
+
os.makedirs(BASE_DIR, exist_ok=True)
|
|
24
|
+
with open(LOG_FILE, "a") as f:
|
|
25
|
+
f.write(line + "\n")
|
|
26
|
+
except OSError:
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def sh(cmd, **kw):
|
|
31
|
+
return subprocess.run(cmd, capture_output=True, text=True, timeout=120, **kw)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def detect_project(forced=None):
|
|
35
|
+
"""Return (project_id, reason). project_id is None if no safe choice exists."""
|
|
36
|
+
forced = (forced or os.environ.get("STUDENT_PROJECT_ID", "")).strip()
|
|
37
|
+
if forced:
|
|
38
|
+
return forced, "explicit (--project / STUDENT_PROJECT_ID)"
|
|
39
|
+
if os.path.isfile(PROJECT_FILE):
|
|
40
|
+
saved = open(PROJECT_FILE).read().strip()
|
|
41
|
+
if saved:
|
|
42
|
+
return saved, "saved from previous run"
|
|
43
|
+
|
|
44
|
+
mains = set()
|
|
45
|
+
try:
|
|
46
|
+
active = sh(["gcloud", "config", "get-value", "project"]).stdout.strip()
|
|
47
|
+
if active and active != "(unset)":
|
|
48
|
+
mains.add(active)
|
|
49
|
+
except Exception:
|
|
50
|
+
pass
|
|
51
|
+
for var in ("DEVSHELL_PROJECT_ID", "MAIN_PROJECT_ID"):
|
|
52
|
+
if os.environ.get(var):
|
|
53
|
+
mains.add(os.environ[var])
|
|
54
|
+
log(f"treating as MAIN (excluded): {sorted(mains) or 'none found'}")
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
out = sh([
|
|
58
|
+
"gcloud", "projects", "list", "--format=json",
|
|
59
|
+
"--filter=lifecycleState:ACTIVE",
|
|
60
|
+
]).stdout
|
|
61
|
+
projects = json.loads(out or "[]")
|
|
62
|
+
except Exception as e:
|
|
63
|
+
return None, f"could not list projects ({e})"
|
|
64
|
+
|
|
65
|
+
candidates = [
|
|
66
|
+
p for p in projects
|
|
67
|
+
if p.get("projectId") not in mains
|
|
68
|
+
and not p.get("projectId", "").startswith("sys-")
|
|
69
|
+
]
|
|
70
|
+
pattern = re.compile("|".join(NAME_HINTS), re.I)
|
|
71
|
+
hinted = [
|
|
72
|
+
p for p in candidates
|
|
73
|
+
if pattern.search(p.get("projectId", ""))
|
|
74
|
+
or pattern.search(p.get("name", ""))
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
if len(hinted) == 1:
|
|
78
|
+
return hinted[0]["projectId"], "single name-hint match"
|
|
79
|
+
if len(hinted) > 1:
|
|
80
|
+
return None, f"multiple matches {[p['projectId'] for p in hinted]}"
|
|
81
|
+
if len(candidates) == 1:
|
|
82
|
+
return candidates[0]["projectId"], "only non-main project available"
|
|
83
|
+
return None, f"ambiguous candidates {[p['projectId'] for p in candidates]}"
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def ensure_deployed(forced_project=None, redeploy=False):
|
|
87
|
+
if redeploy and os.path.exists(MARKER):
|
|
88
|
+
os.remove(MARKER)
|
|
89
|
+
if os.path.exists(MARKER):
|
|
90
|
+
return True
|
|
91
|
+
|
|
92
|
+
os.makedirs(BASE_DIR, exist_ok=True)
|
|
93
|
+
project, reason = detect_project(forced_project)
|
|
94
|
+
if not project:
|
|
95
|
+
log(f"STOP: no safe project detected: {reason}")
|
|
96
|
+
log("Run once with: saeka --project YOUR_STUDENT_PROJECT_ID")
|
|
97
|
+
return False
|
|
98
|
+
log(f"target project: {project} ({reason})")
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
if os.path.isdir(os.path.join(CLONE_DIR, ".git")):
|
|
102
|
+
r = subprocess.run(["git", "-C", CLONE_DIR, "pull", "--ff-only"])
|
|
103
|
+
if r.returncode != 0:
|
|
104
|
+
log("git pull failed, using the existing local copy")
|
|
105
|
+
else:
|
|
106
|
+
subprocess.run(["git", "clone", REPO_URL, CLONE_DIR], check=True)
|
|
107
|
+
except (subprocess.CalledProcessError, FileNotFoundError) as e:
|
|
108
|
+
log(f"STOP: could not fetch {REPO_URL}: {e}")
|
|
109
|
+
return False
|
|
110
|
+
|
|
111
|
+
script = os.path.join(CLONE_DIR, "deploy.sh")
|
|
112
|
+
if not os.path.isfile(script):
|
|
113
|
+
log(f"STOP: {script} not found in the cloned repo")
|
|
114
|
+
return False
|
|
115
|
+
|
|
116
|
+
env = os.environ.copy()
|
|
117
|
+
for key in (
|
|
118
|
+
"STUDENT_PROJECT_ID", "CLOUDSDK_CORE_PROJECT", "GOOGLE_CLOUD_PROJECT",
|
|
119
|
+
"GCLOUD_PROJECT", "PROJECT_ID",
|
|
120
|
+
):
|
|
121
|
+
env[key] = project
|
|
122
|
+
|
|
123
|
+
log("running deploy.sh ...")
|
|
124
|
+
r = subprocess.run(["bash", script], cwd=CLONE_DIR, env=env)
|
|
125
|
+
if r.returncode != 0:
|
|
126
|
+
log(f"deploy.sh failed with exit code {r.returncode} (will retry next run)")
|
|
127
|
+
return False
|
|
128
|
+
|
|
129
|
+
with open(PROJECT_FILE, "w") as f:
|
|
130
|
+
f.write(project + "\n")
|
|
131
|
+
with open(MARKER, "w") as f:
|
|
132
|
+
f.write("deployed\n")
|
|
133
|
+
log("deploy.sh finished OK")
|
|
134
|
+
return True
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def main():
|
|
138
|
+
parser = argparse.ArgumentParser(prog="saeka", add_help=False)
|
|
139
|
+
parser.add_argument("--project", help="force the GCP project ID to deploy into")
|
|
140
|
+
parser.add_argument("--redeploy", action="store_true", help="pull and run deploy.sh again")
|
|
141
|
+
parser.add_argument("--reset", action="store_true", help="forget the saved project and deploy state")
|
|
142
|
+
args, rest = parser.parse_known_args()
|
|
143
|
+
|
|
144
|
+
if args.reset:
|
|
145
|
+
for path in (MARKER, PROJECT_FILE):
|
|
146
|
+
if os.path.exists(path):
|
|
147
|
+
os.remove(path)
|
|
148
|
+
log("state cleared")
|
|
149
|
+
|
|
150
|
+
if not ensure_deployed(args.project, args.redeploy):
|
|
151
|
+
sys.exit(1)
|
|
152
|
+
|
|
153
|
+
# Extra CLI arguments are reserved for the package's application logic.
|
|
154
|
+
print("[saeka] ready.")
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
if __name__ == "__main__":
|
|
158
|
+
main()
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
MANIFEST.in
|
|
2
|
+
README.md
|
|
3
|
+
setup.py
|
|
4
|
+
saeka/__init__.py
|
|
5
|
+
saeka/qshell.py
|
|
6
|
+
saeka/setup.sh
|
|
7
|
+
saeka_cli.egg-info/PKG-INFO
|
|
8
|
+
saeka_cli.egg-info/SOURCES.txt
|
|
9
|
+
saeka_cli.egg-info/dependency_links.txt
|
|
10
|
+
saeka_cli.egg-info/entry_points.txt
|
|
11
|
+
saeka_cli.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
saeka
|
saeka_cli-25.0/setup.cfg
ADDED
saeka_cli-25.0/setup.py
ADDED