geoseeq 0.7.0__py3-none-any.whl → 0.7.2__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/cli/main.py +1 -1
- geoseeq/cli/upload/upload.py +2 -2
- geoseeq/cli/upload/upload_reads.py +1 -1
- geoseeq/dashboard/dashboard.py +103 -0
- geoseeq/file_system/filesystem_download.py +434 -0
- geoseeq/file_system/main.py +122 -0
- geoseeq/plotting/README.md +4 -0
- geoseeq/vc/README.md +48 -0
- {geoseeq-0.7.0.dist-info → geoseeq-0.7.2.dist-info}/METADATA +12 -10
- {geoseeq-0.7.0.dist-info → geoseeq-0.7.2.dist-info}/RECORD +14 -12
- {geoseeq-0.7.0.dist-info → geoseeq-0.7.2.dist-info}/WHEEL +1 -2
- geoseeq-0.7.0.dist-info/top_level.txt +0 -2
- tests/test_api_client.py +0 -283
- tests/test_plotting.py +0 -29
- /tests/__init__.py → /geoseeq/contrib/ncbi/README.md +0 -0
- {geoseeq-0.7.0.dist-info → geoseeq-0.7.2.dist-info}/entry_points.txt +0 -0
- {geoseeq-0.7.0.dist-info → geoseeq-0.7.2.dist-info}/licenses/LICENSE +0 -0
geoseeq/cli/main.py
CHANGED
@@ -55,7 +55,7 @@ def version():
|
|
55
55
|
Use of this tool implies acceptance of the GeoSeeq End User License Agreement.
|
56
56
|
Run `geoseeq eula show` to view the EULA.
|
57
57
|
"""
|
58
|
-
click.echo('0.7.
|
58
|
+
click.echo('0.7.2') # remember to update pyproject.toml
|
59
59
|
|
60
60
|
|
61
61
|
@main.group('advanced')
|
geoseeq/cli/upload/upload.py
CHANGED
@@ -128,9 +128,9 @@ def cli_upload_file(state, cores, threads_per_upload, num_retries, chunk_size_mb
|
|
128
128
|
)
|
129
129
|
for geoseeq_file_name, file_path in name_pairs:
|
130
130
|
if isfile(file_path):
|
131
|
-
upload_manager.add_local_file_to_result_folder(result_folder, file_path)
|
131
|
+
upload_manager.add_local_file_to_result_folder(result_folder, file_path, geoseeq_file_name=geoseeq_file_name)
|
132
132
|
elif isdir(file_path) and recursive:
|
133
|
-
upload_manager.add_local_folder_to_result_folder(result_folder, file_path, recursive=recursive, hidden_files=hidden, prefix=file_path)
|
133
|
+
upload_manager.add_local_folder_to_result_folder(result_folder, file_path, recursive=recursive, hidden_files=hidden, prefix=file_path, geoseeq_file_name=geoseeq_file_name)
|
134
134
|
elif isdir(file_path) and not recursive:
|
135
135
|
raise click.UsageError('Cannot upload a folder without --recursive')
|
136
136
|
click.echo(upload_manager.get_preview_string(), err=True)
|
@@ -238,7 +238,7 @@ def cli_upload_reads_wizard(state, cores, overwrite, yes, regex, private, link_t
|
|
238
238
|
filepaths = {basename(line): line for line in flatten_list_of_fastxs(fastq_files)}
|
239
239
|
click.echo(f'Found {len(filepaths)} files to upload.', err=True)
|
240
240
|
regex = _get_regex(knex, filepaths, module_name, proj, regex)
|
241
|
-
groups = _group_files(knex, filepaths, module_name, regex, yes)
|
241
|
+
groups = _group_files(knex, filepaths, module_name, regex, yes, name_map)
|
242
242
|
_do_upload(groups, module_name, link_type, proj, filepaths, overwrite, no_new_versions, cores, state)
|
243
243
|
|
244
244
|
|
@@ -0,0 +1,103 @@
|
|
1
|
+
|
2
|
+
import logging
|
3
|
+
import json
|
4
|
+
from typing import Literal
|
5
|
+
from geoseeq.remote_object import RemoteObject
|
6
|
+
from geoseeq.id_constructors import result_file_from_blob
|
7
|
+
from geoseeq import ProjectResultFile
|
8
|
+
|
9
|
+
logger = logging.getLogger("geoseeq_api")
|
10
|
+
|
11
|
+
|
12
|
+
class Dashboard(RemoteObject):
|
13
|
+
parent_field = "project"
|
14
|
+
remote_fields = ["is_default"]
|
15
|
+
|
16
|
+
def __init__(self, knex, project, name="Default dashboard", is_default=False):
|
17
|
+
super().__init__(self)
|
18
|
+
self.knex = knex
|
19
|
+
self.project = project
|
20
|
+
self._name = name
|
21
|
+
self.tiles = []
|
22
|
+
self.is_default = is_default
|
23
|
+
|
24
|
+
def _get(self, allow_overwrite=False):
|
25
|
+
blob = self.knex.get(f"sample_groups/{self.project.uuid}/dashboard-list")
|
26
|
+
blob = blob["dashboard_data"][self.name]
|
27
|
+
for tile_blob in blob["tiles"]:
|
28
|
+
tile = DashboardTile.from_blob(self, tile_blob)
|
29
|
+
self.tiles.append(tile)
|
30
|
+
blob.pop("tiles")
|
31
|
+
self.load_blob(blob, allow_overwrite=allow_overwrite)
|
32
|
+
|
33
|
+
def _save(self):
|
34
|
+
self.save_tiles()
|
35
|
+
|
36
|
+
def save_tiles(self):
|
37
|
+
post_data = {"tiles": [tile._get_post_data() for tile in self.tiles]}
|
38
|
+
blob = self.knex.post(f"sample_groups/{self.project.uuid}/dashboard/{self.name}/tiles", json=post_data, json_response=False)
|
39
|
+
print(blob)
|
40
|
+
|
41
|
+
def _create(self):
|
42
|
+
post_data = {"name": self.name, "is_default": self.is_default}
|
43
|
+
blob = self.knex.post(f"sample_groups/{self.project.uuid}/dashboard", json=post_data)
|
44
|
+
self.load_blob(blob)
|
45
|
+
|
46
|
+
def tile(self, title, result_file, style: Literal["col-span-1", "col-span-2"]="col-span-1"):
|
47
|
+
result_file.get()
|
48
|
+
tile = DashboardTile(self.knex, self, title, result_file, style=style)
|
49
|
+
self.tiles.append(tile)
|
50
|
+
self._modified = True
|
51
|
+
return tile
|
52
|
+
|
53
|
+
def add_tile(self, tile):
|
54
|
+
self.tiles.append(tile)
|
55
|
+
self._modified = True
|
56
|
+
|
57
|
+
@property
|
58
|
+
def name(self):
|
59
|
+
return self._name
|
60
|
+
|
61
|
+
def __str__(self):
|
62
|
+
return f"<Geoseeq Dashboard: {self.project.grn} \"{self.name}\"/>"
|
63
|
+
|
64
|
+
def __repr__(self):
|
65
|
+
return str(self)
|
66
|
+
|
67
|
+
@property
|
68
|
+
def grn(self):
|
69
|
+
return f"grn:dashboard:{self.project.uuid}:\"{self.name}\""
|
70
|
+
|
71
|
+
def pre_hash(self):
|
72
|
+
return "DASH" + self.project.uuid + self.name
|
73
|
+
|
74
|
+
|
75
|
+
class DashboardTile:
|
76
|
+
|
77
|
+
def __init__(self, knex, dashboard, title, result_file, style="col-span-1"):
|
78
|
+
self.knex = knex
|
79
|
+
self.dashboard = dashboard
|
80
|
+
self.title = title
|
81
|
+
self.style = style
|
82
|
+
self.result_file = result_file
|
83
|
+
|
84
|
+
def _get_post_data(self):
|
85
|
+
out = {
|
86
|
+
"field_uuid": self.result_file.uuid,
|
87
|
+
"field_type": "group" if isinstance(self.result_file, ProjectResultFile) else "sample",
|
88
|
+
"style": self.style,
|
89
|
+
"title": self.title,
|
90
|
+
"has_related_field": False,
|
91
|
+
}
|
92
|
+
return out
|
93
|
+
|
94
|
+
@classmethod
|
95
|
+
def from_blob(cls, dashboard, blob):
|
96
|
+
result_file = result_file_from_blob(blob["viz_field"])
|
97
|
+
return cls(dashboard.knex, dashboard, blob["title"], result_file, style=blob["style"])
|
98
|
+
|
99
|
+
def __str__(self) -> str:
|
100
|
+
return f"<Geoseeq DashboardTile: {self.dashboard.grn} \"{self.title}\" />"
|
101
|
+
|
102
|
+
def __repr__(self) -> str:
|
103
|
+
return str(self)
|
@@ -0,0 +1,434 @@
|
|
1
|
+
|
2
|
+
import os
|
3
|
+
import json
|
4
|
+
from geoseeq import (
|
5
|
+
result_file_from_id,
|
6
|
+
result_folder_from_id,
|
7
|
+
sample_from_id,
|
8
|
+
project_from_id,
|
9
|
+
)
|
10
|
+
from geoseeq.utils import md5_checksum
|
11
|
+
from time import time
|
12
|
+
|
13
|
+
FILE_STATUS_MODIFIED_REMOTE = 'MODIFIED_REMOTE'
|
14
|
+
FILE_STATUS_MODIFIED_LOCAL = 'MODIFIED_LOCAL'
|
15
|
+
FILE_STATUS_NEW_LOCAL = 'NEW_LOCAL'
|
16
|
+
FILE_STATUS_NEW_REMOTE = 'NEW_REMOTE'
|
17
|
+
FILE_STATUS_IS_LOCAL_STUB = 'IS_LOCAL_STUB'
|
18
|
+
|
19
|
+
|
20
|
+
def dedupe_modified_files(modified_files):
|
21
|
+
"""Remove duplicates from a list of modified files.
|
22
|
+
|
23
|
+
This function will remove duplicates from a list of modified files
|
24
|
+
based on the path to the file. The first instance of the file will be
|
25
|
+
kept and all others will be removed.
|
26
|
+
"""
|
27
|
+
seen = set()
|
28
|
+
deduped = []
|
29
|
+
for x in modified_files:
|
30
|
+
if x[2] not in seen:
|
31
|
+
deduped.append(x)
|
32
|
+
seen.add(x[2])
|
33
|
+
return deduped
|
34
|
+
|
35
|
+
|
36
|
+
class ResultFileOnFilesystem:
|
37
|
+
"""
|
38
|
+
|
39
|
+
Note: unlike other filesystem classes the `path` is a file, not
|
40
|
+
a directory. This is because the file is downloaded directly to
|
41
|
+
the path.
|
42
|
+
"""
|
43
|
+
|
44
|
+
def __init__(self, result_file, path, kind):
|
45
|
+
self.result_file = result_file
|
46
|
+
self.path = path
|
47
|
+
self.kind = kind
|
48
|
+
|
49
|
+
@property
|
50
|
+
def info_filepath(self):
|
51
|
+
dirpath = os.path.dirname(self.path)
|
52
|
+
basename = os.path.basename(self.path)
|
53
|
+
return os.path.join(dirpath, f'.gs_result_file__{basename}')
|
54
|
+
|
55
|
+
@property
|
56
|
+
def is_stub(self):
|
57
|
+
return os.path.exists(self.path) and os.path.getsize(self.path) == 0
|
58
|
+
|
59
|
+
def file_is_ok(self, stubs_are_ok=False):
|
60
|
+
if self.is_stub:
|
61
|
+
return stubs_are_ok
|
62
|
+
return self.result_file.download_needs_update(self.path)
|
63
|
+
|
64
|
+
def download(self, use_stubs=False, exists_ok=False):
|
65
|
+
if os.path.exists(self.info_filepath):
|
66
|
+
if exists_ok and self.file_is_ok(stubs_are_ok=use_stubs):
|
67
|
+
return
|
68
|
+
elif not exists_ok:
|
69
|
+
raise ValueError('Result file already exists at path: {}'.format(self.info_filepath))
|
70
|
+
|
71
|
+
# Download the file
|
72
|
+
if use_stubs:
|
73
|
+
open(self.path, 'w').close()
|
74
|
+
else:
|
75
|
+
self.result_file.download(self.path)
|
76
|
+
|
77
|
+
self.write_info_file()
|
78
|
+
|
79
|
+
def local_file_checksum(self):
|
80
|
+
if self.is_stub:
|
81
|
+
return "__STUB__"
|
82
|
+
return md5_checksum(self.path)
|
83
|
+
|
84
|
+
def locally_modified(self):
|
85
|
+
raise NotImplementedError('This function is not implemented')
|
86
|
+
|
87
|
+
def status_is_ok(self, stubs_are_ok=False):
|
88
|
+
# check for an info file
|
89
|
+
if not os.path.exists(self.info_filepath):
|
90
|
+
return False
|
91
|
+
if stubs_are_ok:
|
92
|
+
return True
|
93
|
+
return not self.result_file.download_needs_update(self.path)
|
94
|
+
|
95
|
+
def write_info_file(self):
|
96
|
+
result_file_info = {
|
97
|
+
"uuid": self.result_file.uuid,
|
98
|
+
"kind": self.kind,
|
99
|
+
"checksum": self.local_file_checksum(),
|
100
|
+
}
|
101
|
+
with open(self.info_filepath, 'w') as f:
|
102
|
+
json.dump(result_file_info, f)
|
103
|
+
|
104
|
+
@classmethod
|
105
|
+
def from_path(cls, path):
|
106
|
+
obj = cls(None, path, None)
|
107
|
+
try:
|
108
|
+
with open(obj.info_filepath, 'r') as f:
|
109
|
+
result_file_info = json.load(f)
|
110
|
+
obj.result_file = result_file_from_id(result_file_info['uuid'])
|
111
|
+
obj.kind = result_file_info['kind']
|
112
|
+
obj.stored_checksum = result_file_info['checksum']
|
113
|
+
except FileNotFoundError:
|
114
|
+
pass
|
115
|
+
return obj
|
116
|
+
|
117
|
+
def write_info_file(self):
|
118
|
+
result_file_info = {
|
119
|
+
"uuid": self.result_file.uuid,
|
120
|
+
"kind": self.kind,
|
121
|
+
"checksum": self.local_file_checksum(),
|
122
|
+
}
|
123
|
+
with open(self.info_filepath, 'w') as f:
|
124
|
+
json.dump(result_file_info, f)
|
125
|
+
|
126
|
+
def list_abnormal_objects(self):
|
127
|
+
"""Return a list of files that have been modified.
|
128
|
+
|
129
|
+
Since this class is a single file the list will either be empty
|
130
|
+
or have one element.
|
131
|
+
|
132
|
+
Note that if a file was modified locally then uploaded to the server
|
133
|
+
the file will be marked as modified remote.
|
134
|
+
"""
|
135
|
+
if self.result_file is None:
|
136
|
+
return [('FILE', FILE_STATUS_NEW_LOCAL, self.path, None)]
|
137
|
+
if not os.path.exists(self.path):
|
138
|
+
return [('FILE', FILE_STATUS_NEW_REMOTE, self.path, self.result_file)]
|
139
|
+
if self.is_stub:
|
140
|
+
return [('FILE', FILE_STATUS_IS_LOCAL_STUB, self.path, self.result_file)]
|
141
|
+
if self.result_file and self.result_file.download_needs_update(self.path):
|
142
|
+
return [('FILE', FILE_STATUS_MODIFIED_REMOTE, self.path, self.result_file)]
|
143
|
+
if self.locally_modified():
|
144
|
+
return [('FILE', FILE_STATUS_MODIFIED_LOCAL, self.path, self.result_file)]
|
145
|
+
|
146
|
+
return []
|
147
|
+
|
148
|
+
|
149
|
+
class ResultFolderOnFilesystem:
|
150
|
+
|
151
|
+
def __init__(self, result_folder, path, kind):
|
152
|
+
self.result_folder = result_folder
|
153
|
+
self.path = path
|
154
|
+
self.kind = kind
|
155
|
+
|
156
|
+
@property
|
157
|
+
def info_filepath(self):
|
158
|
+
return os.path.join(self.path, '.gs_result_folder')
|
159
|
+
|
160
|
+
def download(self, use_stubs=False, exists_ok=False):
|
161
|
+
if os.path.exists(self.info_filepath) and not exists_ok:
|
162
|
+
raise ValueError('Result folder already exists at path: {}'.format(self.info_filepath))
|
163
|
+
|
164
|
+
# Download the files in the result folder
|
165
|
+
for result_file in self.result_folder.get_fields():
|
166
|
+
result_file_local_path = os.path.join(self.path, result_file.name)
|
167
|
+
os.makedirs(os.path.dirname(result_file_local_path), exist_ok=True)
|
168
|
+
ResultFileOnFilesystem(result_file, result_file_local_path, self.kind)\
|
169
|
+
.download(use_stubs=use_stubs, exists_ok=exists_ok)
|
170
|
+
|
171
|
+
# Write the result folder data
|
172
|
+
result_folder_info = {
|
173
|
+
"uuid": self.result_folder.uuid,
|
174
|
+
"kind": self.kind
|
175
|
+
}
|
176
|
+
with open(self.info_filepath, 'w') as f:
|
177
|
+
json.dump(result_folder_info, f)
|
178
|
+
|
179
|
+
def status_is_ok(self):
|
180
|
+
# check for an info file
|
181
|
+
if not os.path.exists(self.info_filepath):
|
182
|
+
return False
|
183
|
+
|
184
|
+
# check that all files are downloaded
|
185
|
+
for result_file in self.result_folder.get_files():
|
186
|
+
result_file_path = os.path.join(self.path, result_file.name)
|
187
|
+
if not os.path.exists(result_file_path):
|
188
|
+
return False
|
189
|
+
|
190
|
+
return True
|
191
|
+
|
192
|
+
@classmethod
|
193
|
+
def from_path(cls, path):
|
194
|
+
obj = cls(None, path, None)
|
195
|
+
try:
|
196
|
+
with open(os.path.join(path, '.gs_result_folder'), 'r') as f:
|
197
|
+
result_folder_info = json.load(f)
|
198
|
+
obj.result_folder = result_folder_from_id(result_folder_info['uuid'])
|
199
|
+
obj.kind = result_folder_info['kind']
|
200
|
+
except FileNotFoundError:
|
201
|
+
pass
|
202
|
+
return obj
|
203
|
+
|
204
|
+
def list_abnormal_objects(self):
|
205
|
+
"""Return a list of files that have been modified.
|
206
|
+
|
207
|
+
This function will return a list of tuples where the first element
|
208
|
+
is the status of the file and the second element is the path to the file.
|
209
|
+
"""
|
210
|
+
modified_files = []
|
211
|
+
if not self.result_folder:
|
212
|
+
modified_files.append(('FOLDER', FILE_STATUS_NEW_LOCAL, self.path, None))
|
213
|
+
if not os.path.exists(self.path):
|
214
|
+
modified_files.append(('FOLDER', FILE_STATUS_NEW_REMOTE, self.path, self.result_folder))
|
215
|
+
|
216
|
+
# list local files
|
217
|
+
if os.path.exists(self.path):
|
218
|
+
for local_file in os.listdir(self.path):
|
219
|
+
if local_file.startswith('.gs_'):
|
220
|
+
continue
|
221
|
+
local_file_path = os.path.join(self.path, local_file)
|
222
|
+
result_file_on_fs = ResultFileOnFilesystem.from_path(local_file_path)
|
223
|
+
modified_files.extend(result_file_on_fs.list_abnormal_objects())
|
224
|
+
|
225
|
+
# list remote files
|
226
|
+
if self.result_folder:
|
227
|
+
for result_file in self.result_folder.get_fields():
|
228
|
+
result_file_path = os.path.join(self.path, result_file.name)
|
229
|
+
result_file_on_fs = ResultFileOnFilesystem(result_file, result_file_path, self.kind)
|
230
|
+
modified_files.extend(result_file_on_fs.list_abnormal_objects())
|
231
|
+
|
232
|
+
return dedupe_modified_files(modified_files)
|
233
|
+
|
234
|
+
|
235
|
+
class SampleOnFilesystem:
|
236
|
+
|
237
|
+
def __init__(self, sample, path):
|
238
|
+
self.sample = sample
|
239
|
+
self.path = path if path[-1] != '/' else path[:-1] # remove trailing slash
|
240
|
+
|
241
|
+
@property
|
242
|
+
def info_filepath(self):
|
243
|
+
return os.path.join(self.path, '.gs_sample')
|
244
|
+
|
245
|
+
def download(self, use_stubs=False, exists_ok=False):
|
246
|
+
if os.path.exists(self.info_filepath) and not exists_ok:
|
247
|
+
raise ValueError('Sample already exists at path: {}'.format(self.info_filepath))
|
248
|
+
|
249
|
+
# download result folders
|
250
|
+
for result_folder in self.sample.get_result_folders():
|
251
|
+
result_folder_local_path = os.path.join(self.path, result_folder.name)
|
252
|
+
os.makedirs(result_folder_local_path, exist_ok=True)
|
253
|
+
ResultFolderOnFilesystem(result_folder, result_folder_local_path, "sample")\
|
254
|
+
.download(use_stubs=use_stubs, exists_ok=exists_ok)
|
255
|
+
|
256
|
+
# Write the sample data
|
257
|
+
sample_info = {
|
258
|
+
"uuid": self.sample.uuid
|
259
|
+
}
|
260
|
+
with open(self.info_filepath, 'w') as f:
|
261
|
+
json.dump(sample_info, f)
|
262
|
+
|
263
|
+
def status_is_ok(self):
|
264
|
+
# check for an info file
|
265
|
+
if not os.path.exists(self.info_filepath):
|
266
|
+
return False
|
267
|
+
|
268
|
+
# check that all result folders are downloaded
|
269
|
+
for result_folder in self.sample.get_result_folders():
|
270
|
+
result_folder_local_path = os.path.join(self.path, result_folder.name)
|
271
|
+
result_folder_on_fs = ResultFolderOnFilesystem.from_path(result_folder_local_path, "sample")
|
272
|
+
if not result_folder_on_fs.status_is_ok():
|
273
|
+
return False
|
274
|
+
|
275
|
+
return True
|
276
|
+
|
277
|
+
@classmethod
|
278
|
+
def from_path(cls, path):
|
279
|
+
obj = cls(None, path)
|
280
|
+
try:
|
281
|
+
with open(os.path.join(path, '.gs_sample'), 'r') as f:
|
282
|
+
sample_info = json.load(f)
|
283
|
+
obj.sample = sample_from_id(sample_info['uuid'])
|
284
|
+
except FileNotFoundError:
|
285
|
+
pass
|
286
|
+
return obj
|
287
|
+
|
288
|
+
def list_abnormal_objects(self):
|
289
|
+
"""Return a list of files that have been modified.
|
290
|
+
|
291
|
+
This function will return a list of tuples where the first element
|
292
|
+
is the status of the file and the second element is the path to the file.
|
293
|
+
"""
|
294
|
+
modified_files = []
|
295
|
+
if not self.sample:
|
296
|
+
modified_files.append(('SAMPLE', FILE_STATUS_NEW_LOCAL, self.path, None))
|
297
|
+
if not os.path.exists(self.path):
|
298
|
+
modified_files.append(('SAMPLE', FILE_STATUS_NEW_REMOTE, self.path, self.sample))
|
299
|
+
|
300
|
+
# list local folders
|
301
|
+
if os.path.exists(self.path):
|
302
|
+
for local_folder in os.listdir(self.path):
|
303
|
+
local_folder_path = os.path.join(self.path, local_folder)
|
304
|
+
if not os.path.isdir(local_folder_path):
|
305
|
+
continue
|
306
|
+
result_folder_on_fs = ResultFolderOnFilesystem.from_path(local_folder_path)
|
307
|
+
modified_files.extend(result_folder_on_fs.list_abnormal_objects())
|
308
|
+
|
309
|
+
# list remote folders
|
310
|
+
if self.sample:
|
311
|
+
for result_folder in self.sample.get_result_folders():
|
312
|
+
result_folder_path = os.path.join(self.path, result_folder.name)
|
313
|
+
result_folder_on_fs = ResultFolderOnFilesystem(result_folder, result_folder_path, "sample")
|
314
|
+
modified_files.extend(result_folder_on_fs.list_abnormal_objects())
|
315
|
+
|
316
|
+
return dedupe_modified_files(modified_files)
|
317
|
+
|
318
|
+
|
319
|
+
class ProjectOnFilesystem:
|
320
|
+
|
321
|
+
def __init__(self, project, path):
|
322
|
+
self.project = project
|
323
|
+
self.path = path
|
324
|
+
|
325
|
+
@property
|
326
|
+
def info_filepath(self):
|
327
|
+
return os.path.join(self.path, '.gs_project')
|
328
|
+
|
329
|
+
def download(self, use_stubs=False, exists_ok=False):
|
330
|
+
if os.path.exists(self.info_filepath) and not exists_ok:
|
331
|
+
raise ValueError('Project already exists at path: {}'.format(self.info_filepath))
|
332
|
+
|
333
|
+
# download samples
|
334
|
+
for sample in self.project.get_samples():
|
335
|
+
sample_local_path = os.path.join(self.path, "sample_results", sample.name)
|
336
|
+
os.makedirs(sample_local_path, exist_ok=True)
|
337
|
+
SampleOnFilesystem(sample, sample_local_path)\
|
338
|
+
.download(use_stubs=use_stubs, exists_ok=exists_ok)
|
339
|
+
|
340
|
+
# download project result folders
|
341
|
+
for result_folder in self.project.get_result_folders():
|
342
|
+
result_folder_local_path = os.path.join(self.path, "project_results", result_folder.name)
|
343
|
+
os.makedirs(result_folder_local_path, exist_ok=True)
|
344
|
+
ResultFolderOnFilesystem(result_folder, result_folder_local_path, "project")\
|
345
|
+
.download(use_stubs=use_stubs, exists_ok=exists_ok)
|
346
|
+
|
347
|
+
# Write the project data
|
348
|
+
project_info = {
|
349
|
+
"uuid": self.project.uuid
|
350
|
+
}
|
351
|
+
with open(self.info_filepath, 'w') as f:
|
352
|
+
json.dump(project_info, f)
|
353
|
+
|
354
|
+
def status_is_ok(self):
|
355
|
+
# check for an info file
|
356
|
+
if not os.path.exists(self.info_filepath):
|
357
|
+
return False
|
358
|
+
|
359
|
+
# check that all samples are downloaded
|
360
|
+
for sample in self.project.get_samples():
|
361
|
+
sample_local_path = os.path.join(self.path, "sample_results", sample.name)
|
362
|
+
sample_on_fs = SampleOnFilesystem.from_path(sample_local_path)
|
363
|
+
if not sample_on_fs.status_is_ok():
|
364
|
+
return False
|
365
|
+
|
366
|
+
# check that all project result folders are downloaded
|
367
|
+
for result_folder in self.project.get_result_folders():
|
368
|
+
result_folder_local_path = os.path.join(self.path, "project_results", result_folder.name)
|
369
|
+
result_folder_on_fs = ResultFolderOnFilesystem.from_path(result_folder_local_path, "project")
|
370
|
+
if not result_folder_on_fs.status_is_ok():
|
371
|
+
return False
|
372
|
+
|
373
|
+
return True
|
374
|
+
|
375
|
+
@classmethod
|
376
|
+
def from_path(cls, path, recursive=False):
|
377
|
+
try:
|
378
|
+
with open(os.path.join(path, '.gs_project'), 'r') as f:
|
379
|
+
project_info = json.load(f)
|
380
|
+
project = project_from_id(project_info['uuid'])
|
381
|
+
return cls(project, path)
|
382
|
+
except FileNotFoundError:
|
383
|
+
if not recursive:
|
384
|
+
raise ValueError('No project found in path or parent directories')
|
385
|
+
updir = os.path.dirname(os.path.abspath(path))
|
386
|
+
if updir == path:
|
387
|
+
raise ValueError('No project found in path or parent directories')
|
388
|
+
return cls.from_path(updir, recursive=recursive)
|
389
|
+
|
390
|
+
def path_from_project_root(self, path):
|
391
|
+
if path[0] == "/":
|
392
|
+
return path.replace(self.path, "")[1:]
|
393
|
+
return path
|
394
|
+
|
395
|
+
def list_abnormal_objects(self):
|
396
|
+
"""Return a list of files that have been modified.
|
397
|
+
|
398
|
+
This function will return a list of tuples where the first element
|
399
|
+
is the status of the file and the second element is the path to the file.
|
400
|
+
"""
|
401
|
+
modified_files = []
|
402
|
+
|
403
|
+
# list remote samples
|
404
|
+
for sample in self.project.get_samples():
|
405
|
+
sample_path = os.path.join(self.path, "sample_results", sample.name)
|
406
|
+
sample_on_fs = SampleOnFilesystem(sample, sample_path)
|
407
|
+
modified_files.extend(sample_on_fs.list_abnormal_objects())
|
408
|
+
|
409
|
+
# list remote project result folders
|
410
|
+
for result_folder in self.project.get_result_folders():
|
411
|
+
result_folder_path = os.path.join(self.path, "project_results", result_folder.name)
|
412
|
+
|
413
|
+
result_folder_on_fs = ResultFolderOnFilesystem(result_folder, result_folder_path, "project")
|
414
|
+
modified_files.extend(result_folder_on_fs.list_abnormal_objects())
|
415
|
+
|
416
|
+
# list local samples
|
417
|
+
for local_sample in os.listdir(os.path.join(self.path, "sample_results")):
|
418
|
+
local_sample_path = os.path.join(self.path, "sample_results", local_sample)
|
419
|
+
if not os.path.isdir(local_sample_path):
|
420
|
+
continue
|
421
|
+
sample_on_fs = SampleOnFilesystem.from_path(local_sample_path)
|
422
|
+
modified_files.extend(sample_on_fs.list_abnormal_objects())
|
423
|
+
|
424
|
+
# list local project result folders
|
425
|
+
for local_result_folder in os.listdir(os.path.join(self.path, "project_results")):
|
426
|
+
local_result_folder_path = os.path.join(self.path, "project_results", local_result_folder)
|
427
|
+
if not os.path.isdir(local_result_folder_path):
|
428
|
+
continue
|
429
|
+
result_folder_on_fs = ResultFolderOnFilesystem.from_path(local_result_folder_path)
|
430
|
+
modified_files.extend(result_folder_on_fs.list_abnormal_objects())
|
431
|
+
return dedupe_modified_files(modified_files)
|
432
|
+
|
433
|
+
|
434
|
+
|
@@ -0,0 +1,122 @@
|
|
1
|
+
from fuse import FUSE, Operations
|
2
|
+
import os
|
3
|
+
|
4
|
+
|
5
|
+
class GeoSeeqProjectFileSystem(Operations):
|
6
|
+
"""Mount a GeoSeeq project as a filesystem.
|
7
|
+
|
8
|
+
The project will automatically have this directory structure:
|
9
|
+
- <root>/project_results/<project_result_folder_name>/...
|
10
|
+
- <root>/sample_results/<sample_name>/...
|
11
|
+
- <root>/metadata/sample_metadata.csv
|
12
|
+
- <root>/.config/config.json
|
13
|
+
"""
|
14
|
+
|
15
|
+
def __init__(self, root, project):
|
16
|
+
self.root = root
|
17
|
+
self.project = project
|
18
|
+
|
19
|
+
def access(self, path, mode):
|
20
|
+
pass
|
21
|
+
|
22
|
+
def chmod(self, path, mode):
|
23
|
+
pass
|
24
|
+
|
25
|
+
def chown(self, path, uid, gid):
|
26
|
+
pass
|
27
|
+
|
28
|
+
def getattr(self, path, fh=None):
|
29
|
+
pass
|
30
|
+
|
31
|
+
def readdir(self, path, fh):
|
32
|
+
pass
|
33
|
+
|
34
|
+
def readlink(self, path):
|
35
|
+
pass
|
36
|
+
|
37
|
+
def mknod(self, path, mode, dev):
|
38
|
+
pass
|
39
|
+
|
40
|
+
def rmdir(self, path):
|
41
|
+
pass
|
42
|
+
|
43
|
+
def mkdir(self, path, mode):
|
44
|
+
pass
|
45
|
+
|
46
|
+
def statfs(self, path):
|
47
|
+
pass
|
48
|
+
|
49
|
+
def unlink(self, path):
|
50
|
+
pass
|
51
|
+
|
52
|
+
def symlink(self, name, target):
|
53
|
+
pass
|
54
|
+
|
55
|
+
def rename(self, old, new):
|
56
|
+
pass
|
57
|
+
|
58
|
+
def link(self, target, name):
|
59
|
+
pass
|
60
|
+
|
61
|
+
def utimens(self, path, times=None):
|
62
|
+
pass
|
63
|
+
|
64
|
+
def open(self, path, flags):
|
65
|
+
tkns = path.split('/')
|
66
|
+
if tkns[0] == 'project_results':
|
67
|
+
result_folder_name, result_file_name = tkns[2], '/'.join(tkns[3:])
|
68
|
+
result_folder = self.project.get_result_folder(result_folder_name).get()
|
69
|
+
result_file = result_folder.get_file(result_file_name).get()
|
70
|
+
result_file.download(path)
|
71
|
+
elif tkns[0] == 'sample_results':
|
72
|
+
sample_name, result_folder_name, result_file_name = tkns[2], tkns[3], '/'.join(tkns[4:])
|
73
|
+
sample = self.project.get_sample(sample_name).get()
|
74
|
+
result_folder = sample.get_result_folder(result_folder_name).get()
|
75
|
+
result_file = result_folder.get_file(result_file_name).get()
|
76
|
+
result_file.download(path)
|
77
|
+
elif tkns[0] == 'metadata':
|
78
|
+
raise NotImplementedError('TODO')
|
79
|
+
|
80
|
+
return os.open(self._full_local_path(path), flags)
|
81
|
+
|
82
|
+
def create(self, path, mode, fi=None):
|
83
|
+
tkns = path.split('/')
|
84
|
+
if tkns[0] == 'project_results':
|
85
|
+
result_name, file_name = tkns[2], '/'.join(tkns[3:])
|
86
|
+
result_folder = self.project.get_result_folder(result_name).idem()
|
87
|
+
result_file = result_folder.get_file(file_name).create()
|
88
|
+
result_file.download(path) # nothing to download at this point
|
89
|
+
elif tkns[0] == 'sample_results':
|
90
|
+
sample_name, result_folder_name, result_file_name = tkns[2], tkns[3], '/'.join(tkns[4:])
|
91
|
+
sample = self.project.get_sample(sample_name).idem()
|
92
|
+
result_folder = sample.get_result_folder(result_folder_name).idem()
|
93
|
+
result_file = result_folder.get_file(result_file_name).create()
|
94
|
+
result_file.download(path) # nothing to download at this point
|
95
|
+
elif tkns[0] == 'metadata':
|
96
|
+
raise NotImplementedError('TODO')
|
97
|
+
|
98
|
+
def read(self, path, length, offset, fh):
|
99
|
+
os.lseek(fh, offset, os.SEEK_SET)
|
100
|
+
return os.read(fh, length)
|
101
|
+
|
102
|
+
def write(self, path, buf, offset, fh):
|
103
|
+
pass
|
104
|
+
|
105
|
+
def truncate(self, path, length, fh=None):
|
106
|
+
pass
|
107
|
+
|
108
|
+
def flush(self, path, fh):
|
109
|
+
pass
|
110
|
+
|
111
|
+
def release(self, path, fh):
|
112
|
+
pass
|
113
|
+
|
114
|
+
def fsync(self, path, fdatasync, fh):
|
115
|
+
pass
|
116
|
+
|
117
|
+
def _full_local_path(self, partial):
|
118
|
+
if partial.startswith("/"):
|
119
|
+
partial = partial[1:]
|
120
|
+
return os.path.join(self.root, partial)
|
121
|
+
|
122
|
+
|
geoseeq/vc/README.md
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
# GeoSeeq Version Control
|
2
|
+
|
3
|
+
GeoSeeq VC is a toolkit that makes it easy to use geoseeq with version control systems like `git`.
|
4
|
+
|
5
|
+
Most of the files stored on GeoSeeq are too large to version control directly. This package creates lightweight stub files that record a files location and checksum. This package also contains tools to download files, and check that local files match those stored on the server. The stub files are small enough to version control directly.
|
6
|
+
|
7
|
+
## CLI
|
8
|
+
|
9
|
+
You can clone a project or sample from GeoSeeq. You will need to get the `brn` number from the project or sample then run these commands from your CLI
|
10
|
+
|
11
|
+
```
|
12
|
+
geoseeq-api vc clone brn:gsr1:project:bed763fb-f6b8-4739-8320-95c06d68f442 # creates a directory tree with stub files
|
13
|
+
geoseeq-api vc download # downloads the files that the stubs link to
|
14
|
+
geoseeq-api vc status # checks that the local files match the files on GeoSeeq
|
15
|
+
```
|
16
|
+
|
17
|
+
If you are using git you probably want to add files from geoseeq to your `.gitignore`
|
18
|
+
|
19
|
+
```
|
20
|
+
geoseeq-api vc list >> .gitignore
|
21
|
+
```
|
22
|
+
|
23
|
+
## Shared Cache
|
24
|
+
|
25
|
+
To avoid downloading or storing the same file multiple times users can specify a cache by setting the `GEOSEEQ_VC_CACHE_DIR` envvar to an absolute filepath. When set geoseeq will download files to a location in the cahce directory and create symlinks to those files.
|
26
|
+
|
27
|
+
## Stub Files
|
28
|
+
|
29
|
+
GeoSeeq interfaces with version control by creating small stub files that represent a larger file (AKA result field) stored on GeoSeeq. This package can be used to download those files and validate that the checksum of local files matches them. Since these files are small they can be easily stored with a version control system while the larger files can be ignored.
|
30
|
+
|
31
|
+
The stub files are JSON and have the following structure:
|
32
|
+
|
33
|
+
```
|
34
|
+
{
|
35
|
+
"__schema_version__": "v0",
|
36
|
+
"brn": "brn:<instance name>:result_field:<uuid>", # optional if stub is new and parent_info is set
|
37
|
+
"checksum": {
|
38
|
+
"value": "<value>",
|
39
|
+
"method": "md5",
|
40
|
+
},
|
41
|
+
"local_path": "<filename>", # a filepath relative to the stub file
|
42
|
+
"parent_info": { # optional if brn is set
|
43
|
+
"parent_obj_brn": "brn:<brn info>",
|
44
|
+
"result_module_name": <string>,
|
45
|
+
"result_module_replicate": <string>,
|
46
|
+
} # NB if brn and parent_info are both set but disagree `brn` is correct
|
47
|
+
}
|
48
|
+
```
|
@@ -1,24 +1,26 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: geoseeq
|
3
|
-
Version: 0.7.
|
3
|
+
Version: 0.7.2
|
4
4
|
Summary: GeoSeeq command line tools and python API
|
5
|
-
Author: David C. Danko
|
6
|
-
Author-email: "David C. Danko" <dcdanko@biotia.io>
|
7
5
|
Project-URL: Homepage, https://github.com/biotia/geoseeq_api_client
|
8
6
|
Project-URL: Issues, https://github.com/biotia/geoseeq_api_client/issues
|
9
|
-
|
7
|
+
Author-email: "David C. Danko" <dcdanko@biotia.io>
|
8
|
+
License-File: LICENSE
|
10
9
|
Classifier: License :: OSI Approved :: MIT License
|
11
10
|
Classifier: Operating System :: OS Independent
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
12
12
|
Requires-Python: >=3.8
|
13
|
-
|
14
|
-
License-File: LICENSE
|
15
|
-
Requires-Dist: requests
|
13
|
+
Requires-Dist: biopython
|
16
14
|
Requires-Dist: click
|
17
15
|
Requires-Dist: pandas
|
18
|
-
Requires-Dist:
|
16
|
+
Requires-Dist: requests
|
19
17
|
Requires-Dist: tqdm
|
20
|
-
|
21
|
-
|
18
|
+
Provides-Extra: test
|
19
|
+
Requires-Dist: pytest-cov>=4.0; extra == 'test'
|
20
|
+
Requires-Dist: pytest-mock>=3.10; extra == 'test'
|
21
|
+
Requires-Dist: pytest>=7.0; extra == 'test'
|
22
|
+
Requires-Dist: requests-mock>=1.11; extra == 'test'
|
23
|
+
Description-Content-Type: text/markdown
|
22
24
|
|
23
25
|
# Geoseeq API Client
|
24
26
|
|
@@ -25,7 +25,7 @@ geoseeq/cli/download.py,sha256=Znjuc9IFOcIa5_Od9mFXHJdYAJtgw9Bc_wPPcOVXn7s,21298
|
|
25
25
|
geoseeq/cli/fastq_utils.py,sha256=-bmeQLaiMBm57zWOF0R5OlWTU0_3sh1JBC1RYw2BOFM,3083
|
26
26
|
geoseeq/cli/find_grn.py,sha256=oMDxkzGQBQb2_cCuvmwoeHOsFHqyO9RLeJzrB6bAe5M,439
|
27
27
|
geoseeq/cli/get_eula.py,sha256=79mbUwyiF7O1r0g6UTxG9kJGQEqKuH805E6eLkPC6Y4,997
|
28
|
-
geoseeq/cli/main.py,sha256=
|
28
|
+
geoseeq/cli/main.py,sha256=uGCf-uREHdrDzQZDiBLFt6KcKcyDRHnN6vHZRSwZaYU,3982
|
29
29
|
geoseeq/cli/manage.py,sha256=wGXAcVaXqE5JQEU8Jh6OlHr02nB396bpS_SFcOZdrEo,5929
|
30
30
|
geoseeq/cli/progress_bar.py,sha256=p1Xl01nkYxSBZCB30ue2verIIi22W93m3ZAMAxipD0g,738
|
31
31
|
geoseeq/cli/project.py,sha256=V5SdXm2Hwo2lxrkpwRDedw-mAE4XnM2uwT-Gj1D90VQ,3030
|
@@ -42,15 +42,19 @@ geoseeq/cli/shared_params/id_handlers.py,sha256=KtzflnplYVkXsyqI5Ej6r-_BwQnuXVHP
|
|
42
42
|
geoseeq/cli/shared_params/obj_getters.py,sha256=ZSkt6LnDkVFlNVYKgLrjzg60-6BthZMr3eeD3HNqzac,2741
|
43
43
|
geoseeq/cli/shared_params/opts_and_args.py,sha256=_DcJ-TqgrbBaeDd-kuHEx2gLZPQN6EHZYWh8Ag-d8Vg,2091
|
44
44
|
geoseeq/cli/upload/__init__.py,sha256=tRrljqG7uBM6zVBKpDw1zevHz6Vy418HuIYZKwByyWg,862
|
45
|
-
geoseeq/cli/upload/upload.py,sha256=
|
45
|
+
geoseeq/cli/upload/upload.py,sha256=tBga8TW_5qXM4avXH5BAfLrYNms_5eqdyJZex47hZ90,14294
|
46
46
|
geoseeq/cli/upload/upload_advanced.py,sha256=1pjtbe8EidIo4eR1_oVD00rPCTxYj8CRqB8D9buA62M,12432
|
47
|
-
geoseeq/cli/upload/upload_reads.py,sha256=
|
47
|
+
geoseeq/cli/upload/upload_reads.py,sha256=dZK_Mh3a3NdC4zVB4Yyfz-NViCK9_rROQbmLWTrh8Pc,11642
|
48
48
|
geoseeq/contrib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
49
|
+
geoseeq/contrib/ncbi/README.md,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
49
50
|
geoseeq/contrib/ncbi/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
50
51
|
geoseeq/contrib/ncbi/api.py,sha256=WQeLoGA_-Zha-QeSO8_i7HpvXyD8UkV0qc5okm11KiA,1056
|
51
52
|
geoseeq/contrib/ncbi/bioproject.py,sha256=_oThTd_iLDOC8cLOlJKAatSr362OBYZCEV3YrqodhFg,4341
|
52
53
|
geoseeq/contrib/ncbi/cli.py,sha256=j9zEcaZPTryK3a4xluRxigcJKDhRpRxbp3KZSx-Bfhk,2400
|
53
54
|
geoseeq/contrib/ncbi/setup_logging.py,sha256=Tp1bY1U0f-o739aHpvVYriG2qdd1lFvCYBXZeXQgt-w,175
|
55
|
+
geoseeq/dashboard/dashboard.py,sha256=HOUJoQcFGLc3JL5UVIYETq6CkQEMP2I4WaPr2u9YV3Q,3297
|
56
|
+
geoseeq/file_system/filesystem_download.py,sha256=8bcnxjWltekmCvb5N0b1guBIjLp4-CL2VtsEok-snv4,16963
|
57
|
+
geoseeq/file_system/main.py,sha256=4HgYGq7WhlF96JlVIf16iFBTDujlBpxImmtoh4VCzDA,3627
|
54
58
|
geoseeq/id_constructors/__init__.py,sha256=w5E0PNQ9UuAxBeZbDI7KBnUoERd85gGz3nScz45bd2o,126
|
55
59
|
geoseeq/id_constructors/from_blobs.py,sha256=wZp6P6m7X0lhZAQxorHKJQn1CTBvKyfANzu2VFexp44,6315
|
56
60
|
geoseeq/id_constructors/from_ids.py,sha256=SRylajToFdnRON5EhPIvcAi64XuIzseaasZlOooqTpg,3984
|
@@ -58,6 +62,7 @@ geoseeq/id_constructors/from_names.py,sha256=RqgFjDsAwQcidMkZwX7oB00OvBAKTiilHYe
|
|
58
62
|
geoseeq/id_constructors/from_uuids.py,sha256=loqTeZqDZuvfVVHpS7m-Y_spNaY3h9VRjt0wDiMxHbs,3522
|
59
63
|
geoseeq/id_constructors/resolvers.py,sha256=8hp5xJSCoZrAXtMT54Hp4okt63l909XqJU3IQx-VCgc,2676
|
60
64
|
geoseeq/id_constructors/utils.py,sha256=Up_vXoZDwa2mJGllJ6CmcbaOGUvA-sM0gMmFnXK8wn4,1255
|
65
|
+
geoseeq/plotting/README.md,sha256=qlPnEzJWVxBeLvgyFXIN2du_-6GprvX8lRk4ROw6aMs,82
|
61
66
|
geoseeq/plotting/__init__.py,sha256=RkGoXxgu7jEfK0B7NmdalPS2AbU7I7dZwDbi4rn9CKM,154
|
62
67
|
geoseeq/plotting/constants.py,sha256=CGUlm8WAFG3YRKdicc9Rcy5hFxUdUm2RgK0iXZWLuX8,285
|
63
68
|
geoseeq/plotting/highcharts.py,sha256=AGzdW4VSUsL_rfEI-RiVbbtaTLFMARvzLzVUDrKTlnU,4096
|
@@ -77,6 +82,7 @@ geoseeq/result/resumable_download_tracker.py,sha256=YEzqHBBnE7L3XokTvlTAhHZ8TcDT
|
|
77
82
|
geoseeq/result/resumable_upload_tracker.py,sha256=2aI09gYz2yw63jEXqs8lmCRKQ79TIc3YuPETvP0Jeek,3811
|
78
83
|
geoseeq/result/smart_objects.py,sha256=-krK9h9HcznIglf189uPghOidF_BzOeSxmBWxrtFFo8,1991
|
79
84
|
geoseeq/result/utils.py,sha256=C-CxGzB3WddlnRiqFSkrY78I_m0yFgNqsTBRzGU-y8Q,2772
|
85
|
+
geoseeq/vc/README.md,sha256=igLGXl1jpp2A_tTfPutspTejwE1lkObeCsSvCYPBqOM,2316
|
80
86
|
geoseeq/vc/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
81
87
|
geoseeq/vc/checksum.py,sha256=y8rh1asUZNbE_NLiFO0-9hImLNiTOc2YXQBRKORWK7k,710
|
82
88
|
geoseeq/vc/cli.py,sha256=rUspr6FUJiUWIVMvf-pmyC5psKfufN45alk8392fAfA,2803
|
@@ -86,12 +92,8 @@ geoseeq/vc/vc_cache.py,sha256=P4LXTbq2zOIv1OhP7Iw5MmypR2vXuy29Pq5K6gRvi-M,730
|
|
86
92
|
geoseeq/vc/vc_dir.py,sha256=A9CLTh2wWCRzZjiLyqXD1vhtsWZGD3OjaMT5KqlfAXI,457
|
87
93
|
geoseeq/vc/vc_sample.py,sha256=qZeioWydXvfu4rGMs20nICfNcp46y_XkND-bHdV6P5M,3850
|
88
94
|
geoseeq/vc/vc_stub.py,sha256=IQr8dI0zsWKVAeY_5ybDD6n49_3othcgfHS3P0O9tuY,3110
|
89
|
-
geoseeq-0.7.
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
geoseeq-0.7.
|
94
|
-
geoseeq-0.7.0.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
|
95
|
-
geoseeq-0.7.0.dist-info/entry_points.txt,sha256=yF-6KDM8zXib4Al0qn49TX-qM7PUkWUIcYtsgt36rjM,45
|
96
|
-
geoseeq-0.7.0.dist-info/top_level.txt,sha256=zZk7mmeaqAYqFJG8nq2DTgSQPbflRjJwkDIhNURPDEU,14
|
97
|
-
geoseeq-0.7.0.dist-info/RECORD,,
|
95
|
+
geoseeq-0.7.2.dist-info/METADATA,sha256=nBGfEYFfG9yWP6LWxczthpgwC5vZ5f7xi7VyaxVmz4Y,5107
|
96
|
+
geoseeq-0.7.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
97
|
+
geoseeq-0.7.2.dist-info/entry_points.txt,sha256=yF-6KDM8zXib4Al0qn49TX-qM7PUkWUIcYtsgt36rjM,45
|
98
|
+
geoseeq-0.7.2.dist-info/licenses/LICENSE,sha256=IuhIl1XCxXLPLJT_coN1CNqQU4Khlq7x4IdW7ioOJD8,1067
|
99
|
+
geoseeq-0.7.2.dist-info/RECORD,,
|
tests/test_api_client.py
DELETED
@@ -1,283 +0,0 @@
|
|
1
|
-
"""Test suite for experimental functions."""
|
2
|
-
import random
|
3
|
-
import sys
|
4
|
-
from os import environ
|
5
|
-
from unittest import TestCase, skip
|
6
|
-
|
7
|
-
from geoseeq import (
|
8
|
-
Knex,
|
9
|
-
Organization,
|
10
|
-
GeoseeqOtherError,
|
11
|
-
GeoseeqNotFoundError,
|
12
|
-
)
|
13
|
-
from requests.exceptions import ConnectionError
|
14
|
-
|
15
|
-
ENDPOINT = environ.get("GEOSEEQ_API_TESTING_ENDPOINT", "http://127.0.0.1:8000")
|
16
|
-
TOKEN = environ.get("GEOSEEQ_API_TOKEN", "<no_token>")
|
17
|
-
|
18
|
-
|
19
|
-
def random_str(len=12):
|
20
|
-
"""Return a random alphanumeric string of length `len`."""
|
21
|
-
out = random.choices("abcdefghijklmnopqrtuvwxyzABCDEFGHIJKLMNOPQRTUVWXYZ0123456789", k=len)
|
22
|
-
return "".join(out)
|
23
|
-
|
24
|
-
|
25
|
-
class TestGeoseeqApiClient(TestCase):
|
26
|
-
"""Test suite for packet building."""
|
27
|
-
|
28
|
-
def setUp(self):
|
29
|
-
self.knex = Knex(ENDPOINT)
|
30
|
-
# Creates a test user and an API token for the user in database. Returns the token.
|
31
|
-
if TOKEN == "<no_token>":
|
32
|
-
try:
|
33
|
-
api_token = self.knex.post("/users/test-user",
|
34
|
-
json={"email": f"clitestuser_{random_str()}@gmail.com"})
|
35
|
-
except GeoseeqOtherError:
|
36
|
-
print(f"Could not create test user on \"{ENDPOINT}\". If you are running this test suite "\
|
37
|
-
"against a live server, please set the GEOSEEQ_API_TOKEN environment variable to a "\
|
38
|
-
"valid API token.",
|
39
|
-
file=sys.stderr)
|
40
|
-
raise
|
41
|
-
except ConnectionError:
|
42
|
-
print(f"Could not connect to GeoSeeq Server at \"{ENDPOINT}\".",
|
43
|
-
file=sys.stderr)
|
44
|
-
raise
|
45
|
-
self.knex.add_api_token(api_token)
|
46
|
-
else:
|
47
|
-
self.knex.add_api_token(TOKEN)
|
48
|
-
try:
|
49
|
-
me = self.knex.get("/users/me") # Test that the token is valid
|
50
|
-
self.username = me["name"]
|
51
|
-
self.org_name = f"API_TEST_ORG 1 {self.username}"
|
52
|
-
except GeoseeqNotFoundError:
|
53
|
-
print(f"Could not connect to GeoSeeq Server at \"{ENDPOINT}\" with the provided token. "\
|
54
|
-
"Is it possible that you set thd testing endpoint to a front end url instead of "\
|
55
|
-
"the corresponding backend url?",
|
56
|
-
file=sys.stderr)
|
57
|
-
raise
|
58
|
-
|
59
|
-
def test_create_org(self):
|
60
|
-
"""Test that we can create an org."""
|
61
|
-
org = Organization(self.knex, self.org_name)
|
62
|
-
org.idem()
|
63
|
-
self.assertTrue(org.uuid)
|
64
|
-
|
65
|
-
|
66
|
-
def test_create_project(self):
|
67
|
-
"""Test that we can create a project."""
|
68
|
-
key = random_str()
|
69
|
-
org = Organization(self.knex, self.org_name)
|
70
|
-
|
71
|
-
proj = org.project(f"my_client_test_project {key}")
|
72
|
-
proj.create()
|
73
|
-
self.assertTrue(org.uuid)
|
74
|
-
self.assertTrue(proj.uuid)
|
75
|
-
|
76
|
-
def test_create_project_result_folder(self):
|
77
|
-
"""Test that we can create a result folder in a project."""
|
78
|
-
key = random_str()
|
79
|
-
org = Organization(self.knex, self.org_name)
|
80
|
-
proj = org.project(f"my_client_test_proj {key}")
|
81
|
-
# N.B. It should NOT be necessary to call <parent>.create()
|
82
|
-
result_folder = proj.result_folder(f"my_client_test_module_name") # no {key} necessary
|
83
|
-
result_folder.create()
|
84
|
-
self.assertTrue(org.uuid)
|
85
|
-
self.assertTrue(proj.uuid)
|
86
|
-
self.assertTrue(result_folder.uuid)
|
87
|
-
|
88
|
-
def test_create_project_result_file(self):
|
89
|
-
"""Test that we can create a result file in a project."""
|
90
|
-
key = random_str()
|
91
|
-
org = Organization(self.knex, self.org_name)
|
92
|
-
proj = org.project(f"my_client_test_proj {key}")
|
93
|
-
result_folder = proj.result_folder(f"my_client_test_module_name") # no {key} necessary
|
94
|
-
# N.B. It should NOT be necessary to call <parent>.create()
|
95
|
-
result_file = result_folder.result_file("my_client_test_field_name", {"foo": "bar"})
|
96
|
-
result_file.create()
|
97
|
-
self.assertTrue(org.uuid)
|
98
|
-
self.assertTrue(proj.uuid)
|
99
|
-
self.assertTrue(result_folder.uuid)
|
100
|
-
self.assertTrue(result_file.uuid)
|
101
|
-
|
102
|
-
def test_create_sample(self):
|
103
|
-
"""Test that we can create a sample."""
|
104
|
-
key = random_str()
|
105
|
-
org = Organization(self.knex, self.org_name)
|
106
|
-
proj = org.project(f"my_client_test_proj {key}")
|
107
|
-
# N.B. It should NOT be necessary to call <parent>.create()
|
108
|
-
samp = proj.sample(f"my_client_test_sample {key}")
|
109
|
-
samp.create()
|
110
|
-
self.assertTrue(org.uuid)
|
111
|
-
self.assertTrue(proj.uuid)
|
112
|
-
self.assertTrue(samp.uuid)
|
113
|
-
|
114
|
-
def test_add_sample(self):
|
115
|
-
"""Test that we can create a sample and add it to a different project."""
|
116
|
-
key = random_str()
|
117
|
-
org = Organization(self.knex, self.org_name)
|
118
|
-
proj1 = org.project(f"my_client_test_proj1 {key}")
|
119
|
-
samp = proj1.sample(f"my_client_test_sample {key}").create()
|
120
|
-
|
121
|
-
proj2 = org.project(f"my_client_test_proj2 {key}").create()
|
122
|
-
proj2.add_sample(samp).save()
|
123
|
-
self.assertIn(samp.uuid, {samp.uuid for samp in proj2.get_samples()})
|
124
|
-
|
125
|
-
def test_get_samples_project(self):
|
126
|
-
"""Test that we can get the samples in a project."""
|
127
|
-
key = random_str()
|
128
|
-
org = Organization(self.knex, self.org_name)
|
129
|
-
proj = org.project(f"my_client_test_proj {key}")
|
130
|
-
samp_names = [f"my_client_test_sample_{i} {key}" for i in range(10)]
|
131
|
-
for samp_name in samp_names:
|
132
|
-
proj.sample(samp_name).create()
|
133
|
-
retrieved_proj = org.project(f"my_client_test_proj {key}").get()
|
134
|
-
retrieved_names = set()
|
135
|
-
for samp in retrieved_proj.get_samples():
|
136
|
-
retrieved_names.add(samp.name)
|
137
|
-
self.assertTrue(samp.uuid)
|
138
|
-
for samp_name in samp_names:
|
139
|
-
self.assertIn(samp_name, retrieved_names)
|
140
|
-
|
141
|
-
def test_get_result_folders_in_project(self):
|
142
|
-
"""Test that we can get the result folders in a project."""
|
143
|
-
key = random_str()
|
144
|
-
org = Organization(self.knex, self.org_name)
|
145
|
-
proj = org.project(f"my_client_test_proj {key}")
|
146
|
-
result_names = [("my_client_test_module", f"replicate_{i}") for i in range(10)]
|
147
|
-
for module_name, replicate in result_names:
|
148
|
-
proj.result_folder(module_name, replicate=replicate).create()
|
149
|
-
retrieved_proj = org.project(f"my_client_test_proj {key}").get()
|
150
|
-
retrieved_names = set()
|
151
|
-
for result in retrieved_proj.get_result_folders():
|
152
|
-
retrieved_names.add((result.module_name, result.replicate))
|
153
|
-
self.assertTrue(result.uuid)
|
154
|
-
for result_name_rep in result_names:
|
155
|
-
self.assertIn(result_name_rep, retrieved_names)
|
156
|
-
|
157
|
-
def test_get_result_folders_in_sample(self):
|
158
|
-
"""Test that we can get the result folders in a sample."""
|
159
|
-
key = random_str()
|
160
|
-
org = Organization(self.knex, self.org_name)
|
161
|
-
proj = org.project(f"my_client_test_proj {key}")
|
162
|
-
samp = proj.sample(f"my_client_test_sample {key}").create()
|
163
|
-
result_names = [("my_client_test_module", f"replicate_{i}") for i in range(10)]
|
164
|
-
for module_name, replicate in result_names:
|
165
|
-
samp.result_folder(module_name, replicate=replicate).create()
|
166
|
-
retrieved = proj.sample(f"my_client_test_sample {key}").get()
|
167
|
-
retrieved_names = set()
|
168
|
-
for result in retrieved.get_result_folders():
|
169
|
-
retrieved_names.add((result.module_name, result.replicate))
|
170
|
-
self.assertTrue(result.uuid)
|
171
|
-
for result_name_rep in result_names:
|
172
|
-
self.assertIn(result_name_rep, retrieved_names)
|
173
|
-
|
174
|
-
def test_get_result_files(self):
|
175
|
-
"""Test that we can get the files in a result folder."""
|
176
|
-
key = random_str()
|
177
|
-
org = Organization(self.knex, self.org_name)
|
178
|
-
proj = org.project(f"my_client_test_proj {key}")
|
179
|
-
samp = proj.sample(f"my_client_test_sample {key}")
|
180
|
-
result_folder = samp.result_folder("my_client_test_module").create()
|
181
|
-
self.assertTrue(proj.uuid)
|
182
|
-
|
183
|
-
field_names = [f"field_{i}" for i in range(10)]
|
184
|
-
for field_name in field_names:
|
185
|
-
result_folder.field(field_name).create()
|
186
|
-
|
187
|
-
retrieved = samp.result_folder("my_client_test_module").get()
|
188
|
-
retrieved_names = set()
|
189
|
-
for result in retrieved.get_fields():
|
190
|
-
retrieved_names.add(result.name)
|
191
|
-
self.assertTrue(result.uuid)
|
192
|
-
for result_name_rep in field_names:
|
193
|
-
self.assertIn(result_name_rep, retrieved_names)
|
194
|
-
|
195
|
-
def test_modify_sample(self):
|
196
|
-
"""Test that we can modify a sample after creation"""
|
197
|
-
key = random_str()
|
198
|
-
org = Organization(self.knex, self.org_name)
|
199
|
-
proj = org.project(f"my_client_test_proj {key}")
|
200
|
-
# N.B. It should NOT be necessary to call <parent>.create()
|
201
|
-
samp = proj.sample(f"my_client_test_sample {key}")
|
202
|
-
samp.create()
|
203
|
-
self.assertTrue(samp.uuid)
|
204
|
-
self.assertTrue(samp._already_fetched)
|
205
|
-
self.assertFalse(samp._modified)
|
206
|
-
samp.metadata = {f"metadata_{key}": "some_new_metadata"}
|
207
|
-
self.assertTrue(samp._modified)
|
208
|
-
samp.save()
|
209
|
-
self.assertTrue(samp._already_fetched)
|
210
|
-
self.assertFalse(samp._modified)
|
211
|
-
retrieved = proj.sample(f"my_client_test_sample {key}").get()
|
212
|
-
self.assertIn(f"metadata_{key}", retrieved.metadata)
|
213
|
-
|
214
|
-
def test_create_sample_result_folder(self):
|
215
|
-
"""Test that we can create a result folder in a sample."""
|
216
|
-
key = random_str()
|
217
|
-
org = Organization(self.knex, self.org_name)
|
218
|
-
proj = org.project(f"my_client_test_proj {key}")
|
219
|
-
samp = proj.sample(f"my_client_test_sample {key}")
|
220
|
-
# N.B. It should NOT be necessary to call <parent>.create()
|
221
|
-
result_folder = samp.result_folder(f"my_client_test_module_name") # no {key} necessary
|
222
|
-
result_folder.create()
|
223
|
-
self.assertTrue(org.uuid)
|
224
|
-
self.assertTrue(proj.uuid)
|
225
|
-
self.assertTrue(samp.uuid)
|
226
|
-
self.assertTrue(result_folder.uuid)
|
227
|
-
|
228
|
-
def test_create_sample_result_file(self):
|
229
|
-
"""Test that we can create a result file in a sample."""
|
230
|
-
key = random_str()
|
231
|
-
org = Organization(self.knex, self.org_name)
|
232
|
-
proj = org.project(f"my_client_test_proj {key}")
|
233
|
-
samp = proj.sample(f"my_client_test_sample {key}")
|
234
|
-
result_folder = samp.result_folder(f"my_client_test_module_name") # no {key} necessary
|
235
|
-
# N.B. It should NOT be necessary to call <parent>.create()
|
236
|
-
result_file = result_folder.result_file("my_client_test_field_name", {"foo": "bar"})
|
237
|
-
result_file.create()
|
238
|
-
self.assertTrue(org.uuid)
|
239
|
-
self.assertTrue(proj.uuid)
|
240
|
-
self.assertTrue(samp.uuid)
|
241
|
-
self.assertTrue(result_folder.uuid)
|
242
|
-
self.assertTrue(result_file.uuid)
|
243
|
-
|
244
|
-
@skip("failing on server (2023-10-24)")
|
245
|
-
def test_modify_sample_result_file(self):
|
246
|
-
"""Test that we can modify a result file in a sample."""
|
247
|
-
key = random_str()
|
248
|
-
org = Organization(self.knex, self.org_name)
|
249
|
-
proj = org.project(f"my_client_test_proj {key}")
|
250
|
-
samp = proj.sample(f"my_client_test_sample {key}")
|
251
|
-
result_folder = samp.result_folder(f"my_client_test_module_name") # no {key} necessary
|
252
|
-
# N.B. It should NOT be necessary to call <parent>.create()
|
253
|
-
result_file = result_folder.result_file(f"my_client_test_file_name {key}", {"foo": "bar"})
|
254
|
-
result_file.create()
|
255
|
-
self.assertTrue(result_file.uuid)
|
256
|
-
result_file.stored_data = {"foo": "bizz"} # TODO: handle deep modifications
|
257
|
-
result_file.save()
|
258
|
-
retrieved = result_folder.result_file(f"my_client_test_file_name {key}").get()
|
259
|
-
self.assertEqual(retrieved.stored_data["foo"], "bizz")
|
260
|
-
|
261
|
-
def test_get_project_manifest(self):
|
262
|
-
"""Test that we can get a project manifest."""
|
263
|
-
key = random_str()
|
264
|
-
org = Organization(self.knex, self.org_name)
|
265
|
-
proj = org.project(f"my_client_test_proj {key}")
|
266
|
-
samp = proj.sample(f"my_client_test_sample {key}")
|
267
|
-
result_folder = samp.result_folder(f"my_client_test_module_name") # no {key} necessary
|
268
|
-
result_file = result_folder.result_file("my_client_test_field_name", {"foo": "bar"})
|
269
|
-
result_file.create()
|
270
|
-
manifest = proj.get_manifest()
|
271
|
-
self.assertTrue(manifest)
|
272
|
-
|
273
|
-
def test_get_sample_manifest(self):
|
274
|
-
"""Test that we can get a sample manifest."""
|
275
|
-
key = random_str()
|
276
|
-
org = Organization(self.knex, self.org_name)
|
277
|
-
proj = org.project(f"my_client_test_proj {key}")
|
278
|
-
samp = proj.sample(f"my_client_test_sample {key}")
|
279
|
-
result_folder = samp.result_folder(f"my_client_test_module_name") # no {key} necessary
|
280
|
-
result_file = result_folder.result_file("my_client_test_field_name", {"foo": "bar"})
|
281
|
-
result_file.create()
|
282
|
-
manifest = samp.get_manifest()
|
283
|
-
self.assertTrue(manifest)
|
tests/test_plotting.py
DELETED
@@ -1,29 +0,0 @@
|
|
1
|
-
"""Test suite for plotting library."""
|
2
|
-
import random
|
3
|
-
from os import environ
|
4
|
-
from os.path import dirname, join
|
5
|
-
from unittest import TestCase, skip
|
6
|
-
|
7
|
-
from geoseeq.plotting.map import Map
|
8
|
-
|
9
|
-
|
10
|
-
class TestGeoseeqPlotting(TestCase):
|
11
|
-
"""Test suite for packet building."""
|
12
|
-
|
13
|
-
def test_make_map_complex(self):
|
14
|
-
"""Test that we can create a map and turn it into a dict."""
|
15
|
-
map = Map()\
|
16
|
-
.set_center(0, 0)\
|
17
|
-
.set_zoom(2)\
|
18
|
-
.add_light_base_map()\
|
19
|
-
.add_administrative_overlay()\
|
20
|
-
.add_places_overlay()
|
21
|
-
map.to_dict()
|
22
|
-
|
23
|
-
def test_make_map_simple(self):
|
24
|
-
"""Test that we can create a map and turn it into a dict."""
|
25
|
-
map = Map()\
|
26
|
-
.add_light_base_map()
|
27
|
-
map.to_dict()
|
28
|
-
|
29
|
-
|
File without changes
|
File without changes
|
File without changes
|