pybiolib 1.1.1957__py3-none-any.whl → 1.1.1971__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.
- biolib/_internal/fuse_mount/__init__.py +1 -0
- biolib/_internal/fuse_mount/experiment_fuse_mount.py +198 -0
- biolib/_internal/libs/__init__.py +1 -0
- biolib/_internal/libs/fusepy/__init__.py +1257 -0
- biolib/biolib_binary_format/utils.py +9 -2
- biolib/experiments/experiment.py +29 -30
- {pybiolib-1.1.1957.dist-info → pybiolib-1.1.1971.dist-info}/METADATA +1 -1
- {pybiolib-1.1.1957.dist-info → pybiolib-1.1.1971.dist-info}/RECORD +11 -7
- {pybiolib-1.1.1957.dist-info → pybiolib-1.1.1971.dist-info}/LICENSE +0 -0
- {pybiolib-1.1.1957.dist-info → pybiolib-1.1.1971.dist-info}/WHEEL +0 -0
- {pybiolib-1.1.1957.dist-info → pybiolib-1.1.1971.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1 @@
|
|
1
|
+
from .experiment_fuse_mount import ExperimentFuseMount
|
@@ -0,0 +1,198 @@
|
|
1
|
+
import errno
|
2
|
+
import os
|
3
|
+
import stat
|
4
|
+
from datetime import datetime, timezone
|
5
|
+
from time import time
|
6
|
+
from typing import Iterable
|
7
|
+
|
8
|
+
from biolib._internal.libs.fusepy import FUSE, FuseOSError, Operations
|
9
|
+
from biolib.biolib_errors import BioLibError
|
10
|
+
from biolib.jobs import Job
|
11
|
+
from biolib.typing_utils import Dict, Optional, Tuple, TypedDict
|
12
|
+
|
13
|
+
|
14
|
+
class _AttributeDict(TypedDict):
|
15
|
+
st_atime: int
|
16
|
+
st_ctime: int
|
17
|
+
st_gid: int
|
18
|
+
st_mode: int
|
19
|
+
st_mtime: int
|
20
|
+
st_nlink: int
|
21
|
+
st_size: int
|
22
|
+
st_uid: int
|
23
|
+
|
24
|
+
|
25
|
+
class ExperimentFuseMount(Operations):
|
26
|
+
def __init__(self, experiment):
|
27
|
+
self._experiment = experiment
|
28
|
+
self._job_names_map: Optional[Dict[str, Job]] = None
|
29
|
+
self._jobs_last_fetched_at: float = 0.0
|
30
|
+
self._root_path: str = '/'
|
31
|
+
self._mounted_at_epoch_seconds: int = int(time())
|
32
|
+
|
33
|
+
@staticmethod
|
34
|
+
def mount(experiment, path: str) -> None:
|
35
|
+
FUSE(
|
36
|
+
operations=ExperimentFuseMount(experiment),
|
37
|
+
mountpoint=path,
|
38
|
+
nothreads=True,
|
39
|
+
foreground=True,
|
40
|
+
allow_other=False,
|
41
|
+
)
|
42
|
+
|
43
|
+
def getattr(self, path: str, fh=None) -> _AttributeDict:
|
44
|
+
full_path = self._full_path(path)
|
45
|
+
if full_path == '/':
|
46
|
+
# return folder dir
|
47
|
+
return self._get_folder_attr(timestamp_epoch_seconds=self._mounted_at_epoch_seconds)
|
48
|
+
|
49
|
+
job_name, job_path = self._parse_path(path)
|
50
|
+
job = self._get_job_names_map().get(job_name)
|
51
|
+
if not job:
|
52
|
+
# job not found
|
53
|
+
raise FuseOSError(errno.ENOENT)
|
54
|
+
|
55
|
+
job_finished_at_epoch_seconds: int = int(
|
56
|
+
datetime.fromisoformat(job.to_dict()['finished_at'].rstrip('Z')).replace(tzinfo=timezone.utc).timestamp()
|
57
|
+
)
|
58
|
+
if not job_path or job_path == '/':
|
59
|
+
# job root path
|
60
|
+
return self._get_folder_attr(timestamp_epoch_seconds=job_finished_at_epoch_seconds)
|
61
|
+
|
62
|
+
try:
|
63
|
+
file = job.get_output_file(job_path)
|
64
|
+
except BioLibError:
|
65
|
+
# file not found
|
66
|
+
raise FuseOSError(errno.ENOENT) from None
|
67
|
+
|
68
|
+
return _AttributeDict(
|
69
|
+
st_atime=job_finished_at_epoch_seconds,
|
70
|
+
st_ctime=job_finished_at_epoch_seconds,
|
71
|
+
st_gid=os.getgid(),
|
72
|
+
st_mode=stat.S_IFREG | 0o444, # Regular file with read permissions for owner, group, and others.
|
73
|
+
st_mtime=job_finished_at_epoch_seconds,
|
74
|
+
st_nlink=1,
|
75
|
+
st_size=file.length,
|
76
|
+
st_uid=os.getuid(),
|
77
|
+
)
|
78
|
+
|
79
|
+
def readdir(self, path: str, fh: int) -> Iterable[str]:
|
80
|
+
directory_entries = ['.', '..']
|
81
|
+
if path == '/':
|
82
|
+
for name in self._get_job_names_map(refresh_jobs=True):
|
83
|
+
directory_entries.append(name)
|
84
|
+
else:
|
85
|
+
job_name, job_path = self._parse_path(path)
|
86
|
+
job = self._get_job_names_map()[job_name]
|
87
|
+
in_target_directory = set(
|
88
|
+
[k.path.split('/')[1] for k in job.list_output_files() if k.path.startswith(job_path)]
|
89
|
+
)
|
90
|
+
|
91
|
+
for key in in_target_directory:
|
92
|
+
directory_entries.append(key)
|
93
|
+
|
94
|
+
yield from directory_entries
|
95
|
+
|
96
|
+
def open(self, path: str, flags: int) -> int:
|
97
|
+
job_name, job_path = self._parse_path(path)
|
98
|
+
job = self._get_job_names_map()[job_name]
|
99
|
+
try:
|
100
|
+
job.get_output_file(job_path)
|
101
|
+
return 0 # return dummy file handle
|
102
|
+
except BioLibError:
|
103
|
+
# file not found
|
104
|
+
raise FuseOSError(errno.ENOENT) from None
|
105
|
+
|
106
|
+
def read(self, path: str, size: int, offset: int, fh: int) -> bytes:
|
107
|
+
job_name, job_path = self._parse_path(path)
|
108
|
+
job = self._get_job_names_map()[job_name]
|
109
|
+
try:
|
110
|
+
file = job.get_output_file(job_path)
|
111
|
+
except BioLibError:
|
112
|
+
# file not found
|
113
|
+
raise FuseOSError(errno.ENOENT) from None
|
114
|
+
|
115
|
+
return file.get_data(start=offset, length=size)
|
116
|
+
|
117
|
+
def _get_folder_attr(self, timestamp_epoch_seconds: int) -> _AttributeDict:
|
118
|
+
return _AttributeDict(
|
119
|
+
st_atime=timestamp_epoch_seconds,
|
120
|
+
st_ctime=timestamp_epoch_seconds,
|
121
|
+
st_gid=os.getgid(),
|
122
|
+
st_mode=stat.S_IFDIR | 0o555, # Directory that is readable and executable by owner, group, and others.
|
123
|
+
st_mtime=timestamp_epoch_seconds,
|
124
|
+
st_nlink=1,
|
125
|
+
st_size=1,
|
126
|
+
st_uid=os.getuid(),
|
127
|
+
)
|
128
|
+
|
129
|
+
def _get_job_names_map(self, refresh_jobs=False) -> Dict[str, Job]:
|
130
|
+
current_time = time()
|
131
|
+
if not self._job_names_map or (current_time - self._jobs_last_fetched_at > 1 and refresh_jobs):
|
132
|
+
self._jobs_last_fetched_at = current_time
|
133
|
+
self._job_names_map = {job.get_name(): job for job in self._experiment.get_jobs(status='completed')}
|
134
|
+
|
135
|
+
return self._job_names_map
|
136
|
+
|
137
|
+
def _full_path(self, partial: str) -> str:
|
138
|
+
if partial.startswith('/'):
|
139
|
+
partial = partial[1:]
|
140
|
+
|
141
|
+
return os.path.join(self._root_path, partial)
|
142
|
+
|
143
|
+
def _parse_path(self, path: str) -> Tuple[str, str]:
|
144
|
+
full_path = self._full_path(path)
|
145
|
+
full_path_splitted = full_path.split('/')
|
146
|
+
job_name = full_path_splitted[1]
|
147
|
+
job_path = '/'.join(full_path_splitted[2:])
|
148
|
+
return job_name, job_path
|
149
|
+
|
150
|
+
# ----------------------------------- File system methods not implemented below -----------------------------------
|
151
|
+
|
152
|
+
def chmod(self, path, mode):
|
153
|
+
raise FuseOSError(errno.EACCES)
|
154
|
+
|
155
|
+
def chown(self, path, uid, gid):
|
156
|
+
raise FuseOSError(errno.EACCES)
|
157
|
+
|
158
|
+
def mknod(self, path, mode, dev):
|
159
|
+
raise FuseOSError(errno.EACCES)
|
160
|
+
|
161
|
+
def rmdir(self, path):
|
162
|
+
raise FuseOSError(errno.EACCES)
|
163
|
+
|
164
|
+
def mkdir(self, path, mode):
|
165
|
+
raise FuseOSError(errno.EACCES)
|
166
|
+
|
167
|
+
def unlink(self, path):
|
168
|
+
raise FuseOSError(errno.EACCES)
|
169
|
+
|
170
|
+
def symlink(self, target, source):
|
171
|
+
raise FuseOSError(errno.EACCES)
|
172
|
+
|
173
|
+
def rename(self, old, new):
|
174
|
+
raise FuseOSError(errno.EACCES)
|
175
|
+
|
176
|
+
def link(self, target, source):
|
177
|
+
raise FuseOSError(errno.EACCES)
|
178
|
+
|
179
|
+
def utimens(self, path, times=None):
|
180
|
+
raise FuseOSError(errno.EACCES)
|
181
|
+
|
182
|
+
def create(self, path, mode, fi=None):
|
183
|
+
raise FuseOSError(errno.EACCES)
|
184
|
+
|
185
|
+
def write(self, path, data, offset, fh):
|
186
|
+
raise FuseOSError(errno.EACCES)
|
187
|
+
|
188
|
+
def truncate(self, path, length, fh=None):
|
189
|
+
raise FuseOSError(errno.EACCES)
|
190
|
+
|
191
|
+
def flush(self, path, fh):
|
192
|
+
raise FuseOSError(errno.EACCES)
|
193
|
+
|
194
|
+
def release(self, path, fh):
|
195
|
+
raise FuseOSError(errno.EACCES)
|
196
|
+
|
197
|
+
def fsync(self, path, datasync, fh):
|
198
|
+
raise FuseOSError(errno.EACCES)
|
@@ -0,0 +1 @@
|
|
1
|
+
# Note: this directory is purely for libraries to be directly included instead of as dependencies
|