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

Added new projects lookup behavior. Exceptions listing workspaces and projects. #615

Merged
merged 1 commit into from
Jul 5, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

### Features
- Heuristics to help users find out they're writing legacy code with new client API or vice versa ([#607](https://github.com/neptune-ai/neptune-client/pull/607))
- Lookup for projects without workspace specification and listing user projects and workspaces ([#615](https://github.com/neptune-ai/neptune-client/pull/615))

## neptune-client 0.9.19

Expand Down
164 changes: 104 additions & 60 deletions neptune/new/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from neptune.new import envs
from neptune.new.envs import CUSTOM_RUN_ID_ENV_NAME
from neptune.new.internal.utils import replace_patch_version
from neptune.new.internal.backends.api_model import Project, Workspace
from neptune.exceptions import STYLES


Expand Down Expand Up @@ -129,63 +130,98 @@ def __init__(self, status, response):
super().__init__(message.format(**inputs))


class ProjectNotFound(NeptuneException):
def __init__(self, project_id):
class ExceptionWithProjectsWorkspacesListing(NeptuneException):
def __init__(self,
message: str,
available_projects: List[Project] = (),
available_workspaces: List[Workspace] = (),
**kwargs):
available_projects_message = """
Did you mean any of these?
{projects}
"""

available_workspaces_message = """
You can check all of your projects on the Projects page:
{workspaces_urls}
"""

projects_formated_list = '\n'.join(
map(lambda project: f' - {project.workspace}/{project.name}', available_projects)
)

workspaces_formated_list = '\n'.join(
map(lambda workspace: f' - https://app.neptune.ai/{workspace.name}/-/projects', available_workspaces)
)

self.inputs = {
'available_projects_message': available_projects_message.format(
projects=projects_formated_list
) if available_projects else '',
'available_workspaces_message': available_workspaces_message.format(
workspaces_urls=workspaces_formated_list
) if available_workspaces else '',
**STYLES,
**kwargs
}

super().__init__(message.format(**self.inputs))


class ProjectNotFound(ExceptionWithProjectsWorkspacesListing):
def __init__(self,
project_id: str,
available_projects: List[Project] = (),
available_workspaces: List[Workspace] = ()):
message = """
{h1}
----ProjectNotFound-------------------------------------------------------------------------
----NeptuneProjectNotFoundException------------------------------------
{end}
Project {python}{project}{end} not found.
We couldn’t find project {fail}"{project}"{end}.
{available_projects_message}{available_workspaces_message}
You may also want to check the following docs pages:
- https://docs.neptune.ai/administration/workspace-project-and-user-management/projects
- https://docs.neptune.ai/getting-started/hello-world#project

Verify if your project's name was not misspelled.
You can find proper name after logging into Neptune UI.
{correct}Need help?{end}-> https://docs.neptune.ai/getting-started/getting-help
"""
inputs = dict(list({'project': project_id}.items()) + list(STYLES.items()))
super().__init__(message.format(**inputs))


class RunNotFound(NeptuneException):

def __init__(self, run_id: str) -> None:
super().__init__("Run {} not found.".format(run_id))


class RunUUIDNotFound(NeptuneException):
def __init__(self, run_uuid: uuid.UUID):
super().__init__("Run with UUID {} not found. Could be deleted.".format(run_uuid))
super().__init__(message=message,
available_projects=available_projects,
available_workspaces=available_workspaces,
project=project_id)


class InactiveRunException(NeptuneException):
def __init__(self, short_id: str):
class ProjectNameCollision(ExceptionWithProjectsWorkspacesListing):
def __init__(self,
project_id: str,
available_projects: List[Project] = ()):
message = """
{h1}
----InactiveRunException----------------------------------------
----NeptuneProjectNameCollisionException------------------------------------
{end}
It seems you are trying to log (or fetch) metadata to a run that was stopped ({short_id}).
What should I do?
- Resume the run to continue logging to it:
https://docs.neptune.ai/how-to-guides/neptune-api/resume-run#how-to-resume-run
- Don't invoke `stop()` on a run that you want to access. If you want to stop monitoring only,
you can resume a run in read-only mode:
https://docs.neptune.ai/you-should-know/connection-modes#read-only
Cannot resolve project {fail}"{project}"{end}.
{available_projects_message}
You may also want to check the following docs pages:
- https://docs.neptune.ai/api-reference/run#stop
- https://docs.neptune.ai/how-to-guides/neptune-api/resume-run#how-to-resume-run
- https://docs.neptune.ai/you-should-know/connection-modes
- https://docs.neptune.ai/administration/workspace-project-and-user-management/projects
- https://docs.neptune.ai/getting-started/hello-world#project

{correct}Need help?{end}-> https://docs.neptune.ai/getting-started/getting-help
"""
inputs = dict(list({'short_id': short_id}.items()) + list(STYLES.items()))
super().__init__(message.format(**inputs))
super().__init__(message=message,
available_projects=available_projects,
project=project_id)


class NeptuneMissingProjectNameException(NeptuneException):
def __init__(self):
class NeptuneMissingProjectNameException(ExceptionWithProjectsWorkspacesListing):
def __init__(self,
available_projects: List[Project] = (),
available_workspaces: List[Workspace] = ()):
message = """
{h1}
----NeptuneMissingProjectNameException----------------------------------------
{end}
Neptune client couldn't find your project name.

{available_projects_message}{available_workspaces_message}
There are two options two add it:
- specify it in your code
- set an environment variable in your operating system.
Expand Down Expand Up @@ -214,36 +250,43 @@ def __init__(self):

{correct}Need help?{end}-> https://docs.neptune.ai/getting-started/getting-help
"""
inputs = dict(list({'env_project': envs.PROJECT_ENV_NAME}.items()) + list(STYLES.items()))
super().__init__(message.format(**inputs))
super().__init__(message=message,
available_projects=available_projects,
available_workspaces=available_workspaces,
env_project=envs.PROJECT_ENV_NAME)


class NeptuneIncorrectProjectNameException(NeptuneException):
def __init__(self, project):
message = """
{h1}
----NeptuneIncorrectProjectNameException------------------------------------
{end}
Project name {fail}"{project}"{end} you specified seems to be incorrect.
class RunNotFound(NeptuneException):

The correct project name should look like this {correct}WORKSPACE/PROJECT_NAME{end}.
It has two parts:
- {correct}WORKSPACE{end}: which can be your username or your organization name
- {correct}PROJECT_NAME{end}: which is the actual project name you chose
def __init__(self, run_id: str) -> None:
super().__init__("Run {} not found.".format(run_id))

For example, a project {correct}neptune-ai/credit-default-prediction{end} parts are:
- {correct}neptune-ai{end}: {underline}WORKSPACE{end} our company organization name
- {correct}credit-default-prediction{end}: {underline}PROJECT_NAME{end} a project name

The URL to this project looks like this: https://app.neptune.ai/neptune-ai/credit-default-prediction
class RunUUIDNotFound(NeptuneException):
def __init__(self, run_uuid: uuid.UUID):
super().__init__("Run with UUID {} not found. Could be deleted.".format(run_uuid))

You may also want to check the following docs pages:
- https://docs.neptune.ai/administration/workspace-project-and-user-management/projects
- https://docs.neptune.ai/getting-started/hello-world#project

class InactiveRunException(NeptuneException):
def __init__(self, short_id: str):
message = """
{h1}
----InactiveRunException----------------------------------------
{end}
It seems you are trying to log (or fetch) metadata to a run that was stopped ({short_id}).
What should I do?
- Resume the run to continue logging to it:
https://docs.neptune.ai/how-to-guides/neptune-api/resume-run#how-to-resume-run
- Don't invoke `stop()` on a run that you want to access. If you want to stop monitoring only,
you can resume a run in read-only mode:
https://docs.neptune.ai/you-should-know/connection-modes#read-only
You may also want to check the following docs pages:
- https://docs.neptune.ai/api-reference/run#stop
- https://docs.neptune.ai/how-to-guides/neptune-api/resume-run#how-to-resume-run
- https://docs.neptune.ai/you-should-know/connection-modes
{correct}Need help?{end}-> https://docs.neptune.ai/getting-started/getting-help
"""
inputs = dict(list({'project': project}.items()) + list(STYLES.items()))
inputs = dict(list({'short_id': short_id}.items()) + list(STYLES.items()))
super().__init__(message.format(**inputs))


Expand Down Expand Up @@ -673,8 +716,9 @@ def __init__(self, matplotlib_version, plotly_version):
"Unable to convert plotly figure to matplotlib format. "
"Your matplotlib ({}) and plotlib ({}) versions are not compatible. "
"See https://stackoverflow.com/q/63120058 for details. "
"Downgrade matplotlib to version 3.2 or use as_image to log static chart."
.format(matplotlib_version, plotly_version))
"Downgrade matplotlib to version 3.2 or use as_image to log static chart.".format(
matplotlib_version,
plotly_version))


class NeptunePossibleLegacyUsageException(NeptuneException):
Expand Down
7 changes: 7 additions & 0 deletions neptune/new/internal/backends/api_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ def __init__(self, _uuid: uuid.UUID, name: str, workspace: str):
self.workspace = workspace


class Workspace:

def __init__(self, _uuid: uuid.UUID, name: str):
self.uuid = _uuid
self.name = name


@dataclass
class ApiRun:
uuid: uuid.UUID
Expand Down
70 changes: 69 additions & 1 deletion neptune/new/internal/backends/hosted_neptune_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
from packaging import version

from neptune.new.envs import NEPTUNE_ALLOW_SELF_SIGNED_CERTIFICATE
from neptune.patterns import PROJECT_QUALIFIED_NAME_PATTERN
from neptune.new.exceptions import (
ClientHttpError,
FetchAttributeNotFoundException,
Expand All @@ -38,6 +39,7 @@
NeptuneException,
NeptuneLegacyProjectException,
ProjectNotFound,
ProjectNameCollision,
NeptuneStorageLimitException,
UnsupportedClientVersion,
)
Expand All @@ -55,6 +57,7 @@
IntAttribute,
LeaderboardEntry,
Project,
Workspace,
StringAttribute,
StringSeriesAttribute,
StringSetAttribute,
Expand Down Expand Up @@ -174,7 +177,29 @@ def websockets_factory(self, project_uuid: uuid.UUID, run_uuid: uuid.UUID) -> Op
def get_project(self, project_id: str) -> Project:
verify_type("project_id", project_id, str)

project_spec = re.search(PROJECT_QUALIFIED_NAME_PATTERN, project_id)
workspace, name = project_spec['workspace'], project_spec['project']

try:
if not workspace:
available_projects = list(filter(lambda p: p.name == name,
self.get_available_projects(search_term=name)))

if len(available_projects) == 1:
project = available_projects[0]
project_id = f'{project.workspace}/{project.name}'
elif len(available_projects) > 1:
raise ProjectNameCollision(
project_id=project_id,
available_projects=available_projects
)
else:
raise ProjectNotFound(
project_id=project_id,
available_projects=self.get_available_projects(),
available_workspaces=self.get_available_workspaces()
)

response = self.backend_client.api.getProject(
projectIdentifier=project_id,
**self.DEFAULT_REQUEST_KWARGS,
Expand All @@ -188,7 +213,50 @@ def get_project(self, project_id: str) -> Project:
raise NeptuneLegacyProjectException(project_id)
return Project(uuid.UUID(project.id), project.name, project.organizationName)
except HTTPNotFound:
raise ProjectNotFound(project_id)
raise ProjectNotFound(project_id,
available_projects=self.get_available_projects(workspace_id=workspace),
available_workspaces=list() if workspace else self.get_available_workspaces())

@with_api_exceptions_handler
def get_available_projects(self,
workspace_id: Optional[str] = None,
search_term: Optional[str] = None
) -> List[Project]:
try:
response = self.backend_client.api.listProjects(
limit=5,
organizationIdentifier=workspace_id,
searchTerm=search_term,
sortBy=['lastViewed'],
sortDirection=['descending'],
userRelation='memberOrHigher',
**self.DEFAULT_REQUEST_KWARGS,
).response()
warning = response.metadata.headers.get('X-Server-Warning')
if warning:
click.echo(warning) # TODO print in color once colored exceptions are added
projects = response.result.entries
return list(map(
lambda project: Project(uuid.UUID(project.id), project.name, project.organizationName),
projects))
except HTTPNotFound:
return []

@with_api_exceptions_handler
def get_available_workspaces(self) -> List[Workspace]:
try:
response = self.backend_client.api.listOrganizations(
**self.DEFAULT_REQUEST_KWARGS,
).response()
warning = response.metadata.headers.get('X-Server-Warning')
if warning:
click.echo(warning) # TODO print in color once colored exceptions are added
workspaces = response.result
return list(map(
lambda workspace: Workspace(_uuid=uuid.UUID(workspace.id), name=workspace.name),
workspaces))
except HTTPNotFound:
return []

@with_api_exceptions_handler
def get_run(self, run_id: str):
Expand Down
12 changes: 12 additions & 0 deletions neptune/new/internal/backends/neptune_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
FloatSeriesAttribute,
IntAttribute,
Project,
Workspace,
StringAttribute,
StringSeriesAttribute,
StringSetAttribute,
Expand All @@ -55,6 +56,17 @@ def websockets_factory(self, project_uuid: uuid.UUID, run_uuid: uuid.UUID) -> Op
def get_project(self, project_id: str) -> Project:
pass

@abc.abstractmethod
def get_available_projects(self,
workspace_id: Optional[str] = None,
search_term: Optional[str] = None
) -> List[Project]:
pass

@abc.abstractmethod
def get_available_workspaces(self) -> List[Workspace]:
pass

@abc.abstractmethod
def get_run(self, run_id: str) -> ApiRun:
pass
Expand Down
Loading