Skip to content
New issue

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

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

Already on GitHub? # to your account

Added interface to show all of user's projects filtered by project name #654

Merged
merged 2 commits into from
Jan 15, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
14 changes: 12 additions & 2 deletions examples/tutorials/notebook/notebook_tutorial_2D_crm.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -909,7 +909,12 @@
" operating_condition_params = fl.SimulationParams(\n",
" # Create operating conditions using mach and reynolds number\n",
" operating_condition=fl.operating_condition_from_mach_reynolds(\n",
" mach=0.2, reynolds=5e6, temperature=272.1, alpha=16 * u.deg, beta=0 * u.deg, project_length_unit=1 * u.m\n",
" mach=0.2,\n",
" reynolds=5e6,\n",
" temperature=272.1,\n",
" alpha=16 * u.deg,\n",
" beta=0 * u.deg,\n",
" project_length_unit=1 * u.m,\n",
" ),\n",
" )"
]
Expand Down Expand Up @@ -1642,7 +1647,12 @@
" moment_center=[0.25, 0, 0], moment_length=[1, 1, 1], area=0.01\n",
" ),\n",
" operating_condition=fl.operating_condition_from_mach_reynolds(\n",
" mach=0.2, reynolds=5e6, temperature=272.1, alpha=16 * u.deg, beta=0 * u.deg, project_length_unit=1 * u.m\n",
" mach=0.2,\n",
" reynolds=5e6,\n",
" temperature=272.1,\n",
" alpha=16 * u.deg,\n",
" beta=0 * u.deg,\n",
" project_length_unit=1 * u.m,\n",
" ),\n",
" time_stepping=fl.Steady(\n",
" max_steps=3000, CFL=fl.RampCFL(initial=20, final=300, ramp_steps=500)\n",
Expand Down
13 changes: 13 additions & 0 deletions flow360/cli/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import toml

from flow360.cli import dict_utils
from flow360.component.project_utils import show_projects_with_keyword_filter

home = expanduser("~")
# pylint: disable=invalid-name
Expand Down Expand Up @@ -85,4 +86,16 @@ def configure(apikey, profile, dev, suppress_submit_warning, beta_features):
click.echo("done.")


# For displaying all projects
@click.command("show_projects", context_settings={"show_default": True})
@click.option("--keyword", help="Filter projects by keyword", default=None, type=str)
def show_projects(keyword):
"""
Display all available projects with optional keyword filter.
"""

show_projects_with_keyword_filter(search_keyword=keyword)


flow360.add_command(configure)
flow360.add_command(show_projects)
13 changes: 13 additions & 0 deletions flow360/component/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
ProjectInterface,
VolumeMeshInterfaceV2,
)
from flow360.component.project_utils import show_projects_with_keyword_filter
from flow360.component.resource_base import Flow360Resource
from flow360.component.simulation.entity_info import GeometryEntityInfo
from flow360.component.simulation.outputs.output_entities import (
Expand Down Expand Up @@ -418,6 +419,18 @@ class Project(pd.BaseModel):
_project_webapi: Optional[RestApi] = pd.PrivateAttr(None)
_root_simulation_json: Optional[dict] = pd.PrivateAttr(None)

@classmethod
def show_remote(cls, search_keyword: Union[None, str] = None):
"""
Shows all projects on the cloud.

Parameters
----------
search_keyword : str, optional

"""
show_projects_with_keyword_filter(search_keyword)

@property
def id(self) -> str:
"""
Expand Down
119 changes: 119 additions & 0 deletions flow360/component/project_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
"""
Support class and functions for project interface.
"""

import datetime
from typing import List, Literal, Optional

import pydantic as pd

from flow360.cloud.rest_api import RestApi
from flow360.component.interfaces import ProjectInterface
from flow360.component.utils import parse_datetime
from flow360.log import log


class AssetStatistics(pd.BaseModel):
"""Statistics for an asset"""

count: int
successCount: int
runningCount: int
divergedCount: int
errorCount: int


class ProjectStatistics(pd.BaseModel):
"""Statistics for a project"""

geometry: Optional[AssetStatistics] = pd.Field(None, alias="Geometry")
surface_mesh: Optional[AssetStatistics] = pd.Field(None, alias="SurfaceMesh")
volume_mesh: Optional[AssetStatistics] = pd.Field(None, alias="VolumeMesh")
case: Optional[AssetStatistics] = pd.Field(None, alias="Case")


class ProjectInfo(pd.BaseModel):
"""Information about a project, retrieved from the projects get API"""

name: str
project_id: str = pd.Field(alias="id")
tags: list[str] = pd.Field()
description: str = pd.Field()
statistics: ProjectStatistics = pd.Field()
solver_version: Optional[str] = pd.Field(
None, alias="solverVersion", description="If None then the project is from old database"
)
created_at: str = pd.Field(alias="createdAt")
root_item_type: Literal["Geometry", "SurfaceMesh", "VolumeMesh"] = pd.Field(
alias="rootItemType"
)

@pd.computed_field
@property
def local_time_zone_created_time(self) -> datetime.datetime:
"""Convert string time to datetime obj and also convert to local time zone"""
return parse_datetime(self.created_at)


class ProjectRecords(pd.BaseModel):
"""Holds all records of a user's project"""

records: List[ProjectInfo] = pd.Field()

def __str__(self):
"""Print out all info about the project"""
if not self.records:
output_str = "No matching projects found. Try skip naming patterns to show all."
return output_str
output_str = ">>> Projects sorted by creation time:\n"
# pylint: disable=not-an-iterable
for item in self.records:
output_str += f" Name: {item.name}\n"
output_str += f" Created at: {item.local_time_zone_created_time.strftime('%Y-%m-%d %H:%M %Z')}\n"
output_str += f" Created with: {item.root_item_type}\n"
output_str += f" ID: {item.project_id}\n"
output_str += (
f" Link: https://flow360.simulation.cloud/workbench/{item.project_id}\n"
)
if item.tags:
output_str += f" Tags: {item.tags}\n"
if item.description:
output_str += f" Description: {item.description}\n"
if item.statistics.geometry:
output_str += f" Geometry count: {item.statistics.geometry.count}\n"
if item.statistics.surface_mesh:
output_str += f" Surface Mesh count: {item.statistics.surface_mesh.count}\n"
if item.statistics.volume_mesh:
output_str += f" Volume Mesh count: {item.statistics.volume_mesh.count}\n"
if item.statistics.case:
output_str += f" Case count: {item.statistics.case.count}\n"

output_str += "\n"
return output_str


def show_projects_with_keyword_filter(search_keyword: str):
"""Show all projects with a keyword filter"""
# pylint: disable=invalid-name
MAX_DISPLAYABLE_ITEM_COUNT = 200
MAX_SEARCHABLE_ITEM_COUNT = 1000
_api = RestApi(ProjectInterface.endpoint, id=None)
resp = _api.get(
params={
"page": "0",
"size": MAX_SEARCHABLE_ITEM_COUNT,
"filterKeywords": search_keyword,
"sortFields": ["createdAt"],
"sortDirections": ["asc"],
}
)

all_projects = ProjectRecords.model_validate({"records": resp["records"]})
log.info("%s", str(all_projects))

if resp["total"] > MAX_DISPLAYABLE_ITEM_COUNT:
log.warning(
f"Total number of projects matching the keyword on the cloud is {resp['total']}, "
f"but only the latest {MAX_DISPLAYABLE_ITEM_COUNT} will be displayed. "
)
log.info("Total number of matching projects on the cloud: %d", resp["total"])
10 changes: 2 additions & 8 deletions flow360/component/simulation/framework/entity_registry.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"""Registry for managing and storing instances of various entity types."""

import re
from typing import Any, Dict, Union

import pydantic as pd
Expand All @@ -11,6 +10,7 @@
MergeConflictError,
_merge_objects,
)
from flow360.component.utils import _naming_pattern_handler
from flow360.log import log


Expand Down Expand Up @@ -109,13 +109,7 @@ def find_by_naming_pattern(
List[EntityBase]: A list of entities whose names match the pattern.
"""
matched_entities = []
if "*" in pattern:
# Convert wildcard to regex pattern
regex_pattern = "^" + pattern.replace("*", ".*") + "$"
else:
regex_pattern = f"^{pattern}$" # Exact match if no '*'

regex = re.compile(regex_pattern)
regex = _naming_pattern_handler(pattern=pattern)
# pylint: disable=no-member
for entity_list in self.internal_registry.values():
matched_entities.extend(filter(lambda x: regex.match(x.name), entity_list))
Expand Down
17 changes: 17 additions & 0 deletions flow360/component/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -709,3 +709,20 @@ def _local_download_file(
shutil.copy(expected_local_file, new_local_file)

return _local_download_file


def _naming_pattern_handler(pattern: str) -> re.Pattern[str]:
"""
Handler of the user supplied naming pattern.
This enables both glob pattern and regexp pattern.
If "*" is found in the pattern, it will be treated as a glob pattern.
"""

if "*" in pattern:
# Convert wildcard to regex pattern
regex_pattern = "^" + pattern.replace("*", ".*") + "$"
else:
regex_pattern = f"^{pattern}$" # Exact match if no '*'

regex = re.compile(regex_pattern)
return regex
Loading
Loading