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

Weekly patch release v1.9.4 #16884

Merged
merged 10 commits into from
Feb 28, 2023
2 changes: 1 addition & 1 deletion docs/source-app/examples/file_server/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ def test_file_server():
def test_file_server_in_cloud():
# You need to provide the directory containing the app file.
app_dir = "docs/source-app/examples/file_server"
with run_app_in_cloud(app_dir) as (admin_page, view_page, get_logs_fn):
with run_app_in_cloud(app_dir) as (admin_page, view_page, get_logs_fn, name):
"""# 1. `admin_page` and `view_page` are playwright Page Objects.

# Check out https://playwright.dev/python/ doc to learn more.
Expand Down
4 changes: 2 additions & 2 deletions docs/source-pytorch/cli/lightning_cli_intermediate_2.rst
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ To support multiple models, when instantiating ``LightningCLI`` omit the ``model

# main.py
from pytorch_lightning.cli import LightningCLI
from pytorch_lightning.demos.boring_classes import DemoModel
from pytorch_lightning.demos.boring_classes import DemoModel, BoringDataModule


class Model1(DemoModel):
Expand Down Expand Up @@ -101,7 +101,7 @@ To support multiple data modules, when instantiating ``LightningCLI`` omit the `
# main.py
import torch
from pytorch_lightning.cli import LightningCLI
from pytorch_lightning.demos.boring_classes import BoringDataModule
from pytorch_lightning.demos.boring_classes import DemoModel, BoringDataModule


class FakeDataset1(BoringDataModule):
Expand Down
2 changes: 1 addition & 1 deletion requirements/app/base.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
lightning-cloud>=0.5.26
lightning-cloud>=0.5.27
packaging
typing-extensions>=4.0.0, <=4.4.0
deepdiff>=5.7.0, <6.2.4
Expand Down
8 changes: 8 additions & 0 deletions src/lightning_app/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).


## [1.9.4] - 2023-02-28

### Removed

- Removed implicit ui testing with `testing.run_app_in_cloud` in favor of headless login and app selection ([#16741](https://github.com/Lightning-AI/lightning/pull/16741))


## [1.9.3] - 2023-02-21

### Fixed
Expand Down
3 changes: 2 additions & 1 deletion src/lightning_app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
from lightning_app.core.flow import LightningFlow # noqa: E402
from lightning_app.core.work import LightningWork # noqa: E402
from lightning_app.perf import pdb # noqa: E402
from lightning_app.plugin.plugin import LightningPlugin # noqa: E402
from lightning_app.utilities.packaging.build_config import BuildConfig # noqa: E402
from lightning_app.utilities.packaging.cloud_compute import CloudCompute # noqa: E402

Expand All @@ -43,4 +44,4 @@
_PACKAGE_ROOT = os.path.dirname(__file__)
_PROJECT_ROOT = os.path.dirname(os.path.dirname(_PACKAGE_ROOT))

__all__ = ["LightningApp", "LightningFlow", "LightningWork", "BuildConfig", "CloudCompute", "pdb"]
__all__ = ["LightningApp", "LightningFlow", "LightningWork", "LightningPlugin", "BuildConfig", "CloudCompute", "pdb"]
20 changes: 15 additions & 5 deletions src/lightning_app/cli/lightning_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,21 @@ def main() -> None:
# Check environment and versions if not in the cloud and not testing
is_testing = bool(int(os.getenv("LIGHTING_TESTING", "0")))
if not is_testing and "LIGHTNING_APP_STATE_URL" not in os.environ:
# Enforce running in PATH Python
_check_environment_and_redirect()

# Check for newer versions and upgrade
_check_version_and_upgrade()
try:
# Enforce running in PATH Python
_check_environment_and_redirect()

# Check for newer versions and upgrade
_check_version_and_upgrade()
except SystemExit:
raise
except Exception:
# Note: We intentionally ignore all exceptions here so that we never panic if one of the above calls fails.
# If they fail for some reason users should still be able to continue with their command.
click.echo(
"We encountered an unexpected problem while checking your environment."
"We will still proceed with the command, however, there is a chance that errors may occur."
)

# 1: Handle connection to a Lightning App.
if len(sys.argv) > 1 and sys.argv[1] in ("connect", "disconnect", "logout"):
Expand Down
5 changes: 5 additions & 0 deletions src/lightning_app/plugin/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import lightning_app.plugin.actions as actions
from lightning_app.plugin.actions import NavigateTo, Toast, ToastSeverity
from lightning_app.plugin.plugin import LightningPlugin

__all__ = ["LightningPlugin", "actions", "Toast", "ToastSeverity", "NavigateTo"]
72 changes: 72 additions & 0 deletions src/lightning_app/plugin/actions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Copyright The Lightning AI team.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from dataclasses import dataclass
from enum import Enum
from typing import Union

from lightning_cloud.openapi.models import V1CloudSpaceAppAction, V1CloudSpaceAppActionType


class _Action:
"""Actions are returned by `LightningPlugin` objects to perform actions in the UI."""

def to_spec(self) -> V1CloudSpaceAppAction:
"""Convert this action to a ``V1CloudSpaceAppAction``"""
raise NotImplementedError


@dataclass
class NavigateTo(_Action):
"""The ``NavigateTo`` action can be used to navigate to a relative URL within the Lightning frontend.

Args:
url: The relative URL to navigate to. E.g. ``/<username>/<project>``.
"""

url: str

def to_spec(self) -> V1CloudSpaceAppAction:
return V1CloudSpaceAppAction(
type=V1CloudSpaceAppActionType.NAVIGATE_TO,
content=self.url,
)


class ToastSeverity(Enum):
ERROR = "error"
INFO = "info"
SUCCESS = "success"
WARNING = "warning"

def __str__(self) -> str:
return self.value


@dataclass
class Toast(_Action):
"""The ``Toast`` action can be used to display a toast message to the user.

Args:
severity: The severity level of the toast. One of: "error", "info", "success", "warning".
message: The message body.
"""

severity: Union[ToastSeverity, str]
message: str

def to_spec(self) -> V1CloudSpaceAppAction:
return V1CloudSpaceAppAction(
type=V1CloudSpaceAppActionType.TOAST,
content=f"{self.severity}:{self.message}",
)
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import tarfile
import tempfile
from pathlib import Path
from typing import Dict, List, Optional
from typing import Any, Dict, List, Optional
from urllib.parse import urlparse

import requests
Expand All @@ -24,6 +24,8 @@
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel

from lightning_app.core import constants
from lightning_app.plugin.actions import _Action
from lightning_app.utilities.app_helpers import Logger
from lightning_app.utilities.component import _set_flow_context
from lightning_app.utilities.enum import AppStage
Expand All @@ -41,16 +43,20 @@ def __init__(self) -> None:
self.cloudspace_id = ""
self.cluster_id = ""

def run(self, *args: str, **kwargs: str) -> None:
def run(self, *args: str, **kwargs: str) -> Optional[List[_Action]]:
"""Override with the logic to execute on the cloudspace."""
raise NotImplementedError

def run_job(self, name: str, app_entrypoint: str, env_vars: Optional[Dict[str, str]] = None) -> None:
def run_job(self, name: str, app_entrypoint: str, env_vars: Optional[Dict[str, str]] = None) -> str:
"""Run a job in the cloudspace associated with this plugin.

Args:
name: The name of the job.
app_entrypoint: The path of the file containing the app to run.
env_vars: Additional env vars to set when running the app.

Returns:
The relative URL of the created job.
"""
from lightning_app.runners.cloud import CloudRuntime

Expand All @@ -74,12 +80,14 @@ def run_job(self, name: str, app_entrypoint: str, env_vars: Optional[Dict[str, s
# Used to indicate Lightning has been dispatched
os.environ["LIGHTNING_DISPATCHED"] = "1"

runtime.cloudspace_dispatch(
url = runtime.cloudspace_dispatch(
project_id=self.project_id,
cloudspace_id=self.cloudspace_id,
name=name,
cluster_id=self.cluster_id,
)
# Return a relative URL so it can be used with the NavigateTo action.
return url.replace(constants.get_lightning_cloud_url(), "")

def _setup(
self,
Expand All @@ -101,7 +109,7 @@ class _Run(BaseModel):
plugin_arguments: Dict[str, str]


def _run_plugin(run: _Run) -> List:
def _run_plugin(run: _Run) -> Dict[str, Any]:
"""Create a run with the given name and entrypoint under the cloudspace with the given ID."""
with tempfile.TemporaryDirectory() as tmpdir:
download_path = os.path.join(tmpdir, "source.tar.gz")
Expand All @@ -115,6 +123,9 @@ def _run_plugin(run: _Run) -> List:

response = requests.get(source_code_url)

# TODO: Backoff retry a few times in case the URL is flaky
response.raise_for_status()

with open(download_path, "wb") as f:
f.write(response.content)
except Exception as e:
Expand Down Expand Up @@ -152,17 +163,15 @@ def _run_plugin(run: _Run) -> List:
cloudspace_id=run.cloudspace_id,
cluster_id=run.cluster_id,
)
plugin.run(**run.plugin_arguments)
actions = plugin.run(**run.plugin_arguments) or []
return {"actions": [action.to_spec().to_dict() for action in actions]}
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Error running plugin: {str(e)}."
)
finally:
os.chdir(cwd)

# TODO: Return actions from the plugin here
return []


def _start_plugin_server(host: str, port: int) -> None:
"""Start the plugin server which can be used to dispatch apps or run plugins."""
Expand Down
26 changes: 14 additions & 12 deletions src/lightning_app/runners/cloud.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ def cloudspace_dispatch(
cloudspace_id: str,
name: str,
cluster_id: str,
):
) -> str:
"""Slim dispatch for creating runs from a cloudspace. This dispatch avoids resolution of some properties
such as the project and cluster IDs that are instead passed directly.

Expand All @@ -210,12 +210,15 @@ def cloudspace_dispatch(
ApiException: If there was an issue in the backend.
RuntimeError: If there are validation errors.
ValueError: If there are validation errors.

Returns:
The URL of the created job.
"""
# Dispatch in four phases: resolution, validation, spec creation, API transactions
# Resolution
root = self._resolve_root()
repo = self._resolve_repo(root)
self._resolve_cloudspace(project_id, cloudspace_id)
project = self._resolve_project(project_id=project_id)
existing_instances = self._resolve_run_instances_by_name(project_id, name)
name = self._resolve_run_name(name, existing_instances)
queue_server_type = self._resolve_queue_server_type()
Expand All @@ -240,7 +243,7 @@ def cloudspace_dispatch(
run = self._api_create_run(project_id, cloudspace_id, run_body)
self._api_package_and_upload_repo(repo, run)

self._api_create_run_instance(
run_instance = self._api_create_run_instance(
cluster_id,
project_id,
name,
Expand All @@ -251,6 +254,8 @@ def cloudspace_dispatch(
env_vars,
)

return self._get_app_url(project, run_instance, "logs" if run.is_headless else "web-ui")

def dispatch(
self,
name: str = "",
Expand Down Expand Up @@ -367,6 +372,10 @@ def dispatch(
click.launch(
self._get_app_url(project, run_instance, "logs" if run.is_headless else "web-ui", needs_credits)
)

if bool(int(os.getenv("LIGHTING_TESTING", "0"))):
print(f"APP_LOGS_URL: {self._get_app_url(project, run_instance, 'logs')}")

except ApiException as e:
logger.error(e.body)
sys.exit(1)
Expand Down Expand Up @@ -447,16 +456,9 @@ def _resolve_repo(

return LocalSourceCodeDir(path=root, ignore_functions=ignore_functions)

def _resolve_project(self) -> V1Membership:
def _resolve_project(self, project_id: Optional[str] = None) -> V1Membership:
"""Determine the project to run on, choosing a default if multiple projects are found."""
return _get_project(self.backend.client)

def _resolve_cloudspace(self, project_id: str, cloudspace_id: str) -> V1CloudSpace:
"""Get a cloudspace by project / cloudspace ID."""
return self.backend.client.cloud_space_service_get_cloud_space(
project_id=project_id,
id=cloudspace_id,
)
return _get_project(self.backend.client, project_id=project_id)

def _resolve_existing_cloudspaces(self, project_id: str, cloudspace_name: str) -> List[V1CloudSpace]:
"""Lists all the cloudspaces with a name matching the provided cloudspace name."""
Expand Down
Loading