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.
- ttnn_visualizer/app.py +151 -49
- ttnn_visualizer/csv_queries.py +154 -45
- ttnn_visualizer/decorators.py +0 -9
- ttnn_visualizer/exceptions.py +0 -7
- ttnn_visualizer/models.py +20 -1
- ttnn_visualizer/queries.py +8 -0
- ttnn_visualizer/serializers.py +53 -9
- ttnn_visualizer/settings.py +24 -10
- ttnn_visualizer/ssh_client.py +1 -4
- ttnn_visualizer/static/assets/allPaths-DWjqav_8.js +1 -0
- ttnn_visualizer/static/assets/allPathsLoader-B0eRT9aL.js +2 -0
- ttnn_visualizer/static/assets/index-BE2R-cuu.css +1 -0
- ttnn_visualizer/static/assets/index-BZITDwoa.js +1 -0
- ttnn_visualizer/static/assets/{index-DVrPLQJ7.js → index-DDrUX09k.js} +274 -479
- ttnn_visualizer/static/assets/index-voJy5fZe.js +1 -0
- ttnn_visualizer/static/assets/splitPathsBySizeLoader-_GpmIkFm.js +1 -0
- ttnn_visualizer/static/index.html +2 -2
- ttnn_visualizer/tests/test_serializers.py +2 -0
- ttnn_visualizer/tests/test_utils.py +362 -0
- ttnn_visualizer/utils.py +142 -0
- ttnn_visualizer/views.py +181 -87
- {ttnn_visualizer-0.49.0.dist-info → ttnn_visualizer-0.64.0.dist-info}/METADATA +58 -30
- ttnn_visualizer-0.64.0.dist-info/RECORD +44 -0
- {ttnn_visualizer-0.49.0.dist-info → ttnn_visualizer-0.64.0.dist-info}/licenses/LICENSE +6 -0
- ttnn_visualizer/remote_sqlite_setup.py +0 -100
- ttnn_visualizer/static/assets/allPaths-G_CNx_x1.js +0 -1
- ttnn_visualizer/static/assets/allPathsLoader-s_Yfmxfp.js +0 -2
- ttnn_visualizer/static/assets/index-CnPrfHYh.js +0 -1
- ttnn_visualizer/static/assets/index-Cnc1EkDo.js +0 -1
- ttnn_visualizer/static/assets/index-UuXdrHif.css +0 -7
- ttnn_visualizer/static/assets/splitPathsBySizeLoader-ivxxaHxa.js +0 -1
- ttnn_visualizer-0.49.0.dist-info/RECORD +0 -44
- {ttnn_visualizer-0.49.0.dist-info → ttnn_visualizer-0.64.0.dist-info}/WHEEL +0 -0
- {ttnn_visualizer-0.49.0.dist-info → ttnn_visualizer-0.64.0.dist-info}/entry_points.txt +0 -0
- {ttnn_visualizer-0.49.0.dist-info → ttnn_visualizer-0.64.0.dist-info}/licenses/LICENSE_understanding.txt +0 -0
- {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-
|
|
38
|
-
<link rel="stylesheet" crossorigin href="/static/assets/index-
|
|
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:
|