litlogger 0.1.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- litlogger/__init__.py +64 -0
- litlogger/_module.py +86 -0
- litlogger/_preinit.py +55 -0
- litlogger/api/__init__.py +18 -0
- litlogger/api/artifacts_api.py +190 -0
- litlogger/api/auth_api.py +43 -0
- litlogger/api/client.py +103 -0
- litlogger/api/metrics_api.py +254 -0
- litlogger/api/utils.py +146 -0
- litlogger/artifacts.py +221 -0
- litlogger/background.py +295 -0
- litlogger/capture/__init__.py +46 -0
- litlogger/capture/posix.py +64 -0
- litlogger/capture/windows.py +95 -0
- litlogger/colors.py +250 -0
- litlogger/diagnostics.py +216 -0
- litlogger/experiment.py +559 -0
- litlogger/file_writer.py +170 -0
- litlogger/generator.py +1296 -0
- litlogger/init.py +143 -0
- litlogger/logger.py +424 -0
- litlogger/printer.py +590 -0
- litlogger/types.py +92 -0
- litlogger-0.1.5.dist-info/LICENSE +201 -0
- litlogger-0.1.5.dist-info/METADATA +219 -0
- litlogger-0.1.5.dist-info/RECORD +28 -0
- litlogger-0.1.5.dist-info/WHEEL +5 -0
- litlogger-0.1.5.dist-info/top_level.txt +1 -0
litlogger/__init__.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Copyright The Lightning AI team.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
"""Use litlogger to track machine learning experiments with Lightning.ai.
|
|
15
|
+
|
|
16
|
+
For guides and examples, see https://lightning.ai.
|
|
17
|
+
|
|
18
|
+
For reference documentation, see https://github.com/Lightning-AI/litlogger.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
__version__ = "0.1.5"
|
|
22
|
+
|
|
23
|
+
# Import core classes
|
|
24
|
+
# Import preinit utilities
|
|
25
|
+
from litlogger._preinit import pre_init_callable
|
|
26
|
+
from litlogger.experiment import Experiment
|
|
27
|
+
|
|
28
|
+
# Import SDK functions
|
|
29
|
+
from litlogger.init import finish, init
|
|
30
|
+
|
|
31
|
+
# Global variables
|
|
32
|
+
experiment: Experiment | None = None
|
|
33
|
+
log = pre_init_callable("litlogger.log", Experiment.log_metrics)
|
|
34
|
+
log_metrics = pre_init_callable("litlogger.log_metrics", Experiment.log_metrics)
|
|
35
|
+
log_file = pre_init_callable("litlogger.log_file", Experiment.log_file)
|
|
36
|
+
get_file = pre_init_callable("litlogger.get_file", Experiment.get_file)
|
|
37
|
+
log_model = pre_init_callable("litlogger.log_model", Experiment.log_model)
|
|
38
|
+
get_model = pre_init_callable("litlogger.get_model", Experiment.get_model)
|
|
39
|
+
log_model_artifact = pre_init_callable("litlogger.log_model_artifact", Experiment.log_model_artifact)
|
|
40
|
+
get_model_artifact = pre_init_callable("litlogger.get_model_artifact", Experiment.get_model_artifact)
|
|
41
|
+
finalize = pre_init_callable("litlogger.finalize", Experiment.finalize)
|
|
42
|
+
|
|
43
|
+
__all__ = [
|
|
44
|
+
"Experiment",
|
|
45
|
+
"init",
|
|
46
|
+
"finish",
|
|
47
|
+
"experiment",
|
|
48
|
+
"log",
|
|
49
|
+
"log_metrics",
|
|
50
|
+
"log_file",
|
|
51
|
+
"get_file",
|
|
52
|
+
"log_model",
|
|
53
|
+
"get_model",
|
|
54
|
+
"log_model_artifact",
|
|
55
|
+
"get_model_artifact",
|
|
56
|
+
"finalize",
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
from litlogger.logger import LightningLogger
|
|
61
|
+
|
|
62
|
+
__all__ += ["LightningLogger"]
|
|
63
|
+
except ImportError:
|
|
64
|
+
pass
|
litlogger/_module.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Copyright The Lightning AI team.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
"""Global state management for litlogger."""
|
|
15
|
+
|
|
16
|
+
from typing import Any, Callable, Dict
|
|
17
|
+
|
|
18
|
+
import litlogger
|
|
19
|
+
from litlogger._preinit import pre_init_callable
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def set_global(
|
|
23
|
+
experiment: Any | None = None,
|
|
24
|
+
log: Callable | None = None,
|
|
25
|
+
log_metrics: Callable | None = None,
|
|
26
|
+
log_file: Callable | None = None,
|
|
27
|
+
get_file: Callable | None = None,
|
|
28
|
+
log_model: Callable | None = None,
|
|
29
|
+
get_model: Callable | None = None,
|
|
30
|
+
log_model_artifact: Callable | None = None,
|
|
31
|
+
get_model_artifact: Callable | None = None,
|
|
32
|
+
finalize: Callable | None = None,
|
|
33
|
+
) -> None:
|
|
34
|
+
"""Set global litlogger state after initialization."""
|
|
35
|
+
if experiment:
|
|
36
|
+
litlogger.experiment = experiment
|
|
37
|
+
if log:
|
|
38
|
+
litlogger.log = log
|
|
39
|
+
if log_metrics:
|
|
40
|
+
litlogger.log_metrics = log_metrics
|
|
41
|
+
if log_file:
|
|
42
|
+
litlogger.log_file = log_file
|
|
43
|
+
if get_file:
|
|
44
|
+
litlogger.get_file = get_file
|
|
45
|
+
if log_model:
|
|
46
|
+
litlogger.log_model = log_model
|
|
47
|
+
if get_model:
|
|
48
|
+
litlogger.get_model = get_model
|
|
49
|
+
if log_model_artifact:
|
|
50
|
+
litlogger.log_model_artifact = log_model_artifact
|
|
51
|
+
if get_model_artifact:
|
|
52
|
+
litlogger.get_model_artifact = get_model_artifact
|
|
53
|
+
if finalize:
|
|
54
|
+
litlogger.finalize = finalize
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def get_global() -> Dict[str, Any]:
|
|
58
|
+
"""Get the global litlogger state."""
|
|
59
|
+
return {
|
|
60
|
+
"experiment": litlogger.experiment,
|
|
61
|
+
"log": litlogger.log,
|
|
62
|
+
"log_metrics": litlogger.log_metrics,
|
|
63
|
+
"log_file": litlogger.log_file,
|
|
64
|
+
"get_file": litlogger.get_file,
|
|
65
|
+
"log_model": litlogger.log_model,
|
|
66
|
+
"get_model": litlogger.get_model,
|
|
67
|
+
"log_model_artifact": litlogger.log_model_artifact,
|
|
68
|
+
"get_model_artifact": litlogger.get_model_artifact,
|
|
69
|
+
"finalize": litlogger.finalize,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def unset_globals() -> None:
|
|
74
|
+
"""Reset global litlogger state to pre-init state."""
|
|
75
|
+
from litlogger.experiment import Experiment
|
|
76
|
+
|
|
77
|
+
litlogger.experiment = None
|
|
78
|
+
litlogger.log = pre_init_callable("litlogger.log", Experiment.log_metrics)
|
|
79
|
+
litlogger.log_metrics = pre_init_callable("litlogger.log_metrics", Experiment.log_metrics)
|
|
80
|
+
litlogger.log_file = pre_init_callable("litlogger.log_file", Experiment.log_file)
|
|
81
|
+
litlogger.get_file = pre_init_callable("litlogger.get_file", Experiment.get_file)
|
|
82
|
+
litlogger.log_model = pre_init_callable("litlogger.log_model", Experiment.log_model)
|
|
83
|
+
litlogger.get_model = pre_init_callable("litlogger.get_model", Experiment.get_model)
|
|
84
|
+
litlogger.log_model_artifact = pre_init_callable("litlogger.log_model_artifact", Experiment.log_model_artifact)
|
|
85
|
+
litlogger.get_model_artifact = pre_init_callable("litlogger.get_model_artifact", Experiment.get_model_artifact)
|
|
86
|
+
litlogger.finalize = pre_init_callable("litlogger.finalize", Experiment.finalize)
|
litlogger/_preinit.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Copyright The Lightning AI team.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
"""Pre-initialization wrappers for litlogger to provide helpful error messages."""
|
|
15
|
+
|
|
16
|
+
from typing import Any, Callable
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class PreInitObject:
|
|
20
|
+
"""Object that raises an error if accessed before litlogger.init() is called."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, name: str, destination: Any | None = None) -> None:
|
|
23
|
+
self._name = name
|
|
24
|
+
|
|
25
|
+
if destination is not None:
|
|
26
|
+
self.__doc__ = destination.__doc__
|
|
27
|
+
|
|
28
|
+
def __getitem__(self, key: str) -> None:
|
|
29
|
+
raise RuntimeError(f"You must call litlogger.init() before {self._name}[{key!r}]")
|
|
30
|
+
|
|
31
|
+
def __setitem__(self, key: str, value: Any) -> Any:
|
|
32
|
+
raise RuntimeError(f"You must call litlogger.init() before {self._name}[{key!r}]")
|
|
33
|
+
|
|
34
|
+
def __setattr__(self, key: str, value: Any) -> Any:
|
|
35
|
+
if not key.startswith("_"):
|
|
36
|
+
raise RuntimeError(f"You must call litlogger.init() before {self._name}.{key}")
|
|
37
|
+
return object.__setattr__(self, key, value)
|
|
38
|
+
|
|
39
|
+
def __getattr__(self, key: str) -> Any:
|
|
40
|
+
if not key.startswith("_"):
|
|
41
|
+
raise RuntimeError(f"You must call litlogger.init() before {self._name}.{key}")
|
|
42
|
+
raise AttributeError
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def pre_init_callable(name: str, destination: Any | None = None) -> Callable:
|
|
46
|
+
"""Create a callable that raises an error if called before litlogger.init()."""
|
|
47
|
+
|
|
48
|
+
def preinit_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
49
|
+
raise RuntimeError(f"You must call litlogger.init() before {name}()")
|
|
50
|
+
|
|
51
|
+
preinit_wrapper.__name__ = str(name)
|
|
52
|
+
if destination:
|
|
53
|
+
preinit_wrapper.__wrapped__ = destination # type: ignore
|
|
54
|
+
preinit_wrapper.__doc__ = destination.__doc__
|
|
55
|
+
return preinit_wrapper
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Copyright The Lightning AI team.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
"""litlogger API module."""
|
|
15
|
+
|
|
16
|
+
__all__ = ("MetricsApi",)
|
|
17
|
+
|
|
18
|
+
from .metrics_api import MetricsApi
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# Copyright The Lightning AI team.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
import os
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from lightning_sdk import Teamspace
|
|
18
|
+
from lightning_sdk.api.utils import _FileUploader
|
|
19
|
+
from lightning_sdk.lightning_cloud.openapi import LitLoggerServiceCreateLoggerArtifactBody
|
|
20
|
+
|
|
21
|
+
from litlogger.api.client import LitRestClient
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ArtifactsApi:
|
|
25
|
+
"""API layer for artifact upload and download operations."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, client: LitRestClient | None = None) -> None:
|
|
28
|
+
"""Initialize the ArtifactsApi.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
client: Optional pre-configured LitRestClient. If None, creates a new one.
|
|
32
|
+
"""
|
|
33
|
+
self.client = client or LitRestClient(max_retries=5)
|
|
34
|
+
|
|
35
|
+
def upload_experiment_file_artifact(
|
|
36
|
+
self,
|
|
37
|
+
teamspace: Teamspace,
|
|
38
|
+
metrics_store: Any,
|
|
39
|
+
experiment_name: str,
|
|
40
|
+
file_path: str,
|
|
41
|
+
remote_path: str,
|
|
42
|
+
) -> None:
|
|
43
|
+
"""Upload a file as an artifact to the teamspace drive.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
teamspace: Teamspace object where the file will be uploaded.
|
|
47
|
+
metrics_store: Metrics store object containing the stream ID.
|
|
48
|
+
experiment_name: Experiment name for organizing artifacts.
|
|
49
|
+
file_path: Local path to the file to upload.
|
|
50
|
+
remote_path: Path relative to experiment root for storage and display.
|
|
51
|
+
|
|
52
|
+
Raises:
|
|
53
|
+
FileNotFoundError: If the file doesn't exist.
|
|
54
|
+
"""
|
|
55
|
+
if not os.path.isfile(file_path):
|
|
56
|
+
raise FileNotFoundError(f"file not found: {file_path}")
|
|
57
|
+
|
|
58
|
+
# Upload to teamspace drive under experiments folder
|
|
59
|
+
full_remote_path = f"experiments/{experiment_name}/{remote_path}"
|
|
60
|
+
|
|
61
|
+
teamspace.upload_file(file_path=file_path, remote_path=full_remote_path, progress_bar=False)
|
|
62
|
+
|
|
63
|
+
# Register the artifact with the metrics stream
|
|
64
|
+
self.client.lit_logger_service_create_logger_artifact(
|
|
65
|
+
project_id=teamspace.id,
|
|
66
|
+
metrics_stream_id=metrics_store.id,
|
|
67
|
+
body=LitLoggerServiceCreateLoggerArtifactBody(path=remote_path),
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
def download_experiment_file_artifact(
|
|
71
|
+
self,
|
|
72
|
+
teamspace: Teamspace,
|
|
73
|
+
experiment_name: str,
|
|
74
|
+
filename: str,
|
|
75
|
+
local_path: str | None = None,
|
|
76
|
+
) -> str:
|
|
77
|
+
"""Download a file artifact from the teamspace drive.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
teamspace: Teamspace object where the file is stored.
|
|
81
|
+
experiment_name: Experiment name where the artifact was uploaded.
|
|
82
|
+
filename: Name of the file to download.
|
|
83
|
+
local_path: Optional local path where the file should be saved.
|
|
84
|
+
If None, saves to current directory with the same filename.
|
|
85
|
+
|
|
86
|
+
Raises:
|
|
87
|
+
FileNotFoundError: If the remote file doesn't exist.
|
|
88
|
+
"""
|
|
89
|
+
# Construct the remote path
|
|
90
|
+
remote_path = f"experiments/{experiment_name}/{filename}"
|
|
91
|
+
|
|
92
|
+
# Determine local save path
|
|
93
|
+
if local_path is None:
|
|
94
|
+
local_path = filename
|
|
95
|
+
elif os.path.isdir(local_path):
|
|
96
|
+
local_path = os.path.join(local_path, filename)
|
|
97
|
+
|
|
98
|
+
# Convert to absolute path for cross-platform compatibility (Windows needs this)
|
|
99
|
+
local_path = os.path.abspath(local_path)
|
|
100
|
+
|
|
101
|
+
# Create directory if needed
|
|
102
|
+
local_dir = os.path.dirname(local_path)
|
|
103
|
+
if local_dir and not os.path.exists(local_dir):
|
|
104
|
+
os.makedirs(local_dir, exist_ok=True)
|
|
105
|
+
|
|
106
|
+
# Download from teamspace drive
|
|
107
|
+
teamspace.download_file(remote_path=remote_path, file_path=local_path)
|
|
108
|
+
|
|
109
|
+
return local_path
|
|
110
|
+
|
|
111
|
+
def upload_file(
|
|
112
|
+
self,
|
|
113
|
+
teamspace: Teamspace,
|
|
114
|
+
local_path: str,
|
|
115
|
+
remote_path: str,
|
|
116
|
+
) -> str:
|
|
117
|
+
"""Upload a file to the teamspace drive (generic, not experiment-specific).
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
teamspace: Teamspace object where the file will be uploaded.
|
|
121
|
+
local_path: Local path to the file to upload.
|
|
122
|
+
remote_path: Remote path in the teamspace drive.
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
str: The remote path where the file was uploaded.
|
|
126
|
+
|
|
127
|
+
Raises:
|
|
128
|
+
FileNotFoundError: If the local file doesn't exist.
|
|
129
|
+
"""
|
|
130
|
+
if not os.path.isfile(local_path):
|
|
131
|
+
raise FileNotFoundError(f"File not found: {local_path}")
|
|
132
|
+
|
|
133
|
+
# Upload to teamspace drive
|
|
134
|
+
teamspace.upload_file(file_path=local_path, remote_path=remote_path, progress_bar=False)
|
|
135
|
+
|
|
136
|
+
return remote_path
|
|
137
|
+
|
|
138
|
+
def download_file(
|
|
139
|
+
self,
|
|
140
|
+
teamspace: Teamspace,
|
|
141
|
+
remote_path: str,
|
|
142
|
+
local_path: str,
|
|
143
|
+
) -> str:
|
|
144
|
+
"""Download a file from the teamspace drive (generic, not experiment-specific).
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
teamspace: Teamspace object where the file is stored.
|
|
148
|
+
remote_path: Remote path in the teamspace drive.
|
|
149
|
+
local_path: Local path where the file should be saved.
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
str: The local path where the file was saved.
|
|
153
|
+
"""
|
|
154
|
+
# Convert to absolute path for cross-platform compatibility (Windows needs this)
|
|
155
|
+
local_path = os.path.abspath(local_path)
|
|
156
|
+
|
|
157
|
+
# Create directory if needed
|
|
158
|
+
local_dir = os.path.dirname(local_path)
|
|
159
|
+
if local_dir and not os.path.exists(local_dir):
|
|
160
|
+
os.makedirs(local_dir, exist_ok=True)
|
|
161
|
+
|
|
162
|
+
# Download from teamspace drive
|
|
163
|
+
teamspace.download_file(remote_path=remote_path, file_path=local_path)
|
|
164
|
+
|
|
165
|
+
return local_path
|
|
166
|
+
|
|
167
|
+
def upload_metrics_binary(
|
|
168
|
+
self,
|
|
169
|
+
teamspace_id: str,
|
|
170
|
+
cloud_account: str,
|
|
171
|
+
file_path: str,
|
|
172
|
+
remote_path: str,
|
|
173
|
+
) -> None:
|
|
174
|
+
"""Upload a metrics binary tar.gz file to the teamspace.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
teamspace_id: The teamspace ID.
|
|
178
|
+
cloud_account: Cloud account identifier.
|
|
179
|
+
file_path: Local path to the tar.gz file to upload.
|
|
180
|
+
remote_path: Remote path where the file will be uploaded.
|
|
181
|
+
"""
|
|
182
|
+
file_uploader = _FileUploader(
|
|
183
|
+
client=self.client,
|
|
184
|
+
teamspace_id=teamspace_id,
|
|
185
|
+
cloud_account=cloud_account,
|
|
186
|
+
file_path=file_path,
|
|
187
|
+
remote_path=remote_path,
|
|
188
|
+
progress_bar=False,
|
|
189
|
+
)
|
|
190
|
+
file_uploader()
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Copyright The Lightning AI team.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
from lightning_sdk.lightning_cloud.login import Auth
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AuthApi:
|
|
18
|
+
def __init__(self) -> None:
|
|
19
|
+
self.auth = Auth()
|
|
20
|
+
|
|
21
|
+
def authenticate(self) -> bool:
|
|
22
|
+
"""Authenticate the user or perform a guest login.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
bool: True if the user is authenticated, False if the user is a guest.
|
|
26
|
+
"""
|
|
27
|
+
self.auth.load()
|
|
28
|
+
|
|
29
|
+
if getattr(self.auth, "user_id", None) and getattr(self.auth, "api_key", None):
|
|
30
|
+
self.auth.authenticate()
|
|
31
|
+
return True
|
|
32
|
+
|
|
33
|
+
self.auth.guest_login()
|
|
34
|
+
return False
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def guest_id(self) -> str:
|
|
38
|
+
"""Get the guest ID.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
str: The guest ID.
|
|
42
|
+
"""
|
|
43
|
+
return self.auth.api_key
|
litlogger/api/client.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Copyright The Lightning AI team.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
"""Retry-enabled REST client for Lightning Cloud.
|
|
15
|
+
|
|
16
|
+
Provides a thin wrapper around GridRestClient that decorates API calls with an
|
|
17
|
+
exponential backoff strategy for transient network/server errors.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import time
|
|
21
|
+
from functools import wraps
|
|
22
|
+
from logging import Logger
|
|
23
|
+
from typing import Any, Callable
|
|
24
|
+
|
|
25
|
+
import urllib3
|
|
26
|
+
from lightning_sdk.lightning_cloud.openapi.rest import ApiException
|
|
27
|
+
from lightning_sdk.lightning_cloud.rest_client import GridRestClient, _get_next_backoff_time, create_swagger_client
|
|
28
|
+
|
|
29
|
+
logger = Logger(__name__)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _should_retry(ex: BaseException) -> bool:
|
|
33
|
+
"""Return True if the exception is transient and the request should be retried."""
|
|
34
|
+
if isinstance(ex, urllib3.exceptions.HTTPError):
|
|
35
|
+
return True
|
|
36
|
+
|
|
37
|
+
if "not found" in str(ex):
|
|
38
|
+
return False
|
|
39
|
+
|
|
40
|
+
if str(ex.status).startswith("4") and ex.status not in (400, 401, 404):
|
|
41
|
+
return True
|
|
42
|
+
|
|
43
|
+
return str(ex.status).startswith("5")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _retry_wrapper(self: Any, func: Callable, max_retries: int = -1) -> Callable:
|
|
47
|
+
"""Returns the function decorated by a wrapper that retries the call several times if a connection error occurs.
|
|
48
|
+
|
|
49
|
+
The retries follow an exponential backoff.
|
|
50
|
+
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
@wraps(func)
|
|
54
|
+
def wrapped(*args: Any, **kwargs: Any) -> Any:
|
|
55
|
+
consecutive_errors = 0
|
|
56
|
+
|
|
57
|
+
while True:
|
|
58
|
+
try:
|
|
59
|
+
return func(self, *args, **kwargs)
|
|
60
|
+
except (ApiException, urllib3.exceptions.HTTPError) as ex:
|
|
61
|
+
if not _should_retry(ex):
|
|
62
|
+
raise ex
|
|
63
|
+
|
|
64
|
+
msg = f"error: {ex!s}" if isinstance(ex, urllib3.exceptions.HTTPError) else f"response: {ex.status}"
|
|
65
|
+
|
|
66
|
+
if consecutive_errors == max_retries:
|
|
67
|
+
raise RuntimeError(f"The {func.__name__} request failed to reach the server, {msg}.") from ex
|
|
68
|
+
|
|
69
|
+
consecutive_errors += 1
|
|
70
|
+
backoff_time = _get_next_backoff_time(consecutive_errors)
|
|
71
|
+
logger.warning(
|
|
72
|
+
f"The {func.__name__} request failed to reach the server, {msg}."
|
|
73
|
+
f" Retrying after {backoff_time} seconds."
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
time.sleep(backoff_time)
|
|
77
|
+
|
|
78
|
+
return wrapped
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class LitRestClient(GridRestClient):
|
|
82
|
+
"""The LitRestClient is a wrapper around the GridRestClient.
|
|
83
|
+
|
|
84
|
+
It wraps all methods to monitor connection exceptions and employs a retry strategy.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
max_retries: Maximum number of attempts where each delay between retries is exponential.
|
|
88
|
+
If set to -1, it will retry forever, in contrast if set 0, it runs it only once.
|
|
89
|
+
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
def __init__(self, max_retries: int = -1) -> None:
|
|
93
|
+
super().__init__(api_client=create_swagger_client())
|
|
94
|
+
if max_retries == 0:
|
|
95
|
+
return
|
|
96
|
+
for base_class in GridRestClient.__mro__:
|
|
97
|
+
for name, attribute in base_class.__dict__.items():
|
|
98
|
+
if callable(attribute) and attribute.__name__ != "__init__":
|
|
99
|
+
setattr(
|
|
100
|
+
self,
|
|
101
|
+
name,
|
|
102
|
+
_retry_wrapper(self, attribute, max_retries=max_retries),
|
|
103
|
+
)
|