Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
4107bbd
tmp: temporarily points the dandischema dependency to the `devendoriz…
candleindark Jul 9, 2025
af36aa0
feat: expand `ServerInfo` to conclude `instance_config`
candleindark Jul 11, 2025
7b15519
feat: provide func to fetch server info
candleindark Jul 11, 2025
e28b6e7
feat: provide func to bind client to server
candleindark Jul 11, 2025
04f7c26
feat: provide func to initialize the DANDI client
candleindark Jul 11, 2025
50a67ca
feat: delay import of `dandischema.models` in `dandi.dandiapi`
candleindark Jul 17, 2025
c6aec31
feat: delay import of `dandischema.models` in `dandi.files.bases`
candleindark Jul 17, 2025
a437bcb
feat: delay import of `dandischema.models` in `dandi.metadata.core`
candleindark Jul 17, 2025
7503e4b
feat: delay import of `dandischema.models` in `dandi.files.bases`
candleindark Jul 18, 2025
f21d848
feat: delay import of `dandi.misctypes.DUMMY_DANDI_ZARR_CHECKSUM` in …
candleindark Jul 20, 2025
eb96390
feat: replace `Digest` constants in `misctypes.py` with cache funcs
candleindark Jul 20, 2025
2c06e3d
feat: delay `dandischema.models` import in `dandi/misctypes.py`
candleindark Jul 20, 2025
19171a5
feat: delay `dandischema.models` import in `dandi/files/zarr.py`
candleindark Jul 20, 2025
571de07
feat: delay `dandischema.models` import in `dandi/files/bids.py`
candleindark Jul 20, 2025
007e89a
feat: delay `dandischema.models` import in `dandi/dandiset.py`
candleindark Jul 20, 2025
f7861df
feat: delay `dandischema.models` import in `dandi/cli/cmd_ls.py`
candleindark Jul 20, 2025
066e16e
feat: delay `dandischema.models` import in `dandi/download.py`
candleindark Jul 20, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions dandi/cli/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,82 @@
import os

import click
from dandischema.conf import set_instance_config
import requests
from yarl import URL

from dandi.consts import known_instances
from dandi.utils import ServerInfo

from .. import get_logger

lgr = get_logger()


def get_server_info(dandi_id: str) -> ServerInfo:
"""
Get server info from a particular DANDI instance

Parameters
----------
dandi_id : str
The ID specifying the particular known DANDI instance to query for server info.
This is a key in the `dandi.consts.known_instances` dictionary.

Returns
-------
ServerInfo
An object representing the server information responded by the DANDI instance.

Raises
------
valueError
If the provided `dandi_id` is not a valid key in the
`dandi.consts.known_instances` dictionary.
"""
if dandi_id not in known_instances:
raise ValueError(f"Unknown DANDI instance: {dandi_id}")

info_url = str(URL(known_instances[dandi_id].api) / "info/")
resp = requests.get(info_url)
resp.raise_for_status()
return ServerInfo.model_validate(resp.json())


def bind_client(server_info: ServerInfo) -> None:
"""
Bind the DANDI client to a specific DANDI server instance. I.e., to set the DANDI
server instance as the context of subsequent command executions by the DANDI client

Parameters
----------
server_info : ServerInfo
An object containing the information of the DANDI server instance to bind to.
This is typically obtained by calling `get_server_info()`.
"""
set_instance_config(server_info.instance_config)


def init_client(dandi_id: str) -> None:
"""
Initialize the DANDI client, including binding the client to a specific DANDI server
instance

Parameters
----------
dandi_id : str
The ID specifying the particular known DANDI instance to bind the client to.
This is a key in the `dandi.consts.known_instances` dictionary.

Raises
------
ValueError
If the provided `dandi_id` is not a valid key in the
`dandi.consts.known_instances` dictionary.
"""
bind_client(get_server_info(dandi_id))


# Aux common functionality


Expand Down
3 changes: 2 additions & 1 deletion dandi/cli/cmd_ls.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import os.path as op

import click
from dandischema import models

from .base import devel_option, lgr, map_to_click_exceptions
from .formatter import JSONFormatter, JSONLinesFormatter, PYOUTFormatter, YAMLFormatter
Expand Down Expand Up @@ -92,6 +91,8 @@ def ls(
):
"""List .nwb files and dandisets metadata."""

from dandischema import models

# TODO: more logical ordering in case of fields = None
common_fields = ("path", "size")
if schema is not None:
Expand Down
23 changes: 21 additions & 2 deletions dandi/dandiapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
from typing import TYPE_CHECKING, Any, Dict, List, Optional

import click
from dandischema import models
from pydantic import BaseModel, Field, PrivateAttr
import requests
import tenacity
Expand All @@ -36,7 +35,6 @@
)
from .exceptions import HTTP404Error, NotFoundError, SchemaVersionError
from .keyring import keyring_lookup, keyring_save
from .misctypes import Digest, RemoteReadableAsset
from .utils import (
USER_AGENT,
check_dandi_version,
Expand All @@ -50,8 +48,11 @@
)

if TYPE_CHECKING:
from dandischema import models
from typing_extensions import Self

from .misctypes import Digest, RemoteReadableAsset


lgr = get_logger()

Expand Down Expand Up @@ -653,6 +654,8 @@ def check_schema_version(self, schema_version: str | None = None) -> None:
uses; if not set, the schema version for the installed
``dandischema`` library is used
"""
from dandischema import models

if schema_version is None:
schema_version = models.get_schema_version()
server_info = self.get("/info/")
Expand Down Expand Up @@ -1065,6 +1068,8 @@ def get_metadata(self) -> models.Dandiset:
metadata. Consider using `get_raw_metadata()` instead in order to
fetch unstructured, possibly-invalid metadata.
"""
from dandischema import models

return models.Dandiset.model_validate(self.get_raw_metadata())

def get_raw_metadata(self) -> dict[str, Any]:
Expand Down Expand Up @@ -1469,6 +1474,8 @@ def get_metadata(self) -> models.Asset:
valid metadata. Consider using `get_raw_metadata()` instead in
order to fetch unstructured, possibly-invalid metadata.
"""
from dandischema import models

return models.Asset.model_validate(self.get_raw_metadata())

def get_raw_metadata(self) -> dict[str, Any]:
Expand All @@ -1495,6 +1502,8 @@ def get_raw_digest(self, digest_type: str | models.DigestType | None = None) ->
.. versionchanged:: 0.36.0
Renamed from ``get_digest()`` to ``get_raw_digest()``
"""
from dandischema import models

if digest_type is None:
digest_type = self.digest_type.value
elif isinstance(digest_type, models.DigestType):
Expand All @@ -1517,6 +1526,8 @@ def get_digest(self) -> Digest:
a dandi-etag digest for blob resources or a dandi-zarr-checksum for
Zarr resources
"""
from .misctypes import Digest

algorithm = self.digest_type
return Digest(algorithm=algorithm, value=self.get_raw_digest(algorithm))

Expand Down Expand Up @@ -1651,6 +1662,8 @@ def digest_type(self) -> models.DigestType:
determined based on its underlying data: dandi-etag for blob resources,
dandi-zarr-checksum for Zarr resources
"""
from dandischema import models

if self.asset_type is AssetType.ZARR:
return models.DigestType.dandi_zarr_checksum
else:
Expand Down Expand Up @@ -1683,6 +1696,8 @@ def as_readable(self) -> RemoteReadableAsset:
Returns a `Readable` instance that can be used to obtain a file-like
object for reading bytes directly from the asset on the server
"""
from .misctypes import RemoteReadableAsset

md = self.get_raw_metadata()
local_prefix = self.client.api_url.lower()
for url in md.get("contentUrl", []):
Expand Down Expand Up @@ -1965,6 +1980,10 @@ def from_server_data(
cls, asset: BaseRemoteZarrAsset, data: ZarrEntryServerData
) -> RemoteZarrEntry:
""":meta private:"""
from dandischema import models

from .misctypes import Digest

return cls(
client=asset.client,
zarr_id=asset.zarr,
Expand Down
4 changes: 2 additions & 2 deletions dandi/dandiset.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@
from pathlib import Path, PurePath, PurePosixPath
from typing import TYPE_CHECKING

from dandischema.models import get_schema_version

from . import get_logger
from .consts import dandiset_metadata_file
from .files import DandisetMetadataFile, LocalAsset, dandi_file, find_dandi_files
Expand All @@ -32,6 +30,8 @@ def __init__(
schema_version: str | None = None,
) -> None:
if schema_version is not None:
from dandischema.models import get_schema_version

current_version = get_schema_version()
if schema_version != current_version:
raise ValueError(
Expand Down
3 changes: 2 additions & 1 deletion dandi/download.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
from typing import IO, Any, Literal

from dandischema.digests.dandietag import ETagHashlike
from dandischema.models import DigestType
from fasteners import InterProcessLock
import humanize
from interleave import FINISH_CURRENT, lazy_interleave
Expand Down Expand Up @@ -995,6 +994,8 @@ def digest_callback(path: str, algoname: str, d: str) -> None:
digests[path] = d

def downloads_gen():
from dandischema.models import DigestType

for entry in asset.iterfiles():
entries.append(entry)
etag = entry.digest
Expand Down
35 changes: 28 additions & 7 deletions dandi/files/bases.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,12 @@
from pathlib import Path
import re
from threading import Lock
from typing import IO, Any, Generic
from typing import IO, TYPE_CHECKING, Any, Generic, TypeVar
from xml.etree.ElementTree import fromstring

import dandischema
from dandischema.consts import DANDI_SCHEMA_VERSION
from dandischema.digests.dandietag import DandiETag
from dandischema.models import BareAsset, CommonModel
from dandischema.models import Dandiset as DandisetMeta
from dandischema.models import get_schema_version
from packaging.version import Version
from pydantic import ValidationError
from pydantic_core import ErrorDetails
Expand All @@ -27,7 +24,6 @@
import dandi
from dandi.dandiapi import RemoteAsset, RemoteDandiset, RESTFullAPIClient
from dandi.metadata.core import get_default_metadata
from dandi.misctypes import DUMMY_DANDI_ETAG, Digest, LocalReadableFile, P
from dandi.utils import post_upload_size_check, pre_upload_size_check, yaml_load
from dandi.validate_types import (
ORIGIN_INTERNAL_DANDI,
Expand All @@ -41,6 +37,15 @@
Validator,
)

if TYPE_CHECKING:
from dandischema.models import BareAsset, CommonModel
from dandischema.models import Dandiset as DandisetMeta

# noinspection PyUnresolvedReferences
from dandi.misctypes import BasePath, Digest, LocalReadableFile

Check notice

Code scanning / CodeQL

Unused import Note

Import of 'BasePath' is not used.

P = TypeVar("P", bound="BasePath")

lgr = dandi.get_logger()

# TODO -- should come from schema. This is just a simplistic example for now
Expand Down Expand Up @@ -107,6 +112,8 @@
ignore_errors: bool = True,
) -> DandisetMeta:
"""Return the Dandiset metadata inside the file"""
from dandischema.models import Dandiset as DandisetMeta

with open(self.filepath) as f:
meta = yaml_load(f, typ="safe")
return DandisetMeta.model_construct(**meta)
Expand All @@ -126,6 +133,9 @@
meta, _required_dandiset_metadata_fields, str(self.filepath)
)
else:
from dandischema.models import Dandiset as DandisetMeta
from dandischema.models import get_schema_version

current_version = get_schema_version()
if schema_version != current_version:
raise ValueError(
Expand All @@ -147,6 +157,8 @@

Returns a `Readable` instance wrapping the local file
"""
from dandi.misctypes import LocalReadableFile

return LocalReadableFile(self.filepath)


Expand All @@ -161,7 +173,11 @@
#: (i.e., relative to the Dandiset's root)
path: str

_DUMMY_DIGEST = DUMMY_DANDI_ETAG
@staticmethod
def _get_dummy_digest() -> Digest:
from dandi.misctypes import get_dummy_dandi_etag

return get_dummy_dandi_etag()

@abstractmethod
def get_digest(self) -> Digest:
Expand All @@ -186,6 +202,8 @@
schema_version: str | None = None,
devel_debug: bool = False,
) -> list[ValidationResult]:
from dandischema.models import BareAsset, get_schema_version

current_version = get_schema_version()
if schema_version is None:
schema_version = current_version
Expand All @@ -194,7 +212,7 @@
f"Unsupported schema version: {schema_version}; expected {current_version}"
)
try:
asset = self.get_metadata(digest=self._DUMMY_DIGEST)
asset = self.get_metadata(digest=self._get_dummy_digest())
BareAsset(**asset.model_dump())
except ValidationError as e:
if devel_debug:
Expand Down Expand Up @@ -309,6 +327,7 @@
def get_digest(self) -> Digest:
"""Calculate a dandi-etag digest for the asset"""
# Avoid heavy import by importing within function:
from dandi.misctypes import Digest
from dandi.support.digests import get_digest

value = get_digest(self.filepath, digest="dandi-etag")
Expand Down Expand Up @@ -476,6 +495,8 @@

Returns a `Readable` instance wrapping the local file
"""
from dandi.misctypes import LocalReadableFile

return LocalReadableFile(self.filepath)


Expand Down
10 changes: 8 additions & 2 deletions dandi/files/bids.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@
from datetime import datetime
from pathlib import Path
from threading import Lock
from typing import TYPE_CHECKING
import weakref

from dandischema.models import BareAsset

from dandi.bids_validator_deno import bids_validate

from .bases import GenericAsset, LocalFileAsset, NWBAsset
Expand All @@ -18,6 +17,9 @@
from ..misctypes import Digest
from ..validate_types import ValidationResult

if TYPE_CHECKING:
from dandischema.models import BareAsset

BIDS_ASSET_ERRORS = ("BIDS.NON_BIDS_PATH_PLACEHOLDER",)
BIDS_DATASET_ERRORS = ("BIDS.MANDATORY_FILE_MISSING_PLACEHOLDER",)

Expand Down Expand Up @@ -84,6 +86,8 @@ def _get_metadata(self) -> None:

This populates `self._asset_metadata`
"""
from dandischema.models import BareAsset

with self._lock:
if self._asset_metadata is None:
# Import here to avoid circular import
Expand Down Expand Up @@ -236,6 +240,8 @@ def get_metadata(
digest: Digest | None = None,
ignore_errors: bool = True,
) -> BareAsset:
from dandischema.models import BareAsset

bids_metadata = BIDSAsset.get_metadata(self, digest, ignore_errors)
nwb_metadata = NWBAsset.get_metadata(self, digest, ignore_errors)
return BareAsset(
Expand Down
Loading
Loading