ttnn-visualizer 0.44.1__py3-none-any.whl → 0.46.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 (30) hide show
  1. ttnn_visualizer/csv_queries.py +1 -54
  2. ttnn_visualizer/decorators.py +3 -3
  3. ttnn_visualizer/exceptions.py +7 -1
  4. ttnn_visualizer/models.py +1 -0
  5. ttnn_visualizer/remote_sqlite_setup.py +6 -82
  6. ttnn_visualizer/sftp_operations.py +34 -106
  7. ttnn_visualizer/ssh_client.py +352 -0
  8. ttnn_visualizer/static/assets/allPaths-esBqnTg5.js +1 -0
  9. ttnn_visualizer/static/assets/allPathsLoader-KPOKJ-lr.js +2 -0
  10. ttnn_visualizer/static/assets/index-03c8d4Gh.js +1 -0
  11. ttnn_visualizer/static/assets/{index-B2fHW2_O.js → index-BANm1CMY.js} +383 -383
  12. ttnn_visualizer/static/assets/index-BuHal8Ii.css +7 -0
  13. ttnn_visualizer/static/assets/index-PKNBViIU.js +1 -0
  14. ttnn_visualizer/static/assets/splitPathsBySizeLoader-DYuDhweD.js +1 -0
  15. ttnn_visualizer/static/index.html +2 -2
  16. ttnn_visualizer/views.py +80 -181
  17. {ttnn_visualizer-0.44.1.dist-info → ttnn_visualizer-0.46.0.dist-info}/METADATA +7 -3
  18. ttnn_visualizer-0.46.0.dist-info/RECORD +43 -0
  19. {ttnn_visualizer-0.44.1.dist-info → ttnn_visualizer-0.46.0.dist-info}/licenses/LICENSE +6 -0
  20. ttnn_visualizer/static/assets/allPaths-CFKU23gh.js +0 -1
  21. ttnn_visualizer/static/assets/allPathsLoader-CpaihUCo.js +0 -2
  22. ttnn_visualizer/static/assets/index-B-fsa5Ru.js +0 -1
  23. ttnn_visualizer/static/assets/index-BueCaPcI.css +0 -7
  24. ttnn_visualizer/static/assets/index-DLOviMB1.js +0 -1
  25. ttnn_visualizer/static/assets/splitPathsBySizeLoader-BEb-7YZm.js +0 -1
  26. ttnn_visualizer-0.44.1.dist-info/RECORD +0 -42
  27. {ttnn_visualizer-0.44.1.dist-info → ttnn_visualizer-0.46.0.dist-info}/WHEEL +0 -0
  28. {ttnn_visualizer-0.44.1.dist-info → ttnn_visualizer-0.46.0.dist-info}/entry_points.txt +0 -0
  29. {ttnn_visualizer-0.44.1.dist-info → ttnn_visualizer-0.46.0.dist-info}/licenses/LICENSE_understanding.txt +0 -0
  30. {ttnn_visualizer-0.44.1.dist-info → ttnn_visualizer-0.46.0.dist-info}/top_level.txt +0 -0
@@ -4,7 +4,6 @@
4
4
  import csv
5
5
  import json
6
6
  import os
7
- import subprocess
8
7
  import tempfile
9
8
  from io import StringIO
10
9
  from pathlib import Path
@@ -13,62 +12,10 @@ from typing import Dict, List, Optional, Union
13
12
  import pandas as pd
14
13
  import zstd
15
14
  from tt_perf_report import perf_report
16
- from ttnn_visualizer.exceptions import (
17
- AuthenticationException,
18
- DataFormatError,
19
- NoValidConnectionsError,
20
- SSHException,
21
- )
15
+ from ttnn_visualizer.exceptions import DataFormatError
22
16
  from ttnn_visualizer.models import Instance, RemoteConnection
23
17
 
24
18
 
25
- def handle_ssh_subprocess_error(
26
- e: subprocess.CalledProcessError, remote_connection: RemoteConnection
27
- ):
28
- """
29
- Convert subprocess SSH errors to appropriate SSH exceptions.
30
-
31
- :param e: The subprocess.CalledProcessError
32
- :param remote_connection: The RemoteConnection object for context
33
- :raises: SSHException, AuthenticationException, or NoValidConnectionsError
34
- """
35
- stderr = e.stderr.lower() if e.stderr else ""
36
-
37
- # Check for authentication failures
38
- if any(
39
- auth_err in stderr
40
- for auth_err in [
41
- "permission denied",
42
- "authentication failed",
43
- "publickey",
44
- "password",
45
- "host key verification failed",
46
- ]
47
- ):
48
- raise AuthenticationException(f"SSH authentication failed: {e.stderr}")
49
-
50
- # Check for connection failures
51
- elif any(
52
- conn_err in stderr
53
- for conn_err in [
54
- "connection refused",
55
- "network is unreachable",
56
- "no route to host",
57
- "name or service not known",
58
- "connection timed out",
59
- ]
60
- ):
61
- raise NoValidConnectionsError(f"SSH connection failed: {e.stderr}")
62
-
63
- # Check for general SSH protocol errors
64
- elif "ssh:" in stderr or "protocol" in stderr:
65
- raise SSHException(f"SSH protocol error: {e.stderr}")
66
-
67
- # Default to generic SSH exception
68
- else:
69
- raise SSHException(f"SSH command failed: {e.stderr}")
70
-
71
-
72
19
  class LocalCSVQueryRunner:
73
20
  def __init__(self, file_path: str, offset: int = 0):
74
21
  self.file_path = file_path
@@ -90,13 +90,13 @@ def remote_exception_handler(func):
90
90
  current_app.logger.error(f"File not found: {str(err)}")
91
91
  raise RemoteConnectionException(
92
92
  status=ConnectionTestStates.FAILED,
93
- message=f"Unable to open path {connection.path}: {str(err)}",
93
+ message=f"Unable to open path: {str(err)}",
94
94
  )
95
95
  except NoProjectsException as err:
96
96
  current_app.logger.error(f"No projects: {str(err)}")
97
97
  raise RemoteConnectionException(
98
98
  status=ConnectionTestStates.FAILED,
99
- message=f"No projects found at remote location: {connection.path}",
99
+ message=f"No projects found at remote location: {str(err)}",
100
100
  )
101
101
  except NoValidConnectionsError as err:
102
102
  current_app.logger.warning(
@@ -124,7 +124,7 @@ def remote_exception_handler(func):
124
124
  status=ConnectionTestStates.FAILED, message=message
125
125
  )
126
126
  except IOError as err:
127
- message = f"Error opening remote folder {connection.path}: {str(err)}"
127
+ message = f"Error opening remote folder: {str(err)}"
128
128
  if "Name or service not known" in str(err):
129
129
  message = f"Unable to connect to {connection.host} - check hostname"
130
130
  raise RemoteConnectionException(
@@ -14,10 +14,12 @@ class RemoteConnectionException(Exception):
14
14
  message,
15
15
  status: ConnectionTestStates,
16
16
  http_status_code: Optional[HTTPStatus] = None,
17
+ detail: Optional[str] = None,
17
18
  ):
18
19
  super().__init__(message)
19
20
  self.message = message
20
21
  self.status = status
22
+ self.detail = detail
21
23
  self._http_status_code = http_status_code
22
24
 
23
25
  @property
@@ -37,12 +39,16 @@ class AuthenticationFailedException(RemoteConnectionException):
37
39
  """Exception for SSH authentication failures that should return HTTP 422"""
38
40
 
39
41
  def __init__(
40
- self, message, status: ConnectionTestStates = ConnectionTestStates.FAILED
42
+ self,
43
+ message,
44
+ status: ConnectionTestStates = ConnectionTestStates.FAILED,
45
+ detail: Optional[str] = None,
41
46
  ):
42
47
  super().__init__(
43
48
  message=message,
44
49
  status=status,
45
50
  http_status_code=HTTPStatus.UNPROCESSABLE_ENTITY, # 422
51
+ detail=detail,
46
52
  )
47
53
 
48
54
 
ttnn_visualizer/models.py CHANGED
@@ -174,6 +174,7 @@ class RemoteConnection(SerializeableModel):
174
174
  class StatusMessage(SerializeableModel):
175
175
  status: ConnectionTestStates
176
176
  message: str
177
+ detail: Optional[str] = None
177
178
 
178
179
 
179
180
  class ActiveReports(SerializeableModel):
@@ -3,100 +3,24 @@
3
3
  # SPDX-FileCopyrightText: © 2025 Tenstorrent AI ULC
4
4
 
5
5
  import re
6
- import subprocess
7
6
 
8
7
  from ttnn_visualizer.decorators import remote_exception_handler
9
8
  from ttnn_visualizer.enums import ConnectionTestStates
10
- from ttnn_visualizer.exceptions import (
11
- AuthenticationException,
12
- NoValidConnectionsError,
13
- RemoteSqliteException,
14
- SSHException,
15
- )
9
+ from ttnn_visualizer.exceptions import RemoteSqliteException, SSHException
16
10
  from ttnn_visualizer.models import RemoteConnection
17
-
18
-
19
- def handle_ssh_subprocess_error(
20
- e: subprocess.CalledProcessError, remote_connection: RemoteConnection
21
- ):
22
- """
23
- Convert subprocess SSH errors to appropriate SSH exceptions.
24
-
25
- :param e: The subprocess.CalledProcessError
26
- :param remote_connection: The RemoteConnection object for context
27
- :raises: SSHException, AuthenticationException, or NoValidConnectionsError
28
- """
29
- stderr = e.stderr.lower() if e.stderr else ""
30
-
31
- # Check for authentication failures
32
- if any(
33
- auth_err in stderr
34
- for auth_err in [
35
- "permission denied",
36
- "authentication failed",
37
- "publickey",
38
- "password",
39
- "host key verification failed",
40
- ]
41
- ):
42
- raise AuthenticationException(f"SSH authentication failed: {e.stderr}")
43
-
44
- # Check for connection failures
45
- elif any(
46
- conn_err in stderr
47
- for conn_err in [
48
- "connection refused",
49
- "network is unreachable",
50
- "no route to host",
51
- "name or service not known",
52
- "connection timed out",
53
- ]
54
- ):
55
- raise NoValidConnectionsError(f"SSH connection failed: {e.stderr}")
56
-
57
- # Check for general SSH protocol errors
58
- elif "ssh:" in stderr or "protocol" in stderr:
59
- raise SSHException(f"SSH protocol error: {e.stderr}")
60
-
61
- # Default to generic SSH exception
62
- else:
63
- raise SSHException(f"SSH command failed: {e.stderr}")
64
-
11
+ from ttnn_visualizer.ssh_client import SSHClient
65
12
 
66
13
  MINIMUM_SQLITE_VERSION = "3.38.0"
67
14
 
68
15
 
69
16
  def _execute_ssh_command(remote_connection: RemoteConnection, command: str) -> str:
70
17
  """Execute an SSH command and return the output."""
71
- ssh_cmd = ["ssh", "-o", "PasswordAuthentication=no"]
72
-
73
- # Handle non-standard SSH port
74
- if remote_connection.port != 22:
75
- ssh_cmd.extend(["-p", str(remote_connection.port)])
76
-
77
- ssh_cmd.extend([f"{remote_connection.username}@{remote_connection.host}", command])
78
-
18
+ ssh_client = SSHClient(remote_connection)
79
19
  try:
80
- result = subprocess.run(
81
- ssh_cmd, capture_output=True, text=True, check=True, timeout=30
82
- )
83
- return result.stdout
84
- except subprocess.CalledProcessError as e:
85
- if e.returncode == 255: # SSH protocol errors
86
- handle_ssh_subprocess_error(e, remote_connection)
87
- # This line should never be reached as handle_ssh_subprocess_error raises an exception
88
- raise RemoteSqliteException(
89
- message=f"SSH command failed: {e.stderr}",
90
- status=ConnectionTestStates.FAILED,
91
- )
92
- else:
93
- raise RemoteSqliteException(
94
- message=f"SSH command failed: {e.stderr}",
95
- status=ConnectionTestStates.FAILED,
96
- )
97
- except subprocess.TimeoutExpired:
20
+ return ssh_client.execute_command(command, timeout=30)
21
+ except SSHException as e:
98
22
  raise RemoteSqliteException(
99
- message=f"SSH command timed out: {command}",
23
+ message=str(e),
100
24
  status=ConnectionTestStates.FAILED,
101
25
  )
102
26
 
@@ -4,7 +4,6 @@
4
4
 
5
5
  import json
6
6
  import logging
7
- import re
8
7
  import subprocess
9
8
  import time
10
9
  from pathlib import Path
@@ -16,14 +15,13 @@ from flask import current_app
16
15
  from ttnn_visualizer.decorators import remote_exception_handler
17
16
  from ttnn_visualizer.enums import ConnectionTestStates
18
17
  from ttnn_visualizer.exceptions import (
19
- AuthenticationException,
20
18
  NoProjectsException,
21
- NoValidConnectionsError,
22
19
  RemoteConnectionException,
23
20
  SSHException,
24
21
  )
25
22
  from ttnn_visualizer.models import RemoteConnection, RemoteReportFolder
26
23
  from ttnn_visualizer.sockets import FileProgress, FileStatus, emit_file_status
24
+ from ttnn_visualizer.ssh_client import SSHClient
27
25
  from ttnn_visualizer.utils import update_last_synced
28
26
 
29
27
  logger = logging.getLogger(__name__)
@@ -33,53 +31,6 @@ TEST_PROFILER_FILE = "profile_log_device.csv"
33
31
  REPORT_DATA_DIRECTORY = Path(__file__).parent.absolute().joinpath("data")
34
32
 
35
33
 
36
- def handle_ssh_subprocess_error(
37
- e: subprocess.CalledProcessError, remote_connection: RemoteConnection
38
- ):
39
- """
40
- Convert subprocess SSH errors to appropriate SSH exceptions.
41
-
42
- :param e: The subprocess.CalledProcessError
43
- :param remote_connection: The RemoteConnection object for context
44
- :raises: SSHException, AuthenticationException, or NoValidConnectionsError
45
- """
46
- stderr = e.stderr.lower() if e.stderr else ""
47
-
48
- # Check for authentication failures
49
- if any(
50
- auth_err in stderr
51
- for auth_err in [
52
- "permission denied",
53
- "authentication failed",
54
- "publickey",
55
- "password",
56
- "host key verification failed",
57
- ]
58
- ):
59
- raise AuthenticationException(f"SSH authentication failed: {e.stderr}")
60
-
61
- # Check for connection failures
62
- elif any(
63
- conn_err in stderr
64
- for conn_err in [
65
- "connection refused",
66
- "network is unreachable",
67
- "no route to host",
68
- "name or service not known",
69
- "connection timed out",
70
- ]
71
- ):
72
- raise NoValidConnectionsError(f"SSH connection failed: {e.stderr}")
73
-
74
- # Check for general SSH protocol errors
75
- elif "ssh:" in stderr or "protocol" in stderr:
76
- raise SSHException(f"SSH protocol error: {e.stderr}")
77
-
78
- # Default to generic SSH exception
79
- else:
80
- raise SSHException(f"SSH command failed: {e.stderr}")
81
-
82
-
83
34
  def start_background_task(task, *args):
84
35
  with current_app.app_context():
85
36
  if current_app.config["USE_WEBSOCKETS"]:
@@ -663,48 +614,36 @@ def read_remote_file(
663
614
  else:
664
615
  path = Path(remote_connection.profilerPath)
665
616
 
666
- logger.info(f"Reading remote file {path}")
667
-
668
- # Build SSH command to read the file
669
- ssh_cmd = ["ssh", "-o", "PasswordAuthentication=no"]
670
-
671
- # Handle non-standard SSH port
672
- if remote_connection.port != 22:
673
- ssh_cmd.extend(["-p", str(remote_connection.port)])
674
-
675
- ssh_cmd.extend(
676
- [f"{remote_connection.username}@{remote_connection.host}", f"cat '{path}'"]
677
- )
678
-
679
- try:
680
- result = subprocess.run(ssh_cmd, capture_output=True, check=True, timeout=30)
681
- return result.stdout
682
- except subprocess.CalledProcessError as e:
683
- if e.returncode == 255: # SSH protocol errors
684
- handle_ssh_subprocess_error(e, remote_connection)
685
- return None
686
- else:
687
- # File not found or other command error
688
- logger.error(f"File not found or cannot be read: {path}")
689
- return None
690
- except subprocess.TimeoutExpired:
691
- logger.error(f"Timeout reading remote file: {path}")
692
- return None
693
- except Exception as e:
694
- logger.error(f"Error reading remote file {path}: {e}")
695
- return None
617
+ ssh_client = SSHClient(remote_connection)
618
+ return ssh_client.read_file(path, timeout=30)
696
619
 
697
620
 
698
621
  @remote_exception_handler
699
622
  def check_remote_path_for_reports(remote_connection):
700
- """Check the remote path for config files."""
701
- remote_config_paths = find_folders_by_files(
623
+ remote_profiler_paths = find_folders_by_files(
702
624
  remote_connection, remote_connection.profilerPath, [TEST_CONFIG_FILE]
703
625
  )
704
- if not remote_config_paths:
626
+
627
+ remote_performance_paths = find_folders_by_files(
628
+ remote_connection, remote_connection.performancePath, [TEST_PROFILER_FILE]
629
+ )
630
+
631
+ errors = []
632
+ if not remote_profiler_paths and remote_connection.profilerPath:
633
+ errors.append(
634
+ f"No matching profiler projects found: {remote_connection.profilerPath}"
635
+ )
636
+ if not remote_performance_paths and remote_connection.performancePath:
637
+ errors.append(
638
+ f"No matching performance projects found: {remote_connection.performancePath}"
639
+ )
640
+
641
+ if errors:
705
642
  raise NoProjectsException(
706
- message="No projects found at path", status=ConnectionTestStates.FAILED
643
+ message="; ".join(errors),
644
+ status=ConnectionTestStates.FAILED,
707
645
  )
646
+
708
647
  return True
709
648
 
710
649
 
@@ -713,39 +652,28 @@ def check_remote_path_exists(remote_connection: RemoteConnection, path_key: str)
713
652
  """Check if a remote path exists using SSH test command."""
714
653
  path = getattr(remote_connection, path_key)
715
654
 
716
- # Build SSH command to test if path exists
717
- ssh_cmd = ["ssh", "-o", "PasswordAuthentication=no"]
718
-
719
- # Handle non-standard SSH port
720
- if remote_connection.port != 22:
721
- ssh_cmd.extend(["-p", str(remote_connection.port)])
722
-
723
- ssh_cmd.extend(
724
- [f"{remote_connection.username}@{remote_connection.host}", f"test -d '{path}'"]
725
- )
655
+ ssh_client = SSHClient(remote_connection)
726
656
 
727
657
  try:
728
- result = subprocess.run(ssh_cmd, capture_output=True, check=True, timeout=10)
729
- # If command succeeds, directory exists
730
- return True
731
- except subprocess.CalledProcessError as e:
732
- if e.returncode == 255: # SSH protocol errors
733
- handle_ssh_subprocess_error(e, remote_connection)
658
+ if ssh_client.check_path_exists(path, timeout=10):
659
+ return True
734
660
  else:
735
661
  # Directory does not exist or is inaccessible
736
662
  if path_key == "performancePath":
737
663
  message = "Performance directory does not exist or cannot be accessed"
738
- else:
664
+ if path_key == "profilerPath":
739
665
  message = "Profiler directory does not exist or cannot be accessed"
666
+ else:
667
+ message = f"Remote path '{path}' does not exist or cannot be accessed"
740
668
 
741
669
  logger.error(message)
742
670
  raise RemoteConnectionException(
743
671
  message=message, status=ConnectionTestStates.FAILED
744
672
  )
745
- except subprocess.TimeoutExpired:
746
- logger.error(f"Timeout checking remote path: {path}")
673
+ except SSHException as e:
674
+ logger.error(f"Error checking remote path: {path}")
747
675
  raise RemoteConnectionException(
748
- message=f"Timeout checking remote path: {path}",
676
+ message=f"Error checking remote path: {path}: {str(e)}",
749
677
  status=ConnectionTestStates.FAILED,
750
678
  )
751
679
 
@@ -902,11 +830,11 @@ def sync_remote_profiler_folders(
902
830
  def sync_remote_performance_folders(
903
831
  remote_connection: RemoteConnection,
904
832
  path_prefix: str,
905
- profile: RemoteReportFolder,
833
+ performance: RemoteReportFolder,
906
834
  exclude_patterns: Optional[List[str]] = None,
907
835
  sid=None,
908
836
  ):
909
- remote_folder_path = profile.remotePath
837
+ remote_folder_path = performance.remotePath
910
838
  profile_folder = Path(remote_folder_path).name
911
839
  destination_dir = Path(
912
840
  REPORT_DATA_DIRECTORY,