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.
- azure_explorer-0.1.0/PKG-INFO +80 -0
- azure_explorer-0.1.0/README.md +56 -0
- azure_explorer-0.1.0/azure_explorer/__init__.py +13 -0
- azure_explorer-0.1.0/azure_explorer/config.py +5 -0
- azure_explorer-0.1.0/azure_explorer/managers/__init__.py +6 -0
- azure_explorer-0.1.0/azure_explorer/managers/base.py +5 -0
- azure_explorer-0.1.0/azure_explorer/managers/container/__init__.py +3 -0
- azure_explorer-0.1.0/azure_explorer/managers/container/base.py +131 -0
- azure_explorer-0.1.0/azure_explorer/managers/container/blob_storage.py +64 -0
- azure_explorer-0.1.0/azure_explorer/managers/container/data_lake.py +79 -0
- azure_explorer-0.1.0/azure_explorer/managers/container/table.py +0 -0
- azure_explorer-0.1.0/azure_explorer/managers/models.py +14 -0
- azure_explorer-0.1.0/azure_explorer/managers/storage_account.py +47 -0
- azure_explorer-0.1.0/azure_explorer/managers/subscription.py +76 -0
- azure_explorer-0.1.0/azure_explorer/managers/tenant.py +35 -0
- azure_explorer-0.1.0/azure_explorer/short_cuts.py +53 -0
- azure_explorer-0.1.0/azure_explorer/widget/__init__.py +1 -0
- azure_explorer-0.1.0/azure_explorer/widget/app.py +29 -0
- azure_explorer-0.1.0/azure_explorer/widget/browsers/__init__.py +5 -0
- azure_explorer-0.1.0/azure_explorer/widget/browsers/base.py +112 -0
- azure_explorer-0.1.0/azure_explorer/widget/browsers/container.py +74 -0
- azure_explorer-0.1.0/azure_explorer/widget/browsers/storage_account.py +50 -0
- azure_explorer-0.1.0/azure_explorer/widget/browsers/subscription.py +22 -0
- azure_explorer-0.1.0/azure_explorer/widget/browsers/tenant.py +22 -0
- 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
|
+

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

|
|
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,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
|
+
)
|
|
File without changes
|
|
@@ -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,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
|