Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[STU-337] Cloud integration: Loading test profile #1156

Merged
merged 35 commits into from
Apr 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
782928e
[stu-337-test-profile] chore: Working prototype
LatentDream Apr 14, 2024
426132f
[stu-337-test-profile] chore: migrate logic to hooks
LatentDream Apr 14, 2024
aaa4d88
[stu-337-test-profile] chore: typo in cmd
LatentDream Apr 15, 2024
eee0e39
chore: using run with capture_output for MacOS compatibility
LatentDream Apr 15, 2024
db1ccf8
[stu-337-test-profile] chore(test_profile): update / checkout route +…
LatentDream Apr 15, 2024
b53d674
[stu-337-test-profile] chore: load and upload hash
LatentDream Apr 15, 2024
e8685a6
[stu-337-test-profile] chore: remove the update route in favor of aut…
LatentDream Apr 15, 2024
29790f8
[stu-337-test-profile] chore: working live update
LatentDream Apr 15, 2024
55b07f3
[stu-337-test-profile] chore: prod line -> Test Profile
LatentDream Apr 16, 2024
915ced9
[stu-337-test-profile] Merge branch 'main' into stu-337-test-profile
LatentDream Apr 16, 2024
b9d512b
chore: format
LatentDream Apr 16, 2024
1d791ef
[stu-337-test-profile] fix(sequencer): upload problem
LatentDream Apr 16, 2024
66568b9
[stu-337-test-profile] chore: fix text in modal
LatentDream Apr 16, 2024
547a54a
[stu-337-test-profile] chore(ui): fix margin and empty space while be…
LatentDream Apr 16, 2024
c98fd9f
[stu-337-test-profile] chore(ui): switch "paused" to "pending" when a…
LatentDream Apr 16, 2024
3e4d893
[stu-337-test-profile] chore: viewer is obligated to load the test pr…
LatentDream Apr 16, 2024
458db59
[stu-337-test-profile] chore: clear sequencer before loading a test p…
LatentDream Apr 16, 2024
9b52688
[stu-337-test-profile] chore: clear sequencer in file option
LatentDream Apr 16, 2024
12e5dad
[stu-337-test-profile] chore: viewer permission with Cloud integratio…
LatentDream Apr 16, 2024
402a232
chore: format
LatentDream Apr 16, 2024
b92d03b
[stu-337-test-profile] Merge with main
LatentDream Apr 16, 2024
125f347
[stu-337-test-profile] chore: verif cloud form before run when Upload…
LatentDream Apr 16, 2024
1418ab3
[stu-337-test-profile] chore: Eslint fix
LatentDream Apr 16, 2024
5dfb553
chore: fix typo
LatentDream Apr 16, 2024
929c518
chore: Nullish coalescing operator
LatentDream Apr 16, 2024
afddadf
chore: making things a bit more readable
LatentDream Apr 16, 2024
5eed02d
chore: formatting
LatentDream Apr 16, 2024
10a21f7
Merge branch 'main' into stu-337-test-profile
LatentDream Apr 16, 2024
6c99d5b
Merge branch 'main' into stu-337-test-profile
LatentDream Apr 16, 2024
7826e4c
[stu-337-test-profile] revert: default for transform
LatentDream Apr 16, 2024
e1bf35e
chore: formatting
LatentDream Apr 16, 2024
9b8e854
[stu-337-test-profile] chore: adding create_at field
LatentDream Apr 16, 2024
66b02c9
[stu-337-test-profile] Merge branch 'stu-337-test-profile' of https:/…
LatentDream Apr 16, 2024
9247c0b
chore: removing header for cloud health check
LatentDream Apr 16, 2024
a616500
Merge branch 'main' into stu-337-test-profile
LatentDream Apr 16, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions captain/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
devices,
flowchart,
key,
update,
test_profile,
ws,
log,
test_sequence,
Expand Down Expand Up @@ -42,7 +42,7 @@ async def startup_event(app: FastAPI):
app.include_router(flowchart.router)
app.include_router(log.router)
app.include_router(key.router)
app.include_router(update.router)
app.include_router(test_profile.router)
app.include_router(blocks.router)
app.include_router(devices.router)
app.include_router(test_sequence.router)
Expand Down
11 changes: 7 additions & 4 deletions captain/routes/cloud.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,17 +136,18 @@ class Unit(CloudModel):
part_variation_id: str = Field(..., alias="partVariationId")


class Measurement(CloudModel):
class Measurement(BaseModel):
test_id: str = Field(..., alias="testId")
sequence_name: str = Field(..., alias="sequenceName")
cycle_number: int = Field(..., alias="cycleNumber")
name: str
pass_: Optional[bool]
completion_time: float = Field(..., alias="completionTime")
created_at: str = Field(..., alias="createdAt")


class Session(CloudModel):
serial_number: str
class Session(BaseModel):
serial_number: str = Field(..., alias="serialNumber")
station_id: str = Field(..., alias="stationId")
integrity: bool
aborted: bool
Expand Down Expand Up @@ -218,6 +219,7 @@ async def get_cloud_projects():
"label": p.name,
"value": p.id,
"part": part_var.model_dump(by_alias=True),
"repoUrl": p.repo_url,
"productName": part.product_name,
}
)
Expand Down Expand Up @@ -273,6 +275,7 @@ async def post_cloud_session(_: Response, body: Session):
logging.info("Posting session")
url = get_flojoy_cloud_url() + "session/"
payload = body.model_dump(by_alias=True)
payload["createdAt"] = utcnow_str()
for i, m in enumerate(payload["measurements"]):
m["data"] = make_payload(get_measurement(body.measurements[i]))
m["pass"] = m.pop("pass_")
Expand Down Expand Up @@ -318,7 +321,7 @@ async def get_cloud_health(url: Annotated[str | None, Header()]):
if url is None:
url = get_flojoy_cloud_url()
url = url + "health/"
response = requests.get(url, headers=headers_builder())
response = requests.get(url)
if response.status_code == 200:
return Response(status_code=200)
else:
Expand Down
159 changes: 159 additions & 0 deletions captain/routes/test_profile.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import logging
import traceback
import subprocess
from typing import Annotated
from fastapi import APIRouter, Header, Response
import os
import json
from captain.utils.blocks_path import get_flojoy_dir


router = APIRouter(tags=["test_profile"])


@router.get("/test_profile/install/")
async def install(url: Annotated[str, Header()]):
"""
Download a git repo to the local machine if it's doesn't exist + verify its state
- Currently done for Github. (infer that the repo doesn't contain space)
- Private repo is not (directly) supported
TODO:
- [ ] Backup if git is not install on the system
"""
try:
logging.info(f"Installing the profile from the url: {url}")
verify_git_install()
profiles_path = get_profiles_dir()
profile_path = get_profile_path_from_url(profiles_path, url)

# Find the profile
if not os.path.exists(profile_path):
# Clone the repo if it doesn't exist
cmd = ["git", "clone", "--depth", "1", url, profile_path]
res = subprocess.run(cmd, capture_output=True)
if res.returncode != 0:
stdout = res.stdout.decode("utf-8").strip()
stderr = res.stderr.decode("utf-8").strip()
logging.error(f"Error while cloning url: {stdout} - {stderr}")
raise Exception(f"Not able to clone {url} - Error: {res.returncode}")
else:
update_to_origin_main(profile_path)

commit_hash = get_commit_hash(profile_path)

# Always use / in the path for compatibility
profile_path = profile_path.replace(os.sep, "/")

return Response(
status_code=200,
content=json.dumps({"profile_root": profile_path, "hash": commit_hash}),
)

except Exception as e:
logging.error(f"Exception occured while installing {url}: {e}")
logging.error(traceback.format_exc())
Response(status_code=500, content=json.dumps({"error": f"{e}"}))


@router.post("/test_profile/checkout/{commit_hash}/")
async def checkout(url: Annotated[str, Header()], commit_hash: str):
try:
logging.info(f"Switching to the commit: {commit_hash} for the profile: {url}")

verify_git_install()
profiles_path = get_profiles_dir()
profile_path = get_profile_path_from_url(profiles_path, url)
curr_commit_hash = get_commit_hash(profile_path)

if curr_commit_hash != commit_hash:
# Fetch the lastest change
cmd = ["git", "-C", profile_path, "fetch", "--all"]
res = subprocess.run(cmd, capture_output=True)
if res.returncode != 0:
raise Exception(f"Not able to fetch the repo - Error: {res.returncode}")

# Switch to the specific commit
cmd = ["git", "-C", profile_path, "checkout", commit_hash]
res = subprocess.run(cmd, capture_output=True)
if res.returncode != 0:
raise Exception(
f"Not able to checkout the commit - Error: {res.returncode}"
)

commit_hash = get_commit_hash(profile_path)

return Response(
status_code=200,
content=json.dumps({"profile_root": profile_path, "hash": commit_hash}),
)

except Exception as e:
logging.error(f"Exception occured while installing {url}: {e}")
logging.error(traceback.format_exc())
Response(status_code=500, content=json.dumps({"error": f"{e}"}))


# Helper functions ------------------------------------------------------------


def get_profile_path_from_url(profiles_path: str, url: str):
"""Get the profile directory name from the url"""
profile_name = url.split("/")[-1].strip(".git")
logging.info(f"Profile name: {profile_name}")
profile_root = os.path.join(profiles_path, profile_name)
return profile_root


def verify_git_install():
"""Verify if git is installed on the system"""
cmd = ["git", "--version"]
res = subprocess.run(cmd, capture_output=True)
if res.returncode != 0:
raise NotImplementedError("Git is not found on you system")


def get_profiles_dir():
profiles_dir = os.path.join(get_flojoy_dir(), f"test_profiles{os.sep}")
if not os.path.exists(profiles_dir):
os.makedirs(profiles_dir)
return profiles_dir


def get_commit_hash(profile_path: str):
"""Get the commit hash of the current env."""
cmd = ["git", "-C", profile_path, "rev-parse", "HEAD"]
res = subprocess.run(cmd, capture_output=True)
if res.returncode != 0:
raise Exception(
f"Not able to get the commit ID of the local branch - Error: {res.returncode}"
)
return res.stdout.strip().decode()


def update_to_origin_main(profile_path: str):
"""Update the local repo to the lastest version"""
logging.info("Updating the repo to the origin main")

# Verify the repo is clean (no changes so the user doesn't lose any work)
cmd = ["git", "-C", profile_path, "status", "--porcelain"]
res = subprocess.run(cmd, capture_output=True)
if res.returncode != 0:
raise Exception(
f"Not able to check the status of the repo - Error: {res.returncode}"
)
if res.stdout.strip() != b"":
raise Exception(f"Repo is not clean - {res.stdout}")

# Get the lastest change
cmd = ["git", "-C", profile_path, "fetch", "--all"]
res = subprocess.run(cmd, capture_output=True)
if res.returncode != 0:
raise Exception(f"Not able to fetch the repo - Error: {res.returncode}")

# Switch to the lastest change
cmd = ["git", "-C", profile_path, "checkout", "origin/main"]
res = subprocess.run(cmd, capture_output=True)
if res.returncode != 0:
raise Exception(
f"Not able to checkout the remote origin main - Error: {res.returncode}"
)
40 changes: 0 additions & 40 deletions captain/routes/update.py

This file was deleted.

6 changes: 3 additions & 3 deletions src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,14 +145,14 @@ export default {
): Promise<{ filePath: string; fileContent: string }[] | undefined> =>
ipcRenderer.invoke(API.openFilesPicker, allowedExtensions, title),

openAllFilesInFolderPicker: (
openAllFilesInFolder: (
folderPath: string,
allowedExtensions: string[] = ["json"],
title: string = "Select Folder",
): Promise<{ filePath: string; fileContent: string }[] | undefined> =>
ipcRenderer.invoke(
API.openAllFilesInFolderPicker,
folderPath,
allowedExtensions,
title,
),

getFileContent: (filepath: string): Promise<string> =>
Expand Down
59 changes: 25 additions & 34 deletions src/main/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,42 +188,33 @@ export const openFilesPicker = (

export const openAllFilesInFolderPicker = (
_,
folderPath: string,
allowedExtensions: string[] = ["json"],
title: string = "Select Folder",
): Promise<{ filePath: string; fileContent: string }[] | undefined> => {
// Return mutiple files or all file with the allowed extensions if a folder is selected
return dialog
.showOpenDialog(global.mainWindow, {
title: title,
properties: ["openDirectory"],
})
.then((selectedPaths) => {
if (
selectedPaths.filePaths.length === 1 &&
fs.lstatSync(selectedPaths.filePaths[0]).isDirectory()
) {
// If a folder is selected, found all file with the allowed extensions from that folder
const folerPath = selectedPaths.filePaths[0];
const paths: string[] = [];
fs.readdirSync(folerPath, { withFileTypes: true }).forEach((dirent) => {
if (dirent.isFile()) {
const nameAndExt = dirent.name.split(".");
const ext = nameAndExt[nameAndExt.length - 1];
if (allowedExtensions.includes(ext)) {
paths.push(join(folerPath, dirent.name));
}
}
});
const files = paths.map((path) => {
return {
filePath: path.split(sep).join(posix.sep),
fileContent: fs.readFileSync(path, { encoding: "utf-8" }),
};
});
return files;
): { filePath: string; fileContent: string }[] | undefined => {
// Return multiple files or all files with the allowed extensions if a folder is selected
if (!fs.existsSync(folderPath) || !fs.lstatSync(folderPath).isDirectory()) {
return undefined;
}
// If a folder is selected, find all files with the allowed extensions from that folder
const paths: string[] = [];
fs.readdirSync(folderPath, { withFileTypes: true }).forEach((dirent) => {
if (dirent.isFile()) {
const nameAndExt = dirent.name.split(".");
const ext = nameAndExt[nameAndExt.length - 1];
if (allowedExtensions.includes(ext)) {
paths.push(join(folderPath, dirent.name));
}
return undefined;
});
}
});
// Read the content of the files
const files = paths.map((path) => {
return {
filePath: path.split(sep).join(posix.sep),
fileContent: fs.readFileSync(path, { encoding: "utf-8" }),
};
});

return files;
};

export const cleanup = async () => {
Expand Down
Loading
Loading