dnastack-client-library 3.1.114a0__py3-none-any.whl → 3.1.120a0__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.
- dnastack/cli/commands/publisher/__init__.py +2 -0
- dnastack/cli/commands/publisher/datasources/__init__.py +9 -0
- dnastack/cli/commands/publisher/datasources/commands.py +35 -0
- dnastack/cli/commands/publisher/datasources/utils.py +18 -0
- dnastack/client/constants.py +5 -3
- dnastack/client/datasources/__init__.py +3 -0
- dnastack/client/datasources/client.py +124 -0
- dnastack/client/datasources/model.py +11 -0
- dnastack/constants.py +1 -1
- {dnastack_client_library-3.1.114a0.dist-info → dnastack_client_library-3.1.120a0.dist-info}/METADATA +1 -1
- {dnastack_client_library-3.1.114a0.dist-info → dnastack_client_library-3.1.120a0.dist-info}/RECORD +15 -9
- {dnastack_client_library-3.1.114a0.dist-info → dnastack_client_library-3.1.120a0.dist-info}/WHEEL +1 -1
- {dnastack_client_library-3.1.114a0.dist-info → dnastack_client_library-3.1.120a0.dist-info}/LICENSE +0 -0
- {dnastack_client_library-3.1.114a0.dist-info → dnastack_client_library-3.1.120a0.dist-info}/entry_points.txt +0 -0
- {dnastack_client_library-3.1.114a0.dist-info → dnastack_client_library-3.1.120a0.dist-info}/top_level.txt +0 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from dnastack.cli.commands.publisher.collections import collections_command_group
|
|
2
|
+
from dnastack.cli.commands.publisher.datasources import datasources_command_group
|
|
2
3
|
from dnastack.cli.core.group import formatted_group
|
|
3
4
|
|
|
4
5
|
|
|
@@ -8,3 +9,4 @@ def publisher_command_group():
|
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
publisher_command_group.add_command(collections_command_group)
|
|
12
|
+
publisher_command_group.add_command(datasources_command_group)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from dnastack.cli.core.group import formatted_group
|
|
2
|
+
from dnastack.cli.commands.publisher.datasources.commands import init_datasources_commands
|
|
3
|
+
|
|
4
|
+
@formatted_group("datasources")
|
|
5
|
+
def datasources_command_group():
|
|
6
|
+
""" Interact with data sources """
|
|
7
|
+
|
|
8
|
+
# Initialize all commands
|
|
9
|
+
init_datasources_commands(datasources_command_group)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
from click import Group
|
|
4
|
+
|
|
5
|
+
from dnastack.cli.commands.publisher.datasources.utils import _get_datasource_client, _filter_datasource_fields
|
|
6
|
+
from dnastack.cli.core.command import formatted_command
|
|
7
|
+
from dnastack.cli.core.command_spec import RESOURCE_OUTPUT_ARG, ArgumentSpec
|
|
8
|
+
from dnastack.cli.helpers.iterator_printer import show_iterator
|
|
9
|
+
from dnastack.common.tracing import Span
|
|
10
|
+
|
|
11
|
+
def init_datasources_commands(group: Group):
|
|
12
|
+
@formatted_command(
|
|
13
|
+
group=group,
|
|
14
|
+
name='list',
|
|
15
|
+
specs=[
|
|
16
|
+
RESOURCE_OUTPUT_ARG,
|
|
17
|
+
ArgumentSpec(
|
|
18
|
+
name='type',
|
|
19
|
+
arg_names=['--type'],
|
|
20
|
+
help='Filter datasources by type (e.g., "AWS")',
|
|
21
|
+
required=False
|
|
22
|
+
),
|
|
23
|
+
]
|
|
24
|
+
)
|
|
25
|
+
def list_datasources(output: Optional[str] = None, type: Optional[str] = None):
|
|
26
|
+
""" List all data sources """
|
|
27
|
+
with Span("list_datasources") as span:
|
|
28
|
+
client = _get_datasource_client()
|
|
29
|
+
response = client.list_datasources(trace=span, type=type)
|
|
30
|
+
|
|
31
|
+
show_iterator(output,
|
|
32
|
+
[
|
|
33
|
+
_filter_datasource_fields(datasource.dict())
|
|
34
|
+
for datasource in response.connections
|
|
35
|
+
])
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from typing import Dict, Any, Optional
|
|
2
|
+
from imagination import container
|
|
3
|
+
from dnastack.cli.helpers.client_factory import ConfigurationBasedClientFactory
|
|
4
|
+
from dnastack.client.datasources.client import DataSourceServiceClient
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _get_datasource_client() -> DataSourceServiceClient:
|
|
8
|
+
"""Get the data source service client."""
|
|
9
|
+
factory: ConfigurationBasedClientFactory = container.get(ConfigurationBasedClientFactory)
|
|
10
|
+
return factory.get(DataSourceServiceClient)
|
|
11
|
+
|
|
12
|
+
def _filter_datasource_fields(datasource: Dict[str, Any]) -> Dict[str, Any]:
|
|
13
|
+
"""Filter and transform datasource fields for display."""
|
|
14
|
+
return {
|
|
15
|
+
'id': datasource.get('id'),
|
|
16
|
+
'name': datasource.get('name'),
|
|
17
|
+
'type': datasource.get('type'),
|
|
18
|
+
}
|
dnastack/client/constants.py
CHANGED
|
@@ -3,6 +3,7 @@ from typing import TypeVar
|
|
|
3
3
|
from dnastack.client.base_client import BaseServiceClient
|
|
4
4
|
from dnastack.client.collections.client import CollectionServiceClient
|
|
5
5
|
from dnastack.client.data_connect import DataConnectClient
|
|
6
|
+
from dnastack.client.datasources.client import DataSourceServiceClient
|
|
6
7
|
from dnastack.client.drs import DrsClient
|
|
7
8
|
from dnastack.client.service_registry.client import ServiceRegistry
|
|
8
9
|
from dnastack.client.workbench.ewes.client import EWesClient
|
|
@@ -15,12 +16,12 @@ from dnastack.client.workbench.workflow.client import WorkflowClient
|
|
|
15
16
|
ALL_SERVICE_CLIENT_CLASSES = (
|
|
16
17
|
CollectionServiceClient, DataConnectClient, DrsClient, ServiceRegistry, EWesClient, StorageClient, SamplesClient,
|
|
17
18
|
WorkflowClient,
|
|
18
|
-
WorkbenchUserClient)
|
|
19
|
+
WorkbenchUserClient, DataSourceServiceClient)
|
|
19
20
|
|
|
20
21
|
# All client classes for data access
|
|
21
22
|
DATA_SERVICE_CLIENT_CLASSES = (
|
|
22
23
|
CollectionServiceClient, DataConnectClient, DrsClient, EWesClient, StorageClient, WorkflowClient,
|
|
23
|
-
WorkbenchUserClient)
|
|
24
|
+
WorkbenchUserClient, DataSourceServiceClient)
|
|
24
25
|
|
|
25
26
|
# Type variable for the service client
|
|
26
27
|
SERVICE_CLIENT_CLASS = TypeVar('SERVICE_CLIENT_CLASS',
|
|
@@ -33,4 +34,5 @@ SERVICE_CLIENT_CLASS = TypeVar('SERVICE_CLIENT_CLASS',
|
|
|
33
34
|
DataConnectClient,
|
|
34
35
|
DrsClient,
|
|
35
36
|
SamplesClient,
|
|
36
|
-
ServiceRegistry
|
|
37
|
+
ServiceRegistry,
|
|
38
|
+
DataSourceServiceClient)
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
from pprint import pformat
|
|
2
|
+
from typing import List, Optional
|
|
3
|
+
from pydantic import BaseModel, ValidationError
|
|
4
|
+
from dnastack.client.base_client import BaseServiceClient
|
|
5
|
+
from dnastack.client.base_exceptions import (
|
|
6
|
+
UnauthenticatedApiAccessError, UnauthorizedApiAccessError
|
|
7
|
+
)
|
|
8
|
+
from dnastack.client.collections.client import STANDARD_COLLECTION_SERVICE_TYPE_V1_0
|
|
9
|
+
from dnastack.client.collections.model import PageableApiError
|
|
10
|
+
from dnastack.client.datasources.model import DataSource, DataSourceListOptions
|
|
11
|
+
from dnastack.client.result_iterator import ResultLoader, InactiveLoaderError, ResultIterator
|
|
12
|
+
from dnastack.http.session import HttpSession, HttpError
|
|
13
|
+
from dnastack.common.tracing import Span
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class DataSourcesResponse(BaseModel):
|
|
17
|
+
connections: List[DataSource]
|
|
18
|
+
|
|
19
|
+
def items(self) -> List[DataSource]:
|
|
20
|
+
return self.connections
|
|
21
|
+
|
|
22
|
+
class DataSourceListResultLoader(ResultLoader):
|
|
23
|
+
"""
|
|
24
|
+
Result loader for handling data source fetching without pagination.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, service_url: str, http_session: HttpSession, trace: Span,
|
|
28
|
+
list_options: Optional[dict] = None, max_results: Optional[int] = None):
|
|
29
|
+
self.__service_url = service_url
|
|
30
|
+
self.__http_session = http_session
|
|
31
|
+
self.__list_options = list_options or {}
|
|
32
|
+
self.__max_results = max_results
|
|
33
|
+
self.__loaded_results = 0
|
|
34
|
+
self.__active = True # Determines when we are done fetching results
|
|
35
|
+
self.__trace = trace
|
|
36
|
+
|
|
37
|
+
def has_more(self) -> bool:
|
|
38
|
+
"""Checks if there are more results to fetch."""
|
|
39
|
+
return self.__active
|
|
40
|
+
|
|
41
|
+
def extract_api_response(self, response_body: dict) -> DataSourcesResponse:
|
|
42
|
+
"""
|
|
43
|
+
Converts the API response body into a DataSourcesResponse object.
|
|
44
|
+
"""
|
|
45
|
+
return DataSourcesResponse(**response_body)
|
|
46
|
+
|
|
47
|
+
def load(self) -> List[DataSource]:
|
|
48
|
+
"""Fetches the data from the API."""
|
|
49
|
+
if not self.__active:
|
|
50
|
+
raise InactiveLoaderError(self.__service_url)
|
|
51
|
+
|
|
52
|
+
with self.__http_session as session:
|
|
53
|
+
try:
|
|
54
|
+
# Perform the GET request
|
|
55
|
+
response = session.get(
|
|
56
|
+
self.__service_url,
|
|
57
|
+
params=self.__list_options,
|
|
58
|
+
trace_context=self.__trace
|
|
59
|
+
)
|
|
60
|
+
except HttpError as e:
|
|
61
|
+
error_feedback = f"Failed to load data from {self.__service_url}: {e.response.text}"
|
|
62
|
+
if e.response.status_code == 401:
|
|
63
|
+
raise UnauthenticatedApiAccessError(error_feedback)
|
|
64
|
+
elif e.response.status_code == 403:
|
|
65
|
+
raise UnauthorizedApiAccessError(error_feedback)
|
|
66
|
+
else:
|
|
67
|
+
raise PageableApiError(error_feedback, e.response.status_code, e.response.text)
|
|
68
|
+
|
|
69
|
+
# Parse the API response
|
|
70
|
+
response_body = response.json() if response.text else {}
|
|
71
|
+
try:
|
|
72
|
+
api_response = self.extract_api_response(response_body)
|
|
73
|
+
except ValidationError:
|
|
74
|
+
raise PageableApiError(
|
|
75
|
+
f"Invalid response body: {response_body}",
|
|
76
|
+
response.status_code,
|
|
77
|
+
response.text,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# Retrieve the data source items
|
|
81
|
+
items = api_response.items()
|
|
82
|
+
|
|
83
|
+
# Deactivate the loader after the first fetch since there's no pagination
|
|
84
|
+
self.__active = False
|
|
85
|
+
|
|
86
|
+
# Handle max_results constraint
|
|
87
|
+
if self.__max_results and len(items) > self.__max_results:
|
|
88
|
+
return items[:self.__max_results]
|
|
89
|
+
|
|
90
|
+
return items
|
|
91
|
+
|
|
92
|
+
class DataSourceServiceClient(BaseServiceClient):
|
|
93
|
+
"""
|
|
94
|
+
Client to interact with the Data Sources API.
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
@staticmethod
|
|
98
|
+
def get_adapter_type() -> str:
|
|
99
|
+
return 'collections'
|
|
100
|
+
|
|
101
|
+
@classmethod
|
|
102
|
+
def get_supported_service_types(cls) -> List[str]:
|
|
103
|
+
"""
|
|
104
|
+
Returns supported service types.
|
|
105
|
+
"""
|
|
106
|
+
return [ STANDARD_COLLECTION_SERVICE_TYPE_V1_0,]
|
|
107
|
+
|
|
108
|
+
def list_datasources(self, type: Optional[str] = None, trace: Optional[Span] = None,
|
|
109
|
+
max_results: Optional[int] = None) -> DataSourcesResponse:
|
|
110
|
+
# Set up query parameters
|
|
111
|
+
list_options = {}
|
|
112
|
+
if type:
|
|
113
|
+
list_options["type"] = type
|
|
114
|
+
|
|
115
|
+
trace = trace or Span(origin=self)
|
|
116
|
+
loader = DataSourceListResultLoader(
|
|
117
|
+
service_url=f"{self.url}/connections/data-sources",
|
|
118
|
+
http_session=self.create_http_session(),
|
|
119
|
+
trace=trace,
|
|
120
|
+
list_options=list_options,
|
|
121
|
+
max_results=max_results
|
|
122
|
+
)
|
|
123
|
+
connections = list(ResultIterator(loader))
|
|
124
|
+
return DataSourcesResponse(connections=connections)
|
dnastack/constants.py
CHANGED
{dnastack_client_library-3.1.114a0.dist-info → dnastack_client_library-3.1.120a0.dist-info}/RECORD
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
dnastack/__init__.py,sha256=Bpfm77MeoZNmZHSgDK9wlgVPkcdE73ur6_7Zihorb4o,263
|
|
2
2
|
dnastack/__main__.py,sha256=3rydT8oj5G1IN0asiqv9oaQw2xmyIlVge8c8wUjo0HA,3532
|
|
3
|
-
dnastack/constants.py,sha256=
|
|
3
|
+
dnastack/constants.py,sha256=emInQGO_r53MWwHbI63wDj3e9qlVQaEQ5oP5zP86Qx0,115
|
|
4
4
|
dnastack/feature_flags.py,sha256=RK_V_Ovncoe6NeTheAA_frP-kYkZC1fDlTbbup2KYG4,1419
|
|
5
5
|
dnastack/json_path.py,sha256=TyghhDf7nGQmnsUWBhenU_fKsE_Ez-HLVER6HgH5-hU,2700
|
|
6
6
|
dnastack/omics_cli.py,sha256=ZppKZTHv_XjUUZyRIzSkx0Ug5ODAYrCOTsU0ezCOVrA,3694
|
|
@@ -49,12 +49,15 @@ dnastack/cli/commands/dataconnect/utils.py,sha256=7psRouHUsg2QEemZAhzHVsjy1rza63
|
|
|
49
49
|
dnastack/cli/commands/drs/__init__.py,sha256=XGPfCsdOyZyc67ptYmM903US741hDCKaTowVtysC6H8,325
|
|
50
50
|
dnastack/cli/commands/drs/commands.py,sha256=9n-f24ODZNCpxgIirdzsIEnZqVRw2jFkvXiCsW4ZnOI,5405
|
|
51
51
|
dnastack/cli/commands/drs/utils.py,sha256=tGogaIlXKnk03GqitzeDfo893JlP7Nc_X28sH-yQUrI,427
|
|
52
|
-
dnastack/cli/commands/publisher/__init__.py,sha256=
|
|
52
|
+
dnastack/cli/commands/publisher/__init__.py,sha256=G8WNdx1UwanoA4X349ghv8Zyv5YizB8ryeJmwu9px8o,443
|
|
53
53
|
dnastack/cli/commands/publisher/collections/__init__.py,sha256=KmclN_KY3ctVhtv-i8rxXpWTshPCj1tY6yhud4vrXYQ,636
|
|
54
54
|
dnastack/cli/commands/publisher/collections/commands.py,sha256=A82NphvnD-9JuN2dVU_07EbAvB1NE7Em07IU7eDhagc,9454
|
|
55
55
|
dnastack/cli/commands/publisher/collections/items.py,sha256=gmSCaXXqxk4lElZnqkU6KZCLa5QSDgTd5Zz0LWSBMDQ,6101
|
|
56
56
|
dnastack/cli/commands/publisher/collections/tables.py,sha256=N348MU_p-OKweJwKYtNbpvCD3ZmGGbS-quz7gnsAc1c,2130
|
|
57
57
|
dnastack/cli/commands/publisher/collections/utils.py,sha256=GNgGv29z7eumQqbw1IFFasJeDyxH2KQGQl0q6UPooVU,6244
|
|
58
|
+
dnastack/cli/commands/publisher/datasources/__init__.py,sha256=90suC61lfFF1WzIU9f8lO0_DiLWIQF2Dy46_Yad3CD8,327
|
|
59
|
+
dnastack/cli/commands/publisher/datasources/commands.py,sha256=TdR55CxqlpN62GiHeWIfPfRsorfrzDj7GwZlmBqseA0,1298
|
|
60
|
+
dnastack/cli/commands/publisher/datasources/utils.py,sha256=DGVZY6e0Faa0sNRXxqu50z7kGACX4YYkF-cgtMLaUQc,745
|
|
58
61
|
dnastack/cli/commands/workbench/__init__.py,sha256=H4CGbc31-21mRZqJMtzi2Cg4e_D9a9ibqFjwXQTcXNY,1092
|
|
59
62
|
dnastack/cli/commands/workbench/utils.py,sha256=V2I192k4XAg2h-DrsdatqIZfguflh6WR7q5pSBojUX0,4354
|
|
60
63
|
dnastack/cli/commands/workbench/engines/__init__.py,sha256=qB1rfGXNJ6_N6F5UYXos4hHDwhm8PtlbrCCt_uNhIIw,653
|
|
@@ -103,7 +106,7 @@ dnastack/cli/helpers/printer.py,sha256=IdGoEnBWtzgPAE2KLoZEfwshn0tkrTu-XXtN-KXj8
|
|
|
103
106
|
dnastack/client/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
104
107
|
dnastack/client/base_client.py,sha256=ZhaWHHTQcuPun6RdrtCdShGRYoY18hEKAIHmr8Z728M,3730
|
|
105
108
|
dnastack/client/base_exceptions.py,sha256=QoYa-LzT5C0o7eq6Ek4R_-clBL0nZ5v7qdFBcCoUiw4,2888
|
|
106
|
-
dnastack/client/constants.py,sha256=
|
|
109
|
+
dnastack/client/constants.py,sha256=6nB1QKz-GEx8BSnPLFafKGG1WDF3l0P0_HlC01FSvwc,1849
|
|
107
110
|
dnastack/client/data_connect.py,sha256=6qDx6-dl-fmakEMYK_jfTgUcYe1yl8eTCMPI_8tRUwg,26211
|
|
108
111
|
dnastack/client/drs.py,sha256=7iNu58YAtQHKFFFNllFyXATfasT_ndkgSkBKKgDbiKc,21225
|
|
109
112
|
dnastack/client/factory.py,sha256=PQGYUGhKeqyJtEtc-bZM5OlEup9K7lB-qwmNiII_4HU,6288
|
|
@@ -112,6 +115,9 @@ dnastack/client/result_iterator.py,sha256=00zEs7YbPJyt8d-6j7eHnmSVurP8hIOarj4Fws
|
|
|
112
115
|
dnastack/client/collections/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
113
116
|
dnastack/client/collections/client.py,sha256=ea7N7fDP5jdyPT3_gwd3WVm_wsGZ4ApTm6qdUr3X70k,15376
|
|
114
117
|
dnastack/client/collections/model.py,sha256=JO-eC_jc7hY9PBvST3x1vkmJE0LPDAbcUq5VBKcGyLM,3492
|
|
118
|
+
dnastack/client/datasources/__init__.py,sha256=HxDIHuQX8KMWr3o70ucL3x79pXKaIHbBq7JqmyoRGxM,179
|
|
119
|
+
dnastack/client/datasources/client.py,sha256=AfYxu18QRLh3tNe6uwS_Z00VwZCZNw7fUYMsEDDMe40,4734
|
|
120
|
+
dnastack/client/datasources/model.py,sha256=dV9Sf05ivIq0ubwIIYK3kSv1xJ_TtjxvVp_ddI9aHEk,214
|
|
115
121
|
dnastack/client/service_registry/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
116
122
|
dnastack/client/service_registry/client.py,sha256=r7D8CnPJLbNkc03g2PYHt880Ba1oPW2d8B0ShP0p4Eo,1131
|
|
117
123
|
dnastack/client/service_registry/factory.py,sha256=l_H4ipFzv9j6VkWoPtyHOHJvCCmS6ceVpazjD5qfXM0,10741
|
|
@@ -177,9 +183,9 @@ dnastack/http/authenticators/oauth2_adapter/client_credential.py,sha256=cCVZa4B1
|
|
|
177
183
|
dnastack/http/authenticators/oauth2_adapter/device_code_flow.py,sha256=_OMHPf7qekPn_oSCBb41iYg9100sQMVzfJiKZHdh26w,6529
|
|
178
184
|
dnastack/http/authenticators/oauth2_adapter/factory.py,sha256=r8K6swt5zhraP74KhTL2K4sQ71HWAMLM0oHg8qQT4BA,965
|
|
179
185
|
dnastack/http/authenticators/oauth2_adapter/models.py,sha256=U11r8DZsWvjIRNCJE1mmQMuprZw3fpFwFBg7vmI5w48,660
|
|
180
|
-
dnastack_client_library-3.1.
|
|
181
|
-
dnastack_client_library-3.1.
|
|
182
|
-
dnastack_client_library-3.1.
|
|
183
|
-
dnastack_client_library-3.1.
|
|
184
|
-
dnastack_client_library-3.1.
|
|
185
|
-
dnastack_client_library-3.1.
|
|
186
|
+
dnastack_client_library-3.1.120a0.dist-info/LICENSE,sha256=uwybO-wUbQhxkosgjhJlxmYATMy-AzoULFO9FUedE34,11580
|
|
187
|
+
dnastack_client_library-3.1.120a0.dist-info/METADATA,sha256=6fbWkDqg_o_lE-mKC7oDKWMkqeqKlHl3s8dDDdsKBeU,968
|
|
188
|
+
dnastack_client_library-3.1.120a0.dist-info/WHEEL,sha256=52BFRY2Up02UkjOa29eZOS2VxUrpPORXg1pkohGGUS8,91
|
|
189
|
+
dnastack_client_library-3.1.120a0.dist-info/entry_points.txt,sha256=Y6OeicsiyGn3-8D-SiV4NiKlJgXfkSqK88kFBR6R1rY,89
|
|
190
|
+
dnastack_client_library-3.1.120a0.dist-info/top_level.txt,sha256=P2RgRyqJ7hfNy1wLVRoVLJYEppUVkCX3syGK9zBqkt8,9
|
|
191
|
+
dnastack_client_library-3.1.120a0.dist-info/RECORD,,
|
{dnastack_client_library-3.1.114a0.dist-info → dnastack_client_library-3.1.120a0.dist-info}/LICENSE
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|