lamindb_setup 1.8.0__tar.gz → 1.8.2__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 (106) hide show
  1. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/PKG-INFO +1 -1
  2. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/docs/hub-cloud/06-connect-hosted-instance.ipynb +3 -2
  3. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/docs/hub-cloud/07-keep-artifacts-local.ipynb +2 -3
  4. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/docs/hub-prod/test-cloud-sync.ipynb +30 -7
  5. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/lamindb_setup/__init__.py +1 -1
  6. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/lamindb_setup/_connect_instance.py +20 -1
  7. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/lamindb_setup/core/_hub_client.py +24 -9
  8. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/lamindb_setup/core/_hub_core.py +5 -3
  9. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/lamindb_setup/core/_settings.py +1 -1
  10. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/lamindb_setup/core/_settings_instance.py +32 -16
  11. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/lamindb_setup/core/_settings_storage.py +7 -45
  12. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/lamindb_setup/core/upath.py +181 -137
  13. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/tests/storage/test_storage_stats.py +7 -0
  14. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/tests/storage/test_to_url.py +7 -6
  15. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/.github/workflows/build.yml +0 -0
  16. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/.github/workflows/doc-changes.yml +0 -0
  17. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/.gitignore +0 -0
  18. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/.pre-commit-config.yaml +0 -0
  19. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/LICENSE +0 -0
  20. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/README.md +0 -0
  21. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/docs/changelog.md +0 -0
  22. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/docs/hub-cloud/01-init-local-instance.ipynb +0 -0
  23. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/docs/hub-cloud/02-connect-local-instance.ipynb +0 -0
  24. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/docs/hub-cloud/03-add-managed-storage.ipynb +0 -0
  25. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/docs/hub-cloud/04-test-bionty.ipynb +0 -0
  26. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/docs/hub-cloud/05-init-hosted-instance.ipynb +0 -0
  27. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/docs/hub-cloud/08-test-multi-session.ipynb +0 -0
  28. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/docs/hub-cloud/test_notebooks.py +0 -0
  29. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/docs/hub-prod/test-cache-management.ipynb +0 -0
  30. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/docs/hub-prod/test-connect-anonymously.ipynb +0 -0
  31. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/docs/hub-prod/test-empty-init.ipynb +0 -0
  32. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/docs/hub-prod/test-import-schema.ipynb +0 -0
  33. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/docs/hub-prod/test-init-load-local-anonymously.ipynb +0 -0
  34. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/docs/hub-prod/test-insufficient-user-info.ipynb +0 -0
  35. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/docs/hub-prod/test-invalid-schema.ipynb +0 -0
  36. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/docs/hub-prod/test-sqlite-lock.ipynb +0 -0
  37. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/docs/hub-prod/test_notebooks2.py +0 -0
  38. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/docs/index.md +0 -0
  39. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/docs/notebooks.md +0 -0
  40. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/docs/reference.md +0 -0
  41. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/lamindb_setup/_cache.py +0 -0
  42. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/lamindb_setup/_check.py +0 -0
  43. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/lamindb_setup/_check_setup.py +0 -0
  44. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/lamindb_setup/_delete.py +0 -0
  45. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/lamindb_setup/_disconnect.py +0 -0
  46. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/lamindb_setup/_django.py +0 -0
  47. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/lamindb_setup/_entry_points.py +0 -0
  48. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/lamindb_setup/_exportdb.py +0 -0
  49. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/lamindb_setup/_importdb.py +0 -0
  50. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/lamindb_setup/_init_instance.py +0 -0
  51. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/lamindb_setup/_migrate.py +0 -0
  52. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/lamindb_setup/_register_instance.py +0 -0
  53. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/lamindb_setup/_schema.py +0 -0
  54. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/lamindb_setup/_schema_metadata.py +0 -0
  55. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/lamindb_setup/_set_managed_storage.py +0 -0
  56. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/lamindb_setup/_setup_user.py +0 -0
  57. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/lamindb_setup/_silence_loggers.py +0 -0
  58. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/lamindb_setup/core/__init__.py +0 -0
  59. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/lamindb_setup/core/_aws_options.py +0 -0
  60. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/lamindb_setup/core/_aws_storage.py +0 -0
  61. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/lamindb_setup/core/_deprecated.py +0 -0
  62. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/lamindb_setup/core/_docs.py +0 -0
  63. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/lamindb_setup/core/_hub_crud.py +0 -0
  64. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/lamindb_setup/core/_hub_utils.py +0 -0
  65. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/lamindb_setup/core/_private_django_api.py +0 -0
  66. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/lamindb_setup/core/_settings_load.py +0 -0
  67. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/lamindb_setup/core/_settings_save.py +0 -0
  68. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/lamindb_setup/core/_settings_store.py +0 -0
  69. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/lamindb_setup/core/_settings_user.py +0 -0
  70. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/lamindb_setup/core/_setup_bionty_sources.py +0 -0
  71. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/lamindb_setup/core/cloud_sqlite_locker.py +0 -0
  72. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/lamindb_setup/core/django.py +0 -0
  73. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/lamindb_setup/core/exceptions.py +0 -0
  74. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/lamindb_setup/core/hashing.py +0 -0
  75. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/lamindb_setup/core/types.py +0 -0
  76. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/lamindb_setup/errors.py +0 -0
  77. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/lamindb_setup/py.typed +0 -0
  78. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/lamindb_setup/types.py +0 -0
  79. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/noxfile.py +0 -0
  80. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/pyproject.toml +0 -0
  81. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/tests/hub-cloud/scripts/script-init-pass-user-no-writes.py +0 -0
  82. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/tests/hub-cloud/scripts/script-to-fail-managed-storage.py +0 -0
  83. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/tests/hub-cloud/test_connect_instance.py +0 -0
  84. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/tests/hub-cloud/test_delete_instance.py +0 -0
  85. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/tests/hub-cloud/test_edge_request.py +0 -0
  86. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/tests/hub-cloud/test_fail_managed_storage.py +0 -0
  87. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/tests/hub-cloud/test_init_instance.py +0 -0
  88. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/tests/hub-cloud/test_init_pass_user_no_writes.py +0 -0
  89. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/tests/hub-cloud/test_login.py +0 -0
  90. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/tests/hub-cloud/test_migrate.py +0 -0
  91. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/tests/hub-cloud/test_set_storage.py +0 -0
  92. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/tests/hub-local/conftest.py +0 -0
  93. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/tests/hub-local/scripts/script-connect-fine-grained-access.py +0 -0
  94. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/tests/hub-local/test_all.py +0 -0
  95. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/tests/hub-local/test_update_schema_in_hub.py +0 -0
  96. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/tests/hub-prod/conftest.py +0 -0
  97. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/tests/hub-prod/test_aws_options_manager.py +0 -0
  98. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/tests/hub-prod/test_django.py +0 -0
  99. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/tests/hub-prod/test_global_settings.py +0 -0
  100. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/tests/hub-prod/test_switch_and_fallback_env.py +0 -0
  101. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/tests/hub-prod/test_upath.py +0 -0
  102. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/tests/storage/test_entry_point.py +0 -0
  103. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/tests/storage/test_hashing.py +0 -0
  104. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/tests/storage/test_storage_access.py +0 -0
  105. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/tests/storage/test_storage_basis.py +0 -0
  106. {lamindb_setup-1.8.0 → lamindb_setup-1.8.2}/tests/storage/test_storage_settings.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: lamindb_setup
3
- Version: 1.8.0
3
+ Version: 1.8.2
4
4
  Summary: Setup & configure LaminDB.
5
5
  Author-email: Lamin Labs <open-source@lamin.ai>
6
6
  Requires-Python: >=3.10
@@ -16,6 +16,7 @@
16
16
  "import pytest\n",
17
17
  "import shutil\n",
18
18
  "import lamindb_setup as ln_setup\n",
19
+ "from lamindb_setup._connect_instance import _connect_cli\n",
19
20
  "from lamindb_setup.core.upath import UPath\n",
20
21
  "from lamindb_setup.core._hub_core import delete_instance\n",
21
22
  "from lamindb_setup.core._hub_client import connect_hub_with_auth\n",
@@ -31,7 +32,7 @@
31
32
  "metadata": {},
32
33
  "outputs": [],
33
34
  "source": [
34
- "ln_setup.connect(f\"testuser1/{instance_name}\")"
35
+ "_connect_cli(f\"testuser1/{instance_name}\") # cover here for cloud sqlite"
35
36
  ]
36
37
  },
37
38
  {
@@ -223,7 +224,7 @@
223
224
  "name": "python",
224
225
  "nbconvert_exporter": "python",
225
226
  "pygments_lexer": "ipython3",
226
- "version": "3.9.17"
227
+ "version": "3.10.16"
227
228
  }
228
229
  },
229
230
  "nbformat": 4,
@@ -61,9 +61,8 @@
61
61
  "ln_setup.settings.instance._keep_artifacts_local = True\n",
62
62
  "with pytest.raises(ValueError) as error:\n",
63
63
  " ln_setup.settings.instance.local_storage\n",
64
- "assert (\n",
65
- " error.exconly()\n",
66
- " == \"ValueError: No storage location found in current environment: create one via, e.g., ln.Storage(root='/dir/our_shared_dir', host='our-server-123).save()\"\n",
64
+ "assert error.exconly().startswith(\n",
65
+ " \"ValueError: No local storage location found in current environment:\"\n",
67
66
  ")"
68
67
  ]
69
68
  },
@@ -229,7 +229,7 @@
229
229
  ")\n",
230
230
  "assert dir_sync_local == settings.cache_dir / \"dir_cache/key\"\n",
231
231
  "\n",
232
- "assert dir_sync.synchronize(dir_sync_local, just_check=True)\n",
232
+ "assert dir_sync.synchronize_to(dir_sync_local, just_check=True)\n",
233
233
  "assert not dir_sync_local.exists()"
234
234
  ]
235
235
  },
@@ -240,7 +240,7 @@
240
240
  "metadata": {},
241
241
  "outputs": [],
242
242
  "source": [
243
- "assert dir_sync.synchronize(dir_sync_local, just_check=False)\n",
243
+ "assert dir_sync.synchronize_to(dir_sync_local, just_check=False)\n",
244
244
  "assert dir_sync_local.is_dir()\n",
245
245
  "assert num_files(dir_sync_local) == 2"
246
246
  ]
@@ -308,6 +308,28 @@
308
308
  "http_local.unlink()"
309
309
  ]
310
310
  },
311
+ {
312
+ "cell_type": "code",
313
+ "execution_count": null,
314
+ "id": "7e610d67",
315
+ "metadata": {},
316
+ "outputs": [],
317
+ "source": [
318
+ "# test download_to\n",
319
+ "http_path.download_to(http_local, print_progress=True)\n",
320
+ "assert not http_path.synchronize_to(http_local)"
321
+ ]
322
+ },
323
+ {
324
+ "cell_type": "code",
325
+ "execution_count": null,
326
+ "id": "1e293d0a",
327
+ "metadata": {},
328
+ "outputs": [],
329
+ "source": [
330
+ "http_local.unlink()"
331
+ ]
332
+ },
311
333
  {
312
334
  "cell_type": "markdown",
313
335
  "id": "574c3f95",
@@ -323,7 +345,8 @@
323
345
  "metadata": {},
324
346
  "outputs": [],
325
347
  "source": [
326
- "dir_sync_local = settings.paths.cloud_to_local(dir_sync)"
348
+ "dir_sync_local = settings.paths.cloud_to_local(dir_sync)\n",
349
+ "assert not dir_sync.synchronize_to(dir_sync_local)"
327
350
  ]
328
351
  },
329
352
  {
@@ -459,7 +482,7 @@
459
482
  "source": [
460
483
  "dir_sync_local = settings.paths.cloud_to_local(dir_sync)\n",
461
484
  "\n",
462
- "assert num_files(dir_sync_local) == 2\n",
485
+ "assert num_files(dir_sync_local) == 2, list(dir_sync_local.rglob(\"*\"))\n",
463
486
  "assert not local_file_new.exists()\n",
464
487
  "assert not local_file_new_parent.exists()\n",
465
488
  "\n",
@@ -759,7 +782,7 @@
759
782
  "metadata": {},
760
783
  "outputs": [],
761
784
  "source": [
762
- "assert hf_path.synchronize(hf_path_local, just_check=True)\n",
785
+ "assert hf_path.synchronize_to(hf_path_local, just_check=True)\n",
763
786
  "assert not hf_path_local.exists()"
764
787
  ]
765
788
  },
@@ -770,7 +793,7 @@
770
793
  "metadata": {},
771
794
  "outputs": [],
772
795
  "source": [
773
- "assert hf_path.synchronize(hf_path_local)\n",
796
+ "assert hf_path.synchronize_to(hf_path_local)\n",
774
797
  "assert hf_path_local.is_file()"
775
798
  ]
776
799
  },
@@ -793,7 +816,7 @@
793
816
  "source": [
794
817
  "hf_path = UPath(\"hf://datasets/Koncopd/lamindb-test@main/does_not_exist.file\")\n",
795
818
  "with pytest.raises(FileNotFoundError):\n",
796
- " hf_path.synchronize(UPath(\"./does_not_exist.file\"), error_no_origin=True)"
819
+ " hf_path.synchronize_to(UPath(\"./does_not_exist.file\"), error_no_origin=True)"
797
820
  ]
798
821
  },
799
822
  {
@@ -35,7 +35,7 @@ Modules & settings:
35
35
 
36
36
  """
37
37
 
38
- __version__ = "1.8.0" # denote a release candidate for 0.1.0 with 0.1rc1
38
+ __version__ = "1.8.2" # denote a release candidate for 0.1.0 with 0.1rc1
39
39
 
40
40
  import os
41
41
 
@@ -188,13 +188,32 @@ def _connect_instance(
188
188
  return isettings
189
189
 
190
190
 
191
+ def _connect_cli(instance: str) -> None:
192
+ from lamindb_setup import settings as settings_
193
+
194
+ settings_.auto_connect = True
195
+ owner, name = get_owner_name_from_identifier(instance)
196
+ isettings = _connect_instance(owner, name)
197
+ isettings._persist(write_to_disk=True)
198
+ if not isettings.is_on_hub or isettings._is_cloud_sqlite:
199
+ # there are two reasons to call the full-blown connect
200
+ # (1) if the instance is not on the hub, we need to register
201
+ # potential users through register_user()
202
+ # (2) if the instance is cloud sqlite, we need to lock it
203
+ connect(_write_settings=False, _reload_lamindb=False)
204
+ else:
205
+ logger.important(f"connected lamindb: {isettings.slug}")
206
+ return None
207
+
208
+
191
209
  @unlock_cloud_sqlite_upon_exception(ignore_prev_locker=True)
192
210
  def connect(instance: str | None = None, **kwargs: Any) -> str | tuple | None:
193
211
  """Connect to an instance.
194
212
 
195
213
  Args:
196
214
  instance: Pass a slug (`account/name`) or URL (`https://lamin.ai/account/name`).
197
- If `None`, looks for an environment variable `LAMIN_CURRENT_INSTANCE` to get the instance identifier. If it doesn't find this variable, it connects to the instance that was connected with `lamin connect` through the CLI.
215
+ If `None`, looks for an environment variable `LAMIN_CURRENT_INSTANCE` to get the instance identifier.
216
+ If it doesn't find this variable, it connects to the instance that was connected with `lamin connect` through the CLI.
198
217
  """
199
218
  # validate kwargs
200
219
  valid_kwargs = {
@@ -5,6 +5,7 @@ import os
5
5
  from typing import Literal
6
6
  from urllib.request import urlretrieve
7
7
 
8
+ from httpx import HTTPTransport
8
9
  from lamin_utils import logger
9
10
  from pydantic_settings import BaseSettings
10
11
  from supabase import Client, create_client # type: ignore
@@ -60,20 +61,29 @@ class Environment:
60
61
  self.supabase_anon_key: str = key
61
62
 
62
63
 
64
+ DEFAULT_TIMEOUT = 20
65
+
66
+
63
67
  # runs ~0.5s
64
68
  def connect_hub(
65
69
  fallback_env: bool = False, client_options: ClientOptions | None = None
66
70
  ) -> Client:
67
71
  env = Environment(fallback=fallback_env)
68
72
  if client_options is None:
69
- # function_client_timeout=5 by default
70
- # increase to avoid rare timeouts for edge functions
71
73
  client_options = ClientOptions(
72
74
  auto_refresh_token=False,
73
- function_client_timeout=30,
74
- postgrest_client_timeout=20,
75
+ function_client_timeout=DEFAULT_TIMEOUT,
76
+ postgrest_client_timeout=DEFAULT_TIMEOUT,
75
77
  )
76
- return create_client(env.supabase_api_url, env.supabase_anon_key, client_options)
78
+ client = create_client(env.supabase_api_url, env.supabase_anon_key, client_options)
79
+ # needed to enable retries for http requests in supabase
80
+ # these are separate clients and need separate transports
81
+ # retries are done only in case an httpx.ConnectError or an httpx.ConnectTimeout occurs
82
+ transport_kwargs = {"verify": True, "http2": True, "retries": 2}
83
+ client.auth._http_client._transport = HTTPTransport(**transport_kwargs)
84
+ client.functions._client._transport = HTTPTransport(**transport_kwargs)
85
+ client.postgrest.session._transport = HTTPTransport(**transport_kwargs)
86
+ return client
77
87
 
78
88
 
79
89
  def connect_hub_with_auth(
@@ -210,11 +220,16 @@ def request_with_auth(
210
220
  headers["Authorization"] = f"Bearer {access_token}"
211
221
 
212
222
  make_request = getattr(requests, method)
213
- response = make_request(url, headers=headers, **kwargs)
214
- # upate access_token and try again if failed
215
- if response.status_code != 200 and renew_token:
223
+ timeout = kwargs.pop("timeout", DEFAULT_TIMEOUT)
224
+
225
+ response = make_request(url, headers=headers, timeout=timeout, **kwargs)
226
+ status_code = response.status_code
227
+ # update access_token and try again if failed
228
+ if not (200 <= status_code < 300) and renew_token:
216
229
  from lamindb_setup import settings
217
230
 
231
+ logger.debug(f"{method} {url} failed: {status_code} {response.text}")
232
+
218
233
  access_token = get_access_token(
219
234
  settings.user.email, settings.user.password, settings.user.api_key
220
235
  )
@@ -224,5 +239,5 @@ def request_with_auth(
224
239
 
225
240
  headers["Authorization"] = f"Bearer {access_token}"
226
241
 
227
- response = make_request(url, headers=headers, **kwargs)
242
+ response = make_request(url, headers=headers, timeout=timeout, **kwargs)
228
243
  return response
@@ -531,11 +531,13 @@ def access_db(
531
531
  url = instance_api_url + url
532
532
 
533
533
  response = request_with_auth(url, "get", access_token, renew_token) # type: ignore
534
- response_json = response.json()
535
- if response.status_code != 200:
534
+ status_code = response.status_code
535
+ if not (200 <= status_code < 300):
536
536
  raise PermissionError(
537
- f"Fine-grained access to {instance_slug} failed: {response_json}"
537
+ f"Fine-grained access to {instance_slug} failed: {status_code} {response.text}"
538
538
  )
539
+
540
+ response_json = response.json()
539
541
  if "token" not in response_json:
540
542
  raise RuntimeError("The response of access_db does not contain a db token.")
541
543
  return response_json["token"]
@@ -350,7 +350,7 @@ class SetupPaths:
350
350
  local_filepath = SetupPaths.cloud_to_local_no_update(filepath, cache_key)
351
351
  if not isinstance(filepath, LocalPathClasses):
352
352
  local_filepath.parent.mkdir(parents=True, exist_ok=True)
353
- filepath.synchronize(local_filepath, **kwargs) # type: ignore
353
+ filepath.synchronize_to(local_filepath, **kwargs) # type: ignore
354
354
  return local_filepath
355
355
 
356
356
 
@@ -32,7 +32,7 @@ if TYPE_CHECKING:
32
32
 
33
33
  from ._settings_user import UserSettings
34
34
 
35
- LOCAL_STORAGE_MESSAGE = "No storage location found in current environment: create one via, e.g., ln.Storage(root='/dir/our_shared_dir', host='our-server-123).save()"
35
+ LOCAL_STORAGE_MESSAGE = "No local storage location found in current environment: defaulting to cloud storage"
36
36
 
37
37
 
38
38
  def sanitize_git_repo_url(repo_url: str) -> str:
@@ -156,9 +156,17 @@ class InstanceSettings:
156
156
  found = []
157
157
  for record in all_local_records:
158
158
  root_path = Path(record.root)
159
- if root_path.exists():
159
+ try:
160
+ root_path_exists = root_path.exists()
161
+ except PermissionError:
162
+ continue
163
+ if root_path_exists:
160
164
  marker_path = root_path / STORAGE_UID_FILE_KEY
161
- if not marker_path.exists():
165
+ try:
166
+ marker_path_exists = marker_path.exists()
167
+ except PermissionError:
168
+ continue
169
+ if not marker_path_exists:
162
170
  legacy_filepath = root_path / LEGACY_STORAGE_UID_FILE_KEY
163
171
  if legacy_filepath.exists():
164
172
  logger.warning(
@@ -193,15 +201,19 @@ class InstanceSettings:
193
201
  def keep_artifacts_local(self) -> bool:
194
202
  """Default to keeping artifacts local.
195
203
 
196
- Enable this optional setting for cloud instances on lamin.ai.
197
-
198
204
  Guide: :doc:`faq/keep-artifacts-local`
199
205
  """
200
206
  return self._keep_artifacts_local
201
207
 
208
+ @keep_artifacts_local.setter
209
+ def keep_artifacts_local(self, value: bool):
210
+ if not isinstance(value, bool):
211
+ raise ValueError("keep_artifacts_local must be a boolean value.")
212
+ self._keep_artifacts_local = value
213
+
202
214
  @property
203
215
  def storage(self) -> StorageSettings:
204
- """Default storage.
216
+ """Default storage of instance.
205
217
 
206
218
  For a cloud instance, this is cloud storage. For a local instance, this
207
219
  is a local directory.
@@ -210,13 +222,13 @@ class InstanceSettings:
210
222
 
211
223
  @property
212
224
  def local_storage(self) -> StorageSettings:
213
- """An additional local storage location.
225
+ """An alternative default local storage location in the current environment.
214
226
 
215
- Is only available if :attr:`keep_artifacts_local` is enabled.
227
+ Serves as the default storage location if :attr:`keep_artifacts_local` is enabled.
216
228
 
217
229
  Guide: :doc:`faq/keep-artifacts-local`
218
230
  """
219
- if not self._keep_artifacts_local:
231
+ if not self.keep_artifacts_local:
220
232
  raise ValueError("`keep_artifacts_local` is not enabled for this instance.")
221
233
  if self._local_storage is None:
222
234
  self._local_storage = self._search_local_root()
@@ -235,7 +247,7 @@ class InstanceSettings:
235
247
  local_root, host = local_root_host
236
248
 
237
249
  local_root = Path(local_root)
238
- if not self._keep_artifacts_local:
250
+ if not self.keep_artifacts_local:
239
251
  raise ValueError("`keep_artifacts_local` is not enabled for this instance.")
240
252
  local_storage = self._search_local_root(
241
253
  local_root=StorageSettings(local_root).root_as_str, mute_warning=True
@@ -264,17 +276,21 @@ class InstanceSettings:
264
276
  )
265
277
  local_root = UPath(local_root)
266
278
  assert isinstance(local_root, LocalPathClasses)
267
- self._local_storage, _ = init_storage(
279
+ tentative_storage, hub_status = init_storage(
268
280
  local_root,
269
281
  instance_id=self._id,
270
282
  instance_slug=self.slug,
271
283
  register_hub=True,
272
284
  region=host,
273
285
  ) # type: ignore
274
- register_storage_in_instance(self._local_storage) # type: ignore
275
- logger.important(
276
- f"defaulting to local storage: {self._local_storage.root} on host {host}"
277
- )
286
+ if hub_status in ["hub-record-created", "hub-record-retrieved"]:
287
+ register_storage_in_instance(tentative_storage) # type: ignore
288
+ self._local_storage = tentative_storage
289
+ logger.important(
290
+ f"defaulting to local storage: {self._local_storage.root} on host {host}"
291
+ )
292
+ else:
293
+ logger.warning(f"could not set this local storage location: {local_root}")
278
294
 
279
295
  @property
280
296
  @deprecated("local_storage")
@@ -366,7 +382,7 @@ class InstanceSettings:
366
382
  self._check_sqlite_lock()
367
383
  sqlite_file = self._sqlite_file
368
384
  cache_file = self.storage.cloud_to_local_no_update(sqlite_file)
369
- sqlite_file.synchronize(cache_file, print_progress=True) # type: ignore
385
+ sqlite_file.synchronize_to(cache_file, print_progress=True) # type: ignore
370
386
 
371
387
  def _check_sqlite_lock(self):
372
388
  if not self._cloud_sqlite_locker.has_lock:
@@ -19,7 +19,13 @@ from ._aws_options import (
19
19
  from ._aws_storage import find_closest_aws_region
20
20
  from ._deprecated import deprecated
21
21
  from .hashing import hash_and_encode_as_b62
22
- from .upath import LocalPathClasses, UPath, _split_path_query, create_path
22
+ from .upath import (
23
+ LocalPathClasses,
24
+ UPath,
25
+ _split_path_query,
26
+ create_path,
27
+ get_storage_region,
28
+ )
23
29
 
24
30
  if TYPE_CHECKING:
25
31
  from lamindb_setup.types import StorageType, UPathStr
@@ -43,50 +49,6 @@ def instance_uid_from_uuid(instance_id: UUID) -> str:
43
49
  return hash_and_encode_as_b62(instance_id.hex)[:12]
44
50
 
45
51
 
46
- def get_storage_region(path: UPathStr) -> str | None:
47
- path_str = str(path)
48
- if path_str.startswith("s3://"):
49
- import botocore.session
50
- from botocore.config import Config
51
- from botocore.exceptions import ClientError
52
-
53
- # check for endpoint_url in storage options if upath
54
- if isinstance(path, UPath):
55
- endpoint_url = path.storage_options.get("endpoint_url", None)
56
- else:
57
- endpoint_url = None
58
- path_part = path_str.replace("s3://", "")
59
- # check for endpoint_url in the path string
60
- if "?" in path_part:
61
- assert endpoint_url is None
62
- path_part, query = _split_path_query(path_part)
63
- endpoint_url = query.get("endpoint_url", [None])[0]
64
- bucket = path_part.split("/")[0]
65
- session = botocore.session.get_session()
66
- credentials = session.get_credentials()
67
- if credentials is None or credentials.access_key is None:
68
- config = Config(signature_version=botocore.session.UNSIGNED)
69
- else:
70
- config = None
71
- s3_client = session.create_client(
72
- "s3", endpoint_url=endpoint_url, config=config
73
- )
74
- try:
75
- response = s3_client.head_bucket(Bucket=bucket)
76
- except ClientError as exc:
77
- response = getattr(exc, "response", {})
78
- if response.get("Error", {}).get("Code") == "404":
79
- raise exc
80
- region = (
81
- response.get("ResponseMetadata", {})
82
- .get("HTTPHeaders", {})
83
- .get("x-amz-bucket-region", None)
84
- )
85
- else:
86
- region = None
87
- return region
88
-
89
-
90
52
  def get_storage_type(root_as_str: str) -> StorageType:
91
53
  import fsspec
92
54
 
@@ -23,6 +23,7 @@ from upath.registry import register_implementation
23
23
  from lamindb_setup.errors import StorageNotEmpty
24
24
 
25
25
  from ._aws_options import HOSTED_BUCKETS, get_aws_options_manager
26
+ from ._deprecated import deprecated
26
27
  from .hashing import HASH_LENGTH, b16_to_b64, hash_from_hashes_list, hash_string
27
28
 
28
29
  if TYPE_CHECKING:
@@ -381,42 +382,29 @@ def upload_from(
381
382
  return self
382
383
 
383
384
 
384
- def synchronize(
385
- self,
386
- objectpath: Path,
385
+ def synchronize_to(
386
+ origin: UPath,
387
+ destination: Path,
387
388
  error_no_origin: bool = True,
388
389
  print_progress: bool = False,
389
- callback: fsspec.callbacks.Callback | None = None,
390
- timestamp: float | None = None,
391
390
  just_check: bool = False,
391
+ **kwargs,
392
392
  ) -> bool:
393
393
  """Sync to a local destination path."""
394
- protocol = self.protocol
395
- # optimize the number of network requests
396
- if timestamp is not None:
397
- is_dir = False
394
+ destination = destination.resolve()
395
+ protocol = origin.protocol
396
+ try:
397
+ cloud_info = origin.stat().as_info()
398
398
  exists = True
399
- cloud_mts = timestamp
400
- else:
401
- try:
402
- cloud_stat = self.stat()
403
- cloud_info = cloud_stat.as_info()
404
- exists = True
405
- is_dir = cloud_info["type"] == "directory"
406
- if not is_dir:
407
- # hf requires special treatment
408
- if protocol == "hf":
409
- cloud_mts = cloud_info["last_commit"].date.timestamp()
410
- else:
411
- cloud_mts = cloud_stat.st_mtime
412
- except FileNotFoundError:
413
- exists = False
399
+ is_dir = cloud_info["type"] == "directory"
400
+ except FileNotFoundError:
401
+ exists = False
414
402
 
415
403
  if not exists:
416
- warn_or_error = f"The original path {self} does not exist anymore."
417
- if objectpath.exists():
404
+ warn_or_error = f"The original path {origin} does not exist anymore."
405
+ if destination.exists():
418
406
  warn_or_error += (
419
- f"\nHowever, the local path {objectpath} still exists, you might want"
407
+ f"\nHowever, the local path {destination} still exists, you might want"
420
408
  " to reupload the object back."
421
409
  )
422
410
  logger.warning(warn_or_error)
@@ -425,113 +413,114 @@ def synchronize(
425
413
  raise FileNotFoundError(warn_or_error)
426
414
  return False
427
415
 
428
- # synchronization logic for directories
429
- # to synchronize directories, it should be possible to get modification times
416
+ use_size: bool = False
417
+ # use casting to int to avoid problems when the local filesystem
418
+ # discards fractional parts of timestamps
419
+ if protocol == "s3":
420
+ get_modified = lambda file_stat: int(file_stat["LastModified"].timestamp())
421
+ elif protocol == "gs":
422
+ get_modified = lambda file_stat: int(file_stat["mtime"].timestamp())
423
+ elif protocol == "hf":
424
+ get_modified = lambda file_stat: int(file_stat["last_commit"].date.timestamp())
425
+ else: # http etc
426
+ use_size = True
427
+ get_modified = lambda file_stat: file_stat["size"]
428
+
429
+ if use_size:
430
+ is_sync_needed = lambda cloud_size, local_stat: cloud_size != local_stat.st_size
431
+ else:
432
+ # no need to cast local_stat.st_mtime to int
433
+ # because if it has the fractional part and cloud_mtime doesn't
434
+ # and they have the same integer part then cloud_mtime can't be bigger
435
+ is_sync_needed = (
436
+ lambda cloud_mtime, local_stat: cloud_mtime > local_stat.st_mtime
437
+ )
438
+
439
+ local_paths: list[Path] = []
440
+ cloud_stats: dict[str, int]
430
441
  if is_dir:
431
- files = self.fs.find(str(self), detail=True)
432
- if protocol == "s3":
433
- get_modified = lambda file_stat: file_stat["LastModified"]
434
- elif protocol == "gs":
435
- get_modified = lambda file_stat: file_stat["mtime"]
436
- elif protocol == "hf":
437
- get_modified = lambda file_stat: file_stat["last_commit"].date
438
- else:
439
- raise ValueError(f"Can't synchronize a directory for {protocol}.")
440
- if objectpath.exists():
441
- destination_exists = True
442
- cloud_mts_max = max(
443
- get_modified(file) for file in files.values()
444
- ).timestamp()
445
- local_mts = [
446
- file.stat().st_mtime for file in objectpath.rglob("*") if file.is_file()
447
- ]
448
- n_local_files = len(local_mts)
449
- local_mts_max = max(local_mts)
450
- if local_mts_max == cloud_mts_max:
451
- need_synchronize = n_local_files != len(files)
452
- elif local_mts_max > cloud_mts_max:
453
- need_synchronize = False
454
- else:
455
- need_synchronize = True
456
- else:
457
- destination_exists = False
458
- need_synchronize = True
459
- # just check if synchronization is needed
460
- if just_check:
461
- return need_synchronize
462
- if need_synchronize:
463
- callback = ProgressCallback.requires_progress(
464
- callback, print_progress, objectpath.name, "synchronizing"
465
- )
466
- callback.set_size(len(files))
467
- origin_file_keys = []
468
- for file, stat in callback.wrap(files.items()):
469
- file_key = PurePosixPath(file).relative_to(self.path).as_posix()
470
- origin_file_keys.append(file_key)
471
- timestamp = get_modified(stat).timestamp()
472
- origin = f"{protocol}://{file}"
473
- destination = objectpath / file_key
474
- child = callback.branched(origin, destination.as_posix())
475
- UPath(origin, **self.storage_options).synchronize(
476
- destination, callback=child, timestamp=timestamp
442
+ cloud_stats = {
443
+ file: get_modified(stat)
444
+ for file, stat in origin.fs.find(origin.as_posix(), detail=True).items()
445
+ }
446
+ for cloud_path in cloud_stats:
447
+ file_key = PurePosixPath(cloud_path).relative_to(origin.path).as_posix()
448
+ local_paths.append(destination / file_key)
449
+ else:
450
+ cloud_stats = {origin.path: get_modified(cloud_info)}
451
+ local_paths.append(destination)
452
+
453
+ local_paths_all: dict[Path, os.stat_result] = {}
454
+ if destination.exists():
455
+ if is_dir:
456
+ local_paths_all = {
457
+ path: path.stat() for path in destination.rglob("*") if path.is_file()
458
+ }
459
+ if not use_size:
460
+ # cast to int to remove the fractional parts
461
+ # there is a problem when a fractional part is allowed on one filesystem
462
+ # but not on the other
463
+ # so just normalize both to int
464
+ cloud_mts_max: int = max(cloud_stats.values())
465
+ local_mts_max: int = int(
466
+ max(stat.st_mtime for stat in local_paths_all.values())
477
467
  )
478
- child.close()
479
- if destination_exists:
480
- local_files = [file for file in objectpath.rglob("*") if file.is_file()]
481
- if len(local_files) > len(files):
482
- for file in local_files:
483
- if (
484
- file.relative_to(objectpath).as_posix()
485
- not in origin_file_keys
486
- ):
487
- file.unlink()
488
- parent = file.parent
489
- if next(parent.iterdir(), None) is None:
490
- parent.rmdir()
491
- return need_synchronize
492
-
493
- # synchronization logic for files
494
- callback = ProgressCallback.requires_progress(
495
- callback, print_progress, objectpath.name, "synchronizing"
496
- )
497
- objectpath_exists = objectpath.exists()
498
- if objectpath_exists:
499
- if cloud_mts != 0:
500
- local_mts_obj = objectpath.stat().st_mtime
501
- need_synchronize = cloud_mts > local_mts_obj
468
+ if local_mts_max > cloud_mts_max:
469
+ return False
470
+ elif local_mts_max == cloud_mts_max:
471
+ if len(local_paths_all) == len(cloud_stats):
472
+ return False
473
+ elif just_check:
474
+ return True
502
475
  else:
503
- # this is true for http for example
504
- # where size is present but st_mtime is not
505
- # we assume that any change without the change in size is unlikely
506
- cloud_size = cloud_stat.st_size
507
- local_size_obj = objectpath.stat().st_size
508
- need_synchronize = cloud_size != local_size_obj
476
+ local_paths_all = {destination: destination.stat()}
477
+
478
+ cloud_files_sync = []
479
+ local_files_sync = []
480
+ for i, (cloud_file, cloud_stat) in enumerate(cloud_stats.items()):
481
+ local_path = local_paths[i]
482
+ if local_path not in local_paths_all or is_sync_needed(
483
+ cloud_stat, local_paths_all[local_path]
484
+ ):
485
+ cloud_files_sync.append(cloud_file)
486
+ local_files_sync.append(local_path.as_posix())
509
487
  else:
510
- if not just_check:
511
- objectpath.parent.mkdir(parents=True, exist_ok=True)
512
- need_synchronize = True
513
- # just check if synchronization is needed
514
- if just_check:
515
- return need_synchronize
516
- if need_synchronize:
517
- # just to be sure that overwriting an existing file doesn't corrupt it
518
- # we saw some frequent corruption on some systems for unclear reasons
519
- if objectpath_exists:
520
- objectpath.unlink()
521
- # hf has sync filesystem
522
- # on sync filesystems ChildProgressCallback.branched()
523
- # returns the default callback
524
- # this is why a difference between s3 and hf in progress bars
525
- self.download_to(
526
- objectpath, recursive=False, print_progress=False, callback=callback
488
+ cloud_files_sync = list(cloud_stats.keys())
489
+ local_files_sync = [local_path.as_posix() for local_path in local_paths]
490
+
491
+ if cloud_files_sync:
492
+ if just_check:
493
+ return True
494
+
495
+ callback = ProgressCallback.requires_progress(
496
+ maybe_callback=kwargs.pop("callback", None),
497
+ print_progress=print_progress,
498
+ objectname=destination.name,
499
+ action="synchronizing",
500
+ adjust_size=False,
527
501
  )
528
- if cloud_mts != 0:
529
- os.utime(objectpath, times=(cloud_mts, cloud_mts))
502
+ origin.fs.download(
503
+ cloud_files_sync,
504
+ local_files_sync,
505
+ recursive=False,
506
+ callback=callback,
507
+ **kwargs,
508
+ )
509
+ if not use_size:
510
+ for i, cloud_file in enumerate(cloud_files_sync):
511
+ cloud_mtime = cloud_stats[cloud_file]
512
+ os.utime(local_files_sync[i], times=(cloud_mtime, cloud_mtime))
530
513
  else:
531
- # nothing happens if parent_update is not defined
532
- # because of Callback.no_op
533
- callback.parent_update()
534
- return need_synchronize
514
+ return False
515
+
516
+ if is_dir and local_paths_all:
517
+ for path in (path for path in local_paths_all if path not in local_paths):
518
+ path.unlink()
519
+ parent = path.parent
520
+ if next(parent.iterdir(), None) is None:
521
+ parent.rmdir()
522
+
523
+ return True
535
524
 
536
525
 
537
526
  def modified(self) -> datetime | None:
@@ -710,14 +699,7 @@ def to_url(upath):
710
699
  raise ValueError("The provided UPath must be an S3 path.")
711
700
  key = "/".join(upath.parts[1:])
712
701
  bucket = upath.drive
713
- if bucket == "scverse-spatial-eu-central-1":
714
- region = "eu-central-1"
715
- elif f"s3://{bucket}" not in HOSTED_BUCKETS:
716
- response = upath.fs.call_s3("head_bucket", Bucket=bucket)
717
- headers = response["ResponseMetadata"]["HTTPHeaders"]
718
- region = headers.get("x-amz-bucket-region")
719
- else:
720
- region = bucket.replace("lamin_", "")
702
+ region = get_storage_region(upath)
721
703
  if region == "us-east-1":
722
704
  return f"https://{bucket}.s3.amazonaws.com/{key}"
723
705
  else:
@@ -740,7 +722,8 @@ def to_url(upath):
740
722
 
741
723
  # add custom functions
742
724
  UPath.modified = property(modified)
743
- UPath.synchronize = synchronize
725
+ UPath.synchronize = deprecated("synchronize_to")(synchronize_to)
726
+ UPath.synchronize_to = synchronize_to
744
727
  UPath.upload_from = upload_from
745
728
  UPath.to_url = to_url
746
729
  UPath.download_to = download_to
@@ -823,6 +806,67 @@ class S3QueryPath(S3Path):
823
806
  register_implementation("s3", S3QueryPath, clobber=True)
824
807
 
825
808
 
809
+ def get_storage_region(path: UPathStr) -> str | None:
810
+ upath = UPath(path)
811
+
812
+ if upath.protocol != "s3":
813
+ return None
814
+
815
+ bucket = upath.drive
816
+
817
+ if bucket == "scverse-spatial-eu-central-1":
818
+ return "eu-central-1"
819
+ elif f"s3://{bucket}" in HOSTED_BUCKETS:
820
+ return bucket.replace("lamin-", "")
821
+
822
+ from botocore.exceptions import ClientError
823
+
824
+ if isinstance(path, str):
825
+ import botocore.session
826
+ from botocore.config import Config
827
+
828
+ path_part = path.replace("s3://", "")
829
+ # check for endpoint_url in the path string
830
+ if "?" in path_part:
831
+ path_part, query = _split_path_query(path_part)
832
+ endpoint_url = query.get("endpoint_url", [None])[0]
833
+ else:
834
+ endpoint_url = None
835
+ session = botocore.session.get_session()
836
+ credentials = session.get_credentials()
837
+ if credentials is None or credentials.access_key is None:
838
+ config = Config(signature_version=botocore.session.UNSIGNED)
839
+ else:
840
+ config = None
841
+ s3_client = session.create_client(
842
+ "s3", endpoint_url=endpoint_url, config=config
843
+ )
844
+ try:
845
+ response = s3_client.head_bucket(Bucket=bucket)
846
+ except ClientError as exc:
847
+ response = getattr(exc, "response", {})
848
+ if response.get("Error", {}).get("Code") == "404":
849
+ raise exc
850
+ else:
851
+ upath = get_aws_options_manager()._path_inject_options(upath, {})
852
+ try:
853
+ response = upath.fs.call_s3("head_bucket", Bucket=bucket)
854
+ except Exception as exc:
855
+ cause = getattr(exc, "__cause__", None)
856
+ if not isinstance(cause, ClientError):
857
+ raise exc
858
+ response = getattr(cause, "response", {})
859
+ if response.get("Error", {}).get("Code") == "404":
860
+ raise exc
861
+
862
+ region = (
863
+ response.get("ResponseMetadata", {})
864
+ .get("HTTPHeaders", {})
865
+ .get("x-amz-bucket-region", None)
866
+ )
867
+ return region
868
+
869
+
826
870
  def create_path(path: UPathStr, access_token: str | None = None) -> UPath:
827
871
  upath = UPath(path).expanduser()
828
872
 
@@ -81,5 +81,12 @@ def test_get_stat_dir_cloud_hf():
81
81
  def test_get_storage_region():
82
82
  for region in HOSTED_REGIONS:
83
83
  assert get_storage_region(f"s3://lamin-{region}") == region
84
+ assert get_storage_region("s3://scverse-spatial-eu-central-1") == "eu-central-1"
85
+
84
86
  assert get_storage_region(UPath("s3://lamindata", endpoint_url=None)) == "us-east-1"
85
87
  assert get_storage_region("s3://lamindata/?endpoint_url=") == "us-east-1"
88
+ # private bucket
89
+ assert (
90
+ get_storage_region(UPath("s3://lamindb-setup-private-bucket/some-folder"))
91
+ == "us-east-1"
92
+ )
@@ -11,11 +11,12 @@ def test_to_url():
11
11
  == "https://lamindata.s3.amazonaws.com/test-folder"
12
12
  )
13
13
  # private bucket
14
- # next PR
15
- # assert (
16
- # ln_setup.core.upath.create_path("s3://lamindb-setup-private-bucket/test-folder")
17
- # == "https://lamindb-setup-private-bucket.s3.amazonaws.com/test-folder"
18
- # )
14
+ assert (
15
+ ln_setup.core.upath.create_path(
16
+ "s3://lamindb-setup-private-bucket/test-folder"
17
+ ).to_url()
18
+ == "https://lamindb-setup-private-bucket.s3.amazonaws.com/test-folder"
19
+ )
19
20
  # eu-central-1 / AWS Dev
20
21
  assert (
21
22
  ln_setup.core.upath.create_path("s3://lamindata-eu/test-folder").to_url()
@@ -27,5 +28,5 @@ def test_to_url():
27
28
  ln_setup.core.upath.create_path(
28
29
  "s3://lamin-eu-central-1/9fm7UN13/test-folder"
29
30
  ).to_url()
30
- == "https://lamin-eu-central-1.s3-lamin-eu-central-1.amazonaws.com/9fm7UN13/test-folder"
31
+ == "https://lamin-eu-central-1.s3-eu-central-1.amazonaws.com/9fm7UN13/test-folder"
31
32
  )
File without changes
File without changes
File without changes
File without changes