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 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
@@ -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
+ )