ttnn-visualizer 0.49.0__py3-none-any.whl → 0.64.0__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.
Files changed (36) hide show
  1. ttnn_visualizer/app.py +151 -49
  2. ttnn_visualizer/csv_queries.py +154 -45
  3. ttnn_visualizer/decorators.py +0 -9
  4. ttnn_visualizer/exceptions.py +0 -7
  5. ttnn_visualizer/models.py +20 -1
  6. ttnn_visualizer/queries.py +8 -0
  7. ttnn_visualizer/serializers.py +53 -9
  8. ttnn_visualizer/settings.py +24 -10
  9. ttnn_visualizer/ssh_client.py +1 -4
  10. ttnn_visualizer/static/assets/allPaths-DWjqav_8.js +1 -0
  11. ttnn_visualizer/static/assets/allPathsLoader-B0eRT9aL.js +2 -0
  12. ttnn_visualizer/static/assets/index-BE2R-cuu.css +1 -0
  13. ttnn_visualizer/static/assets/index-BZITDwoa.js +1 -0
  14. ttnn_visualizer/static/assets/{index-DVrPLQJ7.js → index-DDrUX09k.js} +274 -479
  15. ttnn_visualizer/static/assets/index-voJy5fZe.js +1 -0
  16. ttnn_visualizer/static/assets/splitPathsBySizeLoader-_GpmIkFm.js +1 -0
  17. ttnn_visualizer/static/index.html +2 -2
  18. ttnn_visualizer/tests/test_serializers.py +2 -0
  19. ttnn_visualizer/tests/test_utils.py +362 -0
  20. ttnn_visualizer/utils.py +142 -0
  21. ttnn_visualizer/views.py +181 -87
  22. {ttnn_visualizer-0.49.0.dist-info → ttnn_visualizer-0.64.0.dist-info}/METADATA +58 -30
  23. ttnn_visualizer-0.64.0.dist-info/RECORD +44 -0
  24. {ttnn_visualizer-0.49.0.dist-info → ttnn_visualizer-0.64.0.dist-info}/licenses/LICENSE +6 -0
  25. ttnn_visualizer/remote_sqlite_setup.py +0 -100
  26. ttnn_visualizer/static/assets/allPaths-G_CNx_x1.js +0 -1
  27. ttnn_visualizer/static/assets/allPathsLoader-s_Yfmxfp.js +0 -2
  28. ttnn_visualizer/static/assets/index-CnPrfHYh.js +0 -1
  29. ttnn_visualizer/static/assets/index-Cnc1EkDo.js +0 -1
  30. ttnn_visualizer/static/assets/index-UuXdrHif.css +0 -7
  31. ttnn_visualizer/static/assets/splitPathsBySizeLoader-ivxxaHxa.js +0 -1
  32. ttnn_visualizer-0.49.0.dist-info/RECORD +0 -44
  33. {ttnn_visualizer-0.49.0.dist-info → ttnn_visualizer-0.64.0.dist-info}/WHEEL +0 -0
  34. {ttnn_visualizer-0.49.0.dist-info → ttnn_visualizer-0.64.0.dist-info}/entry_points.txt +0 -0
  35. {ttnn_visualizer-0.49.0.dist-info → ttnn_visualizer-0.64.0.dist-info}/licenses/LICENSE_understanding.txt +0 -0
  36. {ttnn_visualizer-0.49.0.dist-info → ttnn_visualizer-0.64.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1 @@
1
+ import{p as r,I as s,_ as a}from"./index-DDrUX09k.js";const n=async(o,_)=>{const i=r(o);let t;return _===s.STANDARD?t=await a(()=>import("./index-voJy5fZe.js").then(e=>e.I),[]):t=await a(()=>import("./index-BZITDwoa.js").then(e=>e.I),[]),t[i]};export{n as splitPathsBySizeLoader};
@@ -34,8 +34,8 @@
34
34
  /* SERVER_CONFIG */
35
35
  </script>
36
36
 
37
- <script type="module" crossorigin src="/static/assets/index-DVrPLQJ7.js"></script>
38
- <link rel="stylesheet" crossorigin href="/static/assets/index-UuXdrHif.css">
37
+ <script type="module" crossorigin src="/static/assets/index-DDrUX09k.js"></script>
38
+ <link rel="stylesheet" crossorigin href="/static/assets/index-BE2R-cuu.css">
39
39
  </head>
40
40
  <body>
41
41
 
@@ -134,6 +134,7 @@ class TestSerializers(unittest.TestCase):
134
134
  "device_addresses": [25],
135
135
  }
136
136
  ],
137
+ "error": None,
137
138
  }
138
139
  ]
139
140
 
@@ -447,6 +448,7 @@ class TestSerializers(unittest.TestCase):
447
448
  }
448
449
  ],
449
450
  "stack_trace": "trace1",
451
+ "error": None,
450
452
  }
451
453
 
452
454
  self.assertEqual(result, expected)
@@ -0,0 +1,362 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ #
3
+ # SPDX-FileCopyrightText: © 2025 Tenstorrent AI ULC
4
+
5
+ from unittest.mock import mock_open, patch
6
+
7
+ from ttnn_visualizer.utils import (
8
+ find_gunicorn_path,
9
+ get_app_data_directory,
10
+ is_running_in_container,
11
+ )
12
+
13
+
14
+ @patch("sys.argv", ["/home/user/.local/bin/ttnn-visualizer"])
15
+ @patch("os.access")
16
+ @patch("pathlib.Path.exists")
17
+ @patch("pathlib.Path.is_file")
18
+ @patch("shutil.which")
19
+ def test_find_gunicorn_in_same_directory(
20
+ mock_which, mock_is_file, mock_exists, mock_access
21
+ ):
22
+ """Test finding gunicorn in the same directory as ttnn-visualizer."""
23
+ mock_exists.return_value = True
24
+ mock_is_file.return_value = True
25
+ mock_access.return_value = True
26
+ mock_which.return_value = None # Not in PATH
27
+
28
+ gunicorn_path, warning = find_gunicorn_path()
29
+
30
+ assert gunicorn_path.endswith("/home/user/.local/bin/gunicorn")
31
+ assert warning is None
32
+
33
+
34
+ @patch("sys.argv", ["/home/user/.local/bin/ttnn-visualizer"])
35
+ @patch("os.access")
36
+ @patch("pathlib.Path.exists")
37
+ @patch("pathlib.Path.is_file")
38
+ @patch("shutil.which")
39
+ def test_find_multiple_gunicorn_installations(
40
+ mock_which, mock_is_file, mock_exists, mock_access
41
+ ):
42
+ """Test warning when multiple gunicorn installations are detected."""
43
+ mock_exists.return_value = True
44
+ mock_is_file.return_value = True
45
+ mock_access.return_value = True
46
+ mock_which.return_value = "/usr/bin/gunicorn" # Different one in PATH
47
+
48
+ gunicorn_path, warning = find_gunicorn_path()
49
+
50
+ assert gunicorn_path.endswith("/home/user/.local/bin/gunicorn")
51
+ assert warning is not None
52
+ assert "Multiple gunicorn installations detected" in warning
53
+
54
+
55
+ @patch("sys.argv", ["/home/user/.local/bin/ttnn-visualizer"])
56
+ @patch("os.access")
57
+ @patch("pathlib.Path.exists")
58
+ @patch("pathlib.Path.is_file")
59
+ @patch("shutil.which")
60
+ def test_gunicorn_not_executable(mock_which, mock_is_file, mock_exists, mock_access):
61
+ """Test when gunicorn exists but is not executable."""
62
+ mock_exists.return_value = True
63
+ mock_is_file.return_value = True
64
+ mock_access.return_value = False # Not executable
65
+ mock_which.return_value = "/usr/bin/gunicorn"
66
+
67
+ gunicorn_path, warning = find_gunicorn_path()
68
+
69
+ assert gunicorn_path == "/usr/bin/gunicorn"
70
+ assert warning is not None
71
+ assert "not executable" in warning
72
+ assert "chmod +x" in warning
73
+
74
+
75
+ @patch("sys.argv", ["/home/user/.local/bin/ttnn-visualizer"])
76
+ @patch("os.access")
77
+ @patch("pathlib.Path.exists")
78
+ @patch("pathlib.Path.is_file")
79
+ @patch("shutil.which")
80
+ def test_fallback_to_path(mock_which, mock_is_file, mock_exists, mock_access):
81
+ """Test falling back to PATH when not in same directory."""
82
+ mock_exists.return_value = False
83
+ mock_is_file.return_value = False
84
+ mock_which.return_value = "/usr/bin/gunicorn"
85
+
86
+ gunicorn_path, warning = find_gunicorn_path()
87
+
88
+ assert gunicorn_path == "/usr/bin/gunicorn"
89
+ assert warning is not None
90
+ assert "not found in" in warning
91
+ assert "Falling back" in warning
92
+
93
+
94
+ @patch("sys.argv", ["/home/user/.local/bin/ttnn-visualizer"])
95
+ @patch("os.access")
96
+ @patch("pathlib.Path.exists")
97
+ @patch("pathlib.Path.is_file")
98
+ @patch("shutil.which")
99
+ def test_gunicorn_not_found(mock_which, mock_is_file, mock_exists, mock_access):
100
+ """Test when gunicorn is not found anywhere."""
101
+ mock_exists.return_value = False
102
+ mock_is_file.return_value = False
103
+ mock_which.return_value = None
104
+
105
+ gunicorn_path, warning = find_gunicorn_path()
106
+
107
+ assert gunicorn_path == "gunicorn"
108
+ assert warning is not None
109
+ assert "ERROR" in warning
110
+ assert "not found" in warning
111
+
112
+
113
+ # Tests for is_running_in_container()
114
+
115
+
116
+ @patch("os.path.exists")
117
+ @patch("os.getenv")
118
+ def test_container_detection_via_dockerenv(mock_getenv, mock_exists):
119
+ """Test container detection via /.dockerenv file."""
120
+ mock_exists.return_value = True
121
+ mock_getenv.return_value = None
122
+
123
+ result = is_running_in_container()
124
+
125
+ assert result is True
126
+ mock_exists.assert_called_once_with("/.dockerenv")
127
+
128
+
129
+ @patch("os.path.exists")
130
+ @patch(
131
+ "builtins.open",
132
+ new_callable=mock_open,
133
+ read_data="12:pids:/docker/abc123\n11:cpuset:/docker/abc123",
134
+ )
135
+ @patch("os.getenv")
136
+ def test_container_detection_via_cgroup_docker(mock_getenv, mock_file, mock_exists):
137
+ """Test container detection via /proc/self/cgroup containing 'docker'."""
138
+ mock_exists.return_value = False # No /.dockerenv
139
+ mock_getenv.return_value = None
140
+
141
+ result = is_running_in_container()
142
+
143
+ assert result is True
144
+ mock_file.assert_called_once_with("/proc/self/cgroup", "r")
145
+
146
+
147
+ @patch("os.path.exists")
148
+ @patch(
149
+ "builtins.open",
150
+ new_callable=mock_open,
151
+ read_data="12:pids:/containerd/abc123\n11:cpuset:/containerd/abc123",
152
+ )
153
+ @patch("os.getenv")
154
+ def test_container_detection_via_cgroup_containerd(mock_getenv, mock_file, mock_exists):
155
+ """Test container detection via /proc/self/cgroup containing 'containerd'."""
156
+ mock_exists.return_value = False
157
+ mock_getenv.return_value = None
158
+
159
+ result = is_running_in_container()
160
+
161
+ assert result is True
162
+
163
+
164
+ @patch("os.path.exists")
165
+ @patch(
166
+ "builtins.open",
167
+ new_callable=mock_open,
168
+ read_data="12:pids:/lxc/container123\n11:cpuset:/lxc/container123",
169
+ )
170
+ @patch("os.getenv")
171
+ def test_container_detection_via_cgroup_lxc(mock_getenv, mock_file, mock_exists):
172
+ """Test container detection via /proc/self/cgroup containing 'lxc'."""
173
+ mock_exists.return_value = False
174
+ mock_getenv.return_value = None
175
+
176
+ result = is_running_in_container()
177
+
178
+ assert result is True
179
+
180
+
181
+ @patch("os.path.exists")
182
+ @patch(
183
+ "builtins.open",
184
+ new_callable=mock_open,
185
+ read_data="12:pids:/kubepods/besteffort/pod123\n11:cpuset:/kubepods/besteffort/pod123",
186
+ )
187
+ @patch("os.getenv")
188
+ def test_container_detection_via_cgroup_kubepods(mock_getenv, mock_file, mock_exists):
189
+ """Test container detection via /proc/self/cgroup containing 'kubepods'."""
190
+ mock_exists.return_value = False
191
+ mock_getenv.return_value = None
192
+
193
+ result = is_running_in_container()
194
+
195
+ assert result is True
196
+
197
+
198
+ @patch("os.path.exists")
199
+ @patch("builtins.open", side_effect=FileNotFoundError())
200
+ @patch("os.getenv")
201
+ def test_container_detection_cgroup_file_not_found(mock_getenv, mock_file, mock_exists):
202
+ """Test container detection handles FileNotFoundError from /proc/self/cgroup."""
203
+ mock_exists.return_value = False
204
+
205
+ def getenv_side_effect(key):
206
+ return None
207
+
208
+ mock_getenv.side_effect = getenv_side_effect
209
+
210
+ result = is_running_in_container()
211
+
212
+ assert result is False
213
+
214
+
215
+ @patch("os.path.exists")
216
+ @patch("builtins.open", side_effect=PermissionError())
217
+ @patch("os.getenv")
218
+ def test_container_detection_cgroup_permission_error(
219
+ mock_getenv, mock_file, mock_exists
220
+ ):
221
+ """Test container detection handles PermissionError from /proc/self/cgroup."""
222
+ mock_exists.return_value = False
223
+
224
+ def getenv_side_effect(key):
225
+ return None
226
+
227
+ mock_getenv.side_effect = getenv_side_effect
228
+
229
+ result = is_running_in_container()
230
+
231
+ assert result is False
232
+
233
+
234
+ @patch("os.path.exists")
235
+ @patch(
236
+ "builtins.open",
237
+ new_callable=mock_open,
238
+ read_data="12:pids:/user.slice\n11:cpuset:/",
239
+ )
240
+ def test_container_detection_via_kubernetes_service_host(mock_file, mock_exists):
241
+ """Test container detection via KUBERNETES_SERVICE_HOST environment variable."""
242
+ mock_exists.return_value = False
243
+
244
+ with patch.dict("os.environ", {"KUBERNETES_SERVICE_HOST": "10.0.0.1"}, clear=True):
245
+ result = is_running_in_container()
246
+
247
+ assert result is True
248
+
249
+
250
+ @patch("os.path.exists")
251
+ @patch(
252
+ "builtins.open",
253
+ new_callable=mock_open,
254
+ read_data="12:pids:/user.slice\n11:cpuset:/",
255
+ )
256
+ def test_container_detection_via_kubernetes_port(mock_file, mock_exists):
257
+ """Test container detection via KUBERNETES_PORT environment variable."""
258
+ mock_exists.return_value = False
259
+
260
+ with patch.dict(
261
+ "os.environ", {"KUBERNETES_PORT": "tcp://10.0.0.1:443"}, clear=True
262
+ ):
263
+ result = is_running_in_container()
264
+
265
+ assert result is True
266
+
267
+
268
+ @patch("os.path.exists")
269
+ @patch(
270
+ "builtins.open",
271
+ new_callable=mock_open,
272
+ read_data="12:pids:/user.slice\n11:cpuset:/",
273
+ )
274
+ def test_container_detection_via_container_env(mock_file, mock_exists):
275
+ """Test container detection via 'container' environment variable."""
276
+ mock_exists.return_value = False
277
+
278
+ with patch.dict("os.environ", {"container": "podman"}, clear=True):
279
+ result = is_running_in_container()
280
+
281
+ assert result is True
282
+
283
+
284
+ @patch("os.path.exists")
285
+ @patch(
286
+ "builtins.open",
287
+ new_callable=mock_open,
288
+ read_data="12:pids:/user.slice\n11:cpuset:/",
289
+ )
290
+ @patch("os.getenv")
291
+ def test_no_container_detection(mock_getenv, mock_file, mock_exists):
292
+ """Test that no container is detected when all checks fail."""
293
+ mock_exists.return_value = False # No /.dockerenv
294
+ mock_getenv.return_value = None # No container env vars
295
+
296
+ result = is_running_in_container()
297
+
298
+ assert result is False
299
+
300
+
301
+ # Tests for get_app_data_directory()
302
+
303
+
304
+ def test_get_app_data_directory_with_tt_metal_home():
305
+ """Test that get_app_data_directory returns correct path when tt_metal_home is provided."""
306
+ tt_metal_home = "/path/to/tt-metal"
307
+ application_dir = "/default/app/dir"
308
+
309
+ result = get_app_data_directory(tt_metal_home, application_dir)
310
+
311
+ assert result == "/path/to/tt-metal/generated/ttnn-visualizer"
312
+
313
+
314
+ def test_get_app_data_directory_with_none():
315
+ """Test that get_app_data_directory returns application_dir when tt_metal_home is None."""
316
+ tt_metal_home = None
317
+ application_dir = "/default/app/dir"
318
+
319
+ result = get_app_data_directory(tt_metal_home, application_dir)
320
+
321
+ assert result == "/default/app/dir"
322
+
323
+
324
+ def test_get_app_data_directory_with_empty_string():
325
+ """Test that get_app_data_directory treats empty string as falsy and returns application_dir."""
326
+ tt_metal_home = ""
327
+ application_dir = "/default/app/dir"
328
+
329
+ result = get_app_data_directory(tt_metal_home, application_dir)
330
+
331
+ assert result == "/default/app/dir"
332
+
333
+
334
+ def test_get_app_data_directory_with_special_characters():
335
+ """Test that get_app_data_directory handles paths with special characters correctly."""
336
+ tt_metal_home = "/path/with spaces/and-dashes/tt-metal"
337
+ application_dir = "/default/app/dir"
338
+
339
+ result = get_app_data_directory(tt_metal_home, application_dir)
340
+
341
+ assert result == "/path/with spaces/and-dashes/tt-metal/generated/ttnn-visualizer"
342
+
343
+
344
+ def test_get_app_data_directory_with_relative_path():
345
+ """Test that get_app_data_directory handles relative paths correctly."""
346
+ tt_metal_home = "../relative/path/tt-metal"
347
+ application_dir = "/default/app/dir"
348
+
349
+ result = get_app_data_directory(tt_metal_home, application_dir)
350
+
351
+ assert result == "../relative/path/tt-metal/generated/ttnn-visualizer"
352
+
353
+
354
+ def test_get_app_data_directory_with_trailing_slash():
355
+ """Test that get_app_data_directory handles paths with trailing slashes correctly."""
356
+ tt_metal_home = "/path/to/tt-metal/"
357
+ application_dir = "/default/app/dir"
358
+
359
+ result = get_app_data_directory(tt_metal_home, application_dir)
360
+
361
+ # Path.join handles trailing slashes correctly
362
+ assert result == "/path/to/tt-metal/generated/ttnn-visualizer"
ttnn_visualizer/utils.py CHANGED
@@ -6,7 +6,10 @@ import dataclasses
6
6
  import enum
7
7
  import json
8
8
  import logging
9
+ import os
9
10
  import re
11
+ import shutil
12
+ import sys
10
13
  import time
11
14
  from functools import wraps
12
15
  from pathlib import Path
@@ -18,6 +21,98 @@ logger = logging.getLogger(__name__)
18
21
  LAST_SYNCED_FILE_NAME = ".last-synced"
19
22
 
20
23
 
24
+ def get_app_data_directory(tt_metal_home: Optional[str], application_dir: str) -> str:
25
+ """
26
+ Calculate the APP_DATA_DIRECTORY based on TT_METAL_HOME or fallback to application_dir.
27
+
28
+ Args:
29
+ tt_metal_home: Path to TT-Metal home directory, or None
30
+ application_dir: Fallback application directory path
31
+
32
+ Returns:
33
+ Path to the app data directory
34
+ """
35
+ if tt_metal_home and tt_metal_home.strip():
36
+ return str(Path(tt_metal_home).expanduser() / "generated" / "ttnn-visualizer")
37
+ return application_dir
38
+
39
+
40
+ def find_gunicorn_path() -> tuple[str, Optional[str]]:
41
+ """
42
+ Find the gunicorn executable, prioritizing the same bin directory as ttnn-visualizer.
43
+
44
+ Returns:
45
+ tuple: (gunicorn_path, warning_message)
46
+ - gunicorn_path: Full path to the gunicorn executable to use
47
+ - warning_message: Warning message if there are any issues finding gunicorn
48
+ (e.g., multiple installations, falling back to PATH, or not found),
49
+ or None if found without conflicts.
50
+ """
51
+ # Get the directory where ttnn-visualizer was run from
52
+ ttnn_visualizer_path = Path(sys.argv[0]).resolve()
53
+ bin_dir = ttnn_visualizer_path.parent
54
+
55
+ # Look for gunicorn in the same directory
56
+ expected_gunicorn = bin_dir / "gunicorn"
57
+
58
+ if (
59
+ expected_gunicorn.exists()
60
+ and expected_gunicorn.is_file()
61
+ and os.access(expected_gunicorn, os.X_OK)
62
+ ):
63
+ # Found gunicorn in the same bin directory and it's executable
64
+ gunicorn_path = str(expected_gunicorn)
65
+
66
+ # Check if there's a different gunicorn in PATH
67
+ path_gunicorn = shutil.which("gunicorn")
68
+ warning_message = None
69
+
70
+ if path_gunicorn and Path(path_gunicorn).resolve() != expected_gunicorn:
71
+ warning_message = (
72
+ f"⚠️ WARNING: Multiple gunicorn installations detected!\n"
73
+ f" Using: {gunicorn_path}\n"
74
+ f" Found in PATH: {path_gunicorn}\n"
75
+ f" This may cause version conflicts. Consider using a virtual environment."
76
+ )
77
+
78
+ return gunicorn_path, warning_message
79
+
80
+ # If file exists but isn't executable, add a warning about that
81
+ if expected_gunicorn.exists() and expected_gunicorn.is_file():
82
+ warning_message = (
83
+ f"⚠️ WARNING: gunicorn found at {expected_gunicorn} but it's not executable!\n"
84
+ f" Falling back to PATH. Fix permissions with: chmod +x {expected_gunicorn}"
85
+ )
86
+ path_gunicorn = shutil.which("gunicorn")
87
+ if path_gunicorn:
88
+ return path_gunicorn, warning_message
89
+ # If not in PATH either, return error with permission hint
90
+ error_message = (
91
+ f"❌ ERROR: gunicorn found at {expected_gunicorn} but it's not executable!\n"
92
+ f" Not found in PATH either.\n"
93
+ f" Fix permissions with: chmod +x {expected_gunicorn}"
94
+ )
95
+ return "gunicorn", error_message
96
+
97
+ # Fall back to PATH
98
+ path_gunicorn = shutil.which("gunicorn")
99
+
100
+ if path_gunicorn:
101
+ warning_message = (
102
+ f"⚠️ WARNING: gunicorn not found in {bin_dir}\n"
103
+ f" Falling back to gunicorn from PATH: {path_gunicorn}\n"
104
+ f" This may cause issues if different versions are installed."
105
+ )
106
+ return path_gunicorn, warning_message
107
+
108
+ # Not found anywhere - return "gunicorn" and let subprocess.run fail with a clear error
109
+ warning_message = (
110
+ f"❌ ERROR: gunicorn not found!\n"
111
+ f" Expected location: {expected_gunicorn}\n"
112
+ )
113
+ return "gunicorn", warning_message
114
+
115
+
21
116
  class PathResolver:
22
117
  """Centralized path resolution for both TT-Metal and upload/sync modes."""
23
118
 
@@ -143,6 +238,53 @@ def str_to_bool(string_value):
143
238
  return string_value.lower() in ("yes", "true", "t", "1")
144
239
 
145
240
 
241
+ def is_running_in_container():
242
+ """
243
+ Detect if running inside a container (Docker, Podman, Kubernetes, etc.).
244
+
245
+ Uses multiple detection methods for robustness:
246
+ 1. /.dockerenv file (Docker-specific, fastest check)
247
+ 2. /proc/self/cgroup contains container indicators
248
+ 3. Container-specific environment variables
249
+
250
+ Returns:
251
+ bool: True if running in a container, False otherwise
252
+ """
253
+ # Method 1: Check for /.dockerenv (Docker-specific, most common)
254
+ if os.path.exists("/.dockerenv"):
255
+ logger.info("Container detected via /.dockerenv file")
256
+ return True
257
+
258
+ # Method 2: Check cgroup for container indicators
259
+ try:
260
+ with open("/proc/self/cgroup", "r") as f:
261
+ content = f.read()
262
+ # Check for various container runtimes
263
+ container_indicators = ["docker", "containerd", "lxc", "kubepods"]
264
+ if any(indicator in content for indicator in container_indicators):
265
+ logger.info(
266
+ f"Container detected via /proc/self/cgroup: {content[:100]}"
267
+ )
268
+ return True
269
+ except (FileNotFoundError, PermissionError):
270
+ # Not on Linux or no permission to read cgroup
271
+ pass
272
+
273
+ # Method 3: Check for container-specific environment variables
274
+ container_env_vars = [
275
+ "KUBERNETES_SERVICE_HOST", # Kubernetes
276
+ "KUBERNETES_PORT", # Kubernetes
277
+ "container", # systemd-nspawn and others
278
+ ]
279
+
280
+ for env_var in container_env_vars:
281
+ if os.getenv(env_var):
282
+ logger.info(f"Container detected via environment variable: {env_var}")
283
+ return True
284
+
285
+ return False
286
+
287
+
146
288
  @dataclasses.dataclass
147
289
  class SerializeableDataclass:
148
290
  def to_dict(self) -> dict: