geoseeq 0.7.3.dev0__py3-none-any.whl → 0.7.3.dev1__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.
- geoseeq/dashboard/dashboard.py +111 -40
- geoseeq/sample.py +83 -15
- {geoseeq-0.7.3.dev0.dist-info → geoseeq-0.7.3.dev1.dist-info}/METADATA +1 -1
- {geoseeq-0.7.3.dev0.dist-info → geoseeq-0.7.3.dev1.dist-info}/RECORD +7 -7
- {geoseeq-0.7.3.dev0.dist-info → geoseeq-0.7.3.dev1.dist-info}/WHEEL +0 -0
- {geoseeq-0.7.3.dev0.dist-info → geoseeq-0.7.3.dev1.dist-info}/entry_points.txt +0 -0
- {geoseeq-0.7.3.dev0.dist-info → geoseeq-0.7.3.dev1.dist-info}/licenses/LICENSE +0 -0
geoseeq/dashboard/dashboard.py
CHANGED
@@ -3,6 +3,7 @@ from typing import Literal
|
|
3
3
|
|
4
4
|
from geoseeq import ProjectResultFile
|
5
5
|
from geoseeq.id_constructors import result_file_from_blob
|
6
|
+
from geoseeq.id_constructors.from_ids import result_file_from_id
|
6
7
|
from geoseeq.remote_object import RemoteObject
|
7
8
|
|
8
9
|
logger = logging.getLogger("geoseeq_api")
|
@@ -29,7 +30,7 @@ class Dashboard(RemoteObject):
|
|
29
30
|
blob.pop("tiles")
|
30
31
|
self.load_blob(blob, allow_overwrite=allow_overwrite)
|
31
32
|
|
32
|
-
def
|
33
|
+
def save(self):
|
33
34
|
self.save_tiles()
|
34
35
|
|
35
36
|
def save_tiles(self):
|
@@ -82,64 +83,131 @@ class Dashboard(RemoteObject):
|
|
82
83
|
return "DASH" + self.project.uuid + self.name
|
83
84
|
|
84
85
|
|
86
|
+
class DashboardTile:
|
87
|
+
def __init__(self, knex, dashboard, title, result_file, style="col-span-1"):
|
88
|
+
self.knex = knex
|
89
|
+
self.dashboard = dashboard
|
90
|
+
self.title = title
|
91
|
+
self.style = style
|
92
|
+
self.result_file = result_file
|
93
|
+
|
94
|
+
def _get_post_data(self):
|
95
|
+
out = {
|
96
|
+
"field_uuid": self.result_file.uuid,
|
97
|
+
"field_type": (
|
98
|
+
"group" if isinstance(self.result_file, ProjectResultFile) else "sample"
|
99
|
+
),
|
100
|
+
"style": self.style,
|
101
|
+
"title": self.title,
|
102
|
+
"has_related_field": False,
|
103
|
+
}
|
104
|
+
return out
|
105
|
+
|
106
|
+
@classmethod
|
107
|
+
def from_blob(cls, dashboard, blob):
|
108
|
+
result_file = result_file_from_blob(dashboard.knex, blob["viz_field"])
|
109
|
+
return cls(
|
110
|
+
dashboard.knex, dashboard, blob["title"], result_file, style=blob["style"]
|
111
|
+
)
|
112
|
+
|
113
|
+
def __str__(self) -> str:
|
114
|
+
return f'<Geoseeq DashboardTile: {self.dashboard.grn} "{self.title}" />'
|
115
|
+
|
116
|
+
def __repr__(self) -> str:
|
117
|
+
return str(self)
|
118
|
+
|
119
|
+
|
85
120
|
class SampleDashboard(RemoteObject):
|
86
121
|
"""Dashboard client for a single sample."""
|
87
122
|
|
88
123
|
parent_field = "sample"
|
89
|
-
remote_fields = ["
|
124
|
+
remote_fields = ["uuid", "title", "default", "created_at", "updated_at"]
|
90
125
|
|
91
|
-
def __init__(self, knex, sample,
|
126
|
+
def __init__(self, knex, sample, title="Default dashboard", default=False):
|
92
127
|
super().__init__(self)
|
93
128
|
self.knex = knex
|
94
129
|
self.sample = sample
|
95
|
-
self.
|
130
|
+
self.title = title
|
96
131
|
self.tiles = []
|
97
|
-
self.
|
132
|
+
self.default = default
|
98
133
|
|
99
134
|
def _get(self, allow_overwrite=False):
|
100
|
-
blob = self.knex.get(f"samples/{self.sample.uuid}/
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
135
|
+
blob = self.knex.get(f"samples/{self.sample.uuid}/dashboards")
|
136
|
+
try:
|
137
|
+
blob = [
|
138
|
+
dashboard_blob
|
139
|
+
for dashboard_blob in blob["results"]
|
140
|
+
if dashboard_blob["title"] == self.title
|
141
|
+
][0]
|
142
|
+
except IndexError:
|
143
|
+
raise ValueError(f"There is no existing dashboard with title {self.title}")
|
144
|
+
|
106
145
|
self.load_blob(blob, allow_overwrite=allow_overwrite)
|
107
146
|
|
108
|
-
|
147
|
+
# Load tiles
|
148
|
+
tiles_res = self.knex.get(f"samples/dashboards/{self.uuid}/tiles")
|
149
|
+
for tile_blob in tiles_res["results"]:
|
150
|
+
tile = SampleDashboardTile.from_blob(self, tile_blob)
|
151
|
+
self.tiles.append(tile)
|
152
|
+
|
153
|
+
def save(self):
|
154
|
+
data = self._get_post_data()
|
155
|
+
url = f"samples/{self.sample.uuid}/dashboards/{self.uuid}"
|
156
|
+
self.knex.put(url, json=data)
|
109
157
|
self.save_tiles()
|
110
158
|
|
111
159
|
def save_tiles(self):
|
112
160
|
post_data = {"tiles": [tile._get_post_data() for tile in self.tiles]}
|
113
|
-
self.knex.
|
114
|
-
f"samples/{self.
|
161
|
+
self.knex.put(
|
162
|
+
f"samples/dashboards/{self.uuid}/tiles",
|
115
163
|
json=post_data,
|
116
164
|
json_response=False,
|
117
165
|
)
|
118
166
|
|
167
|
+
def delete(self):
|
168
|
+
self.knex.delete(f"samples/{self.sample.uuid}/dashboards/{self.uuid}")
|
169
|
+
self._already_fetched = False
|
170
|
+
self._deleted = True
|
171
|
+
|
119
172
|
def _create(self):
|
120
|
-
post_data = {
|
121
|
-
|
173
|
+
post_data = {
|
174
|
+
"title": self.title,
|
175
|
+
"sample": self.sample.uuid,
|
176
|
+
"default": self.default,
|
177
|
+
}
|
178
|
+
blob = self.knex.post(f"samples/{self.sample.uuid}/dashboards", json=post_data)
|
122
179
|
self.load_blob(blob)
|
123
180
|
|
124
|
-
def
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
181
|
+
def _get_post_data(self):
|
182
|
+
out = {
|
183
|
+
"sample": self.sample.uuid,
|
184
|
+
"title": self.title,
|
185
|
+
"default": self.default,
|
186
|
+
}
|
187
|
+
return out
|
188
|
+
|
189
|
+
def add_tile(self, result_file, title, width="half", order=None):
|
130
190
|
result_file.get()
|
131
|
-
tile =
|
191
|
+
tile = SampleDashboardTile(
|
192
|
+
self.knex, self, title, result_file, width=width, order=order
|
193
|
+
)
|
132
194
|
self.tiles.append(tile)
|
133
195
|
self._modified = True
|
134
|
-
return tile
|
135
196
|
|
136
|
-
|
137
|
-
|
138
|
-
|
197
|
+
@classmethod
|
198
|
+
def from_blob(cls, sample, blob):
|
199
|
+
instance = cls(
|
200
|
+
sample.knex,
|
201
|
+
sample,
|
202
|
+
blob["title"],
|
203
|
+
blob["default"],
|
204
|
+
)
|
205
|
+
instance.uuid = blob["uuid"]
|
206
|
+
return instance
|
139
207
|
|
140
208
|
@property
|
141
209
|
def name(self):
|
142
|
-
return self.
|
210
|
+
return self.title
|
143
211
|
|
144
212
|
def __str__(self):
|
145
213
|
return f'<Geoseeq SampleDashboard: {self.sample.brn} "{self.name}"/>'
|
@@ -155,32 +223,35 @@ class SampleDashboard(RemoteObject):
|
|
155
223
|
return "DASH" + self.sample.uuid + self.name
|
156
224
|
|
157
225
|
|
158
|
-
class
|
159
|
-
|
160
|
-
def __init__(self, knex, dashboard, title, result_file, style="col-span-1"):
|
226
|
+
class SampleDashboardTile:
|
227
|
+
def __init__(self, knex, dashboard, title, result_file, width="half", order=None):
|
161
228
|
self.knex = knex
|
162
229
|
self.dashboard = dashboard
|
163
230
|
self.title = title
|
164
|
-
self.
|
231
|
+
self.width = width
|
165
232
|
self.result_file = result_file
|
233
|
+
self.order = order
|
166
234
|
|
167
235
|
def _get_post_data(self):
|
168
236
|
out = {
|
169
|
-
"
|
170
|
-
"
|
171
|
-
|
172
|
-
),
|
173
|
-
"style": self.style,
|
237
|
+
"field": self.result_file.uuid,
|
238
|
+
"dashboard": self.dashboard.uuid,
|
239
|
+
"width": self.width,
|
174
240
|
"title": self.title,
|
175
|
-
"
|
241
|
+
"order": self.order,
|
176
242
|
}
|
177
243
|
return out
|
178
244
|
|
179
245
|
@classmethod
|
180
246
|
def from_blob(cls, dashboard, blob):
|
181
|
-
result_file =
|
247
|
+
result_file = result_file_from_id(dashboard.knex, blob["field_obj"]["uuid"])
|
182
248
|
return cls(
|
183
|
-
dashboard.knex,
|
249
|
+
dashboard.knex,
|
250
|
+
dashboard,
|
251
|
+
blob["title"],
|
252
|
+
result_file,
|
253
|
+
width=blob["width"],
|
254
|
+
order=blob["order"],
|
184
255
|
)
|
185
256
|
|
186
257
|
def __str__(self) -> str:
|
geoseeq/sample.py
CHANGED
@@ -1,5 +1,4 @@
|
|
1
1
|
import urllib
|
2
|
-
|
3
2
|
from .remote_object import RemoteObject
|
4
3
|
from .result import SampleResultFile, SampleResultFolder
|
5
4
|
|
@@ -32,7 +31,7 @@ class Sample(RemoteObject):
|
|
32
31
|
|
33
32
|
@property
|
34
33
|
def brn(self):
|
35
|
-
return f
|
34
|
+
return f"brn:{self.knex.instance_code()}:sample:{self.uuid}"
|
36
35
|
|
37
36
|
def nested_url(self):
|
38
37
|
escaped_name = urllib.parse.quote(self.name, safe="")
|
@@ -63,15 +62,19 @@ class Sample(RemoteObject):
|
|
63
62
|
self.load_blob(blob, allow_overwrite=allow_overwrite)
|
64
63
|
|
65
64
|
def get_post_data(self):
|
66
|
-
data = {
|
65
|
+
data = {
|
66
|
+
field: getattr(self, field)
|
67
|
+
for field in self.remote_fields
|
68
|
+
if hasattr(self, field)
|
69
|
+
}
|
67
70
|
data["library"] = self.lib.uuid
|
68
71
|
if self.new_lib:
|
69
72
|
if isinstance(self.new_lib, RemoteObject):
|
70
73
|
data["library"] = self.new_lib.uuid
|
71
74
|
else:
|
72
75
|
data["library"] = self.new_lib
|
73
|
-
if data[
|
74
|
-
data.pop(
|
76
|
+
if data["uuid"] is None:
|
77
|
+
data.pop("uuid")
|
75
78
|
return data
|
76
79
|
|
77
80
|
def _create(self):
|
@@ -96,10 +99,10 @@ class Sample(RemoteObject):
|
|
96
99
|
|
97
100
|
def analysis_result(self, *args, **kwargs):
|
98
101
|
"""Return a SampleResultFolder for this sample.
|
99
|
-
|
102
|
+
|
100
103
|
This is an alias for result_folder."""
|
101
104
|
return self.result_folder(*args, **kwargs)
|
102
|
-
|
105
|
+
|
103
106
|
def get_result_folders(self, cache=True):
|
104
107
|
"""Yield sample analysis results fetched from the server."""
|
105
108
|
self.get()
|
@@ -107,7 +110,7 @@ class Sample(RemoteObject):
|
|
107
110
|
for ar in self._get_result_cache:
|
108
111
|
yield ar
|
109
112
|
return
|
110
|
-
url =
|
113
|
+
url = f"sample_ars?sample_id={self.uuid}"
|
111
114
|
result = self.knex.get(url)
|
112
115
|
for result_blob in result["results"]:
|
113
116
|
result = self.analysis_result(result_blob["module_name"])
|
@@ -126,7 +129,7 @@ class Sample(RemoteObject):
|
|
126
129
|
|
127
130
|
def get_analysis_results(self, cache=True):
|
128
131
|
"""Yield sample analysis results fetched from the server.
|
129
|
-
|
132
|
+
|
130
133
|
This is an alias for get_result_folders.
|
131
134
|
"""
|
132
135
|
return self.get_result_folders(cache=cache)
|
@@ -135,14 +138,15 @@ class Sample(RemoteObject):
|
|
135
138
|
"""Return a manifest for this sample."""
|
136
139
|
url = f"samples/{self.uuid}/manifest"
|
137
140
|
return self.knex.get(url)
|
138
|
-
|
141
|
+
|
139
142
|
def _grn_to_file(self, grn):
|
140
143
|
from geoseeq.id_constructors.from_blobs import sample_result_file_from_blob
|
144
|
+
|
141
145
|
file_uuid = grn.split(":")[-1]
|
142
146
|
file_blob = self.knex.get(f"sample_ar_fields/{file_uuid}")
|
143
147
|
file = sample_result_file_from_blob(self.knex, file_blob)
|
144
148
|
return file
|
145
|
-
|
149
|
+
|
146
150
|
def get_one_fastq(self):
|
147
151
|
"""Return a 2-ple, a fastq ResultFile and a string with the read type.
|
148
152
|
|
@@ -152,10 +156,10 @@ class Sample(RemoteObject):
|
|
152
156
|
blob = self.knex.get(url)
|
153
157
|
file = self._grn_to_file(blob["grn"])
|
154
158
|
return file, blob["read_type"]
|
155
|
-
|
159
|
+
|
156
160
|
def get_one_fastq_folder(self, preference_order=None):
|
157
161
|
"""Return a 3-ple, <read_type:str>, <folder_name:str>, a list with reads.
|
158
|
-
|
162
|
+
|
159
163
|
If the read type is paired end, the list will contain 2-ples with reads.
|
160
164
|
|
161
165
|
Default preference order is:
|
@@ -175,7 +179,7 @@ class Sample(RemoteObject):
|
|
175
179
|
for folder_name, reads in all_fastqs[read_type].items():
|
176
180
|
return read_type, folder_name, reads
|
177
181
|
raise ValueError("No suitable fastq found")
|
178
|
-
|
182
|
+
|
179
183
|
def get_all_fastqs(self):
|
180
184
|
"""Return a dict with the following structure:
|
181
185
|
|
@@ -217,7 +221,7 @@ class Sample(RemoteObject):
|
|
217
221
|
self._grn_to_file(file_grns[0])
|
218
222
|
)
|
219
223
|
return files
|
220
|
-
|
224
|
+
|
221
225
|
def get_one_fasta(self):
|
222
226
|
"""Return a 2-ple, a fasta ResultFile and a string with the read type.
|
223
227
|
|
@@ -228,6 +232,70 @@ class Sample(RemoteObject):
|
|
228
232
|
file = self._grn_to_file(blob["grn"])
|
229
233
|
return file, blob["read_type"]
|
230
234
|
|
235
|
+
def create_dashboard(self, title="Default dashboard", default=False):
|
236
|
+
from geoseeq.dashboard.dashboard import SampleDashboard
|
237
|
+
|
238
|
+
post_data = {
|
239
|
+
"title": title,
|
240
|
+
"sample": self.uuid,
|
241
|
+
"default": default,
|
242
|
+
}
|
243
|
+
blob = self.knex.post(f"samples/{self.uuid}/dashboards", json=post_data)
|
244
|
+
dashboard = SampleDashboard(
|
245
|
+
self.knex, self, title=blob["title"], default=blob["default"]
|
246
|
+
)
|
247
|
+
dashboard.uuid = blob["uuid"]
|
248
|
+
dashboard._already_fetched = True
|
249
|
+
return dashboard
|
250
|
+
|
251
|
+
def get_default_dashbaord(self):
|
252
|
+
from geoseeq.dashboard.dashboard import SampleDashboard
|
253
|
+
|
254
|
+
dashboard_resp = self.knex.get(f"samples/{self.uuid}/dashboards")
|
255
|
+
try:
|
256
|
+
blob = [
|
257
|
+
dashboard_blob
|
258
|
+
for dashboard_blob in dashboard_resp["results"]
|
259
|
+
if dashboard_blob["default"] == True
|
260
|
+
][0]
|
261
|
+
dashboard = SampleDashboard.from_blob(self, blob)
|
262
|
+
dashboard.get() # Tiles are not in the blob
|
263
|
+
return dashboard
|
264
|
+
except IndexError:
|
265
|
+
pass
|
266
|
+
return None
|
267
|
+
|
268
|
+
def get_or_create_default_dashbaord(self):
|
269
|
+
default_dashboard = self.get_default_dashbaord()
|
270
|
+
if default_dashboard:
|
271
|
+
return default_dashboard
|
272
|
+
else:
|
273
|
+
return self.create_dashboard(default=True)
|
274
|
+
|
275
|
+
def get_dashbaord_by_title(self, title):
|
276
|
+
from geoseeq.dashboard.dashboard import SampleDashboard
|
277
|
+
|
278
|
+
dashboard_resp = self.knex.get(f"samples/{self.uuid}/dashboards")
|
279
|
+
try:
|
280
|
+
blob = [
|
281
|
+
dashboard_blob
|
282
|
+
for dashboard_blob in dashboard_resp["results"]
|
283
|
+
if dashboard_blob["title"] == title
|
284
|
+
][0]
|
285
|
+
dashboard = SampleDashboard.from_blob(self, blob)
|
286
|
+
dashboard.get() # Tiles are not in the blob
|
287
|
+
return dashboard
|
288
|
+
except IndexError:
|
289
|
+
pass
|
290
|
+
return None
|
291
|
+
|
292
|
+
def get_or_create_dashbaord_by_title(self, title):
|
293
|
+
default_dashboard = self.get_dashbaord_by_title(title)
|
294
|
+
if default_dashboard:
|
295
|
+
return default_dashboard
|
296
|
+
else:
|
297
|
+
return self.create_dashboard(title=title)
|
298
|
+
|
231
299
|
def __str__(self):
|
232
300
|
return f"<Geoseeq::Sample {self.name} {self.uuid} />"
|
233
301
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: geoseeq
|
3
|
-
Version: 0.7.3.
|
3
|
+
Version: 0.7.3.dev1
|
4
4
|
Summary: GeoSeeq command line tools and python API
|
5
5
|
Project-URL: Homepage, https://github.com/biotia/geoseeq_api_client
|
6
6
|
Project-URL: Issues, https://github.com/biotia/geoseeq_api_client/issues
|
@@ -9,7 +9,7 @@ geoseeq/organization.py,sha256=bJkYL8_D-k6IYAaii2ZbxjwYnXy6lvu6iLXscxKlA3w,2542
|
|
9
9
|
geoseeq/pipeline.py,sha256=89mhWaecsKnm6tyRkdkaVp4dmZh62_v42Ze0oXf8OTY,9873
|
10
10
|
geoseeq/project.py,sha256=_8uttLKc6yVGDdJhPRmQOzzPeMYmUg0zBBPOAJc3yyo,14014
|
11
11
|
geoseeq/remote_object.py,sha256=GYN6PKU7Zz3htIdpFjfZiFejzGqqJHbJyKlefM1Eixk,7151
|
12
|
-
geoseeq/sample.py,sha256=
|
12
|
+
geoseeq/sample.py,sha256=2UmS1636bpPZbydv4PePNw-zXUs70Igt-6RTRZiCg5Q,10563
|
13
13
|
geoseeq/search.py,sha256=gawad6Cx5FxJBPlYkXWb-UKAO-UC0_yhvyU9Ca1kaNI,3388
|
14
14
|
geoseeq/smart_table.py,sha256=rihMsFUIn-vn4w6ukVZTHI9bjDSEr8xHExBfX8mwCHM,6169
|
15
15
|
geoseeq/smart_tree.py,sha256=bSjDlwmOuNXutYJhytA1RovwRCHV6ZxXXJPiIGFhPaA,1825
|
@@ -52,7 +52,7 @@ geoseeq/contrib/ncbi/api.py,sha256=WQeLoGA_-Zha-QeSO8_i7HpvXyD8UkV0qc5okm11KiA,1
|
|
52
52
|
geoseeq/contrib/ncbi/bioproject.py,sha256=_oThTd_iLDOC8cLOlJKAatSr362OBYZCEV3YrqodhFg,4341
|
53
53
|
geoseeq/contrib/ncbi/cli.py,sha256=j9zEcaZPTryK3a4xluRxigcJKDhRpRxbp3KZSx-Bfhk,2400
|
54
54
|
geoseeq/contrib/ncbi/setup_logging.py,sha256=Tp1bY1U0f-o739aHpvVYriG2qdd1lFvCYBXZeXQgt-w,175
|
55
|
-
geoseeq/dashboard/dashboard.py,sha256=
|
55
|
+
geoseeq/dashboard/dashboard.py,sha256=nradpMc6UscnekppJaRh4od2XZ_t601UX3uWrB1StK8,7736
|
56
56
|
geoseeq/file_system/filesystem_download.py,sha256=8bcnxjWltekmCvb5N0b1guBIjLp4-CL2VtsEok-snv4,16963
|
57
57
|
geoseeq/file_system/main.py,sha256=4HgYGq7WhlF96JlVIf16iFBTDujlBpxImmtoh4VCzDA,3627
|
58
58
|
geoseeq/id_constructors/__init__.py,sha256=w5E0PNQ9UuAxBeZbDI7KBnUoERd85gGz3nScz45bd2o,126
|
@@ -92,8 +92,8 @@ geoseeq/vc/vc_cache.py,sha256=P4LXTbq2zOIv1OhP7Iw5MmypR2vXuy29Pq5K6gRvi-M,730
|
|
92
92
|
geoseeq/vc/vc_dir.py,sha256=A9CLTh2wWCRzZjiLyqXD1vhtsWZGD3OjaMT5KqlfAXI,457
|
93
93
|
geoseeq/vc/vc_sample.py,sha256=qZeioWydXvfu4rGMs20nICfNcp46y_XkND-bHdV6P5M,3850
|
94
94
|
geoseeq/vc/vc_stub.py,sha256=IQr8dI0zsWKVAeY_5ybDD6n49_3othcgfHS3P0O9tuY,3110
|
95
|
-
geoseeq-0.7.3.
|
96
|
-
geoseeq-0.7.3.
|
97
|
-
geoseeq-0.7.3.
|
98
|
-
geoseeq-0.7.3.
|
99
|
-
geoseeq-0.7.3.
|
95
|
+
geoseeq-0.7.3.dev1.dist-info/METADATA,sha256=7cN6ntZDU2NtQYiUAB3D12fTbm_fShr46JxHwV5qF4A,5415
|
96
|
+
geoseeq-0.7.3.dev1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
97
|
+
geoseeq-0.7.3.dev1.dist-info/entry_points.txt,sha256=yF-6KDM8zXib4Al0qn49TX-qM7PUkWUIcYtsgt36rjM,45
|
98
|
+
geoseeq-0.7.3.dev1.dist-info/licenses/LICENSE,sha256=IuhIl1XCxXLPLJT_coN1CNqQU4Khlq7x4IdW7ioOJD8,1067
|
99
|
+
geoseeq-0.7.3.dev1.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|