lamindb_setup 0.69.5__py2.py3-none-any.whl → 0.71.0__py2.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.
- lamindb_setup/__init__.py +15 -15
- lamindb_setup/_add_remote_storage.py +22 -33
- lamindb_setup/_cache.py +4 -1
- lamindb_setup/_check.py +3 -0
- lamindb_setup/_check_setup.py +13 -7
- lamindb_setup/_close.py +2 -0
- lamindb_setup/_connect_instance.py +50 -34
- lamindb_setup/_delete.py +121 -22
- lamindb_setup/_django.py +4 -1
- lamindb_setup/_exportdb.py +4 -2
- lamindb_setup/_importdb.py +5 -1
- lamindb_setup/_init_instance.py +58 -46
- lamindb_setup/_migrate.py +20 -14
- lamindb_setup/_register_instance.py +10 -3
- lamindb_setup/_schema.py +6 -3
- lamindb_setup/_setup_user.py +8 -8
- lamindb_setup/_silence_loggers.py +4 -2
- lamindb_setup/core/__init__.py +4 -3
- lamindb_setup/core/_aws_storage.py +3 -0
- lamindb_setup/core/_deprecated.py +2 -7
- lamindb_setup/core/_docs.py +2 -0
- lamindb_setup/core/_hub_client.py +12 -10
- lamindb_setup/core/_hub_core.py +206 -85
- lamindb_setup/core/_hub_crud.py +15 -11
- lamindb_setup/core/_hub_utils.py +11 -8
- lamindb_setup/core/_settings.py +23 -26
- lamindb_setup/core/_settings_instance.py +164 -42
- lamindb_setup/core/_settings_load.py +13 -8
- lamindb_setup/core/_settings_save.py +11 -8
- lamindb_setup/core/_settings_storage.py +104 -95
- lamindb_setup/core/_settings_store.py +3 -2
- lamindb_setup/core/_settings_user.py +10 -6
- lamindb_setup/core/_setup_bionty_sources.py +9 -2
- lamindb_setup/core/cloud_sqlite_locker.py +13 -10
- lamindb_setup/core/django.py +5 -0
- lamindb_setup/core/exceptions.py +4 -2
- lamindb_setup/core/hashing.py +15 -5
- lamindb_setup/core/types.py +5 -2
- lamindb_setup/core/upath.py +202 -92
- {lamindb_setup-0.69.5.dist-info → lamindb_setup-0.71.0.dist-info}/METADATA +6 -4
- lamindb_setup-0.71.0.dist-info/RECORD +43 -0
- lamindb_setup-0.69.5.dist-info/RECORD +0 -43
- {lamindb_setup-0.69.5.dist-info → lamindb_setup-0.71.0.dist-info}/LICENSE +0 -0
- {lamindb_setup-0.69.5.dist-info → lamindb_setup-0.71.0.dist-info}/WHEEL +0 -0
|
@@ -1,20 +1,26 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import os
|
|
2
4
|
import shutil
|
|
3
5
|
from pathlib import Path
|
|
4
|
-
from typing import
|
|
5
|
-
|
|
6
|
+
from typing import TYPE_CHECKING, Literal
|
|
7
|
+
|
|
8
|
+
from lamin_utils import logger
|
|
9
|
+
|
|
6
10
|
from ._hub_client import call_with_fallback
|
|
7
11
|
from ._hub_crud import select_account_handle_name_by_lnid
|
|
8
|
-
from
|
|
12
|
+
from ._hub_utils import LaminDsn, LaminDsnModel
|
|
13
|
+
from ._settings_save import save_instance_settings
|
|
14
|
+
from ._settings_storage import StorageSettings, init_storage, mark_storage_root
|
|
15
|
+
from ._settings_store import current_instance_settings_file, instance_settings_file
|
|
9
16
|
from .cloud_sqlite_locker import (
|
|
10
|
-
InstanceLockedException,
|
|
11
17
|
EXPIRATION_TIME,
|
|
18
|
+
InstanceLockedException,
|
|
12
19
|
)
|
|
13
|
-
from .
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
from
|
|
17
|
-
from .upath import UPath
|
|
20
|
+
from .upath import LocalPathClasses, UPath, convert_pathlike
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from uuid import UUID
|
|
18
24
|
|
|
19
25
|
|
|
20
26
|
def sanitize_git_repo_url(repo_url: str) -> str:
|
|
@@ -31,22 +37,28 @@ class InstanceSettings:
|
|
|
31
37
|
owner: str, # owner handle
|
|
32
38
|
name: str, # instance name
|
|
33
39
|
storage: StorageSettings, # storage location
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
40
|
+
keep_artifacts_local: bool = False, # default to local storage
|
|
41
|
+
uid: str | None = None, # instance uid/lnid
|
|
42
|
+
db: str | None = None, # DB URI
|
|
43
|
+
schema: str | None = None, # comma-separated string of schema names
|
|
44
|
+
git_repo: str | None = None, # a git repo URL
|
|
45
|
+
is_on_hub: bool = False, # initialized from hub
|
|
38
46
|
):
|
|
39
47
|
from ._hub_utils import validate_db_arg
|
|
40
48
|
|
|
41
|
-
self.
|
|
49
|
+
self._id_: UUID = id
|
|
42
50
|
self._owner: str = owner
|
|
43
51
|
self._name: str = name
|
|
44
|
-
self._uid:
|
|
52
|
+
self._uid: str | None = uid
|
|
45
53
|
self._storage: StorageSettings = storage
|
|
46
54
|
validate_db_arg(db)
|
|
47
|
-
self._db:
|
|
48
|
-
self._schema_str:
|
|
55
|
+
self._db: str | None = db
|
|
56
|
+
self._schema_str: str | None = schema
|
|
49
57
|
self._git_repo = None if git_repo is None else sanitize_git_repo_url(git_repo)
|
|
58
|
+
# local storage
|
|
59
|
+
self._keep_artifacts_local = keep_artifacts_local
|
|
60
|
+
self._storage_local: StorageSettings | None = None
|
|
61
|
+
self._is_on_hub = is_on_hub
|
|
50
62
|
|
|
51
63
|
def __repr__(self):
|
|
52
64
|
"""Rich string representation."""
|
|
@@ -85,14 +97,111 @@ class InstanceSettings:
|
|
|
85
97
|
"""Instance name."""
|
|
86
98
|
return self._name
|
|
87
99
|
|
|
100
|
+
def _search_local_root(
|
|
101
|
+
self, local_root: str | None = None, mute_warning: bool = False
|
|
102
|
+
) -> StorageSettings | None:
|
|
103
|
+
from lnschema_core.models import Storage
|
|
104
|
+
|
|
105
|
+
if local_root is not None:
|
|
106
|
+
local_records = Storage.objects.filter(root=local_root)
|
|
107
|
+
else:
|
|
108
|
+
local_records = Storage.objects.filter(type="local")
|
|
109
|
+
found = False
|
|
110
|
+
for record in local_records.all():
|
|
111
|
+
root_path = Path(record.root)
|
|
112
|
+
if root_path.exists():
|
|
113
|
+
marker_path = root_path / ".lamindb/_is_initialized"
|
|
114
|
+
if marker_path.exists():
|
|
115
|
+
uid = marker_path.read_text()
|
|
116
|
+
if uid == record.uid:
|
|
117
|
+
found = True
|
|
118
|
+
break
|
|
119
|
+
elif uid == "":
|
|
120
|
+
# legacy instance that was not yet marked properly
|
|
121
|
+
mark_storage_root(record.root, record.uid)
|
|
122
|
+
else:
|
|
123
|
+
continue
|
|
124
|
+
else:
|
|
125
|
+
# legacy instance that was not yet marked at all
|
|
126
|
+
mark_storage_root(record.root, record.uid)
|
|
127
|
+
break
|
|
128
|
+
if found:
|
|
129
|
+
return StorageSettings(record.root)
|
|
130
|
+
elif not mute_warning:
|
|
131
|
+
logger.warning(
|
|
132
|
+
f"none of the registered local storage locations were found in your environment: {local_records}"
|
|
133
|
+
"\n❗ please register a new local storage location via `ln.settings.storage_local = local_root_path` "
|
|
134
|
+
"and re-load/connect the instance"
|
|
135
|
+
)
|
|
136
|
+
return None
|
|
137
|
+
|
|
138
|
+
@property
|
|
139
|
+
def keep_artifacts_local(self) -> bool:
|
|
140
|
+
"""Default to keeping artifacts local.
|
|
141
|
+
|
|
142
|
+
Enable this optional setting for cloud instances on lamin.ai.
|
|
143
|
+
|
|
144
|
+
Guide: :doc:`faq/keep-artifacts-local`
|
|
145
|
+
"""
|
|
146
|
+
return self._keep_artifacts_local
|
|
147
|
+
|
|
88
148
|
@property
|
|
89
|
-
def
|
|
90
|
-
"""
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
149
|
+
def storage(self) -> StorageSettings:
|
|
150
|
+
"""Default storage.
|
|
151
|
+
|
|
152
|
+
For a cloud instance, this is cloud storage. For a local instance, this
|
|
153
|
+
is a local directory.
|
|
154
|
+
"""
|
|
155
|
+
return self._storage
|
|
156
|
+
|
|
157
|
+
@property
|
|
158
|
+
def storage_local(self) -> StorageSettings:
|
|
159
|
+
"""An additional local default storage.
|
|
160
|
+
|
|
161
|
+
Is only available if :attr:`keep_artifacts_local` is enabled.
|
|
162
|
+
|
|
163
|
+
Guide: :doc:`faq/keep-artifacts-local`
|
|
164
|
+
"""
|
|
165
|
+
if not self._keep_artifacts_local:
|
|
166
|
+
raise ValueError("`keep_artifacts_local` is not enabled for this instance.")
|
|
167
|
+
if self._storage_local is None:
|
|
168
|
+
self._storage_local = self._search_local_root()
|
|
169
|
+
if self._storage_local is None:
|
|
170
|
+
# raise an error, there was a warning just before in search_local_root
|
|
171
|
+
raise ValueError()
|
|
172
|
+
return self._storage_local
|
|
173
|
+
|
|
174
|
+
@storage_local.setter
|
|
175
|
+
def storage_local(self, local_root: Path | str):
|
|
176
|
+
from lamindb_setup._init_instance import register_storage_in_instance
|
|
177
|
+
|
|
178
|
+
local_root = Path(local_root)
|
|
179
|
+
if not self._keep_artifacts_local:
|
|
180
|
+
raise ValueError("`keep_artifacts_local` is not enabled for this instance.")
|
|
181
|
+
storage_local = self._search_local_root(
|
|
182
|
+
local_root=StorageSettings(local_root).root_as_str, mute_warning=True
|
|
94
183
|
)
|
|
95
|
-
|
|
184
|
+
if storage_local is not None:
|
|
185
|
+
# great, we're merely switching storage location
|
|
186
|
+
self._storage_local = storage_local
|
|
187
|
+
logger.important(f"defaulting to local storage: {storage_local.root}")
|
|
188
|
+
return None
|
|
189
|
+
storage_local = self._search_local_root(mute_warning=True)
|
|
190
|
+
if storage_local is not None:
|
|
191
|
+
if os.getenv("LAMIN_TESTING") == "true":
|
|
192
|
+
response = "y"
|
|
193
|
+
else:
|
|
194
|
+
response = input(
|
|
195
|
+
"You already configured a local storage root for this instance in this"
|
|
196
|
+
f" environment: {self.storage_local.root}\nDo you want to register another one? (y/n)"
|
|
197
|
+
)
|
|
198
|
+
if response != "y":
|
|
199
|
+
return None
|
|
200
|
+
local_root = convert_pathlike(local_root)
|
|
201
|
+
assert isinstance(local_root, LocalPathClasses)
|
|
202
|
+
self._storage_local = init_storage(local_root, self._id, register_hub=True) # type: ignore
|
|
203
|
+
register_storage_in_instance(self._storage_local) # type: ignore
|
|
204
|
+
logger.important(f"defaulting to local storage: {self._storage_local.root}")
|
|
96
205
|
|
|
97
206
|
@property
|
|
98
207
|
def slug(self) -> str:
|
|
@@ -100,7 +209,7 @@ class InstanceSettings:
|
|
|
100
209
|
return f"{self.owner}/{self.name}"
|
|
101
210
|
|
|
102
211
|
@property
|
|
103
|
-
def git_repo(self) ->
|
|
212
|
+
def git_repo(self) -> str | None:
|
|
104
213
|
"""Sync transforms with scripts in git repository.
|
|
105
214
|
|
|
106
215
|
Provide the full git repo URL.
|
|
@@ -108,17 +217,19 @@ class InstanceSettings:
|
|
|
108
217
|
return self._git_repo
|
|
109
218
|
|
|
110
219
|
@property
|
|
111
|
-
def
|
|
220
|
+
def _id(self) -> UUID:
|
|
112
221
|
"""The internal instance id."""
|
|
113
|
-
return self.
|
|
222
|
+
return self._id_
|
|
114
223
|
|
|
115
224
|
@property
|
|
116
|
-
def uid(self) ->
|
|
225
|
+
def uid(self) -> str:
|
|
117
226
|
"""The user-facing instance id."""
|
|
118
|
-
|
|
227
|
+
from .hashing import hash_and_encode_as_b62
|
|
228
|
+
|
|
229
|
+
return hash_and_encode_as_b62(self._id.hex)[:12]
|
|
119
230
|
|
|
120
231
|
@property
|
|
121
|
-
def schema(self) ->
|
|
232
|
+
def schema(self) -> set[str]:
|
|
122
233
|
"""Schema modules in addition to core schema."""
|
|
123
234
|
if self._schema_str is None:
|
|
124
235
|
return {} # type: ignore
|
|
@@ -128,7 +239,7 @@ class InstanceSettings:
|
|
|
128
239
|
@property
|
|
129
240
|
def _sqlite_file(self) -> UPath:
|
|
130
241
|
"""SQLite file."""
|
|
131
|
-
return self.storage.key_to_filepath(f"{self.
|
|
242
|
+
return self.storage.key_to_filepath(f"{self._id.hex}.lndb")
|
|
132
243
|
|
|
133
244
|
@property
|
|
134
245
|
def _sqlite_file_local(self) -> Path:
|
|
@@ -211,16 +322,12 @@ class InstanceSettings:
|
|
|
211
322
|
assert self._db.startswith("postgresql"), f"Unexpected DB value: {self._db}"
|
|
212
323
|
return "postgresql"
|
|
213
324
|
|
|
214
|
-
@property
|
|
215
|
-
def session(self):
|
|
216
|
-
raise NotImplementedError
|
|
217
|
-
|
|
218
325
|
@property
|
|
219
326
|
def _is_cloud_sqlite(self) -> bool:
|
|
220
327
|
# can we make this a private property, Sergei?
|
|
221
328
|
# as it's not relevant to the user
|
|
222
329
|
"""Is this a cloud instance with sqlite db."""
|
|
223
|
-
return self.dialect == "sqlite" and self.storage.
|
|
330
|
+
return self.dialect == "sqlite" and self.storage.type_is_cloud
|
|
224
331
|
|
|
225
332
|
@property
|
|
226
333
|
def _cloud_sqlite_locker(self):
|
|
@@ -236,15 +343,10 @@ class InstanceSettings:
|
|
|
236
343
|
else:
|
|
237
344
|
return empty_locker
|
|
238
345
|
|
|
239
|
-
@property
|
|
240
|
-
def storage(self) -> StorageSettings:
|
|
241
|
-
"""Low-level access to storage location."""
|
|
242
|
-
return self._storage
|
|
243
|
-
|
|
244
346
|
@property
|
|
245
347
|
def is_remote(self) -> bool:
|
|
246
348
|
"""Boolean indicating if an instance has no local component."""
|
|
247
|
-
if not self.storage.
|
|
349
|
+
if not self.storage.type_is_cloud:
|
|
248
350
|
return False
|
|
249
351
|
|
|
250
352
|
def is_local_uri(uri: str):
|
|
@@ -263,6 +365,25 @@ class InstanceSettings:
|
|
|
263
365
|
# and remote postgres
|
|
264
366
|
return True
|
|
265
367
|
|
|
368
|
+
@property
|
|
369
|
+
def is_on_hub(self) -> bool:
|
|
370
|
+
"""Is this instance on the hub.
|
|
371
|
+
|
|
372
|
+
Only works if user has access to the instance.
|
|
373
|
+
"""
|
|
374
|
+
if self._is_on_hub is None:
|
|
375
|
+
from ._hub_client import call_with_fallback_auth
|
|
376
|
+
from ._hub_crud import select_instance_by_id
|
|
377
|
+
|
|
378
|
+
response = call_with_fallback_auth(
|
|
379
|
+
select_instance_by_id, instance_id=self._id.hex
|
|
380
|
+
)
|
|
381
|
+
if response is None:
|
|
382
|
+
self._is_on_hub = False
|
|
383
|
+
else:
|
|
384
|
+
self._is_on_hub = True
|
|
385
|
+
return self._is_on_hub
|
|
386
|
+
|
|
266
387
|
def _get_settings_file(self) -> Path:
|
|
267
388
|
return instance_settings_file(self.name, self.owner)
|
|
268
389
|
|
|
@@ -287,7 +408,7 @@ class InstanceSettings:
|
|
|
287
408
|
|
|
288
409
|
def _load_db(
|
|
289
410
|
self, do_not_lock_for_laminapp_admin: bool = False
|
|
290
|
-
) ->
|
|
411
|
+
) -> tuple[bool, str]:
|
|
291
412
|
# Is the database available and initialized as LaminDB?
|
|
292
413
|
# returns a tuple of status code and message
|
|
293
414
|
if self.dialect == "sqlite" and not self._sqlite_file.exists():
|
|
@@ -299,6 +420,7 @@ class InstanceSettings:
|
|
|
299
420
|
)
|
|
300
421
|
return False, f"SQLite file {self._sqlite_file} does not exist"
|
|
301
422
|
from lamindb_setup import settings # to check user
|
|
423
|
+
|
|
302
424
|
from .django import setup_django
|
|
303
425
|
|
|
304
426
|
# lock in all cases except if do_not_lock_for_laminapp_admin is True and
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
from
|
|
2
|
-
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Optional
|
|
3
4
|
from uuid import UUID, uuid4
|
|
5
|
+
|
|
4
6
|
from lamin_utils import logger
|
|
5
7
|
from pydantic.error_wrappers import ValidationError
|
|
6
8
|
|
|
@@ -14,12 +16,15 @@ from ._settings_store import (
|
|
|
14
16
|
)
|
|
15
17
|
from ._settings_user import UserSettings
|
|
16
18
|
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
17
22
|
|
|
18
23
|
class SettingsEnvFileOutdated(Exception):
|
|
19
24
|
pass
|
|
20
25
|
|
|
21
26
|
|
|
22
|
-
def load_instance_settings(instance_settings_file:
|
|
27
|
+
def load_instance_settings(instance_settings_file: Path | None = None):
|
|
23
28
|
if instance_settings_file is None:
|
|
24
29
|
instance_settings_file = current_instance_settings_file()
|
|
25
30
|
if not instance_settings_file.exists():
|
|
@@ -33,10 +38,10 @@ def load_instance_settings(instance_settings_file: Optional[Path] = None):
|
|
|
33
38
|
f"\n\n{error}\n\nYour instance settings file with\n\n{content}\nis invalid"
|
|
34
39
|
f" (likely outdated), please delete {instance_settings_file} &"
|
|
35
40
|
" re-initialize (local) or re-connect to the instance (remote)"
|
|
36
|
-
)
|
|
41
|
+
) from error
|
|
37
42
|
if settings_store.id == "null":
|
|
38
43
|
raise ValueError(
|
|
39
|
-
"Your instance.
|
|
44
|
+
"Your instance._id is undefined, please either load your instance from the"
|
|
40
45
|
f" hub or update {instance_settings_file} with a new id: {uuid4().hex}"
|
|
41
46
|
)
|
|
42
47
|
isettings = setup_instance_from_store(settings_store)
|
|
@@ -60,13 +65,13 @@ def load_or_create_user_settings() -> UserSettings:
|
|
|
60
65
|
def load_user_settings(user_settings_file: Path):
|
|
61
66
|
try:
|
|
62
67
|
settings_store = UserSettingsStore(_env_file=user_settings_file)
|
|
63
|
-
except (ValidationError, TypeError):
|
|
68
|
+
except (ValidationError, TypeError) as error:
|
|
64
69
|
msg = (
|
|
65
70
|
"Your user settings file is invalid, please delete"
|
|
66
71
|
f" {user_settings_file} and log in again."
|
|
67
72
|
)
|
|
68
73
|
print(msg)
|
|
69
|
-
raise SettingsEnvFileOutdated(msg)
|
|
74
|
+
raise SettingsEnvFileOutdated(msg) from error
|
|
70
75
|
settings = setup_user_from_store(settings_store)
|
|
71
76
|
return settings
|
|
72
77
|
|
|
@@ -95,5 +100,5 @@ def setup_user_from_store(store: UserSettingsStore) -> UserSettings:
|
|
|
95
100
|
settings.uid = store.uid
|
|
96
101
|
settings.handle = store.handle if store.handle != "null" else "anonymous"
|
|
97
102
|
settings.name = store.name if store.name != "null" else None
|
|
98
|
-
settings.
|
|
103
|
+
settings._uuid = UUID(store.uuid) if store.uuid != "null" else None
|
|
99
104
|
return settings
|
|
@@ -1,17 +1,20 @@
|
|
|
1
|
-
from
|
|
1
|
+
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import TYPE_CHECKING, Any, Optional, get_type_hints
|
|
4
5
|
from uuid import UUID
|
|
5
6
|
|
|
6
7
|
from ._settings_store import (
|
|
7
|
-
UserSettingsStore,
|
|
8
8
|
InstanceSettingsStore,
|
|
9
|
+
UserSettingsStore,
|
|
9
10
|
current_user_settings_file,
|
|
10
11
|
user_settings_file_email,
|
|
11
12
|
user_settings_file_handle,
|
|
12
13
|
)
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from ._settings_user import UserSettings
|
|
17
|
+
from .upath import UPath
|
|
15
18
|
|
|
16
19
|
|
|
17
20
|
def save_user_settings(settings: UserSettings):
|
|
@@ -31,7 +34,7 @@ def save_user_settings(settings: UserSettings):
|
|
|
31
34
|
def save_settings(
|
|
32
35
|
settings: Any,
|
|
33
36
|
settings_file: Path,
|
|
34
|
-
type_hints:
|
|
37
|
+
type_hints: dict[str, Any],
|
|
35
38
|
prefix: str,
|
|
36
39
|
):
|
|
37
40
|
with open(settings_file, "w") as f:
|
|
@@ -44,7 +47,7 @@ def save_settings(
|
|
|
44
47
|
elif store_key == "storage_region":
|
|
45
48
|
value = settings.storage.region
|
|
46
49
|
else:
|
|
47
|
-
if store_key in {"db", "schema_str", "name_"}:
|
|
50
|
+
if store_key in {"db", "schema_str", "name_", "uuid", "id"}:
|
|
48
51
|
settings_key = f"_{store_key.rstrip('_')}"
|
|
49
52
|
else:
|
|
50
53
|
settings_key = store_key
|
|
@@ -65,7 +68,7 @@ def save_instance_settings(settings: Any, settings_file: Path):
|
|
|
65
68
|
|
|
66
69
|
|
|
67
70
|
def save_system_storage_settings(
|
|
68
|
-
cache_path:
|
|
71
|
+
cache_path: str | Path | UPath | None, settings_file: Path
|
|
69
72
|
):
|
|
70
73
|
cache_path = "null" if cache_path is None else cache_path
|
|
71
74
|
if isinstance(cache_path, Path): # also True for UPath
|