diff --git a/coretex/cli/commands/login.py b/coretex/cli/commands/login.py index e46bd3f7..4189be96 100644 --- a/coretex/cli/commands/login.py +++ b/coretex/cli/commands/login.py @@ -18,7 +18,7 @@ import click from ..modules import user, ui -from ...configuration import UserConfiguration, InvalidConfiguration, ConfigurationNotFound +from ...configuration import UserConfiguration, InvalidConfiguration, ConfigurationNotFound, utils @click.command() @@ -38,5 +38,9 @@ def login() -> None: ui.stdEcho("Please enter your credentials:") userConfig = user.configUser() + + initialData = utils.fetchInitialData() + userConfig.frontendUrl = initialData.get("frontend_url", "app.coretex.ai/") + userConfig.save() ui.successEcho(f"User {userConfig.username} successfully logged in.") diff --git a/coretex/cli/commands/model.py b/coretex/cli/commands/model.py index 6d2d4bb0..96b0e517 100644 --- a/coretex/cli/commands/model.py +++ b/coretex/cli/commands/model.py @@ -31,7 +31,7 @@ def create(name: str, path: str, project: Optional[str], accuracy: float) -> Non model.upload(path) ui.successEcho(f"Model \"{model.name}\" created successfully") - ui.stdEcho(f"A new model has been created. You can open it by clicking on this URL {ui.outputUrl(model.entityUrl())}.") + ui.stdEcho(f"A new model has been created. You can open it by clicking on this URL {ui.outputUrl(userConfig.frontendUrl, model.entityUrl())}.") @click.group() diff --git a/coretex/cli/commands/project.py b/coretex/cli/commands/project.py index f701d881..d4e5e4ce 100644 --- a/coretex/cli/commands/project.py +++ b/coretex/cli/commands/project.py @@ -32,8 +32,8 @@ @click.option("--type", "-t", "projectType", type = int, help = "Project type") @click.option("--description", "-d", type = str, help = "Project description") def create(name: Optional[str], projectType: Optional[int], description: Optional[str]) -> None: - project = project_utils.createProject(name, projectType, description) userConfig = UserConfiguration.load() + project = project_utils.createProject(userConfig.frontendUrl, name, projectType, description) selectNewProject = ui.clickPrompt("Do you want to select the new project as default? (Y/n)", type = bool, default = True) if selectNewProject: @@ -86,7 +86,11 @@ def select(name: str) -> None: userConfig.selectProject(project.id) except ValueError: ui.errorEcho(f"Project \"{name}\" not found.") - project = project_utils.promptProjectCreate("Do you want to create a project with that name?", name) + project = project_utils.promptProjectCreate( + "Do you want to create a project with that name?", + name, + userConfig.frontendUrl + ) if project is None: return diff --git a/coretex/cli/commands/task.py b/coretex/cli/commands/task.py index ffb22dde..18a1e17e 100644 --- a/coretex/cli/commands/task.py +++ b/coretex/cli/commands/task.py @@ -18,14 +18,16 @@ from typing import Optional import click +import webbrowser +from ..modules import ui from ..modules.project_utils import getProject from ..modules.user import initializeUserSession from ..modules.utils import onBeforeCommandExecute from ..modules.project_utils import getProject from ..._folder_manager import folder_manager from ..._task import TaskRunWorker, executeRunLocally, readTaskConfig, runLogger -from ...configuration import UserConfiguration, NodeConfiguration +from ...configuration import UserConfiguration from ...entities import TaskRun, TaskRunStatus from ...resources import PYTHON_ENTRY_POINT_PATH from ..._task import TaskRunWorker, executeRunLocally, readTaskConfig, runLogger @@ -36,7 +38,6 @@ class RunException(Exception): @click.command() -@onBeforeCommandExecute(initializeUserSession) @click.argument("path", type = click.Path(exists = True, dir_okay = False)) @click.option("--name", type = str, default = None) @click.option("--description", type = str, default = None) @@ -58,6 +59,14 @@ def run(path: str, name: Optional[str], description: Optional[str], snapshot: bo if selectedProject is None: return + ui.stdEcho( + "Project info: " + f"\n\tName: {selectedProject.name}" + f"\n\tProject type: {selectedProject.projectType.name}" + f"\n\tDescription: {selectedProject.description}" + f"\n\tCreated on: {selectedProject.createdOn}" + ) + taskRun: TaskRun = TaskRun.runLocal( selectedProject.id, snapshot, @@ -67,6 +76,12 @@ def run(path: str, name: Optional[str], description: Optional[str], snapshot: bo entryPoint = path ) + ui.stdEcho( + "Task Run successfully started. " + f"You can open it by clicking on this URL {ui.outputUrl(userConfig.frontendUrl, taskRun.entityUrl())}." + ) + webbrowser.open(f"{userConfig.frontendUrl}/{taskRun.entityUrl()}") + taskRun.updateStatus(TaskRunStatus.preparingToStart) with TaskRunWorker(userConfig.refreshToken, taskRun.id): @@ -95,3 +110,12 @@ def run(path: str, name: Optional[str], description: Optional[str], snapshot: bo taskRun.updateStatus(TaskRunStatus.completedWithSuccess) folder_manager.clearTempFiles() + + +@click.group() +@onBeforeCommandExecute(initializeUserSession) +def task() -> None: + pass + + +task.add_command(run, "run") diff --git a/coretex/cli/main.py b/coretex/cli/main.py index 96da0e0b..98c22d90 100644 --- a/coretex/cli/main.py +++ b/coretex/cli/main.py @@ -22,7 +22,7 @@ from .commands.login import login from .commands.model import model from .commands.node import node -from .commands.task import run +from .commands.task import task from .commands.project import project from .modules import ui, utils @@ -67,6 +67,6 @@ def cli() -> None: cli.add_command(model) cli.add_command(project) cli.add_command(node) -cli.add_command(run) +cli.add_command(task) cli.add_command(version) cli.add_command(update) diff --git a/coretex/cli/modules/project_utils.py b/coretex/cli/modules/project_utils.py index cbe86618..59f81989 100644 --- a/coretex/cli/modules/project_utils.py +++ b/coretex/cli/modules/project_utils.py @@ -40,7 +40,7 @@ def selectProjectVisibility() -> ProjectVisibility: return selectedProjectVisibility -def promptProjectCreate(message: str, name: str) -> Optional[Project]: +def promptProjectCreate(message: str, name: str, frontendUrl: str) -> Optional[Project]: if not click.confirm(message, default = True): return None @@ -49,7 +49,10 @@ def promptProjectCreate(message: str, name: str) -> Optional[Project]: try: project = Project.createProject(name, selectedProjectType) ui.successEcho(f"Project \"{name}\" created successfully.") - ui.stdEcho(f"A new Project has been created. You can open it by clicking on this URL {ui.outputUrl(project.entityUrl())}.") + ui.stdEcho( + "A new Project has been created. " + f"You can open it by clicking on this URL {ui.outputUrl(frontendUrl, project.entityUrl())}." + ) return project except NetworkRequestError as ex: logging.getLogger("cli").debug(ex, exc_info = ex) @@ -66,7 +69,11 @@ def promptProjectSelect(userConfig: UserConfiguration) -> Optional[Project]: userConfig.selectProject(project.id) except ValueError: ui.errorEcho(f"Project \"{name}\" not found.") - newProject = promptProjectCreate("Do you want to create a project with that name?", name) + newProject = promptProjectCreate( + "Do you want to create a project with that name?", + name, + userConfig.frontendUrl + ) if newProject is None: return None @@ -75,7 +82,7 @@ def promptProjectSelect(userConfig: UserConfiguration) -> Optional[Project]: return project -def createProject(name: Optional[str] = None, projectType: Optional[int] = None, description: Optional[str] = None) -> Project: +def createProject(frontendUrl: str, name: Optional[str] = None, projectType: Optional[int] = None, description: Optional[str] = None) -> Project: if name is None: name = ui.clickPrompt("Please enter name of the project you want to create", type = str) @@ -90,7 +97,10 @@ def createProject(name: Optional[str] = None, projectType: Optional[int] = None, try: project = Project.createProject(name, projectType, description = description) ui.successEcho(f"Project \"{name}\" created successfully.") - ui.stdEcho(f"A new Project has been created. You can open it by clicking on this URL {ui.outputUrl(project.entityUrl())}.") + ui.stdEcho( + "A new Project has been created. " + f"You can open it by clicking on this URL {ui.outputUrl(frontendUrl, project.entityUrl())}." + ) return project except NetworkRequestError as ex: logging.getLogger("cli").debug(ex, exc_info = ex) @@ -104,7 +114,11 @@ def getProject(name: Optional[str], userConfig: UserConfiguration) -> Optional[P return Project.fetchOne(name = name) except: if projectId is None: - return promptProjectCreate("Project not found. Do you want to create a new Project with that name?", name) + return promptProjectCreate( + "Project not found. Do you want to create a new Project with that name?", + name, + userConfig.frontendUrl + ) return Project.fetchById(projectId) @@ -116,6 +130,6 @@ def getProject(name: Optional[str], userConfig: UserConfiguration) -> Optional[P if not click.confirm("Would you like to create a new Project?", default = True): return None - return createProject(name) + return createProject(userConfig.frontendUrl, name) return Project.fetchById(projectId) diff --git a/coretex/cli/modules/ui.py b/coretex/cli/modules/ui.py index 1187423b..38e6f98e 100644 --- a/coretex/cli/modules/ui.py +++ b/coretex/cli/modules/ui.py @@ -83,8 +83,8 @@ def previewNodeConfig(nodeConfig: NodeConfiguration) -> None: stdEcho(tabulate(table)) -def outputUrl(entityUrl: str) -> str: - return ("\033[4m" + f"https://app.coretex.ai/{entityUrl}" + "\033[0m") +def outputUrl(baseUrl: str, entityUrl: str) -> str: + return ("\033[4m" + f"{baseUrl}/{entityUrl}" + "\033[0m") def stdEcho(text: str) -> None: diff --git a/coretex/configuration/user.py b/coretex/configuration/user.py index c81a82ec..0e7c9b6c 100644 --- a/coretex/configuration/user.py +++ b/coretex/configuration/user.py @@ -93,6 +93,14 @@ def projectId(self) -> Optional[int]: def projectId(self, value: Optional[int]) -> None: self._raw["projectId"] = value + @property + def frontendUrl(self) -> str: + return self.getValue("frontendUrl", str, default = "app.coretex.ai") + + @frontendUrl.setter + def frontendUrl(self, value: Optional[str]) -> None: + self._raw["frontendUrl"] = value + def _isConfigValid(self) -> Tuple[bool, List[str]]: isValid = True errorMessages = [] diff --git a/coretex/configuration/utils.py b/coretex/configuration/utils.py index 2f090a23..031d3ddd 100644 --- a/coretex/configuration/utils.py +++ b/coretex/configuration/utils.py @@ -15,7 +15,7 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from typing import Optional, Tuple, Any +from typing import Optional, Tuple, Any, Dict from . import config_defaults from ..networking import networkManager, NetworkRequestError @@ -132,3 +132,12 @@ def fetchNodeId(name: str) -> int: raise TypeError(f"Invalid \"id\" type {type(id)}. Expected: \"int\"") return id + + +def fetchInitialData() -> Dict[str, Any]: + response = networkManager.get("user/initial-data") + + if response.hasFailed(): + raise NetworkRequestError(response, "Failed to fetch user's initial data.") + + return response.getJson(dict) diff --git a/coretex/entities/task_run/task_run.py b/coretex/entities/task_run/task_run.py index 314bd6f1..2d5beb84 100644 --- a/coretex/entities/task_run/task_run.py +++ b/coretex/entities/task_run/task_run.py @@ -89,6 +89,7 @@ class TaskRun(NetworkObject, Generic[DatasetType]): useCachedEnv: bool executionType: ExecutionType metrics: List[Metric] + workflowRunId: int def __init__(self) -> None: super(TaskRun, self).__init__() @@ -164,6 +165,7 @@ def _keyDescriptors(cls) -> Dict[str, KeyDescriptor]: descriptors["taskId"] = KeyDescriptor("sub_project_id") descriptors["taskName"] = KeyDescriptor("sub_project_name") descriptors["executionType"] = KeyDescriptor("execution_type", ExecutionType) + descriptors["workflowRunId"] = KeyDescriptor("pipeline_run_id") # private properties of the object should not be encoded descriptors["__parameters"] = KeyDescriptor(isEncodable = False) @@ -177,7 +179,7 @@ def _endpoint(cls) -> str: @override def entityUrl(self) -> str: - return f"run?id={self.id}" + return f"workflow-run?id={self.workflowRunId}" def onDecode(self) -> None: super().onDecode()