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.
@@ -0,0 +1 @@
1
+ include saeka/setup.sh
@@ -0,0 +1,3 @@
1
+ Metadata-Version: 2.4
2
+ Name: saeka-cli
3
+ Version: 25.0
@@ -0,0 +1,13 @@
1
+ # xgp
2
+
3
+ # saeka
4
+
5
+ This repository contains the `saeka` Python package and its publishing metadata.
6
+
7
+ Install with:
8
+
9
+ ```sh
10
+ python -m pip install .
11
+ ```
12
+
13
+ This provides the `saeka` command-line entry point.
@@ -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,6 @@
1
+ #!/bin/sh
2
+ set -eu
3
+
4
+ # Install the package from the directory containing this script.
5
+ cd "$(dirname "$0")"
6
+ python -m pip install .
@@ -0,0 +1,3 @@
1
+ Metadata-Version: 2.4
2
+ Name: saeka-cli
3
+ Version: 25.0
@@ -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,2 @@
1
+ [console_scripts]
2
+ saeka = saeka.qshell:main
@@ -0,0 +1 @@
1
+ saeka
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,10 @@
1
+ from setuptools import find_packages, setup
2
+
3
+ setup(
4
+ name="saeka-cli",
5
+ version="25.0",
6
+ packages=find_packages(),
7
+ include_package_data=True,
8
+ package_data={"saeka": ["setup.sh"]},
9
+ entry_points={"console_scripts": ["saeka=saeka.qshell:main"]},
10
+ )