18
from dataclasses import asdict, dataclass, field
19
from pathlib import Path
20
from typing import TYPE_CHECKING, Dict, List, Optional, Union
22
from typing_extensions import Self
24
from lightning.app.utilities.app_helpers import Logger
25
from lightning.app.utilities.packaging.cloud_compute import CloudCompute
28
from lightning.app.core.work import LightningWork
30
logger = Logger(__name__)
34
path_dir: str, file_name: str = "base.txt", comment_char: str = "#", unfreeze: bool = True
36
"""Load requirements from a file."""
37
path = os.path.join(path_dir, file_name)
38
if not os.path.isfile(path):
41
with open(path) as file:
42
lines = [ln.strip() for ln in file.readlines()]
47
if comment_char in ln:
48
comment = ln[ln.index(comment_char) :]
49
ln = ln[: ln.index(comment_char)]
52
if not req or req.startswith("http") or "@http" in req:
55
if unfreeze and "<" in req and "strict" not in comment:
56
req = re.sub(r",? *<=? *[\d\.\*]+", "", req).strip()
69
"""The Build Configuration describes how the environment a LightningWork runs in should be set up.
72
requirements: List of requirements or list of paths to requirement files. If not passed, they will be
73
automatically extracted from a `requirements.txt` if it exists.
74
dockerfile: The path to a dockerfile to be used to build your container.
75
You need to add those lines to ensure your container works in the cloud.
77
.. warning:: This feature isn't supported yet, but coming soon.
81
WORKDIR /gridai/project
83
image: The base image that the work runs on. This should be a publicly accessible image from a registry that
84
doesn't enforce rate limits (such as DockerHub) to pull this image, otherwise your application will not
89
requirements: List[str] = field(default_factory=list)
90
dockerfile: Optional[Union[str, Path, _Dockerfile]] = None
91
image: Optional[str] = None
93
def __post_init__(self) -> None:
94
current_frame = inspect.currentframe()
95
co_filename = current_frame.f_back.f_back.f_code.co_filename
96
self._call_dir = os.path.dirname(co_filename)
97
self._prepare_requirements()
98
self._prepare_dockerfile()
100
def build_commands(self) -> List[str]:
101
"""Override to run some commands before your requirements are installed.
103
.. note:: If you provide your own dockerfile, this would be ignored.
107
from dataclasses import dataclass
108
from lightning.app import BuildConfig
111
class MyOwnBuildConfig(BuildConfig):
113
def build_commands(self):
114
return ["apt-get install libsparsehash-dev"]
116
BuildConfig(requirements=["git+https://github.com/mit-han-lab/torchsparse.git@v1.4.0"])
121
def on_work_init(self, work: "LightningWork", cloud_compute: Optional["CloudCompute"] = None) -> None:
122
"""Override with your own logic to load the requirements or dockerfile."""
123
found_requirements = self._find_requirements(work)
124
if self.requirements:
125
if found_requirements and self.requirements != found_requirements:
128
f"A 'requirements.txt' exists with {found_requirements} but {self.requirements} was passed to"
129
f" the `{type(self).__name__}` in {work.name!r}. The `requirements.txt` file will be ignored."
132
self.requirements = found_requirements
133
self._prepare_requirements()
135
found_dockerfile = self._find_dockerfile(work)
137
if found_dockerfile and self.dockerfile != found_dockerfile:
140
f"A Dockerfile exists at {found_dockerfile!r} but {self.dockerfile!r} was passed to"
141
f" the `{type(self).__name__}` in {work.name!r}. {found_dockerfile!r}` will be ignored."
144
self.dockerfile = found_dockerfile
145
self._prepare_dockerfile()
147
def _find_requirements(self, work: "LightningWork", filename: str = "requirements.txt") -> List[str]:
149
file = _get_work_file(work)
153
dirname = os.path.dirname(file)
155
requirements = load_requirements(dirname, filename)
156
except NotADirectoryError:
158
return [r for r in requirements if r != "lightning"]
160
def _find_dockerfile(self, work: "LightningWork", filename: str = "Dockerfile") -> Optional[str]:
162
file = _get_work_file(work)
166
dirname = os.path.dirname(file)
167
dockerfile = os.path.join(dirname, filename)
168
if os.path.isfile(dockerfile):
172
def _prepare_requirements(self) -> None:
174
for req in self.requirements:
176
path = os.path.join(self._call_dir, req)
177
if os.path.isfile(path):
179
new_requirements = load_requirements(self._call_dir, req)
180
except NotADirectoryError:
182
requirements.extend(new_requirements)
184
requirements.append(req)
185
self.requirements = requirements
187
def _prepare_dockerfile(self) -> None:
188
if isinstance(self.dockerfile, (str, Path)):
189
path = os.path.join(self._call_dir, self.dockerfile)
190
if os.path.exists(path):
191
with open(path) as f:
192
self.dockerfile = _Dockerfile(path, f.readlines())
194
def to_dict(self) -> Dict:
195
return {"__build_config__": asdict(self)}
198
def from_dict(cls, d: Dict) -> Self:
199
return cls(**d["__build_config__"])
202
def _get_work_file(work: "LightningWork") -> Optional[str]:
205
return inspect.getfile(cls)
207
logger.debug(f"The {cls.__name__} file couldn't be found.")