From 0b7a7baf1d08be9265c1a6bb780286a2b531155e Mon Sep 17 00:00:00 2001 From: "Ware, Joseph (DLSLtd,RAL,LSCI)" Date: Thu, 20 Feb 2025 14:20:26 +0000 Subject: [PATCH 1/9] Enable and configure importlinter validation for pre-commit and github ci --- .pre-commit-config.yaml | 5 +++++ pyproject.toml | 15 +++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 37332184e..faf3ff693 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -35,3 +35,8 @@ repos: hooks: - id: mypy args: [--strict] + +- repo: https://github.com/seddonym/import-linter + rev: v2.2 + hooks: + - id: import-linter diff --git a/pyproject.toml b/pyproject.toml index 7ca1d182e..1abb99196 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -299,3 +299,18 @@ exclude = ''' | web-frontend ) ''' + +[tool.importlinter] +root_package = "tiled" + +[[tool.importlinter.contracts]] +name="Server cannot import from Client" +type="forbidden" +source_modules=["tiled.server"] +forbidden_modules=["tiled.client"] + +[[tool.importlinter.contracts]] +name="Client cannot import from Server" +type="forbidden" +source_modules=["tiled.client"] +forbidden_modules=["tiled.server"] From 394e4ea8c27788f2efb602697e6802701fe60b0b Mon Sep 17 00:00:00 2001 From: Dan Allan Date: Fri, 21 Feb 2025 12:03:52 -0500 Subject: [PATCH 2/9] Move tree --- docs/source/tutorials/navigation.md | 2 +- tiled/_tests/test_hdf5.py | 2 +- tiled/client/__init__.py | 2 +- tiled/client/utils.py | 105 ++++++++++++++++++++++++++++ tiled/utils.py | 104 --------------------------- 5 files changed, 108 insertions(+), 107 deletions(-) diff --git a/docs/source/tutorials/navigation.md b/docs/source/tutorials/navigation.md index 69f7de919..a61fb90ab 100644 --- a/docs/source/tutorials/navigation.md +++ b/docs/source/tutorials/navigation.md @@ -23,7 +23,7 @@ a directory of files or hierarchical structure like an HDF5 file or XML file. Tiled provides a utility for visualizing a nested structure. ```python ->>> from tiled.utils import tree +>>> from tiled.client import tree >>> tree(client) ├── big_image ├── small_image diff --git a/tiled/_tests/test_hdf5.py b/tiled/_tests/test_hdf5.py index 6067a9f15..b81117741 100644 --- a/tiled/_tests/test_hdf5.py +++ b/tiled/_tests/test_hdf5.py @@ -7,8 +7,8 @@ from ..adapters.hdf5 import HDF5Adapter from ..adapters.mapping import MapAdapter from ..client import Context, from_context, record_history +from ..client import tree as tree_util from ..server.app import build_app -from ..utils import tree as tree_util @pytest.fixture diff --git a/tiled/client/__init__.py b/tiled/client/__init__.py index 415411391..91ed9932c 100644 --- a/tiled/client/__init__.py +++ b/tiled/client/__init__.py @@ -1,9 +1,9 @@ -from ..utils import tree from .constructors import from_context, from_profile, from_uri from .container import ASCENDING, DESCENDING from .context import Context from .logger import hide_logs, record_history, show_logs from .metadata_update import DELETE_KEY +from .utils import tree __all__ = [ "ASCENDING", diff --git a/tiled/client/utils.py b/tiled/client/utils.py index 183999816..3236a95d0 100644 --- a/tiled/client/utils.py +++ b/tiled/client/utils.py @@ -1,4 +1,5 @@ import builtins +import collections import uuid from collections.abc import Hashable from pathlib import Path @@ -313,3 +314,107 @@ def get_asset_filepaths(node): # because it cannot provide a filepath. filepaths.append(path_from_uri(asset.data_uri)) return filepaths + + +def _line(nodes, last): + "Generate a single line for the tree utility" + tee = "├" + vertical = "│ " + horizontal = "── " + L = "└" + blank = " " + indent = "" + for item in last[:-1]: + if item: + indent += blank + else: + indent += vertical + if last[-1]: + return indent + L + horizontal + nodes[-1] + else: + return indent + tee + horizontal + nodes[-1] + + +def walk(tree, nodes=None): + "Walk the entries in a (nested) Tree depth first." + if nodes is None: + for node in tree: + yield from walk(tree, [node]) + else: + value = tree[nodes[-1]] + if hasattr(value, "items"): + yield nodes + for k, v in value.items(): + yield from walk(value, nodes + [k]) + else: + yield nodes + + +def gen_tree(tree, nodes=None, last=None): + "A generator of lines for the tree utility" + + # Normally, traversing a Tree will cause the structure clients to be + # instanitated which in turn triggers import of the associated libraries like + # numpy, pandas, and xarray. We want to avoid paying for that, especially + # when this function is used in a CLI where import overhead can accumulate to + # about 2 seconds, the bulk of the time. Therefore, we do something a bit + # "clever" here to override the normal structure clients with dummy placeholders. + from .client.container import Container + + def dummy_client(*args, **kwargs): + return None + + structure_clients = collections.defaultdict(lambda: dummy_client) + structure_clients["container"] = Container + fast_tree = tree.new_variation(structure_clients=structure_clients) + if nodes is None: + last_index = len(fast_tree) - 1 + for index, node in enumerate(fast_tree): + yield from gen_tree(fast_tree, [node], [index == last_index]) + else: + value = fast_tree[nodes[-1]] + if hasattr(value, "items"): + yield _line(nodes, last) + last_index = len(value) - 1 + for index, (k, v) in enumerate(value.items()): + yield from gen_tree(value, nodes + [k], last + [index == last_index]) + else: + yield _line(nodes, last) + + +def tree(tree, max_lines=20): + """ + Print a visual sketch of Tree structure akin to UNIX `tree`. + + Parameters + ---------- + tree : Tree + max_lines: int or None, optional + By default, output is trucated at 20 lines. ``None`` means "Do not + truncate." + + Examples + -------- + + >>> tree(tree) + ├── A + │ ├── dog + │ ├── cat + │ └── monkey + └── B + ├── snake + ├── bear + └── wolf + + """ + if len(tree) == 0: + print("") + return + for counter, line in enumerate(gen_tree(tree), start=1): + if (max_lines is not None) and (counter > max_lines): + print( + f"" + ) + break + print(line) diff --git a/tiled/utils.py b/tiled/utils.py index 684b79659..8be4afb7c 100644 --- a/tiled/utils.py +++ b/tiled/utils.py @@ -304,110 +304,6 @@ class SpecialUsers(str, enum.Enum): admin = "admin" -def _line(nodes, last): - "Generate a single line for the tree utility" - tee = "├" - vertical = "│ " - horizontal = "── " - L = "└" - blank = " " - indent = "" - for item in last[:-1]: - if item: - indent += blank - else: - indent += vertical - if last[-1]: - return indent + L + horizontal + nodes[-1] - else: - return indent + tee + horizontal + nodes[-1] - - -def walk(tree, nodes=None): - "Walk the entries in a (nested) Tree depth first." - if nodes is None: - for node in tree: - yield from walk(tree, [node]) - else: - value = tree[nodes[-1]] - if hasattr(value, "items"): - yield nodes - for k, v in value.items(): - yield from walk(value, nodes + [k]) - else: - yield nodes - - -def gen_tree(tree, nodes=None, last=None): - "A generator of lines for the tree utility" - - # Normally, traversing a Tree will cause the structure clients to be - # instanitated which in turn triggers import of the associated libraries like - # numpy, pandas, and xarray. We want to avoid paying for that, especially - # when this function is used in a CLI where import overhead can accumulate to - # about 2 seconds, the bulk of the time. Therefore, we do something a bit - # "clever" here to override the normal structure clients with dummy placeholders. - from .client.container import Container - - def dummy_client(*args, **kwargs): - return None - - structure_clients = collections.defaultdict(lambda: dummy_client) - structure_clients["container"] = Container - fast_tree = tree.new_variation(structure_clients=structure_clients) - if nodes is None: - last_index = len(fast_tree) - 1 - for index, node in enumerate(fast_tree): - yield from gen_tree(fast_tree, [node], [index == last_index]) - else: - value = fast_tree[nodes[-1]] - if hasattr(value, "items"): - yield _line(nodes, last) - last_index = len(value) - 1 - for index, (k, v) in enumerate(value.items()): - yield from gen_tree(value, nodes + [k], last + [index == last_index]) - else: - yield _line(nodes, last) - - -def tree(tree, max_lines=20): - """ - Print a visual sketch of Tree structure akin to UNIX `tree`. - - Parameters - ---------- - tree : Tree - max_lines: int or None, optional - By default, output is trucated at 20 lines. ``None`` means "Do not - truncate." - - Examples - -------- - - >>> tree(tree) - ├── A - │ ├── dog - │ ├── cat - │ └── monkey - └── B - ├── snake - ├── bear - └── wolf - - """ - if len(tree) == 0: - print("") - return - for counter, line in enumerate(gen_tree(tree), start=1): - if (max_lines is not None) and (counter > max_lines): - print( - f"" - ) - break - print(line) - - class Sentinel: def __init__(self, name: str) -> None: self.name = name From 41f83238d0c8584be5ef3f361f845554b089d821 Mon Sep 17 00:00:00 2001 From: Dan Allan Date: Fri, 21 Feb 2025 12:12:07 -0500 Subject: [PATCH 3/9] Merge tiled.server.schemas into tiled.schemas. --- tiled/_tests/test_catalog.py | 2 +- tiled/_tests/test_protocols.py | 2 +- tiled/adapters/mapping.py | 2 +- tiled/adapters/protocols.py | 2 +- tiled/authn_database/orm.py | 2 +- tiled/catalog/adapter.py | 2 +- .../2ca16566d692_separate_structure_table.py | 2 +- tiled/catalog/orm.py | 2 +- tiled/schemas.py | 555 +++++++++++++++++- tiled/server/schemas.py | 544 ----------------- 10 files changed, 561 insertions(+), 554 deletions(-) delete mode 100644 tiled/server/schemas.py diff --git a/tiled/_tests/test_catalog.py b/tiled/_tests/test_catalog.py index ff005bcc5..81a7ce68e 100644 --- a/tiled/_tests/test_catalog.py +++ b/tiled/_tests/test_catalog.py @@ -22,8 +22,8 @@ from ..client import Context, from_context from ..client.xarray import write_xarray_dataset from ..queries import Eq, Key +from ..schemas import Asset, DataSource, Management from ..server.app import build_app, build_app_from_config -from ..server.schemas import Asset, DataSource, Management from ..structures.core import StructureFamily from ..utils import ensure_uri from .utils import enter_username_password diff --git a/tiled/_tests/test_protocols.py b/tiled/_tests/test_protocols.py index 9f88afd11..2f3582124 100644 --- a/tiled/_tests/test_protocols.py +++ b/tiled/_tests/test_protocols.py @@ -19,7 +19,7 @@ SparseAdapter, TableAdapter, ) -from tiled.server.schemas import Principal, PrincipalType +from tiled.schemas import Principal, PrincipalType from tiled.structures.array import ArrayStructure, BuiltinDtype from tiled.structures.awkward import AwkwardStructure from tiled.structures.core import Spec, StructureFamily diff --git a/tiled/adapters/mapping.py b/tiled/adapters/mapping.py index fd97e1dfd..5313d70d3 100644 --- a/tiled/adapters/mapping.py +++ b/tiled/adapters/mapping.py @@ -35,7 +35,7 @@ StructureFamilyQuery, ) from ..query_registration import QueryTranslationRegistry -from ..server.schemas import SortingItem +from ..schemas import SortingItem from ..structures.core import Spec, StructureFamily from ..structures.table import TableStructure from ..type_aliases import JSON diff --git a/tiled/adapters/protocols.py b/tiled/adapters/protocols.py index eda44103b..8ba6f4db3 100644 --- a/tiled/adapters/protocols.py +++ b/tiled/adapters/protocols.py @@ -7,7 +7,7 @@ import sparse from numpy.typing import NDArray -from ..server.schemas import Principal +from ..schemas import Principal from ..structures.array import ArrayStructure from ..structures.awkward import AwkwardStructure from ..structures.core import Spec, StructureFamily diff --git a/tiled/authn_database/orm.py b/tiled/authn_database/orm.py index f480e9f2e..642bd2a74 100644 --- a/tiled/authn_database/orm.py +++ b/tiled/authn_database/orm.py @@ -18,7 +18,7 @@ from sqlalchemy.sql import func from sqlalchemy.types import TypeDecorator -from ..server.schemas import PrincipalType +from ..schemas import PrincipalType from .base import Base # Use JSON with SQLite and JSONB with PostgreSQL. diff --git a/tiled/catalog/adapter.py b/tiled/catalog/adapter.py index 1286deab3..e04e8a33e 100644 --- a/tiled/catalog/adapter.py +++ b/tiled/catalog/adapter.py @@ -64,7 +64,7 @@ ZARR_MIMETYPE, ) from ..query_registration import QueryTranslationRegistry -from ..server.schemas import Asset, DataSource, Management, Revision, Spec +from ..schemas import Asset, DataSource, Management, Revision, Spec from ..structures.core import StructureFamily from ..structures.data_source import Storage from ..utils import ( diff --git a/tiled/catalog/migrations/versions/2ca16566d692_separate_structure_table.py b/tiled/catalog/migrations/versions/2ca16566d692_separate_structure_table.py index 94342c8cf..2f4a58ac3 100644 --- a/tiled/catalog/migrations/versions/2ca16566d692_separate_structure_table.py +++ b/tiled/catalog/migrations/versions/2ca16566d692_separate_structure_table.py @@ -14,7 +14,7 @@ from tiled.catalog.orm import JSONVariant from tiled.catalog.utils import compute_structure_id -from tiled.server.schemas import Management +from tiled.schemas import Management # revision identifiers, used by Alembic. revision = "2ca16566d692" diff --git a/tiled/catalog/orm.py b/tiled/catalog/orm.py index 051faf4f8..25a22a8e8 100644 --- a/tiled/catalog/orm.py +++ b/tiled/catalog/orm.py @@ -21,7 +21,7 @@ from sqlalchemy.schema import UniqueConstraint from sqlalchemy.sql import func -from ..server.schemas import Management +from ..schemas import Management from ..structures.core import StructureFamily from .base import Base diff --git a/tiled/schemas.py b/tiled/schemas.py index 81b9b71dc..64168ba75 100644 --- a/tiled/schemas.py +++ b/tiled/schemas.py @@ -1,6 +1,36 @@ -from typing import Any, Dict, List, Literal, Optional +from __future__ import annotations -from pydantic import BaseModel +import enum +import uuid +from datetime import datetime +from typing import ( + TYPE_CHECKING, + Any, + Dict, + Generic, + List, + Literal, + Optional, + TypeVar, + Union, +) + +import pydantic +import pydantic.generics +from pydantic import BaseModel, ConfigDict, Field, StringConstraints +from pydantic_core import PydanticCustomError +from typing_extensions import Annotated, TypedDict + +from ..structures.array import ArrayStructure +from ..structures.awkward import AwkwardStructure +from ..structures.core import STRUCTURE_TYPES, StructureFamily +from ..structures.data_source import Management +from ..structures.sparse import SparseStructure +from ..structures.table import TableStructure + +if TYPE_CHECKING: + import tiled.authn_database.orm + import tiled.catalog.orm class AboutAuthenticationProvider(BaseModel): @@ -34,3 +64,524 @@ class About(BaseModel): authentication: AboutAuthentication links: Dict[str, str] meta: Dict[str, Any] + + +DataT = TypeVar("DataT") +LinksT = TypeVar("LinksT") +MetaT = TypeVar("MetaT") +StructureT = TypeVar("StructureT") + + +class Error(pydantic.BaseModel): + code: int + message: str + + +class Response(pydantic.BaseModel, Generic[DataT, LinksT, MetaT]): + data: Optional[DataT] + error: Optional[Error] = None + links: Optional[LinksT] = None + meta: Optional[MetaT] = None + + @pydantic.field_validator("error") + def check_consistency(cls, v, values): + if v is not None and values["data"] is not None: + raise ValueError("must not provide both data and error") + if v is None and values.get("data") is None: + raise ValueError("must provide data or error") + return v + + +class PaginationLinks(pydantic.BaseModel): + self: str + next: str + prev: str + first: str + last: str + + +class EntryFields(str, enum.Enum): + metadata = "metadata" + structure_family = "structure_family" + structure = "structure" + count = "count" + sorting = "sorting" + specs = "specs" + data_sources = "data_sources" + none = "" + + +class NodeStructure(pydantic.BaseModel): + contents: Optional[Dict[str, Any]] + count: int + + model_config = pydantic.ConfigDict(extra="forbid") + + +class SortingDirection(int, enum.Enum): + ASCENDING = 1 + DECENDING = -1 + + +class SortingItem(pydantic.BaseModel): + key: str + direction: SortingDirection + + +class Spec(pydantic.BaseModel, extra="forbid", frozen=True): + name: Annotated[str, StringConstraints(max_length=255)] + version: Optional[Annotated[str, StringConstraints(max_length=255)]] = None + + +# Wait for fix https://github.com/pydantic/pydantic/issues/3957 +# Specs = pydantic.conlist(Spec, max_length=20, unique_items=True) +MAX_ALLOWED_SPECS = 20 +Specs = Annotated[List[Spec], Field(max_length=MAX_ALLOWED_SPECS)] + + +class Asset(pydantic.BaseModel): + data_uri: str + is_directory: bool + parameter: Optional[str] = None + num: Optional[int] = None + id: Optional[int] = None + + @classmethod + def from_orm(cls, orm: tiled.catalog.orm.Asset) -> Asset: + return cls( + data_uri=orm.data_uri, + is_directory=orm.is_directory, + id=orm.id, + ) + + @classmethod + def from_assoc_orm(cls, orm): + return cls( + data_uri=orm.asset.data_uri, + is_directory=orm.asset.is_directory, + parameter=orm.parameter, + num=orm.num, + id=orm.asset.id, + ) + + +class Revision(pydantic.BaseModel): + revision_number: int + metadata: dict + specs: Specs + time_updated: datetime + + @classmethod + def from_orm(cls, orm: tiled.catalog.orm.Revision) -> Revision: + # Trailing underscore in 'metadata_' avoids collision with + # SQLAlchemy reserved word 'metadata'. + return cls( + revision_number=orm.revision_number, + metadata=orm.metadata_, + specs=orm.specs, + time_updated=orm.time_updated, + ) + + +class DataSource(pydantic.BaseModel, Generic[StructureT]): + id: Optional[int] = None + structure_family: StructureFamily + structure: Optional[StructureT] + mimetype: Optional[str] = None + parameters: dict = {} + assets: List[Asset] = [] + management: Management = Management.writable + + model_config = pydantic.ConfigDict(extra="forbid") + + @classmethod + def from_orm(cls, orm: tiled.catalog.orm.DataSource) -> DataSource: + if hasattr(orm.structure, "structure"): + structure_cls = STRUCTURE_TYPES[orm.structure_family] + structure = structure_cls.from_json(orm.structure.structure) + else: + structure = None + return cls( + id=orm.id, + structure_family=orm.structure_family, + structure=structure, + mimetype=orm.mimetype, + parameters=orm.parameters, + assets=[Asset.from_assoc_orm(assoc) for assoc in orm.asset_associations], + management=orm.management, + ) + + +class NodeAttributes(pydantic.BaseModel): + ancestors: List[str] + structure_family: Optional[StructureFamily] = None + specs: Optional[Specs] = None + metadata: Optional[Dict] = None # free-form, user-specified dict + structure: Optional[ + Union[ + ArrayStructure, + AwkwardStructure, + SparseStructure, + NodeStructure, + TableStructure, + ] + ] = None + + sorting: Optional[List[SortingItem]] = None + data_sources: Optional[List[DataSource]] = None + + model_config = pydantic.ConfigDict(extra="forbid") + + +AttributesT = TypeVar("AttributesT") +ResourceMetaT = TypeVar("ResourceMetaT") +ResourceLinksT = TypeVar("ResourceLinksT") + + +class SelfLinkOnly(pydantic.BaseModel): + self: str + + +class ContainerLinks(pydantic.BaseModel): + self: str + search: str + full: str + + +class ArrayLinks(pydantic.BaseModel): + self: str + full: str + block: str + + +class AwkwardLinks(pydantic.BaseModel): + self: str + buffers: str + full: str + + +class DataFrameLinks(pydantic.BaseModel): + self: str + full: str + partition: str + + +class SparseLinks(pydantic.BaseModel): + self: str + full: str + block: str + + +resource_links_type_by_structure_family = { + StructureFamily.array: ArrayLinks, + StructureFamily.awkward: AwkwardLinks, + StructureFamily.container: ContainerLinks, + StructureFamily.sparse: SparseLinks, + StructureFamily.table: DataFrameLinks, +} + + +class EmptyDict(pydantic.BaseModel): + pass + + +class ContainerMeta(pydantic.BaseModel): + count: int + + +class Resource(pydantic.BaseModel, Generic[AttributesT, ResourceLinksT, ResourceMetaT]): + "A JSON API Resource" + id: Union[str, uuid.UUID] + attributes: AttributesT + links: Optional[ResourceLinksT] = None + meta: Optional[ResourceMetaT] = None + + +class AccessAndRefreshTokens(pydantic.BaseModel): + access_token: str + expires_in: int + refresh_token: str + refresh_token_expires_in: int + token_type: str + + +class RefreshToken(pydantic.BaseModel): + refresh_token: str + + +class DeviceCode(pydantic.BaseModel): + device_code: str + grant_type: str + + +class PrincipalType(str, enum.Enum): + user = "user" + service = "service" + + +class Identity(pydantic.BaseModel): + model_config = pydantic.ConfigDict(from_attributes=True) + id: Annotated[str, StringConstraints(max_length=255)] + provider: Annotated[str, StringConstraints(max_length=255)] + latest_login: Optional[datetime] = None + + @classmethod + def from_orm(cls, orm: tiled.authn_database.orm.Identity) -> Identity: + return cls(id=orm.id, provider=orm.provider, latest_login=orm.latest_login) + + +class Role(pydantic.BaseModel): + model_config = pydantic.ConfigDict(from_attributes=True) + name: str + scopes: List[str] + # principals + + @classmethod + def from_orm(cls, orm: tiled.authn_database.orm.Role) -> Role: + return cls(name=orm.name, scopes=orm.scopes) + + +class APIKey(pydantic.BaseModel): + model_config = pydantic.ConfigDict(from_attributes=True) + first_eight: Annotated[str, StringConstraints(min_length=8, max_length=8)] + expiration_time: Optional[datetime] = None + note: Optional[Annotated[str, StringConstraints(max_length=255)]] = None + scopes: List[str] + latest_activity: Optional[datetime] = None + + @classmethod + def from_orm(cls, orm: tiled.authn_database.orm.APIKey) -> APIKey: + return cls( + first_eight=orm.first_eight, + expiration_time=orm.expiration_time, + note=orm.note, + scopes=orm.scopes, + latest_activity=orm.latest_activity, + ) + + +class APIKeyWithSecret(APIKey): + secret: str # hex-encoded bytes + + @classmethod + def from_orm( + cls, orm: tiled.authn_database.orm.APIKeyWithSecret, secret: str + ) -> APIKeyWithSecret: + return cls( + first_eight=orm.first_eight, + expiration_time=orm.expiration_time, + note=orm.note, + scopes=orm.scopes, + latest_activity=orm.latest_activity, + secret=secret, + ) + + +class Session(pydantic.BaseModel): + """ + This related to refresh tokens, which have a session uuid ("sid") claim. + + When the client attempts to use a refresh token, we first check + here to ensure that the "session", which is associated with a chain + of refresh tokens that came from a single authentication, are still valid. + """ + + # The id field (primary key) is intentionally not exposed to the application. + # It is left as an internal database concern. + model_config = pydantic.ConfigDict(from_attributes=True) + uuid: uuid.UUID + expiration_time: datetime + revoked: bool + + @classmethod + def from_orm(cls, orm: tiled.authn_database.orm.Session) -> Session: + return cls( + uuid=orm.uuid, expiration_time=orm.expiration_time, revoked=orm.revoked + ) + + +class Principal(pydantic.BaseModel): + "Represents a User or Service" + # The id field (primary key) is intentionally not exposed to the application. + # It is left as an internal database concern. + model_config = pydantic.ConfigDict(from_attributes=True) + uuid: uuid.UUID + type: PrincipalType + identities: List[Identity] = [] + roles: List[Role] = [] + api_keys: List[APIKey] = [] + sessions: List[Session] = [] + latest_activity: Optional[datetime] = None + + @classmethod + def from_orm( + cls, + orm: tiled.authn_database.orm.Principal, + latest_activity: Optional[datetime] = None, + ) -> Principal: + return cls( + uuid=orm.uuid, + type=orm.type, + identities=[Identity.from_orm(id_) for id_ in orm.identities], + roles=[Role.from_orm(id_) for id_ in orm.roles], + api_keys=[APIKey.from_orm(api_key) for api_key in orm.api_keys], + sessions=[Session.from_orm(session) for session in orm.sessions], + latest_activity=latest_activity, + ) + + +class APIKeyRequestParams(pydantic.BaseModel): + # Provide an example for expires_in. Otherwise, OpenAPI suggests lifetime=0. + # If the user is not reading carefully, they will be frustrated when they + # try to use the instantly-expiring API key! + expires_in: Optional[int] = pydantic.Field( + ..., json_schema_extra={"example": 600} + ) # seconds + scopes: Optional[List[str]] = pydantic.Field( + ..., json_schema_extra={"example": ["inherit"]} + ) + note: Optional[str] = None + + +class PostMetadataRequest(pydantic.BaseModel): + id: Optional[str] = None + structure_family: StructureFamily + metadata: Dict = {} + data_sources: List[DataSource] = [] + specs: Specs = [] + + # Wait for fix https://github.com/pydantic/pydantic/issues/3957 + # to do this with `unique_items` parameters to `pydantic.constr`. + @pydantic.field_validator("specs") + def specs_uniqueness_validator(cls, v): + if v is None: + return None + for i, value in enumerate(v, start=1): + if value in v[i:]: + raise ValueError + return v + + @pydantic.model_validator(mode="after") + def narrow_strucutre_type(self): + "Convert the structure on each data_source from a dict to the appropriate pydantic model." + for data_source in self.data_sources: + if self.structure_family != StructureFamily.container: + structure_cls = STRUCTURE_TYPES[self.structure_family] + if data_source.structure is not None: + data_source.structure = structure_cls.from_json( + data_source.structure + ) + return self + + +class PutDataSourceRequest(pydantic.BaseModel): + data_source: DataSource + + +class PostMetadataResponse(pydantic.BaseModel, Generic[ResourceLinksT]): + id: str + links: Union[ArrayLinks, DataFrameLinks, SparseLinks] + metadata: Dict + data_sources: List[DataSource] + + +class PutMetadataResponse(pydantic.BaseModel, Generic[ResourceLinksT]): + id: str + links: Union[ArrayLinks, DataFrameLinks, SparseLinks] + # May be None if not altered + metadata: Optional[Dict] = None + data_sources: Optional[List[DataSource]] = None + + +class DistinctValueInfo(pydantic.BaseModel): + value: Any = None + count: Optional[int] = None + + +class GetDistinctResponse(pydantic.BaseModel): + metadata: Optional[Dict[str, List[DistinctValueInfo]]] = None + structure_families: Optional[List[DistinctValueInfo]] = None + specs: Optional[List[DistinctValueInfo]] = None + + +class PutMetadataRequest(pydantic.BaseModel): + # These fields are optional because None means "no changes; do not update". + metadata: Optional[Dict] = None + specs: Optional[Specs] = None + + # Wait for fix https://github.com/pydantic/pydantic/issues/3957 + # to do this with `unique_items` parameters to `pydantic.constr`. + @pydantic.field_validator("specs") + def specs_uniqueness_validator(cls, v): + if v is None: + return None + for i, value in enumerate(v, start=1): + if value in v[i:]: + raise ValueError + return v + + +def JSONPatchType(dtype=Any): + # we use functional syntax with TypedDict here since "from" is a keyword + return TypedDict( + "JSONPatchType", + { + "op": str, + "path": str, + "from": str, + "value": dtype, + }, + total=False, + ) + + +JSONPatchSpec = JSONPatchType(Spec) +JSONPatchAny = JSONPatchType(Any) + + +class HyphenizedBaseModel(pydantic.BaseModel): + # This model configuration allows aliases like "content-type" + model_config = ConfigDict(alias_generator=lambda f: f.replace("_", "-")) + + +class PatchMetadataRequest(HyphenizedBaseModel): + content_type: str + + # These fields are optional because None means "no changes; do not update". + # Dict for merge-patch: + metadata: Optional[Union[List[JSONPatchAny], Dict]] = None + + # Specs for merge-patch. left_to_right mode is used to distinguish between + # merge-patch List[asdict(Spec)] and json-patch List[Dict] + specs: Optional[Union[Specs, List[JSONPatchSpec]]] = Field( + union_mode="left_to_right" + ) + + @pydantic.field_validator("specs") + def specs_uniqueness_validator(cls, v): + if v is None: + return None + if v and isinstance(v[0], Spec): + # This is the MERGE_PATCH case + if len(v) != len(set(v)): + raise PydanticCustomError("specs", "Items must be unique") + elif v and isinstance(v[0], dict): + # This is the JSON_PATCH case + v_new = [v_["value"] for v_ in v if v_["op"] in ["add", "replace"]] + # Note: uniqueness should be checked with existing specs included, + # however since we use replace_metadata to eventually write to db this + # will be caught and an error raised there. + if len(v_new) != len(set(v_new)): + raise PydanticCustomError("specs", "Items must be unique") + return v + + +class PatchMetadataResponse(pydantic.BaseModel, Generic[ResourceLinksT]): + id: str + links: Union[ArrayLinks, DataFrameLinks, SparseLinks] + # May be None if not altered + metadata: Optional[Dict] + data_sources: Optional[List[DataSource]] + + +NodeStructure.model_rebuild() diff --git a/tiled/server/schemas.py b/tiled/server/schemas.py deleted file mode 100644 index 4bc4ede2a..000000000 --- a/tiled/server/schemas.py +++ /dev/null @@ -1,544 +0,0 @@ -from __future__ import annotations - -import enum -import uuid -from datetime import datetime -from typing import TYPE_CHECKING, Any, Dict, Generic, List, Optional, TypeVar, Union - -import pydantic.generics -from pydantic import ConfigDict, Field, StringConstraints -from pydantic_core import PydanticCustomError -from typing_extensions import Annotated, TypedDict - -from ..structures.array import ArrayStructure -from ..structures.awkward import AwkwardStructure -from ..structures.core import STRUCTURE_TYPES, StructureFamily -from ..structures.data_source import Management -from ..structures.sparse import SparseStructure -from ..structures.table import TableStructure - -if TYPE_CHECKING: - import tiled.authn_database.orm - import tiled.catalog.orm - -DataT = TypeVar("DataT") -LinksT = TypeVar("LinksT") -MetaT = TypeVar("MetaT") -StructureT = TypeVar("StructureT") - - -MAX_ALLOWED_SPECS = 20 - - -class Error(pydantic.BaseModel): - code: int - message: str - - -class Response(pydantic.BaseModel, Generic[DataT, LinksT, MetaT]): - data: Optional[DataT] - error: Optional[Error] = None - links: Optional[LinksT] = None - meta: Optional[MetaT] = None - - @pydantic.field_validator("error") - def check_consistency(cls, v, values): - if v is not None and values["data"] is not None: - raise ValueError("must not provide both data and error") - if v is None and values.get("data") is None: - raise ValueError("must provide data or error") - return v - - -class PaginationLinks(pydantic.BaseModel): - self: str - next: str - prev: str - first: str - last: str - - -class EntryFields(str, enum.Enum): - metadata = "metadata" - structure_family = "structure_family" - structure = "structure" - count = "count" - sorting = "sorting" - specs = "specs" - data_sources = "data_sources" - none = "" - - -class NodeStructure(pydantic.BaseModel): - contents: Optional[Dict[str, Any]] - count: int - - model_config = pydantic.ConfigDict(extra="forbid") - - -class SortingDirection(int, enum.Enum): - ASCENDING = 1 - DECENDING = -1 - - -class SortingItem(pydantic.BaseModel): - key: str - direction: SortingDirection - - -class Spec(pydantic.BaseModel, extra="forbid", frozen=True): - name: Annotated[str, StringConstraints(max_length=255)] - version: Optional[Annotated[str, StringConstraints(max_length=255)]] = None - - -# Wait for fix https://github.com/pydantic/pydantic/issues/3957 -# Specs = pydantic.conlist(Spec, max_length=20, unique_items=True) -Specs = Annotated[List[Spec], Field(max_length=MAX_ALLOWED_SPECS)] - - -class Asset(pydantic.BaseModel): - data_uri: str - is_directory: bool - parameter: Optional[str] = None - num: Optional[int] = None - id: Optional[int] = None - - @classmethod - def from_orm(cls, orm: tiled.catalog.orm.Asset) -> Asset: - return cls( - data_uri=orm.data_uri, - is_directory=orm.is_directory, - id=orm.id, - ) - - @classmethod - def from_assoc_orm(cls, orm): - return cls( - data_uri=orm.asset.data_uri, - is_directory=orm.asset.is_directory, - parameter=orm.parameter, - num=orm.num, - id=orm.asset.id, - ) - - -class Revision(pydantic.BaseModel): - revision_number: int - metadata: dict - specs: Specs - time_updated: datetime - - @classmethod - def from_orm(cls, orm: tiled.catalog.orm.Revision) -> Revision: - # Trailing underscore in 'metadata_' avoids collision with - # SQLAlchemy reserved word 'metadata'. - return cls( - revision_number=orm.revision_number, - metadata=orm.metadata_, - specs=orm.specs, - time_updated=orm.time_updated, - ) - - -class DataSource(pydantic.BaseModel, Generic[StructureT]): - id: Optional[int] = None - structure_family: StructureFamily - structure: Optional[StructureT] - mimetype: Optional[str] = None - parameters: dict = {} - assets: List[Asset] = [] - management: Management = Management.writable - - model_config = pydantic.ConfigDict(extra="forbid") - - @classmethod - def from_orm(cls, orm: tiled.catalog.orm.DataSource) -> DataSource: - if hasattr(orm.structure, "structure"): - structure_cls = STRUCTURE_TYPES[orm.structure_family] - structure = structure_cls.from_json(orm.structure.structure) - else: - structure = None - return cls( - id=orm.id, - structure_family=orm.structure_family, - structure=structure, - mimetype=orm.mimetype, - parameters=orm.parameters, - assets=[Asset.from_assoc_orm(assoc) for assoc in orm.asset_associations], - management=orm.management, - ) - - -class NodeAttributes(pydantic.BaseModel): - ancestors: List[str] - structure_family: Optional[StructureFamily] = None - specs: Optional[Specs] = None - metadata: Optional[Dict] = None # free-form, user-specified dict - structure: Optional[ - Union[ - ArrayStructure, - AwkwardStructure, - SparseStructure, - NodeStructure, - TableStructure, - ] - ] = None - - sorting: Optional[List[SortingItem]] = None - data_sources: Optional[List[DataSource]] = None - - model_config = pydantic.ConfigDict(extra="forbid") - - -AttributesT = TypeVar("AttributesT") -ResourceMetaT = TypeVar("ResourceMetaT") -ResourceLinksT = TypeVar("ResourceLinksT") - - -class SelfLinkOnly(pydantic.BaseModel): - self: str - - -class ContainerLinks(pydantic.BaseModel): - self: str - search: str - full: str - - -class ArrayLinks(pydantic.BaseModel): - self: str - full: str - block: str - - -class AwkwardLinks(pydantic.BaseModel): - self: str - buffers: str - full: str - - -class DataFrameLinks(pydantic.BaseModel): - self: str - full: str - partition: str - - -class SparseLinks(pydantic.BaseModel): - self: str - full: str - block: str - - -resource_links_type_by_structure_family = { - StructureFamily.array: ArrayLinks, - StructureFamily.awkward: AwkwardLinks, - StructureFamily.container: ContainerLinks, - StructureFamily.sparse: SparseLinks, - StructureFamily.table: DataFrameLinks, -} - - -class EmptyDict(pydantic.BaseModel): - pass - - -class ContainerMeta(pydantic.BaseModel): - count: int - - -class Resource(pydantic.BaseModel, Generic[AttributesT, ResourceLinksT, ResourceMetaT]): - "A JSON API Resource" - id: Union[str, uuid.UUID] - attributes: AttributesT - links: Optional[ResourceLinksT] = None - meta: Optional[ResourceMetaT] = None - - -class AccessAndRefreshTokens(pydantic.BaseModel): - access_token: str - expires_in: int - refresh_token: str - refresh_token_expires_in: int - token_type: str - - -class RefreshToken(pydantic.BaseModel): - refresh_token: str - - -class DeviceCode(pydantic.BaseModel): - device_code: str - grant_type: str - - -class PrincipalType(str, enum.Enum): - user = "user" - service = "service" - - -class Identity(pydantic.BaseModel): - model_config = pydantic.ConfigDict(from_attributes=True) - id: Annotated[str, StringConstraints(max_length=255)] - provider: Annotated[str, StringConstraints(max_length=255)] - latest_login: Optional[datetime] = None - - @classmethod - def from_orm(cls, orm: tiled.authn_database.orm.Identity) -> Identity: - return cls(id=orm.id, provider=orm.provider, latest_login=orm.latest_login) - - -class Role(pydantic.BaseModel): - model_config = pydantic.ConfigDict(from_attributes=True) - name: str - scopes: List[str] - # principals - - @classmethod - def from_orm(cls, orm: tiled.authn_database.orm.Role) -> Role: - return cls(name=orm.name, scopes=orm.scopes) - - -class APIKey(pydantic.BaseModel): - model_config = pydantic.ConfigDict(from_attributes=True) - first_eight: Annotated[str, StringConstraints(min_length=8, max_length=8)] - expiration_time: Optional[datetime] = None - note: Optional[Annotated[str, StringConstraints(max_length=255)]] = None - scopes: List[str] - latest_activity: Optional[datetime] = None - - @classmethod - def from_orm(cls, orm: tiled.authn_database.orm.APIKey) -> APIKey: - return cls( - first_eight=orm.first_eight, - expiration_time=orm.expiration_time, - note=orm.note, - scopes=orm.scopes, - latest_activity=orm.latest_activity, - ) - - -class APIKeyWithSecret(APIKey): - secret: str # hex-encoded bytes - - @classmethod - def from_orm( - cls, orm: tiled.authn_database.orm.APIKeyWithSecret, secret: str - ) -> APIKeyWithSecret: - return cls( - first_eight=orm.first_eight, - expiration_time=orm.expiration_time, - note=orm.note, - scopes=orm.scopes, - latest_activity=orm.latest_activity, - secret=secret, - ) - - -class Session(pydantic.BaseModel): - """ - This related to refresh tokens, which have a session uuid ("sid") claim. - - When the client attempts to use a refresh token, we first check - here to ensure that the "session", which is associated with a chain - of refresh tokens that came from a single authentication, are still valid. - """ - - # The id field (primary key) is intentionally not exposed to the application. - # It is left as an internal database concern. - model_config = pydantic.ConfigDict(from_attributes=True) - uuid: uuid.UUID - expiration_time: datetime - revoked: bool - - @classmethod - def from_orm(cls, orm: tiled.authn_database.orm.Session) -> Session: - return cls( - uuid=orm.uuid, expiration_time=orm.expiration_time, revoked=orm.revoked - ) - - -class Principal(pydantic.BaseModel): - "Represents a User or Service" - # The id field (primary key) is intentionally not exposed to the application. - # It is left as an internal database concern. - model_config = pydantic.ConfigDict(from_attributes=True) - uuid: uuid.UUID - type: PrincipalType - identities: List[Identity] = [] - roles: List[Role] = [] - api_keys: List[APIKey] = [] - sessions: List[Session] = [] - latest_activity: Optional[datetime] = None - - @classmethod - def from_orm( - cls, - orm: tiled.authn_database.orm.Principal, - latest_activity: Optional[datetime] = None, - ) -> Principal: - return cls( - uuid=orm.uuid, - type=orm.type, - identities=[Identity.from_orm(id_) for id_ in orm.identities], - roles=[Role.from_orm(id_) for id_ in orm.roles], - api_keys=[APIKey.from_orm(api_key) for api_key in orm.api_keys], - sessions=[Session.from_orm(session) for session in orm.sessions], - latest_activity=latest_activity, - ) - - -class APIKeyRequestParams(pydantic.BaseModel): - # Provide an example for expires_in. Otherwise, OpenAPI suggests lifetime=0. - # If the user is not reading carefully, they will be frustrated when they - # try to use the instantly-expiring API key! - expires_in: Optional[int] = pydantic.Field( - ..., json_schema_extra={"example": 600} - ) # seconds - scopes: Optional[List[str]] = pydantic.Field( - ..., json_schema_extra={"example": ["inherit"]} - ) - note: Optional[str] = None - - -class PostMetadataRequest(pydantic.BaseModel): - id: Optional[str] = None - structure_family: StructureFamily - metadata: Dict = {} - data_sources: List[DataSource] = [] - specs: Specs = [] - - # Wait for fix https://github.com/pydantic/pydantic/issues/3957 - # to do this with `unique_items` parameters to `pydantic.constr`. - @pydantic.field_validator("specs") - def specs_uniqueness_validator(cls, v): - if v is None: - return None - for i, value in enumerate(v, start=1): - if value in v[i:]: - raise ValueError - return v - - @pydantic.model_validator(mode="after") - def narrow_strucutre_type(self): - "Convert the structure on each data_source from a dict to the appropriate pydantic model." - for data_source in self.data_sources: - if self.structure_family != StructureFamily.container: - structure_cls = STRUCTURE_TYPES[self.structure_family] - if data_source.structure is not None: - data_source.structure = structure_cls.from_json( - data_source.structure - ) - return self - - -class PutDataSourceRequest(pydantic.BaseModel): - data_source: DataSource - - -class PostMetadataResponse(pydantic.BaseModel, Generic[ResourceLinksT]): - id: str - links: Union[ArrayLinks, DataFrameLinks, SparseLinks] - metadata: Dict - data_sources: List[DataSource] - - -class PutMetadataResponse(pydantic.BaseModel, Generic[ResourceLinksT]): - id: str - links: Union[ArrayLinks, DataFrameLinks, SparseLinks] - # May be None if not altered - metadata: Optional[Dict] = None - data_sources: Optional[List[DataSource]] = None - - -class DistinctValueInfo(pydantic.BaseModel): - value: Any = None - count: Optional[int] = None - - -class GetDistinctResponse(pydantic.BaseModel): - metadata: Optional[Dict[str, List[DistinctValueInfo]]] = None - structure_families: Optional[List[DistinctValueInfo]] = None - specs: Optional[List[DistinctValueInfo]] = None - - -class PutMetadataRequest(pydantic.BaseModel): - # These fields are optional because None means "no changes; do not update". - metadata: Optional[Dict] = None - specs: Optional[Specs] = None - - # Wait for fix https://github.com/pydantic/pydantic/issues/3957 - # to do this with `unique_items` parameters to `pydantic.constr`. - @pydantic.field_validator("specs") - def specs_uniqueness_validator(cls, v): - if v is None: - return None - for i, value in enumerate(v, start=1): - if value in v[i:]: - raise ValueError - return v - - -def JSONPatchType(dtype=Any): - # we use functional syntax with TypedDict here since "from" is a keyword - return TypedDict( - "JSONPatchType", - { - "op": str, - "path": str, - "from": str, - "value": dtype, - }, - total=False, - ) - - -JSONPatchSpec = JSONPatchType(Spec) -JSONPatchAny = JSONPatchType(Any) - - -class HyphenizedBaseModel(pydantic.BaseModel): - # This model configuration allows aliases like "content-type" - model_config = ConfigDict(alias_generator=lambda f: f.replace("_", "-")) - - -class PatchMetadataRequest(HyphenizedBaseModel): - content_type: str - - # These fields are optional because None means "no changes; do not update". - # Dict for merge-patch: - metadata: Optional[Union[List[JSONPatchAny], Dict]] = None - - # Specs for merge-patch. left_to_right mode is used to distinguish between - # merge-patch List[asdict(Spec)] and json-patch List[Dict] - specs: Optional[Union[Specs, List[JSONPatchSpec]]] = Field( - union_mode="left_to_right" - ) - - @pydantic.field_validator("specs") - def specs_uniqueness_validator(cls, v): - if v is None: - return None - if v and isinstance(v[0], Spec): - # This is the MERGE_PATCH case - if len(v) != len(set(v)): - raise PydanticCustomError("specs", "Items must be unique") - elif v and isinstance(v[0], dict): - # This is the JSON_PATCH case - v_new = [v_["value"] for v_ in v if v_["op"] in ["add", "replace"]] - # Note: uniqueness should be checked with existing specs included, - # however since we use replace_metadata to eventually write to db this - # will be caught and an error raised there. - if len(v_new) != len(set(v_new)): - raise PydanticCustomError("specs", "Items must be unique") - return v - - -class PatchMetadataResponse(pydantic.BaseModel, Generic[ResourceLinksT]): - id: str - links: Union[ArrayLinks, DataFrameLinks, SparseLinks] - # May be None if not altered - metadata: Optional[Dict] - data_sources: Optional[List[DataSource]] - - -NodeStructure.model_rebuild() From 36a3896a61fa31b83d7fd8302e0c5ffec880a176 Mon Sep 17 00:00:00 2001 From: Dan Allan Date: Fri, 21 Feb 2025 12:13:18 -0500 Subject: [PATCH 4/9] Update CHANGELOG --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8413aa689..e20fc797c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ Write the date in place of the "Unreleased" in the case a new version is released. --> # Changelog +## Unreleased + +### Changed + +- The client utility `tree` was moved from `tiled.utils` to `tiled.client.utils`. + It remains, as before, re-imported for convenience in `tiled.client`. +- The objects in `tiled.server.schemas` were merged into `tiled.schemas` and + the former was removed. + ## 0.1.0-b19 (2024-02-19) ### Maintenance From 3ee08a61b92ae478ab646242d29ebee7e5ae8eb4 Mon Sep 17 00:00:00 2001 From: Dan Allan Date: Fri, 21 Feb 2025 12:22:28 -0500 Subject: [PATCH 5/9] Add importlinter exemptions for client launching in-process server. --- pyproject.toml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 1abb99196..e8d8b15ce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -314,3 +314,11 @@ name="Client cannot import from Server" type="forbidden" source_modules=["tiled.client"] forbidden_modules=["tiled.server"] +# These exemptions allow for client to launch server on a background thread +# These imports are deferred, in local scope not global scope, and only are +# trigged when an in-process server in launched. The importlinter is not +# fine-grained enough to specify this. +ignore_imports=[ + "tiled.client.constructors -> tiled.server.app", + "tiled.client.constructors -> tiled.server.settings", +] From 725785826dce9768f10a7bfe2b0a8539aa9f96e6 Mon Sep 17 00:00:00 2001 From: Dan Allan Date: Fri, 21 Feb 2025 12:25:56 -0500 Subject: [PATCH 6/9] Fix typo-ed exemption --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index e8d8b15ce..b23c8a49e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -320,5 +320,5 @@ forbidden_modules=["tiled.server"] # fine-grained enough to specify this. ignore_imports=[ "tiled.client.constructors -> tiled.server.app", - "tiled.client.constructors -> tiled.server.settings", + "tiled.client.context -> tiled.server.settings", ] From 5dcd42bd7a2cc1c2e07c791bad5b9ef95fb1d35b Mon Sep 17 00:00:00 2001 From: Dan Allan Date: Fri, 21 Feb 2025 12:27:08 -0500 Subject: [PATCH 7/9] Fix import depth --- tiled/schemas.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tiled/schemas.py b/tiled/schemas.py index 64168ba75..7dafa7c0a 100644 --- a/tiled/schemas.py +++ b/tiled/schemas.py @@ -21,12 +21,12 @@ from pydantic_core import PydanticCustomError from typing_extensions import Annotated, TypedDict -from ..structures.array import ArrayStructure -from ..structures.awkward import AwkwardStructure -from ..structures.core import STRUCTURE_TYPES, StructureFamily -from ..structures.data_source import Management -from ..structures.sparse import SparseStructure -from ..structures.table import TableStructure +from .structures.array import ArrayStructure +from .structures.awkward import AwkwardStructure +from .structures.core import STRUCTURE_TYPES, StructureFamily +from .structures.data_source import Management +from .structures.sparse import SparseStructure +from .structures.table import TableStructure if TYPE_CHECKING: import tiled.authn_database.orm From 7b91f6f2c641c094ab5c1b9af3ee122039926ee7 Mon Sep 17 00:00:00 2001 From: Dan Allan Date: Fri, 21 Feb 2025 12:32:05 -0500 Subject: [PATCH 8/9] Update imports --- tiled/server/app.py | 2 +- tiled/server/authentication.py | 2 +- tiled/server/core.py | 3 +-- tiled/server/router.py | 3 +-- 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/tiled/server/app.py b/tiled/server/app.py index 54429b2c5..e873ab47f 100644 --- a/tiled/server/app.py +++ b/tiled/server/app.py @@ -36,13 +36,13 @@ from tiled.server.protocols import ExternalAuthenticator, InternalAuthenticator +from .. import schemas from ..config import construct_build_app_kwargs from ..media_type_registration import ( compression_registry as default_compression_registry, ) from ..utils import SHARE_TILED_PATH, Conflicts, SpecialUsers, UnsupportedQueryType from ..validation_registration import validation_registry as default_validation_registry -from . import schemas from .authentication import get_current_principal from .compression import CompressionMiddleware from .dependencies import ( diff --git a/tiled/server/authentication.py b/tiled/server/authentication.py index dc974fe04..4dd3be474 100644 --- a/tiled/server/authentication.py +++ b/tiled/server/authentication.py @@ -46,6 +46,7 @@ from pydantic import BaseModel +from .. import schemas from ..authn_database import orm from ..authn_database.connection_pool import get_database_session from ..authn_database.core import ( @@ -58,7 +59,6 @@ lookup_valid_session, ) from ..utils import SHARE_TILED_PATH, SpecialUsers -from . import schemas from .core import DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE, json_or_msgpack from .protocols import InternalAuthenticator, UserSessionState from .settings import Settings, get_settings diff --git a/tiled/server/core.py b/tiled/server/core.py index 65be00c5a..be4496953 100644 --- a/tiled/server/core.py +++ b/tiled/server/core.py @@ -21,7 +21,7 @@ from starlette.status import HTTP_200_OK, HTTP_304_NOT_MODIFIED, HTTP_400_BAD_REQUEST # Some are not directly used, but they register things on import. -from .. import queries +from .. import queries, schemas from ..adapters.mapping import MapAdapter from ..queries import KeyLookup, QueryValueError from ..serialization import register_builtin_serializers @@ -33,7 +33,6 @@ ensure_awaitable, safe_json_dump, ) -from . import schemas from .etag import tokenize from .links import links_for_node from .utils import record_timing diff --git a/tiled/server/router.py b/tiled/server/router.py index d5f613920..fcf852542 100644 --- a/tiled/server/router.py +++ b/tiled/server/router.py @@ -29,11 +29,10 @@ from tiled.schemas import About from tiled.server.protocols import ExternalAuthenticator, InternalAuthenticator -from .. import __version__ +from .. import __version__, schemas from ..structures.core import Spec, StructureFamily from ..utils import ensure_awaitable, patch_mimetypes, path_from_uri from ..validation_registration import ValidationError -from . import schemas from .authentication import get_authenticators, get_current_principal from .core import ( DEFAULT_PAGE_SIZE, From 1c488ac9cd6b6daaff7db239fa6381a7aa0854f2 Mon Sep 17 00:00:00 2001 From: Dan Allan Date: Fri, 21 Feb 2025 13:53:49 -0500 Subject: [PATCH 9/9] Update import --- tiled/client/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tiled/client/utils.py b/tiled/client/utils.py index 3236a95d0..0c4975b26 100644 --- a/tiled/client/utils.py +++ b/tiled/client/utils.py @@ -359,7 +359,7 @@ def gen_tree(tree, nodes=None, last=None): # when this function is used in a CLI where import overhead can accumulate to # about 2 seconds, the bulk of the time. Therefore, we do something a bit # "clever" here to override the normal structure clients with dummy placeholders. - from .client.container import Container + from .container import Container def dummy_client(*args, **kwargs): return None