azure-explorer 0.1.0__tar.gz

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 (25) hide show
  1. azure_explorer-0.1.0/PKG-INFO +80 -0
  2. azure_explorer-0.1.0/README.md +56 -0
  3. azure_explorer-0.1.0/azure_explorer/__init__.py +13 -0
  4. azure_explorer-0.1.0/azure_explorer/config.py +5 -0
  5. azure_explorer-0.1.0/azure_explorer/managers/__init__.py +6 -0
  6. azure_explorer-0.1.0/azure_explorer/managers/base.py +5 -0
  7. azure_explorer-0.1.0/azure_explorer/managers/container/__init__.py +3 -0
  8. azure_explorer-0.1.0/azure_explorer/managers/container/base.py +131 -0
  9. azure_explorer-0.1.0/azure_explorer/managers/container/blob_storage.py +64 -0
  10. azure_explorer-0.1.0/azure_explorer/managers/container/data_lake.py +79 -0
  11. azure_explorer-0.1.0/azure_explorer/managers/container/table.py +0 -0
  12. azure_explorer-0.1.0/azure_explorer/managers/models.py +14 -0
  13. azure_explorer-0.1.0/azure_explorer/managers/storage_account.py +47 -0
  14. azure_explorer-0.1.0/azure_explorer/managers/subscription.py +76 -0
  15. azure_explorer-0.1.0/azure_explorer/managers/tenant.py +35 -0
  16. azure_explorer-0.1.0/azure_explorer/short_cuts.py +53 -0
  17. azure_explorer-0.1.0/azure_explorer/widget/__init__.py +1 -0
  18. azure_explorer-0.1.0/azure_explorer/widget/app.py +29 -0
  19. azure_explorer-0.1.0/azure_explorer/widget/browsers/__init__.py +5 -0
  20. azure_explorer-0.1.0/azure_explorer/widget/browsers/base.py +112 -0
  21. azure_explorer-0.1.0/azure_explorer/widget/browsers/container.py +74 -0
  22. azure_explorer-0.1.0/azure_explorer/widget/browsers/storage_account.py +50 -0
  23. azure_explorer-0.1.0/azure_explorer/widget/browsers/subscription.py +22 -0
  24. azure_explorer-0.1.0/azure_explorer/widget/browsers/tenant.py +22 -0
  25. azure_explorer-0.1.0/pyproject.toml +45 -0
@@ -0,0 +1,80 @@
1
+ Metadata-Version: 2.4
2
+ Name: azure-explorer
3
+ Version: 0.1.0
4
+ Summary:
5
+ Author: uch
6
+ Author-email: uch@energinet.dk
7
+ Requires-Python: >=3.10.8,<4.0.0
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.11
10
+ Classifier: Programming Language :: Python :: 3.12
11
+ Classifier: Programming Language :: Python :: 3.13
12
+ Classifier: Programming Language :: Python :: 3.14
13
+ Requires-Dist: azure-data-tables (>=12.7.0,<13.0.0)
14
+ Requires-Dist: azure-identity (>=1.25.1,<2.0.0)
15
+ Requires-Dist: azure-mgmt-resource (>=24.0.0,<25.0.0)
16
+ Requires-Dist: azure-mgmt-servicebus (>=9.0.0,<10.0.0)
17
+ Requires-Dist: azure-mgmt-storage (>=24.0.0,<25.0.0)
18
+ Requires-Dist: azure-mgmt-subscription (>=3.1.1,<4.0.0)
19
+ Requires-Dist: azure-servicebus (>=7.14.3,<8.0.0)
20
+ Requires-Dist: azure-storage-blob (>=12.27.1,<13.0.0)
21
+ Requires-Dist: azure-storage-file-datalake (>=12.22.0,<13.0.0)
22
+ Requires-Dist: textual (>=7.0.0,<8.0.0)
23
+ Description-Content-Type: text/markdown
24
+
25
+ # 🔎 Azure Explorer
26
+
27
+ The purpose of this package is to provide a simple interface for exploring resources in your Azure Cloud environment. The package consists of a CLI tool and a Python library.
28
+
29
+ ---
30
+
31
+ ## Setup
32
+
33
+ Install the latest version of Azure Explorer with
34
+
35
+ ```console
36
+ pip install azure-explorer
37
+ ```
38
+
39
+ ---
40
+
41
+ ## Usage
42
+
43
+ ##### CLI tool
44
+
45
+ The CLI tool provides a way of navigating your Azure Cloud resources in the command line. Run the CLI tool with
46
+
47
+ ```console
48
+ ax
49
+ ```
50
+
51
+ ![cli-tool](docs/cli-tool.png)
52
+
53
+ ##### Python library
54
+
55
+ The Python library provides a way of interacting with our Azure Cloud resources in in Python code. Use the Python library with
56
+
57
+ ```python
58
+ >>> import azure_explorer as ax
59
+ >>> tenant = ax.get_tenant_manager()
60
+ >>> tenant.list_subscription_names()
61
+ [
62
+ 'development-subscription',
63
+ 'preproduction-subscription',
64
+ 'production-subscription',
65
+ 'test-subscription',
66
+ ]
67
+
68
+ >>> container = ax.get_container_manager(
69
+ storage_account_name='sadataprod',
70
+ container_name='documents',
71
+ )
72
+ >>> container.list_dir()
73
+ [
74
+ 'dir1',
75
+ 'dir2',
76
+ 'file1.txt',
77
+ 'file2.jpg',
78
+ 'file3.pdf',
79
+ ]
80
+ ```
@@ -0,0 +1,56 @@
1
+ # 🔎 Azure Explorer
2
+
3
+ The purpose of this package is to provide a simple interface for exploring resources in your Azure Cloud environment. The package consists of a CLI tool and a Python library.
4
+
5
+ ---
6
+
7
+ ## Setup
8
+
9
+ Install the latest version of Azure Explorer with
10
+
11
+ ```console
12
+ pip install azure-explorer
13
+ ```
14
+
15
+ ---
16
+
17
+ ## Usage
18
+
19
+ ##### CLI tool
20
+
21
+ The CLI tool provides a way of navigating your Azure Cloud resources in the command line. Run the CLI tool with
22
+
23
+ ```console
24
+ ax
25
+ ```
26
+
27
+ ![cli-tool](docs/cli-tool.png)
28
+
29
+ ##### Python library
30
+
31
+ The Python library provides a way of interacting with our Azure Cloud resources in in Python code. Use the Python library with
32
+
33
+ ```python
34
+ >>> import azure_explorer as ax
35
+ >>> tenant = ax.get_tenant_manager()
36
+ >>> tenant.list_subscription_names()
37
+ [
38
+ 'development-subscription',
39
+ 'preproduction-subscription',
40
+ 'production-subscription',
41
+ 'test-subscription',
42
+ ]
43
+
44
+ >>> container = ax.get_container_manager(
45
+ storage_account_name='sadataprod',
46
+ container_name='documents',
47
+ )
48
+ >>> container.list_dir()
49
+ [
50
+ 'dir1',
51
+ 'dir2',
52
+ 'file1.txt',
53
+ 'file2.jpg',
54
+ 'file3.pdf',
55
+ ]
56
+ ```
@@ -0,0 +1,13 @@
1
+ from .managers import (
2
+ ContainerManager,
3
+ StorageAccountManager,
4
+ SubscriptionManager,
5
+ TenantManager,
6
+ )
7
+ from .short_cuts import (
8
+ get_container_manager,
9
+ get_storage_account_manager,
10
+ get_subscription_manager,
11
+ get_tenant_manager,
12
+ read_file,
13
+ )
@@ -0,0 +1,5 @@
1
+ from azure.identity import DefaultAzureCredential
2
+
3
+
4
+ class Config:
5
+ credential = DefaultAzureCredential()
@@ -0,0 +1,6 @@
1
+ from .base import Manager
2
+ from .container import ContainerManager
3
+ from .models import ResourceProperties, SubscriptionProperties
4
+ from .storage_account import StorageAccountManager
5
+ from .subscription import SubscriptionManager
6
+ from .tenant import TenantManager
@@ -0,0 +1,5 @@
1
+ from abc import ABC
2
+
3
+
4
+ class Manager(ABC):
5
+ pass
@@ -0,0 +1,3 @@
1
+ from .base import ContainerManager
2
+ from .blob_storage import BlobStorageContainerManager
3
+ from .data_lake import DataLakeContainerManager
@@ -0,0 +1,131 @@
1
+ from abc import ABC, abstractmethod
2
+ from io import BytesIO
3
+ from pathlib import Path
4
+ from typing import Iterator
5
+
6
+ from azure.core.credentials import TokenCredential
7
+ from azure.storage.blob import StorageStreamDownloader
8
+
9
+
10
+ class FolderNotFoundError(Exception):
11
+ pass
12
+
13
+
14
+ class FileOrFolderNotFoundError(Exception):
15
+ pass
16
+
17
+
18
+ class ContainerManager(ABC):
19
+ @staticmethod
20
+ def create(
21
+ storage_account_name: str,
22
+ container_name: str,
23
+ is_data_lake: bool,
24
+ credential: TokenCredential,
25
+ ):
26
+ from . import BlobStorageContainerManager, DataLakeContainerManager
27
+
28
+ if is_data_lake:
29
+ return DataLakeContainerManager(
30
+ storage_account_name=storage_account_name,
31
+ container_name=container_name,
32
+ credential=credential,
33
+ )
34
+ else:
35
+ return BlobStorageContainerManager(
36
+ storage_account_name=storage_account_name,
37
+ container_name=container_name,
38
+ credential=credential,
39
+ )
40
+
41
+ @abstractmethod
42
+ def _get_blob_stream_downloader(self, path: Path | str) -> StorageStreamDownloader:
43
+ ...
44
+
45
+ @abstractmethod
46
+ def _write_blob_data(
47
+ self, path: Path | str, data: bytes | BytesIO, overwrite: bool = False
48
+ ):
49
+ ...
50
+
51
+ @abstractmethod
52
+ def _iter_dir(self, path: Path | str) -> Iterator[str]:
53
+ ...
54
+
55
+ @abstractmethod
56
+ def is_dir(self, path: Path | str) -> bool:
57
+ ...
58
+
59
+ @abstractmethod
60
+ def is_file(self, path: Path | str) -> bool:
61
+ ...
62
+
63
+ def list_dir(self, path: Path | str = "") -> list[str]:
64
+ if not self.is_dir(path):
65
+ raise FolderNotFoundError(path)
66
+
67
+ subpaths = list(self._iter_dir(path))
68
+
69
+ return sorted(subpaths)
70
+
71
+ def read_file(self, path: Path | str) -> bytes:
72
+ if not self.is_file(path):
73
+ raise FileNotFoundError(path)
74
+
75
+ blob_stream_downloader = self._get_blob_stream_downloader(path)
76
+
77
+ return blob_stream_downloader.readall()
78
+
79
+ def download_file(self, source: Path | str, destination: Path | str):
80
+ destination = Path(destination)
81
+
82
+ if not self.is_file(source):
83
+ raise FileNotFoundError(source)
84
+
85
+ destination.parent.mkdir(parents=True, exist_ok=True)
86
+
87
+ blob_stream_downloader = self._get_blob_stream_downloader(source)
88
+
89
+ with open(destination, "wb") as f:
90
+ blob_stream_downloader.readinto(f)
91
+
92
+ def download_dir(self, source: Path | str, destination: Path):
93
+ destination = Path(destination)
94
+
95
+ if not self.is_dir(source):
96
+ raise FolderNotFoundError(source)
97
+
98
+ for subsource in self.list_dir(source):
99
+ basename = Path(subsource).relative_to(source)
100
+
101
+ subdestination = destination / basename
102
+
103
+ if self.is_dir(subsource):
104
+ self.download_dir(subsource, subdestination)
105
+ else:
106
+ self.download_file(subsource, subdestination)
107
+
108
+ def write_blob(self, data: bytes, path: Path | str, overwrite: bool = False):
109
+ self._write_blob_data(path, data, overwrite=overwrite)
110
+
111
+ def upload_file(self, source: Path | str, path: str, overwrite: bool = False):
112
+ with open(source, "rb") as f:
113
+ self._write_blob_data(path, f, overwrite=overwrite)
114
+
115
+ def upload_dir(
116
+ self, source: Path | str, destination: Path | str, overwrite: bool = False
117
+ ):
118
+ # NOTE: Will not create empty folders
119
+ destination = Path(destination)
120
+ source = Path(source)
121
+
122
+ for subsource in source.iterdir():
123
+
124
+ basename = subsource.name
125
+
126
+ subdestination = str(destination / basename)
127
+
128
+ if subsource.is_file():
129
+ self.upload_file(subsource, subdestination, overwrite)
130
+ else:
131
+ self.upload_file(subsource, subdestination, overwrite)
@@ -0,0 +1,64 @@
1
+ from io import BytesIO
2
+ from pathlib import Path
3
+ from typing import Iterator
4
+
5
+ from azure.core.credentials import TokenCredential
6
+ from azure.storage.blob import ContainerClient, StorageStreamDownloader
7
+
8
+ from . import ContainerManager
9
+
10
+
11
+ class BlobStorageContainerManager(ContainerManager):
12
+ def __init__(
13
+ self,
14
+ storage_account_name: str,
15
+ container_name: str,
16
+ credential: TokenCredential,
17
+ ):
18
+ self.storage_account_name = storage_account_name
19
+ self.container_name = container_name
20
+ self.credential = credential
21
+ self.container_client = ContainerClient(
22
+ f"https://{storage_account_name}.blob.core.windows.net",
23
+ container_name=container_name,
24
+ credential=credential,
25
+ )
26
+
27
+ def is_dir(self, path: Path | str) -> bool:
28
+ if path == "":
29
+ return True
30
+
31
+ prefix = str(path).rstrip("/") + "/"
32
+ items = list(self.container_client.walk_blobs(prefix, delimiter="/"))
33
+
34
+ if len(items) == 0:
35
+ return False
36
+
37
+ if len(items) == 1 and items[0].name == path:
38
+ return False
39
+
40
+ return True
41
+
42
+ def is_file(self, path: Path | str) -> bool:
43
+ blob_client = self.container_client.get_blob_client(path)
44
+ return blob_client.exists()
45
+
46
+ def _iter_dir(self, path: Path | str = "") -> Iterator[str]:
47
+ prefix = str(path).rstrip("/") + "/"
48
+ for item in self.container_client.walk_blobs(prefix, delimiter="/"):
49
+ yield item.name.rstrip("/")
50
+
51
+ def _get_blob_stream_downloader(self, path: Path | str) -> StorageStreamDownloader:
52
+ return self.container_client.download_blob(path)
53
+
54
+ def _write_blob_data(
55
+ self, path: Path | str, data: bytes | BytesIO, overwrite: bool = False
56
+ ):
57
+ self.container_client.upload_blob(path, data, overwrite=overwrite)
58
+
59
+ def __repr__(self) -> str:
60
+ return (
61
+ "BlobStorageContainerManager("
62
+ f"storage_account_name='{self.storage_account_name}', "
63
+ f"container_name='{self.container_name}')"
64
+ )
@@ -0,0 +1,79 @@
1
+ from io import BytesIO
2
+ from pathlib import Path
3
+ from typing import Iterator
4
+
5
+ from azure.core.credentials import TokenCredential
6
+ from azure.storage.filedatalake import FileSystemClient, StorageStreamDownloader
7
+
8
+ from . import ContainerManager
9
+
10
+
11
+ class DataLakeContainerManager(ContainerManager):
12
+ def __init__(
13
+ self,
14
+ storage_account_name: str,
15
+ container_name: str,
16
+ credential: TokenCredential,
17
+ ):
18
+ self.storage_account_name = storage_account_name
19
+ self.container_name = container_name
20
+ self.credential = credential
21
+ self.file_system_client = FileSystemClient(
22
+ f"https://{storage_account_name}.dfs.core.windows.net",
23
+ file_system_name=container_name,
24
+ credential=credential,
25
+ )
26
+
27
+ def is_dir(self, folder: Path | str) -> bool:
28
+ # checking if a folder exist is a bit tricky, but
29
+ # we use the solution proposed here in
30
+ # https://github.com/Azure/azure-sdk-for-python/issues/24814
31
+ if folder == "":
32
+ return True
33
+
34
+ file_client = self.file_system_client.get_file_client(folder)
35
+ if not file_client.exists():
36
+ return False
37
+
38
+ file_metadata = file_client.get_file_properties().metadata
39
+ is_dir_blob = file_metadata.get("hdi_isfolder", "false").lower() == "true"
40
+ if not is_dir_blob:
41
+ return False
42
+
43
+ return True
44
+
45
+ def is_file(self, path):
46
+ if path == "":
47
+ return False
48
+
49
+ file_client = self.file_system_client.get_file_client(path)
50
+ if not file_client.exists():
51
+ return False
52
+
53
+ file_metadata = file_client.get_file_properties().metadata
54
+ is_dir_blob = file_metadata.get("hdi_isfolder", "false").lower() == "true"
55
+ if is_dir_blob:
56
+ return False
57
+
58
+ return True
59
+
60
+ def _iter_dir(self, folder: Path | str = "") -> Iterator[str]:
61
+ for path in self.file_system_client.get_paths(folder, recursive=False):
62
+ yield path.name
63
+
64
+ def _get_blob_stream_downloader(self, path: Path | str) -> StorageStreamDownloader:
65
+ file_client = self.file_system_client.get_file_client(path)
66
+ return file_client.download_file()
67
+
68
+ def _write_blob_data(
69
+ self, path: Path | str, data: bytes | BytesIO, overwrite: bool = False
70
+ ):
71
+ file_client = self.file_system_client.get_file_client(path)
72
+ file_client.upload_data(data, overwrite=overwrite)
73
+
74
+ def __repr__(self) -> str:
75
+ return (
76
+ "DataLakeContainerManager("
77
+ f"storage_account_name='{self.storage_account_name}', "
78
+ f"container_name='{self.container_name}')"
79
+ )
@@ -0,0 +1,14 @@
1
+ from dataclasses import dataclass, field
2
+
3
+
4
+ @dataclass
5
+ class ResourceProperties:
6
+ name: str
7
+ id: str = field(repr=False)
8
+ type: str
9
+
10
+
11
+ @dataclass
12
+ class SubscriptionProperties:
13
+ id: str
14
+ name: str
@@ -0,0 +1,47 @@
1
+ from azure.core.credentials import TokenCredential
2
+ from azure.data.tables import TableServiceClient
3
+ from azure.storage.blob import BlobServiceClient
4
+
5
+ from . import ContainerManager, Manager
6
+
7
+
8
+ class StorageAccountManager(Manager):
9
+ def __init__(self, storage_account_name: str, credential: TokenCredential):
10
+ self.storage_account_name = storage_account_name
11
+ self.credential = credential
12
+ self.blob_service_client = BlobServiceClient(
13
+ f"https://{storage_account_name}.blob.core.windows.net",
14
+ credential=credential,
15
+ )
16
+
17
+ self.is_data_lake = self.blob_service_client.get_account_information().get(
18
+ "is_hns_enabled", False
19
+ )
20
+
21
+ self.table_service_client = TableServiceClient(
22
+ f"https://{storage_account_name}.table.core.windows.net",
23
+ credential=credential,
24
+ )
25
+
26
+ def get_container_manager(self, container_name: str) -> ContainerManager:
27
+ return ContainerManager.create(
28
+ storage_account_name=self.storage_account_name,
29
+ container_name=container_name,
30
+ is_data_lake=self.is_data_lake,
31
+ credential=self.credential,
32
+ )
33
+
34
+ def list_container_names(self) -> list[str]:
35
+ return [
36
+ container.name for container in self.blob_service_client.list_containers()
37
+ ]
38
+
39
+ def list_table_names(self) -> list[str]:
40
+ return [table.name for table in self.table_service_client.list_tables()]
41
+
42
+ def __repr__(self) -> str:
43
+ return (
44
+ "StorageAccountManager("
45
+ f"storage_account_name='{self.storage_account_name}', "
46
+ f"is_data_lake={self.is_data_lake})"
47
+ )
@@ -0,0 +1,76 @@
1
+ from azure.core.credentials import TokenCredential
2
+ from azure.mgmt.resource import ResourceManagementClient
3
+ from azure.mgmt.servicebus import ServiceBusManagementClient
4
+ from azure.mgmt.storage import StorageManagementClient
5
+
6
+ from azure_explorer.managers.models import ResourceProperties
7
+
8
+ from . import Manager, StorageAccountManager
9
+
10
+
11
+ class SubscriptionManager(Manager):
12
+ def __init__(self, subscription_id: str, credential: TokenCredential):
13
+ self.subscription_id = subscription_id
14
+ self.resource_mgtm_client = ResourceManagementClient(
15
+ credential,
16
+ subscription_id,
17
+ )
18
+ self.storage_mgtm_client = StorageManagementClient(
19
+ credential,
20
+ subscription_id,
21
+ )
22
+ self.servicebus_mgtm_client = ServiceBusManagementClient(
23
+ credential,
24
+ subscription_id,
25
+ )
26
+ self.credential = credential
27
+
28
+ def list_storage_accounts(self) -> list[ResourceProperties]:
29
+ items = []
30
+ for storage_account in self.storage_mgtm_client.storage_accounts.list():
31
+ if storage_account.is_hns_enabled:
32
+ resource_type = "data_lake"
33
+ else:
34
+ resource_type = "storage_account"
35
+
36
+ item = ResourceProperties(
37
+ id=storage_account.id,
38
+ name=storage_account.name,
39
+ type=resource_type,
40
+ )
41
+ items.append(item)
42
+
43
+ return items
44
+
45
+ def list_storage_account_ids(self) -> list[str]:
46
+ return [item.id for item in self.list_storage_accounts()]
47
+
48
+ def list_storage_account_names(self) -> list[str]:
49
+ return [item.name for item in self.list_storage_accounts()]
50
+
51
+ def get_storage_account_manager(self, storage_account_name: str):
52
+ return StorageAccountManager(
53
+ storage_account_name=storage_account_name,
54
+ credential=self.credential,
55
+ )
56
+
57
+ def list_service_buses(self) -> list[ResourceProperties]:
58
+ items = []
59
+ for namespace in self.servicebus_mgtm_client.namespaces.list():
60
+ item = ResourceProperties(
61
+ id=namespace.id,
62
+ name=namespace.name,
63
+ type="service_bus",
64
+ )
65
+ items.append(item)
66
+
67
+ return items
68
+
69
+ def list_service_bus_ids(self) -> list[str]:
70
+ return [item.id for item in self.list_service_buses()]
71
+
72
+ def list_service_bus_names(self) -> list[str]:
73
+ return [item.name for item in self.list_service_buses()]
74
+
75
+ def __repr__(self) -> str:
76
+ return f"SubscriptionManager(subscription_id='{self.subscription_id}')"
@@ -0,0 +1,35 @@
1
+ from azure.core.credentials import TokenCredential
2
+ from azure.mgmt.subscription import SubscriptionClient
3
+
4
+ from . import Manager, SubscriptionManager, SubscriptionProperties
5
+
6
+
7
+ class TenantManager(Manager):
8
+ def __init__(self, credential: TokenCredential):
9
+ self.sub_client = SubscriptionClient(credential)
10
+ self.credential = credential
11
+
12
+ def list_subscriptions(self) -> list[SubscriptionProperties]:
13
+ items = []
14
+ for sub in self.sub_client.subscriptions.list():
15
+ item = SubscriptionProperties(
16
+ id=sub.subscription_id,
17
+ name=sub.display_name,
18
+ )
19
+ items.append(item)
20
+ return items
21
+
22
+ def list_subscription_ids(self) -> list[str]:
23
+ return [item.id for item in self.list_subscriptions()]
24
+
25
+ def list_subscription_names(self) -> list[str]:
26
+ return [item.name for item in self.list_subscriptions()]
27
+
28
+ def get_subscription_manager(self, subscription_id: str):
29
+ return SubscriptionManager(
30
+ subscription_id=subscription_id,
31
+ credential=self.credential,
32
+ )
33
+
34
+ def __repr__(self) -> str:
35
+ return "TenantManager()"
@@ -0,0 +1,53 @@
1
+ from azure_explorer.config import Config
2
+ from azure_explorer.managers import (
3
+ ContainerManager,
4
+ StorageAccountManager,
5
+ SubscriptionManager,
6
+ TenantManager,
7
+ )
8
+
9
+
10
+ def get_tenant_manager() -> TenantManager:
11
+ return TenantManager(
12
+ credential=Config.credential,
13
+ )
14
+
15
+
16
+ def get_subscription_manager(subscription_id: str) -> SubscriptionManager:
17
+ return SubscriptionManager(
18
+ subscription_id=subscription_id,
19
+ credential=Config.credential,
20
+ )
21
+
22
+
23
+ def get_storage_account_manager(
24
+ storage_account_name: str,
25
+ ) -> StorageAccountManager:
26
+ return StorageAccountManager(
27
+ storage_account_name=storage_account_name,
28
+ credential=Config.credential,
29
+ )
30
+
31
+
32
+ def get_container_manager(
33
+ storage_account_name: str,
34
+ container_name: str,
35
+ is_data_lake: bool = False,
36
+ ) -> ContainerManager:
37
+ return ContainerManager.create(
38
+ storage_account_name=storage_account_name,
39
+ container_name=container_name,
40
+ is_data_lake=is_data_lake,
41
+ credential=Config.credential,
42
+ )
43
+
44
+
45
+ def read_file(
46
+ storage_account_name: str,
47
+ container_name: str,
48
+ path: str,
49
+ ) -> bytes:
50
+ container_manager = get_container_manager(
51
+ storage_account_name, container_name, is_data_lake=False
52
+ )
53
+ return container_manager.read_file(path)
@@ -0,0 +1 @@
1
+ from . import app
@@ -0,0 +1,29 @@
1
+ from textual.app import App, ComposeResult
2
+ from textual.binding import Binding
3
+ from textual.widgets import Footer, Header
4
+
5
+ from ..config import Config
6
+ from ..managers import TenantManager
7
+ from .browsers import TenantBrowser
8
+
9
+
10
+ class AzureExplorerApp(App):
11
+
12
+ BINDINGS = [
13
+ Binding("ctrl+x", "quit", "Exit", show=True, priority=True),
14
+ ]
15
+
16
+ TITLE = "Azure Explorer"
17
+
18
+ def compose(self) -> ComposeResult:
19
+ yield Header()
20
+ yield Footer()
21
+
22
+ def on_mount(self):
23
+ tenant_manager = TenantManager(credential=Config.credential)
24
+ tenant_browser = TenantBrowser(tenant_manager)
25
+ self.mount(tenant_browser)
26
+
27
+
28
+ def run():
29
+ AzureExplorerApp().run()
@@ -0,0 +1,5 @@
1
+ from .base import Browser
2
+ from .container import ContainerBrowser
3
+ from .storage_account import StorageAccountTypeBrowser
4
+ from .subscription import SubscriptionBrowser
5
+ from .tenant import TenantBrowser
@@ -0,0 +1,112 @@
1
+ from abc import abstractmethod
2
+ from typing import Any
3
+
4
+ from textual.app import ComposeResult
5
+ from textual.binding import Binding
6
+ from textual.containers import VerticalScroll
7
+ from textual.widget import Widget
8
+ from textual.widgets import DataTable, Input, Tabs
9
+
10
+
11
+ class Browser(Widget):
12
+
13
+ BINDINGS = [
14
+ Binding("up", "previous_row", "Previous row", show=False, priority=True),
15
+ Binding("down", "next_row", "Next row", show=False, priority=True),
16
+ Binding("escape", "go_back", "Go Back", show=True, priority=True),
17
+ ]
18
+
19
+ def __init__(self):
20
+ self.parent_browser = None
21
+ self.cached_rows = None
22
+ super().__init__()
23
+
24
+ def compose(self) -> ComposeResult:
25
+ input_ = Input(placeholder="Search...")
26
+ yield input_
27
+
28
+ data_table = DataTable(cursor_type="row")
29
+ data_table.can_focus = False
30
+ yield VerticalScroll(data_table)
31
+
32
+ def action_previous_row(self):
33
+ table = self.query_one(DataTable)
34
+ table.action_cursor_up()
35
+
36
+ def action_next_row(self):
37
+ table = self.query_one(DataTable)
38
+ table.action_cursor_down()
39
+
40
+ def action_go_back(self):
41
+ if self.parent_browser is not None:
42
+ self.remove()
43
+ self.app.mount(self.parent_browser)
44
+
45
+ def set_parent_browser(self, browser: "Browser"):
46
+ self.parent_browser = browser
47
+
48
+ def get_parent_browser(self) -> "Browser":
49
+ return getattr(self, "parent_browser", None)
50
+
51
+ def get_highlighted_item(self) -> Any:
52
+ table = self.query_one(DataTable)
53
+ if table.cursor_row is None:
54
+ return None
55
+ selected_id = table.coordinate_to_cell_key(
56
+ table.cursor_coordinate
57
+ ).row_key.value
58
+ return self.cached_rows[selected_id]
59
+
60
+ def _on_mount(self):
61
+ table = self.query_one(DataTable)
62
+ table.add_columns("Name")
63
+
64
+ self.populate_table()
65
+ self.query_one(Input).focus()
66
+
67
+ def on_data_table_row_selected(self, event: DataTable.RowSelected):
68
+ selected_id = event.row_key.value
69
+
70
+ selected_item = self.cached_rows[selected_id]
71
+
72
+ widget = self.get_item_widget(selected_item)
73
+ if widget is None:
74
+ return
75
+
76
+ widget.set_parent_browser(self)
77
+
78
+ self.remove()
79
+ self.app.mount(widget)
80
+
81
+ def on_input_changed(self, event: Input.Changed):
82
+ self.populate_table()
83
+
84
+ def on_tabs_tab_activated(self, event: Tabs.TabActivated):
85
+ self.populate_table()
86
+
87
+ def on_input_submitted(self):
88
+ self.query_one(DataTable).action_select_cursor()
89
+
90
+ def populate_table(self):
91
+ search_filter = self.query_one(Input).value.lower()
92
+
93
+ table = self.query_one(DataTable)
94
+ table.clear()
95
+
96
+ if self.cached_rows is None:
97
+ self.cached_rows = {
98
+ k: v for k, v in sorted(self.iter_rows(), key=lambda x: x[0].lower())
99
+ }
100
+
101
+ for id_, item in self.cached_rows.items():
102
+ if search_filter not in id_:
103
+ continue
104
+ table.add_row(id_, key=id_)
105
+
106
+ @abstractmethod
107
+ def iter_rows(self) -> list[tuple[str]]:
108
+ ...
109
+
110
+ @abstractmethod
111
+ def get_item_widget(self, id_: str) -> "Browser":
112
+ ...
@@ -0,0 +1,74 @@
1
+ from pathlib import Path
2
+ from typing import Iterator
3
+
4
+ from textual.binding import Binding
5
+ from textual.widget import Widget
6
+
7
+ from ...managers import ContainerManager
8
+ from . import Browser
9
+
10
+
11
+ class ContainerBrowser(Browser):
12
+
13
+ BINDINGS = [
14
+ Binding("ctrl+s", "download", "Download", show=True, priority=True),
15
+ Binding("ctrl+c", "copy_code", "Copy code", show=True, priority=True),
16
+ ]
17
+
18
+ def __init__(self, container_manager: ContainerManager, folder: str = ""):
19
+ self.container_manager = container_manager
20
+ self.folder = folder
21
+ self.blobs = None
22
+ super().__init__()
23
+
24
+ def check_action(self, action: str, parameters: tuple[object, ...]) -> bool | None:
25
+ """Check if an action may run."""
26
+ if action == "copy_code":
27
+ item = self.get_highlighted_item()
28
+ if self.container_manager.is_dir(item):
29
+ return None
30
+ return True
31
+
32
+ def action_download(self):
33
+ item = self.get_highlighted_item()
34
+
35
+ download_folder = Path.cwd() / ".downloads" / item
36
+
37
+ if self.container_manager.is_dir(item):
38
+ self.container_manager.download_dir(item, download_folder)
39
+ else:
40
+ self.container_manager.download_file(item, download_folder)
41
+
42
+ def action_copy_code(self):
43
+ item = self.get_highlighted_item()
44
+ code_snippet = f"""
45
+ import azure_explorer as ax
46
+
47
+ ax.read_file(
48
+ storage_account_name="{self.container_manager.storage_account_name}",
49
+ container_name="{self.container_manager.container_name}",
50
+ path="{item}",
51
+ )
52
+ """
53
+ self.app.copy_to_clipboard(code_snippet)
54
+ self.notify("Code snippet copied to clipboard!", severity="info", timeout=3)
55
+
56
+ def iter_rows(self) -> Iterator[tuple[str, str]]:
57
+ for subpath in self.container_manager.list_dir(self.folder):
58
+ if self.container_manager.is_dir(subpath):
59
+ icon = "📁"
60
+ else:
61
+ icon = "📄"
62
+
63
+ base_name = subpath.split("/")[-1]
64
+
65
+ yield (f"{icon} {base_name}", subpath)
66
+
67
+ def get_item_widget(self, item: str) -> Widget:
68
+ if not self.container_manager.is_dir(item):
69
+ return
70
+
71
+ return ContainerBrowser(
72
+ self.container_manager,
73
+ item,
74
+ )
@@ -0,0 +1,50 @@
1
+ from typing import Iterator
2
+
3
+ from textual.widget import Widget
4
+
5
+ from ...managers import StorageAccountManager
6
+ from . import Browser, ContainerBrowser
7
+
8
+
9
+ class StorageAccountTypeBrowser(Browser):
10
+ def __init__(self, sa_manager: StorageAccountManager):
11
+ self.sa_manager = sa_manager
12
+ super().__init__()
13
+
14
+ def iter_rows(self) -> Iterator[tuple[str, str]]:
15
+ yield ("Containers", "containers")
16
+ yield ("Tables", "tables")
17
+
18
+ def get_item_widget(self, item: str) -> Widget:
19
+ if item == "containers":
20
+ return StorageAccountContainerBrowser(self.sa_manager)
21
+ elif item == "tables":
22
+ return StorageAccountTableBrowser(self.sa_manager)
23
+ else:
24
+ return None
25
+
26
+
27
+ class StorageAccountContainerBrowser(Browser):
28
+ def __init__(self, sa_manager: StorageAccountManager):
29
+ self.sa_manager = sa_manager
30
+ super().__init__()
31
+
32
+ def iter_rows(self) -> Iterator[tuple[str, str]]:
33
+ for container_name in self.sa_manager.list_container_names():
34
+ yield (container_name, container_name)
35
+
36
+ def get_item_widget(self, item: str) -> Widget:
37
+ container_manager = self.sa_manager.get_container_manager(
38
+ container_name=item,
39
+ )
40
+ return ContainerBrowser(container_manager)
41
+
42
+
43
+ class StorageAccountTableBrowser(Browser):
44
+ def __init__(self, sa_manager: StorageAccountManager):
45
+ self.sa_manager = sa_manager
46
+ super().__init__()
47
+
48
+ def iter_rows(self) -> Iterator[tuple[str, str]]:
49
+ for table_name in self.sa_manager.list_table_names():
50
+ yield (table_name, table_name)
@@ -0,0 +1,22 @@
1
+ from typing import Iterator
2
+
3
+ from textual.widget import Widget
4
+
5
+ from ...managers import SubscriptionManager
6
+ from . import Browser, StorageAccountTypeBrowser
7
+
8
+
9
+ class SubscriptionBrowser(Browser):
10
+ def __init__(self, sub_manager: SubscriptionManager):
11
+ self.sub_manager = sub_manager
12
+ super().__init__()
13
+
14
+ def iter_rows(self) -> Iterator[tuple[str, str]]:
15
+ for storage_account in self.sub_manager.list_storage_accounts():
16
+ yield (storage_account.name, storage_account.name)
17
+
18
+ def get_item_widget(self, item: str) -> Widget:
19
+ sa_manager = self.sub_manager.get_storage_account_manager(
20
+ storage_account_name=item,
21
+ )
22
+ return StorageAccountTypeBrowser(sa_manager)
@@ -0,0 +1,22 @@
1
+ from typing import Iterator
2
+
3
+ from textual.widget import Widget
4
+
5
+ from ...managers import TenantManager
6
+ from . import Browser, SubscriptionBrowser
7
+
8
+
9
+ class TenantBrowser(Browser):
10
+ def __init__(self, tenant_manager: TenantManager):
11
+ self.tenant_manager = tenant_manager
12
+ super().__init__()
13
+
14
+ def iter_rows(self) -> Iterator[tuple[str, str]]:
15
+ for sub in self.tenant_manager.list_subscriptions():
16
+ yield (sub.name, sub.id)
17
+
18
+ def get_item_widget(self, item: str) -> Widget:
19
+ sub_manager = self.tenant_manager.get_subscription_manager(
20
+ subscription_id=item,
21
+ )
22
+ return SubscriptionBrowser(sub_manager)
@@ -0,0 +1,45 @@
1
+ [tool.poetry]
2
+ name = "azure-explorer"
3
+ version = "0.1.0"
4
+ description = ""
5
+ authors = ["uch <uch@energinet.dk>"]
6
+ readme = "README.md"
7
+
8
+ [tool.poetry.dependencies]
9
+ python = "^3.10.8"
10
+ azure-data-tables = "^12.7.0"
11
+ azure-identity = "^1.25.1"
12
+ azure-mgmt-resource = "^24.0.0"
13
+ azure-mgmt-servicebus = "^9.0.0"
14
+ azure-mgmt-storage = "^24.0.0"
15
+ azure-mgmt-subscription = "^3.1.1"
16
+ azure-servicebus = "^7.14.3"
17
+ azure-storage-blob = "^12.27.1"
18
+ azure-storage-file-datalake = "^12.22.0"
19
+ textual = "^7.0.0"
20
+
21
+ [tool.poetry.dev-dependencies]
22
+ pytest = "^7"
23
+ pytest-cov = "^3.0.0"
24
+ pre-commit = "^2.20.0"
25
+ pytest-asyncio = "^0.19.0"
26
+ freezegun = "^1.2.2"
27
+ mypy = "^1.19.0"
28
+
29
+ [tool.poetry.scripts]
30
+ ax = "azure_explorer.widget.app:run"
31
+
32
+ [build-system]
33
+ requires = ["poetry-core>=1.0.0"]
34
+ build-backend = "poetry.core.masonry.api"
35
+
36
+ [tool.isort]
37
+ multi_line_output = 3
38
+ line_length = 88
39
+ include_trailing_comma = true
40
+
41
+ [tool.black]
42
+ line_length = 88
43
+
44
+ [tool.mypy]
45
+ ignore_missing_imports = true