lamindb_setup 0.71.3__tar.gz → 0.72.0__tar.gz

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.
Files changed (90) hide show
  1. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/PKG-INFO +1 -1
  2. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/docs/changelog.md +8 -1
  3. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/docs/hub-cloud/03-add-managed-storage.ipynb +22 -1
  4. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/docs/hub-cloud/05-init-hosted-instance.ipynb +1 -1
  5. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/lamindb_setup/__init__.py +1 -1
  6. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/lamindb_setup/_connect_instance.py +78 -61
  7. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/lamindb_setup/_delete.py +10 -49
  8. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/lamindb_setup/_init_instance.py +18 -17
  9. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/lamindb_setup/_set_managed_storage.py +4 -0
  10. lamindb_setup-0.72.0/lamindb_setup/core/_aws_credentials.py +140 -0
  11. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/lamindb_setup/core/_hub_core.py +21 -11
  12. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/lamindb_setup/core/_settings_instance.py +16 -17
  13. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/lamindb_setup/core/_settings_storage.py +18 -2
  14. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/lamindb_setup/core/hashing.py +23 -20
  15. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/lamindb_setup/core/upath.py +4 -68
  16. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/tests/hub-prod/conftest.py +2 -2
  17. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/.github/workflows/build.yml +0 -0
  18. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/.github/workflows/latest-changes.jinja2 +0 -0
  19. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/.github/workflows/latest-changes.yml +0 -0
  20. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/.gitignore +0 -0
  21. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/.pre-commit-config.yaml +0 -0
  22. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/LICENSE +0 -0
  23. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/README.md +0 -0
  24. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/docs/hub-cloud/01-init-local-instance.ipynb +0 -0
  25. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/docs/hub-cloud/02-connect-local-instance.ipynb +0 -0
  26. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/docs/hub-cloud/04-test-bionty.ipynb +0 -0
  27. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/docs/hub-cloud/06-connect-hosted-instance.ipynb +0 -0
  28. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/docs/hub-cloud/07-keep-artifacts-local.ipynb +0 -0
  29. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/docs/hub-cloud/test-multi-session.ipynb +0 -0
  30. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/docs/hub-cloud/test_notebooks.py +0 -0
  31. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/docs/hub-prod/test-cache-management.ipynb +0 -0
  32. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/docs/hub-prod/test-cloud-sync.ipynb +0 -0
  33. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/docs/hub-prod/test-connect-anonymously.ipynb +0 -0
  34. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/docs/hub-prod/test-empty-init.ipynb +0 -0
  35. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/docs/hub-prod/test-import-schema.ipynb +0 -0
  36. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/docs/hub-prod/test-insufficient-user-info.ipynb +0 -0
  37. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/docs/hub-prod/test-invalid-schema.ipynb +0 -0
  38. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/docs/hub-prod/test-sqlite-lock.ipynb +0 -0
  39. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/docs/hub-prod/test_notebooks2.py +0 -0
  40. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/docs/index.md +0 -0
  41. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/docs/notebooks.md +0 -0
  42. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/docs/reference.md +0 -0
  43. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/lamindb_setup/_cache.py +0 -0
  44. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/lamindb_setup/_check.py +0 -0
  45. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/lamindb_setup/_check_setup.py +0 -0
  46. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/lamindb_setup/_close.py +0 -0
  47. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/lamindb_setup/_django.py +0 -0
  48. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/lamindb_setup/_exportdb.py +0 -0
  49. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/lamindb_setup/_importdb.py +0 -0
  50. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/lamindb_setup/_migrate.py +0 -0
  51. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/lamindb_setup/_register_instance.py +0 -0
  52. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/lamindb_setup/_schema.py +0 -0
  53. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/lamindb_setup/_setup_user.py +0 -0
  54. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/lamindb_setup/_silence_loggers.py +0 -0
  55. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/lamindb_setup/core/__init__.py +0 -0
  56. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/lamindb_setup/core/_aws_storage.py +0 -0
  57. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/lamindb_setup/core/_deprecated.py +0 -0
  58. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/lamindb_setup/core/_docs.py +0 -0
  59. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/lamindb_setup/core/_hub_client.py +0 -0
  60. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/lamindb_setup/core/_hub_crud.py +0 -0
  61. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/lamindb_setup/core/_hub_utils.py +0 -0
  62. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/lamindb_setup/core/_settings.py +0 -0
  63. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/lamindb_setup/core/_settings_load.py +0 -0
  64. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/lamindb_setup/core/_settings_save.py +0 -0
  65. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/lamindb_setup/core/_settings_store.py +0 -0
  66. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/lamindb_setup/core/_settings_user.py +0 -0
  67. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/lamindb_setup/core/_setup_bionty_sources.py +0 -0
  68. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/lamindb_setup/core/cloud_sqlite_locker.py +0 -0
  69. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/lamindb_setup/core/django.py +0 -0
  70. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/lamindb_setup/core/exceptions.py +0 -0
  71. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/lamindb_setup/core/types.py +0 -0
  72. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/noxfile.py +0 -0
  73. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/pyproject.toml +0 -0
  74. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/tests/hub-cloud/test_connect_instance.py +0 -0
  75. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/tests/hub-cloud/test_delete_instance.py +0 -0
  76. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/tests/hub-cloud/test_init_instance.py +0 -0
  77. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/tests/hub-cloud/test_login.py +0 -0
  78. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/tests/hub-cloud/test_migrate.py +0 -0
  79. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/tests/hub-cloud/test_set_storage.py +0 -0
  80. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/tests/hub-local/conftest.py +0 -0
  81. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/tests/hub-local/test_all.py +0 -0
  82. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/tests/hub-prod/test_auto_connect.py +0 -0
  83. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/tests/hub-prod/test_django.py +0 -0
  84. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/tests/hub-prod/test_switch_and_fallback_env.py +0 -0
  85. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/tests/hub-prod/test_upath.py +0 -0
  86. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/tests/storage/test_hashing.py +0 -0
  87. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/tests/storage/test_storage_access.py +0 -0
  88. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/tests/storage/test_storage_basis.py +0 -0
  89. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/tests/storage/test_storage_stats.py +0 -0
  90. {lamindb_setup-0.71.3 → lamindb_setup-0.72.0}/tests/storage/test_to_url.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: lamindb_setup
3
- Version: 0.71.3
3
+ Version: 0.72.0
4
4
  Summary: Setup & configure LaminDB.
5
5
  Author-email: Lamin Labs <laminlabs@gmail.com>
6
6
  Description-Content-Type: text/markdown
@@ -3,7 +3,14 @@
3
3
  <!-- prettier-ignore -->
4
4
  Name | PR | Developer | Date | Version
5
5
  --- | --- | --- | --- | ---
6
- ♻️ Better treatment of current_user_id | [754](https://github.com/laminlabs/lamindb-setup/pull/754) | [falexwolf](https://github.com/falexwolf) | 2024-05-06 | 0.72.2
6
+ 🐛 Check empty after storage record root init in delete | [763](https://github.com/laminlabs/lamindb-setup/pull/763) | [Koncopd](https://github.com/Koncopd) | 2024-05-18 |
7
+ ✨ Call `access_aws` for all paths and cache | [762](https://github.com/laminlabs/lamindb-setup/pull/762) | [Koncopd](https://github.com/Koncopd) | 2024-05-18 |
8
+ ⚡️ Speed-up file hash | [761](https://github.com/laminlabs/lamindb-setup/pull/761) | [Koncopd](https://github.com/Koncopd) | 2024-05-11 | 0.71.4
9
+ ♻️ Account for public storage locations | [758](https://github.com/laminlabs/lamindb-setup/pull/758) | [falexwolf](https://github.com/falexwolf) | 2024-05-10 |
10
+ ♻️ Make init_instance_hub idempotent | [757](https://github.com/laminlabs/lamindb-setup/pull/757) | [falexwolf](https://github.com/falexwolf) | 2024-05-09 |
11
+ ♻️ Refactor delete & connect | [756](https://github.com/laminlabs/lamindb-setup/pull/756) | [falexwolf](https://github.com/falexwolf) | 2024-05-09 |
12
+ 🔥 Remove laminapp-admin logic | [755](https://github.com/laminlabs/lamindb-setup/pull/755) | [falexwolf](https://github.com/falexwolf) | 2024-05-08 |
13
+ ♻️ Better treatment of current_user_id | [754](https://github.com/laminlabs/lamindb-setup/pull/754) | [falexwolf](https://github.com/falexwolf) | 2024-05-06 | 0.71.2
7
14
  💚 Fix tests | [753](https://github.com/laminlabs/lamindb-setup/pull/753) | [fredericenard](https://github.com/fredericenard) | 2024-05-06 |
8
15
  ♻️ Actually use local db | [751](https://github.com/laminlabs/lamindb-setup/pull/751) | [falexwolf](https://github.com/falexwolf) | 2024-05-06 | 0.71.2
9
16
  ✏️ Restore view_tree py3.9 compatibility | [750](https://github.com/laminlabs/lamindb-setup/pull/750) | [sunnyosun](https://github.com/sunnyosun) | 2024-05-06 |
@@ -255,7 +255,8 @@
255
255
  "# root.fs contains the underlying fsspec filesystem\n",
256
256
  "assert (\n",
257
257
  " ln_setup.settings.storage.root.fs.cache_regions # set by lamindb to True for s3 by default\n",
258
- ")"
258
+ ")\n",
259
+ "assert ln_setup.settings.storage._instance_id is not None"
259
260
  ]
260
261
  },
261
262
  {
@@ -287,6 +288,26 @@
287
288
  "assert not ln_setup.settings.storage.root.fs.cache_regions"
288
289
  ]
289
290
  },
291
+ {
292
+ "cell_type": "markdown",
293
+ "id": "015b8f36",
294
+ "metadata": {},
295
+ "source": [
296
+ "Cloud storage with mere read access:"
297
+ ]
298
+ },
299
+ {
300
+ "cell_type": "code",
301
+ "execution_count": null,
302
+ "id": "4ef50cab",
303
+ "metadata": {},
304
+ "outputs": [],
305
+ "source": [
306
+ "with pytest.raises(ValueError) as error:\n",
307
+ " set_managed_storage(\"gs://rxrx1-europe-west4/images/test/HEPG2-08\")\n",
308
+ "assert error.exconly().startswith(\"ValueError: Cannot manage storage without write access\")"
309
+ ]
310
+ },
290
311
  {
291
312
  "cell_type": "markdown",
292
313
  "id": "83a2ee6b",
@@ -81,7 +81,7 @@
81
81
  "metadata": {},
82
82
  "outputs": [],
83
83
  "source": [
84
- "from lamindb_setup.core.upath import HOSTED_BUCKETS"
84
+ "from lamindb_setup.core._aws_credentials import HOSTED_BUCKETS"
85
85
  ]
86
86
  },
87
87
  {
@@ -34,7 +34,7 @@ Modules & settings:
34
34
 
35
35
  """
36
36
 
37
- __version__ = "0.71.3" # denote a release candidate for 0.1.0 with 0.1rc1
37
+ __version__ = "0.72.0" # denote a release candidate for 0.1.0 with 0.1rc1
38
38
 
39
39
  import sys
40
40
  from os import name as _os_name
@@ -55,7 +55,10 @@ def check_db_dsn_equal_up_to_credentials(db_dsn_hub, db_dsn_local):
55
55
 
56
56
 
57
57
  def update_db_using_local(
58
- hub_instance_result: dict[str, str], settings_file: Path, db: str | None = None
58
+ hub_instance_result: dict[str, str],
59
+ settings_file: Path,
60
+ db: str | None = None,
61
+ raise_permission_error=True,
59
62
  ) -> str | None:
60
63
  db_updated = None
61
64
  # check if postgres
@@ -77,7 +80,11 @@ def update_db_using_local(
77
80
  db_dsn_local = LaminDsnModel(db=isettings.db)
78
81
  else:
79
82
  # just take the default hub result and ensure there is actually a user
80
- if db_dsn_hub.db.user == "none" and db_dsn_hub.db.password == "none":
83
+ if (
84
+ db_dsn_hub.db.user == "none"
85
+ and db_dsn_hub.db.password == "none"
86
+ and raise_permission_error
87
+ ):
81
88
  raise PermissionError(
82
89
  "No database access, please ask your admin to provide you with"
83
90
  " a DB URL and pass it via --db <db_url>"
@@ -101,6 +108,65 @@ def update_db_using_local(
101
108
  return db_updated
102
109
 
103
110
 
111
+ def _connect_instance(
112
+ owner: str,
113
+ name: str,
114
+ *,
115
+ db: str | None = None,
116
+ raise_permission_error: bool = True,
117
+ ) -> InstanceSettings:
118
+ settings_file = instance_settings_file(name, owner)
119
+ make_hub_request = True
120
+ if settings_file.exists():
121
+ isettings = load_instance_settings(settings_file)
122
+ # skip hub request for a purely local instance
123
+ make_hub_request = isettings.is_remote
124
+ if make_hub_request:
125
+ # the following will return a string if the instance does not exist
126
+ # on the hub
127
+ hub_result = load_instance_from_hub(owner=owner, name=name)
128
+ # if hub_result is not a string, it means it made a request
129
+ # that successfully returned metadata
130
+ if not isinstance(hub_result, str):
131
+ instance_result, storage_result = hub_result
132
+ db_updated = update_db_using_local(
133
+ instance_result,
134
+ settings_file,
135
+ db=db,
136
+ raise_permission_error=raise_permission_error,
137
+ )
138
+ ssettings = StorageSettings(
139
+ root=storage_result["root"],
140
+ region=storage_result["region"],
141
+ uid=storage_result["lnid"],
142
+ uuid=UUID(storage_result["id"]),
143
+ instance_id=UUID(instance_result["id"]),
144
+ )
145
+ isettings = InstanceSettings(
146
+ id=UUID(instance_result["id"]),
147
+ owner=owner,
148
+ name=name,
149
+ storage=ssettings,
150
+ db=db_updated,
151
+ schema=instance_result["schema_str"],
152
+ git_repo=instance_result["git_repo"],
153
+ keep_artifacts_local=bool(instance_result["keep_artifacts_local"]),
154
+ is_on_hub=True,
155
+ )
156
+ check_whether_migrations_in_sync(instance_result["lamindb_version"])
157
+ else:
158
+ message = INSTANCE_NOT_FOUND_MESSAGE.format(
159
+ owner=owner, name=name, hub_result=hub_result
160
+ )
161
+ if settings_file.exists():
162
+ isettings = load_instance_settings(settings_file)
163
+ if isettings.is_remote:
164
+ raise InstanceNotFoundError(message)
165
+ else:
166
+ raise InstanceNotFoundError(message)
167
+ return isettings
168
+
169
+
104
170
  @unlock_cloud_sqlite_upon_exception(ignore_prev_locker=True)
105
171
  def connect(
106
172
  slug: str,
@@ -134,71 +200,22 @@ def connect(
134
200
  elif settings._instance_exists and f"{owner}/{name}" != settings.instance.slug:
135
201
  close_instance(mute=True)
136
202
 
137
- settings_file = instance_settings_file(name, owner)
138
-
139
- make_hub_request = True
140
- if settings_file.exists():
141
- isettings = load_instance_settings(settings_file)
142
- # mimic instance_result from hub
143
- instance_result = {"id": isettings._id.hex}
144
- # skip hub request for a purely local instance
145
- make_hub_request = isettings.is_remote
146
-
147
- if make_hub_request:
148
- # the following will return a string if the instance does not exist
149
- # on the hub
150
- hub_result = load_instance_from_hub(owner=owner, name=name)
151
- # if hub_result is not a string, it means it made a request
152
- # that successfully returned metadata
153
- if not isinstance(hub_result, str):
154
- instance_result, storage_result = hub_result
155
- db_updated = update_db_using_local(
156
- instance_result, settings_file, db=db
157
- )
158
- ssettings = StorageSettings(
159
- root=storage_result["root"],
160
- region=storage_result["region"],
161
- uid=storage_result["lnid"],
162
- uuid=UUID(storage_result["id"]),
163
- instance_id=UUID(instance_result["id"]),
164
- )
165
- isettings = InstanceSettings(
166
- id=UUID(instance_result["id"]),
167
- owner=owner,
168
- name=name,
169
- storage=ssettings,
170
- db=db_updated,
171
- schema=instance_result["schema_str"],
172
- git_repo=instance_result["git_repo"],
173
- keep_artifacts_local=bool(instance_result["keep_artifacts_local"]),
174
- is_on_hub=True,
175
- )
176
- check_whether_migrations_in_sync(instance_result["lamindb_version"])
203
+ try:
204
+ isettings = _connect_instance(owner, name, db=db)
205
+ except InstanceNotFoundError as e:
206
+ if _raise_not_found_error:
207
+ raise e
177
208
  else:
178
- message = INSTANCE_NOT_FOUND_MESSAGE.format(
179
- owner=owner, name=name, hub_result=hub_result
180
- )
181
- if settings_file.exists():
182
- isettings = load_instance_settings(settings_file)
183
- if isettings.is_remote:
184
- if _raise_not_found_error:
185
- raise InstanceNotFoundError(message)
186
- return "instance-not-found"
187
-
188
- else:
189
- if _raise_not_found_error:
190
- raise InstanceNotFoundError(message)
191
- return "instance-not-found"
192
-
209
+ return "instance-not-found"
210
+ if isinstance(isettings, str):
211
+ return isettings
193
212
  if storage is not None:
194
213
  update_isettings_with_storage(isettings, storage)
195
214
  isettings._persist()
196
215
  if _test:
197
216
  return None
198
217
  silence_loggers()
199
- check, msg = isettings._load_db(
200
- do_not_lock_for_laminapp_admin=True
201
- ) # this also updates local SQLite
218
+ check, msg = isettings._load_db()
202
219
  if not check:
203
220
  local_db = (
204
221
  isettings._is_cloud_sqlite and isettings._sqlite_file_local.exists()
@@ -216,7 +233,7 @@ def connect(
216
233
  f"instance exists with id {isettings._id.hex}, but database is not"
217
234
  " loadable: re-initializing"
218
235
  )
219
- return "instance-corrupted-or-deleted", instance_result
236
+ return "instance-corrupted-or-deleted"
220
237
  # this is for testing purposes only
221
238
  if _TEST_FAILED_LOAD:
222
239
  raise RuntimeError("Technical testing error.")
@@ -1,29 +1,24 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import shutil
4
- from typing import TYPE_CHECKING, Optional
4
+ from typing import TYPE_CHECKING
5
5
  from uuid import UUID
6
6
 
7
7
  from lamin_utils import logger
8
8
 
9
- from ._connect_instance import (
10
- INSTANCE_NOT_FOUND_MESSAGE,
11
- InstanceNotFoundError,
12
- get_owner_name_from_identifier,
13
- )
14
- from .core._hub_core import connect_instance as load_instance_from_hub
9
+ from ._connect_instance import _connect_instance, get_owner_name_from_identifier
10
+ from .core._aws_credentials import HOSTED_BUCKETS
15
11
  from .core._hub_core import delete_instance as delete_instance_on_hub
16
12
  from .core._hub_core import get_storage_records_for_instance
17
13
  from .core._settings import settings
18
- from .core._settings_instance import InstanceSettings
19
- from .core._settings_load import load_instance_settings
20
14
  from .core._settings_storage import StorageSettings
21
- from .core._settings_store import instance_settings_file
22
- from .core.upath import HOSTED_BUCKETS, check_storage_is_empty
15
+ from .core.upath import check_storage_is_empty
23
16
 
24
17
  if TYPE_CHECKING:
25
18
  from pathlib import Path
26
19
 
20
+ from .core._settings_instance import InstanceSettings
21
+
27
22
 
28
23
  def delete_cache(cache_dir: Path):
29
24
  if cache_dir is not None and cache_dir.exists():
@@ -67,42 +62,8 @@ def delete(slug: str, force: bool = False, require_empty: bool = True) -> int |
67
62
  force: Whether to skip the confirmation prompt.
68
63
  require_empty: Whether to check if the instance is empty before deleting.
69
64
  """
70
- instance_owner, instance_name = get_owner_name_from_identifier(slug)
71
- if settings._instance_exists and settings.instance.name == instance_name:
72
- isettings = settings.instance
73
- else:
74
- settings_file = instance_settings_file(instance_name, instance_owner)
75
- if not settings_file.exists():
76
- hub_result = load_instance_from_hub(
77
- owner=instance_owner, name=instance_name
78
- )
79
- if isinstance(hub_result, str):
80
- message = INSTANCE_NOT_FOUND_MESSAGE.format(
81
- owner=instance_owner,
82
- name=instance_name,
83
- hub_result=hub_result,
84
- )
85
- raise InstanceNotFoundError(message)
86
- instance_result, storage_result = hub_result
87
- ssettings = StorageSettings(
88
- root=storage_result["root"],
89
- region=storage_result["region"],
90
- uid=storage_result["lnid"],
91
- uuid=UUID(storage_result["id"]),
92
- )
93
- isettings = InstanceSettings(
94
- id=UUID(instance_result["id"]),
95
- owner=instance_owner,
96
- name=instance_name,
97
- storage=ssettings,
98
- keep_artifacts_local=bool(instance_result["keep_artifacts_local"]),
99
- db=instance_result["db"] if "db" in instance_result else None,
100
- schema=instance_result["schema_str"],
101
- git_repo=instance_result["git_repo"],
102
- is_on_hub=True,
103
- )
104
- else:
105
- isettings = load_instance_settings(settings_file)
65
+ owner, name = get_owner_name_from_identifier(slug)
66
+ isettings = _connect_instance(owner, name, raise_permission_error=False)
106
67
  if isettings.dialect != "sqlite":
107
68
  logger.warning(
108
69
  f"delete() does not yet affect your Postgres database at {isettings.db}"
@@ -149,11 +110,11 @@ def delete(slug: str, force: bool = False, require_empty: bool = True) -> int |
149
110
  for storage_record in storage_records:
150
111
  if storage_record["root"] == isettings.storage.root_as_str:
151
112
  continue
113
+ ssettings = StorageSettings(storage_record["root"]) # type: ignore
152
114
  check_storage_is_empty(
153
- storage_record["root"], # type: ignore
115
+ ssettings.root, # type: ignore
154
116
  raise_error=require_empty,
155
117
  )
156
- ssettings = StorageSettings(storage_record["root"]) # type: ignore
157
118
  if ssettings._mark_storage_root.exists():
158
119
  ssettings._mark_storage_root.unlink(
159
120
  missing_ok=True # this is totally weird, but needed on Py3.11
@@ -44,15 +44,17 @@ def register_storage_in_instance(ssettings: StorageSettings):
44
44
 
45
45
  from .core.hashing import hash_and_encode_as_b62
46
46
 
47
- assert ssettings._instance_id is not None
48
-
47
+ if ssettings._instance_id is not None:
48
+ instance_uid = hash_and_encode_as_b62(ssettings._instance_id.hex)[:12]
49
+ else:
50
+ instance_uid = None
49
51
  # how do we ensure that this function is only called passing
50
52
  # the managing instance?
51
53
  defaults = {
52
54
  "root": ssettings.root_as_str,
53
55
  "type": ssettings.type,
54
56
  "region": ssettings.region,
55
- "instance_uid": hash_and_encode_as_b62(ssettings._instance_id.hex)[:12],
57
+ "instance_uid": instance_uid,
56
58
  "created_by_id": current_user_id(),
57
59
  }
58
60
  if ssettings._uid is not None:
@@ -67,20 +69,19 @@ def register_storage_in_instance(ssettings: StorageSettings):
67
69
  def register_user(usettings):
68
70
  from lnschema_core.models import User
69
71
 
70
- if usettings.handle != "laminapp-admin":
71
- try:
72
- # need to have try except because of integer primary key migration
73
- user, created = User.objects.update_or_create(
74
- uid=usettings.uid,
75
- defaults={
76
- "handle": usettings.handle,
77
- "name": usettings.name,
78
- },
79
- )
80
- # for users with only read access, except via ProgrammingError
81
- # ProgrammingError: permission denied for table lnschema_core_user
82
- except (OperationalError, FieldError, ProgrammingError):
83
- pass
72
+ try:
73
+ # need to have try except because of integer primary key migration
74
+ user, created = User.objects.update_or_create(
75
+ uid=usettings.uid,
76
+ defaults={
77
+ "handle": usettings.handle,
78
+ "name": usettings.name,
79
+ },
80
+ )
81
+ # for users with only read access, except via ProgrammingError
82
+ # ProgrammingError: permission denied for table lnschema_core_user
83
+ except (OperationalError, FieldError, ProgrammingError):
84
+ pass
84
85
 
85
86
 
86
87
  def register_user_and_storage_in_instance(isettings: InstanceSettings, usettings):
@@ -31,6 +31,10 @@ def set_managed_storage(root: UPathStr, **fs_kwargs):
31
31
  ssettings = init_storage(
32
32
  root=root, instance_id=settings.instance._id, register_hub=True
33
33
  )
34
+ if ssettings._instance_id is None:
35
+ raise ValueError(
36
+ f"Cannot manage storage without write access: {ssettings.root}"
37
+ )
34
38
  settings.instance._storage = ssettings
35
39
  settings.instance._persist() # this also updates the settings object
36
40
  register_storage_in_instance(ssettings)
@@ -0,0 +1,140 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import time
5
+
6
+ from upath.implementations.cloud import S3Path
7
+
8
+ HOSTED_REGIONS = [
9
+ "eu-central-1",
10
+ "eu-west-2",
11
+ "us-east-1",
12
+ "us-east-2",
13
+ "us-west-1",
14
+ "us-west-2",
15
+ ]
16
+ lamin_env = os.getenv("LAMIN_ENV")
17
+ if lamin_env is None or lamin_env == "prod":
18
+ hosted_buckets_list = [f"s3://lamin-{region}" for region in HOSTED_REGIONS]
19
+ hosted_buckets_list.append("s3://scverse-spatial-eu-central-1")
20
+ HOSTED_BUCKETS = tuple(hosted_buckets_list)
21
+ else:
22
+ HOSTED_BUCKETS = ("s3://lamin-hosted-test",) # type: ignore
23
+
24
+
25
+ AWS_CREDENTIALS_EXPIRATION = 11 * 60 * 60 # refresh credentials after 11 hours
26
+
27
+
28
+ class AWSCredentialsManager:
29
+ def __init__(self):
30
+ self._credentials_cache = {}
31
+
32
+ from s3fs import S3FileSystem
33
+
34
+ # this is cached so will be resued with the connection initialized
35
+ fs = S3FileSystem(cache_regions=True)
36
+ fs.connect()
37
+ self.anon = fs.session._credentials is None
38
+
39
+ def _find_root(self, path_str: str) -> str | None:
40
+ roots = self._credentials_cache.keys()
41
+ if path_str in roots:
42
+ return path_str
43
+ roots = sorted(roots, key=len, reverse=True)
44
+ for root in roots:
45
+ if path_str.startswith(root):
46
+ return root
47
+ return None
48
+
49
+ def _is_active(self, root: str) -> bool:
50
+ return (
51
+ time.time() - self._credentials_cache[root]["time"]
52
+ ) < AWS_CREDENTIALS_EXPIRATION
53
+
54
+ def _set_cached_credentials(self, root: str, credentials: dict):
55
+ if root not in self._credentials_cache:
56
+ self._credentials_cache[root] = {}
57
+ self._credentials_cache[root]["credentials"] = credentials
58
+ self._credentials_cache[root]["time"] = time.time()
59
+
60
+ def _get_cached_credentials(self, root: str) -> dict:
61
+ return self._credentials_cache[root]["credentials"]
62
+
63
+ def _path_inject_options(self, path: S3Path, credentials: dict) -> S3Path:
64
+ if credentials == {}:
65
+ # credentials were specified manually for the path
66
+ if "anon" in path._kwargs:
67
+ anon = path._kwargs["anon"]
68
+ elif path.fs.key is not None and path.fs.secret is not None:
69
+ anon = False
70
+ else:
71
+ anon = self.anon
72
+ connection_options = {"anon": anon}
73
+ else:
74
+ connection_options = credentials
75
+
76
+ if "cache_regions" in path._kwargs:
77
+ cache_regions = path._kwargs["cache_regions"]
78
+ else:
79
+ cache_regions = True
80
+
81
+ return S3Path(path, cache_regions=cache_regions, **connection_options)
82
+
83
+ def enrich_path(self, path: S3Path, access_token: str | None = None) -> S3Path:
84
+ path_str = path.as_posix().rstrip("/")
85
+ root = self._find_root(path_str)
86
+
87
+ if root is not None:
88
+ set_cache = False
89
+ credentials = self._get_cached_credentials(root)
90
+
91
+ if access_token is not None:
92
+ set_cache = True
93
+ elif credentials != {}:
94
+ # update credentials
95
+ if not self._is_active(root):
96
+ set_cache = True
97
+ else:
98
+ set_cache = True
99
+
100
+ if set_cache:
101
+ from ._hub_core import access_aws
102
+
103
+ storage_root_info = access_aws(path_str, access_token=access_token)
104
+ accessibility = storage_root_info["accessibility"]
105
+
106
+ is_managed = accessibility.get("is_managed", False)
107
+ if is_managed:
108
+ credentials = storage_root_info["credentials"]
109
+ else:
110
+ credentials = {}
111
+
112
+ if access_token is None:
113
+ if "storage_root" in accessibility:
114
+ root = accessibility["storage_root"]
115
+ # just to be safe
116
+ root = None if root == "" else root
117
+ if root is None:
118
+ # heuristic
119
+ # do not write the first level for the known hosted buckets
120
+ if path_str.startswith(HOSTED_BUCKETS):
121
+ root = "/".join(path.path.rstrip("/").split("/")[:2])
122
+ else:
123
+ # write the bucket for everything else
124
+ root = path._url.netloc
125
+ root = "s3://" + root
126
+ self._set_cached_credentials(root, credentials)
127
+
128
+ return self._path_inject_options(path, credentials)
129
+
130
+
131
+ _aws_credentials_manager: AWSCredentialsManager | None = None
132
+
133
+
134
+ def get_aws_credentials_manager() -> AWSCredentialsManager:
135
+ global _aws_credentials_manager
136
+
137
+ if _aws_credentials_manager is None:
138
+ _aws_credentials_manager = AWSCredentialsManager()
139
+
140
+ return _aws_credentials_manager
@@ -284,9 +284,11 @@ def _init_instance(isettings: InstanceSettings, client: Client) -> None:
284
284
  # as then init_instance is no longer idempotent
285
285
  try:
286
286
  client.table("instance").insert(fields, returning="minimal").execute()
287
- except APIError as e:
288
- logger.warning("instance likely already exists")
289
- raise e
287
+ except APIError:
288
+ logger.warning(
289
+ f"instance already existed at: https://lamin.ai/{isettings.owner}/{isettings.name}"
290
+ )
291
+ return None
290
292
  client.table("storage").update(
291
293
  {"instance_id": isettings._id.hex, "is_default": True}
292
294
  ).eq("id", isettings.storage._uuid.hex).execute() # type: ignore
@@ -351,35 +353,43 @@ def _connect_instance(
351
353
  return instance, storage
352
354
 
353
355
 
354
- def access_aws(storage_root: str, access_token: str | None = None) -> dict[str, str]:
356
+ def access_aws(storage_root: str, access_token: str | None = None) -> dict[str, dict]:
355
357
  from ._settings import settings
356
358
 
357
359
  if settings.user.handle != "anonymous" or access_token is not None:
358
- credentials = call_with_fallback_auth(
360
+ storage_root_info = call_with_fallback_auth(
359
361
  _access_aws, storage_root=storage_root, access_token=access_token
360
362
  )
361
- return credentials
363
+ return storage_root_info
362
364
  else:
363
365
  raise RuntimeError("Can only get access to AWS if authenticated.")
364
366
 
365
367
 
366
- def _access_aws(*, storage_root: str, client: Client) -> dict[str, str]:
368
+ def _access_aws(*, storage_root: str, client: Client) -> dict[str, dict]:
367
369
  import lamindb_setup
368
370
 
371
+ storage_root_info: dict[str, dict] = {"credentials": {}, "accessibility": {}}
369
372
  response = client.functions.invoke(
370
373
  "access-aws",
371
374
  invoke_options={"body": {"storage_root": storage_root}},
372
375
  )
373
376
  if response is not None and response != b"{}":
374
- loaded_credentials = json.loads(response)["Credentials"]
375
- credentials = {}
377
+ data = json.loads(response)
378
+
379
+ loaded_credentials = data["Credentials"]
380
+ loaded_accessibility = data["StorageAccessibility"]
381
+
382
+ credentials = storage_root_info["credentials"]
376
383
  credentials["key"] = loaded_credentials["AccessKeyId"]
377
384
  credentials["secret"] = loaded_credentials["SecretAccessKey"]
378
385
  credentials["token"] = loaded_credentials["SessionToken"]
379
- return credentials
386
+
387
+ accessibility = storage_root_info["accessibility"]
388
+ accessibility["storage_root"] = loaded_accessibility["storageRoot"]
389
+ accessibility["is_managed"] = loaded_accessibility["isManaged"]
380
390
  elif lamindb_setup._TESTING:
381
391
  raise RuntimeError(f"access-aws errored: {response}")
382
- return {}
392
+ return storage_root_info
383
393
 
384
394
 
385
395
  def get_lamin_site_base_url():
@@ -42,7 +42,7 @@ class InstanceSettings:
42
42
  db: str | None = None, # DB URI
43
43
  schema: str | None = None, # comma-separated string of schema names
44
44
  git_repo: str | None = None, # a git repo URL
45
- is_on_hub: bool = False, # initialized from hub
45
+ is_on_hub: bool | None = None, # initialized from hub
46
46
  ):
47
47
  from ._hub_utils import validate_db_arg
48
48
 
@@ -367,17 +367,25 @@ class InstanceSettings:
367
367
 
368
368
  @property
369
369
  def is_on_hub(self) -> bool:
370
- """Is this instance on the hub.
370
+ """Is this instance on the hub?
371
371
 
372
- Only works if user has access to the instance.
372
+ Can only reliably establish if user has access to the instance. Will
373
+ return `False` in case the instance isn't found.
373
374
  """
374
375
  if self._is_on_hub is None:
375
376
  from ._hub_client import call_with_fallback_auth
376
377
  from ._hub_crud import select_instance_by_id
378
+ from ._settings import settings
377
379
 
378
- response = call_with_fallback_auth(
379
- select_instance_by_id, instance_id=self._id.hex
380
- )
380
+ if settings.user.handle != "anonymous":
381
+ response = call_with_fallback_auth(
382
+ select_instance_by_id, instance_id=self._id.hex
383
+ )
384
+ else:
385
+ response = call_with_fallback(
386
+ select_instance_by_id, instance_id=self._id.hex
387
+ )
388
+ logger.warning("calling anonymously, will miss private instances")
381
389
  if response is None:
382
390
  self._is_on_hub = False
383
391
  else:
@@ -406,9 +414,7 @@ class InstanceSettings:
406
414
 
407
415
  setup_django(self, init=True)
408
416
 
409
- def _load_db(
410
- self, do_not_lock_for_laminapp_admin: bool = False
411
- ) -> tuple[bool, str]:
417
+ def _load_db(self) -> tuple[bool, str]:
412
418
  # Is the database available and initialized as LaminDB?
413
419
  # returns a tuple of status code and message
414
420
  if self.dialect == "sqlite" and not self._sqlite_file.exists():
@@ -423,15 +429,8 @@ class InstanceSettings:
423
429
 
424
430
  from .django import setup_django
425
431
 
426
- # lock in all cases except if do_not_lock_for_laminapp_admin is True and
427
- # user is `laminapp-admin`
428
- # value doesn't matter if not a cloud sqlite instance
429
- lock_cloud_sqlite = self._is_cloud_sqlite and (
430
- not do_not_lock_for_laminapp_admin
431
- or settings.user.handle != "laminapp-admin"
432
- )
433
432
  # we need the local sqlite to setup django
434
- self._update_local_sqlite_file(lock_cloud_sqlite=lock_cloud_sqlite)
433
+ self._update_local_sqlite_file(lock_cloud_sqlite=self._is_cloud_sqlite)
435
434
  # setting up django also performs a check for migrations & prints them
436
435
  # as warnings
437
436
  # this should fail, e.g., if the db is not reachable
@@ -10,11 +10,11 @@ from typing import TYPE_CHECKING, Any, Literal, Optional, Union
10
10
  from appdirs import AppDirs
11
11
  from lamin_utils import logger
12
12
 
13
+ from ._aws_credentials import HOSTED_REGIONS, get_aws_credentials_manager
13
14
  from ._aws_storage import find_closest_aws_region
14
15
  from ._settings_save import save_system_storage_settings
15
16
  from ._settings_store import system_storage_settings_file
16
17
  from .upath import (
17
- HOSTED_REGIONS,
18
18
  LocalPathClasses,
19
19
  UPath,
20
20
  convert_pathlike,
@@ -113,11 +113,20 @@ def init_storage(
113
113
  )
114
114
  # the below might update the uid with one that's already taken on the hub
115
115
  if ssettings.type_is_cloud or register_hub:
116
+ from ._hub_core import delete_storage_record
116
117
  from ._hub_core import init_storage as init_storage_hub
117
118
 
118
119
  init_storage_hub(ssettings)
119
120
  # below comes last only if everything else was successful
120
- mark_storage_root(ssettings.root, ssettings.uid) # type: ignore
121
+ try:
122
+ mark_storage_root(ssettings.root, ssettings.uid) # type: ignore
123
+ except Exception:
124
+ logger.important(
125
+ f"due to lack of write access, LaminDB won't manage storage location: {ssettings.root}"
126
+ )
127
+ if ssettings._uuid is not None:
128
+ delete_storage_record(ssettings._uuid) # type: ignore
129
+ ssettings._instance_id = None
121
130
  return ssettings
122
131
 
123
132
 
@@ -142,6 +151,7 @@ class StorageSettings:
142
151
  uid: str | None = None,
143
152
  uuid: UUID | None = None,
144
153
  instance_id: UUID | None = None,
154
+ # note that passing access_token prevents credentials caching
145
155
  access_token: str | None = None,
146
156
  ):
147
157
  self._uid = uid
@@ -226,6 +236,12 @@ class StorageSettings:
226
236
  if self._root is None:
227
237
  # below makes network requests to get credentials
228
238
  self._root = create_path(self._root_init, access_token=self.access_token)
239
+ elif getattr(self._root, "protocol", "") == "s3":
240
+ # this is needed to be sure that the root always has nonexpired credentials
241
+ # this just checks for time of the cached credentials in most cases
242
+ return get_aws_credentials_manager().enrich_path(
243
+ self._root, access_token=self.access_token
244
+ )
229
245
  return self._root
230
246
 
231
247
  def _set_fs_kwargs(self, **kwargs):
@@ -40,11 +40,11 @@ def hash_set(s: set[str]) -> str:
40
40
  return to_b64_str(hashlib.md5(bstr).digest())[:20]
41
41
 
42
42
 
43
- def hash_md5s_from_dir(etags: list[str]) -> tuple[str, str]:
43
+ def hash_md5s_from_dir(hashes: list[str]) -> tuple[str, str]:
44
44
  # need to sort below because we don't want the order of parsing the dir to
45
45
  # affect the hash
46
46
  digests = b"".join(
47
- hashlib.md5(etag.encode("utf-8")).digest() for etag in sorted(etags)
47
+ hashlib.md5(hash.encode("utf-8")).digest() for hash in sorted(hashes)
48
48
  )
49
49
  digest = hashlib.md5(digests).digest()
50
50
  return to_b64_str(digest)[:22], "md5-d"
@@ -59,24 +59,27 @@ def hash_code(file_path: UPathStr):
59
59
  return hashlib.sha1(blob)
60
60
 
61
61
 
62
- def hash_file(file_path: Path, chunk_size=50 * 1024 * 1024) -> tuple[str, str]:
63
- chunks = []
62
+ def hash_file(
63
+ file_path: Path,
64
+ file_size: int | None = None,
65
+ chunk_size: int | None = 50 * 1024 * 1024,
66
+ ) -> tuple[str, str]:
64
67
  with open(file_path, "rb") as fp:
65
- # read first chunk
66
- chunks = [fp.read(chunk_size)]
67
- # try reading the 2nd chunk
68
- data = fp.read(chunk_size)
69
- if data:
70
- # go to end of file
68
+ if file_size is None:
69
+ fp.seek(0, 2)
70
+ file_size = fp.tell()
71
+ fp.seek(0, 0)
72
+ if chunk_size is None:
73
+ chunk_size = file_size
74
+ first_chunk = fp.read(chunk_size)
75
+ if file_size <= chunk_size:
76
+ digest = hashlib.md5(first_chunk).digest()
77
+ hash_type = "md5"
78
+ else:
71
79
  fp.seek(-chunk_size, 2)
72
- # read last chunk
73
- data = fp.read(chunk_size)
74
- chunks.append(data)
75
- if len(chunks) == 1:
76
- digest = hashlib.md5(chunks[0]).digest()
77
- hash_type = "md5"
78
- else:
79
- digests = b"".join(hashlib.sha1(chunk).digest() for chunk in chunks)
80
- digest = hashlib.sha1(digests).digest()
81
- hash_type = "sha1-fl" # sha1 first last chunk
80
+ last_chunk = fp.read(chunk_size)
81
+ digest = hashlib.sha1(
82
+ hashlib.sha1(first_chunk).digest() + hashlib.sha1(last_chunk).digest()
83
+ ).digest()
84
+ hash_type = "sha1-fl"
82
85
  return to_b64_str(digest)[:22], hash_type
@@ -11,13 +11,13 @@ from itertools import islice
11
11
  from pathlib import Path, PurePosixPath
12
12
  from typing import TYPE_CHECKING, Any, Literal
13
13
 
14
- import botocore.session
15
14
  import fsspec
16
15
  from lamin_utils import logger
17
16
  from upath import UPath
18
17
  from upath.implementations.cloud import CloudPath, S3Path # keep CloudPath!
19
18
  from upath.implementations.local import LocalPath, PosixUPath, WindowsUPath
20
19
 
20
+ from ._aws_credentials import HOSTED_BUCKETS, get_aws_credentials_manager
21
21
  from .hashing import b16_to_b64, hash_md5s_from_dir
22
22
 
23
23
  if TYPE_CHECKING:
@@ -158,7 +158,8 @@ def print_hook(size: int, value: int, objectname: str, action: str):
158
158
  progress_in_percent = (value / size) * 100
159
159
  out = f"... {action} {objectname}:" f" {min(progress_in_percent, 100):4.1f}%"
160
160
  if "NBPRJ_TEST_NBPATH" not in os.environ:
161
- print(out, end="\r")
161
+ end = "\n" if progress_in_percent >= 100 else "\r"
162
+ print(out, end=end)
162
163
 
163
164
 
164
165
  class ProgressCallback(fsspec.callbacks.Callback):
@@ -670,77 +671,12 @@ def convert_pathlike(pathlike: UPathStr) -> UPath:
670
671
  return path
671
672
 
672
673
 
673
- HOSTED_REGIONS = [
674
- "eu-central-1",
675
- "eu-west-2",
676
- "us-east-1",
677
- "us-east-2",
678
- "us-west-1",
679
- "us-west-2",
680
- ]
681
- lamin_env = os.getenv("LAMIN_ENV")
682
- if lamin_env is None or lamin_env == "prod":
683
- hosted_buckets_list = [f"s3://lamin-{region}" for region in HOSTED_REGIONS]
684
- hosted_buckets_list.append("s3://scverse-spatial-eu-central-1")
685
- HOSTED_BUCKETS = tuple(hosted_buckets_list)
686
- else:
687
- HOSTED_BUCKETS = ("s3://lamin-hosted-test",) # type: ignore
688
- credentials_cache: dict[str, dict[str, str]] = {}
689
- AWS_CREDENTIALS_PRESENT = None
690
-
691
-
692
674
  def create_path(path: UPath, access_token: str | None = None) -> UPath:
693
675
  path = convert_pathlike(path)
694
676
  # test whether we have an AWS S3 path
695
677
  if not isinstance(path, S3Path):
696
678
  return path
697
- # test whether AWS credentials are present
698
- global AWS_CREDENTIALS_PRESENT
699
-
700
- if AWS_CREDENTIALS_PRESENT is not None:
701
- anon = AWS_CREDENTIALS_PRESENT
702
- else:
703
- if path.fs.key is not None and path.fs.secret is not None:
704
- anon = False
705
- else:
706
- # we could do path.fs.connect()
707
- # and check path.fs.session._credentials, but it'd be slower
708
- session = botocore.session.get_session()
709
- credentials = session.get_credentials()
710
- if credentials is None or credentials.access_key is None:
711
- anon = True
712
- else:
713
- anon = False
714
- # cache credentials and reuse further
715
- AWS_CREDENTIALS_PRESENT = anon
716
-
717
- # test whether we are on hosted storage or not
718
- path_str = path.as_posix()
719
- is_hosted_storage = path_str.startswith(HOSTED_BUCKETS)
720
-
721
- if not is_hosted_storage:
722
- # make anon request if no credentials present
723
- return UPath(path, cache_regions=True, anon=anon)
724
-
725
- root_folder = "/".join(path_str.replace("s3://", "").split("/")[:2])
726
-
727
- if access_token is None and root_folder in credentials_cache:
728
- credentials = credentials_cache[root_folder]
729
- else:
730
- from ._hub_core import access_aws
731
-
732
- credentials = access_aws(
733
- storage_root=f"s3://{root_folder}", access_token=access_token
734
- )
735
- if access_token is None:
736
- credentials_cache[root_folder] = credentials
737
-
738
- return UPath(
739
- path,
740
- key=credentials["key"],
741
- secret=credentials["secret"],
742
- token=credentials["token"],
743
- )
679
+ return get_aws_credentials_manager().enrich_path(path, access_token)
744
680
 
745
681
 
746
682
  def get_stat_file_cloud(stat: dict) -> tuple[int, str, str]:
@@ -1,5 +1,4 @@
1
1
  import os
2
- from typing import TYPE_CHECKING
3
2
  from uuid import UUID
4
3
 
5
4
  import lamindb_setup
@@ -10,6 +9,7 @@ from lamin_utils import logger
10
9
  def pytest_sessionstart(session: pytest.Session):
11
10
  lamindb_instance_id = UUID("e1a2d3ab762e4592af5a1e53f288284e")
12
11
  os.environ["LAMINDB_INSTANCE_ID_INIT"] = lamindb_instance_id.hex
12
+ assert lamindb_setup.settings.user.handle == "testuser2"
13
13
  lamindb_setup.init(
14
14
  storage="./default_storage",
15
15
  schema="bionty",
@@ -21,4 +21,4 @@ def pytest_sessionstart(session: pytest.Session):
21
21
 
22
22
  def pytest_sessionfinish(session: pytest.Session):
23
23
  logger.set_verbosity(1)
24
- lamindb_setup.delete("lamindb-setup-unit-tests", force=True)
24
+ lamindb_setup.delete("testuser2/lamindb-setup-unit-tests", force=True)
File without changes
File without changes