Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion examples/file.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from brussels.mixins import PrimaryKeyMixin

try:
from obstore.store import MemoryStore # ty: ignore[unresolved-import]
from obstore.store import MemoryStore

from brussels.types.file import RemoteFile, RemoteMetadata, RemoteStorage
except ImportError as exc:
Expand Down
53 changes: 2 additions & 51 deletions src/brussels/__tests__/mixins/test_primary_key_mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,10 @@

import pytest
from sqlalchemy import Engine, Table, create_engine
from sqlalchemy.dialects import postgresql
from sqlalchemy.orm import Mapped, Session, mapped_column

from brussels.base import DataclassBase
from brussels.mixins import PrimaryKeyMixin, TimestampMixin, UUIDv7PrimaryKeyMixin
from brussels.mixins import PrimaryKeyMixin, TimestampMixin


class Widget(DataclassBase, PrimaryKeyMixin, TimestampMixin):
Expand All @@ -18,12 +17,6 @@ class Widget(DataclassBase, PrimaryKeyMixin, TimestampMixin):
name: Mapped[str] = mapped_column()


class UUIDv7Widget(DataclassBase, UUIDv7PrimaryKeyMixin, TimestampMixin):
__tablename__ = "primary_key_v7_widgets"

name: Mapped[str] = mapped_column()


@pytest.fixture
def engine() -> Iterator[Engine]:
engine = create_engine("sqlite:///:memory:")
Expand All @@ -38,11 +31,7 @@ def test_id_column_definition() -> None:
column = table.c.id

assert column.primary_key is True

server_default = column.server_default
assert server_default is not None
compiled = cast("Any", server_default).arg.compile(dialect=postgresql.dialect())
assert "gen_random_uuid" in str(compiled)
assert column.server_default is None


def test_id_not_in_init_signature() -> None:
Expand All @@ -63,41 +52,3 @@ def test_id_default_factory_generates_uuid_on_flush(engine: Engine) -> None:
session.flush()

assert isinstance(widget.id, UUID)


def test_uuidv7_id_column_definition() -> None:
table = cast("Table", UUIDv7Widget.__table__)
column = table.c.id

assert column.primary_key is True

insert_default = column.default
assert insert_default is not None
compiled_insert_default = cast("Any", insert_default).arg.compile(dialect=postgresql.dialect())
assert "uuidv7" in str(compiled_insert_default)

server_default = column.server_default
assert server_default is not None
compiled_server_default = cast("Any", server_default).arg.compile(dialect=postgresql.dialect())
assert "uuidv7" in str(compiled_server_default)


def test_uuidv7_id_not_in_init_signature() -> None:
signature = inspect.signature(UUIDv7Widget)
assert "id" not in signature.parameters

widget_cls = cast("Any", UUIDv7Widget)
with pytest.raises(TypeError):
widget_cls(id=uuid4(), name="widget")


def test_uuidv7_id_is_not_populated_before_flush() -> None:
widget = UUIDv7Widget(name="widget")

assert widget.id is None


def test_uuidv7_models_satisfy_primary_key_mixin_contract() -> None:
widget = UUIDv7Widget(name="widget")

assert isinstance(widget, PrimaryKeyMixin)
148 changes: 3 additions & 145 deletions src/brussels/__tests__/types/file/test_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@

from datetime import UTC, datetime
from typing import TYPE_CHECKING, cast
from uuid import UUID, uuid4
from uuid import UUID

import pytest
from sqlalchemy.orm import Mapped, Session, mapped_column

from brussels.base import Base, DataclassBase
from brussels.mixins import PrimaryKeyMixin, UUIDv7PrimaryKeyMixin
from brussels.mixins import PrimaryKeyMixin

try:
from obstore.store import MemoryStore
Expand All @@ -20,7 +20,6 @@
if TYPE_CHECKING:
from obstore import GetOptions, PutMode
from sqlalchemy import Engine
from sqlalchemy.ext.asyncio import AsyncSession

from brussels.types.file._types import RemoteMetadataField

Expand All @@ -46,12 +45,6 @@ class OtherFileModel(DataclassBase, PrimaryKeyMixin):
file: Mapped[RemoteMetadata | None] = mapped_column(RemoteStorage(store=MemoryStore()), nullable=True, default=None)


class UUIDv7FileModel(DataclassBase, UUIDv7PrimaryKeyMixin):
__tablename__ = "uuidv7_file_model"

file: Mapped[RemoteMetadata | None] = mapped_column(RemoteStorage(store=MemoryStore()), nullable=True, default=None)


class TablelessPrimaryKeyModel(PrimaryKeyMixin):
pass

Expand All @@ -61,29 +54,10 @@ def _configure_store(store_ops: FakeStoreOps) -> None:
remote_storage.store = store_ops


def _configure_uuidv7_store(store_ops: FakeStoreOps) -> None:
remote_storage = cast("RemoteStorage", UUIDv7FileModel.__table__.c["file"].type)
remote_storage.store = store_ops


def _file_handle(model: FileModel) -> RemoteFile:
return RemoteFile.from_metadata(model, FileModel.file)


def _uuidv7_file_handle(model: UUIDv7FileModel) -> RemoteFile:
return RemoteFile.from_metadata(model, UUIDv7FileModel.file)


class AsyncSessionShim:
def __init__(self, sync_session: Session, *, on_flush=None) -> None:
self.sync_session = sync_session
self._on_flush = on_flush

async def flush(self) -> None:
if self._on_flush is not None:
self._on_flush()


def test_from_metadata_rejects_models_without_primary_key_mixin() -> None:
model = NoPrimaryKeyMixinModel(id=1)

Expand Down Expand Up @@ -113,7 +87,7 @@ def test_from_metadata_rejects_non_remote_storage_field() -> None:
model = FileModel()

with pytest.raises(TypeError, match=r"must use brussels\.types\.file\.RemoteStorage"):
RemoteFile.from_metadata(model, FileModel.id) # type: ignore[arg-type]
RemoteFile.from_metadata(model, FileModel.id) # ty: ignore[invalid-argument-type]


def test_from_metadata_rejects_field_from_different_model() -> None:
Expand All @@ -131,14 +105,6 @@ def test_from_metadata_accepts_models_with_primary_key_mixin() -> None:
assert remote_file.field_name == "file"


def test_from_metadata_accepts_uuidv7_primary_key_models() -> None:
model = UUIDv7FileModel()

remote_file = RemoteFile.from_metadata(model, UUIDv7FileModel.file)

assert remote_file.field_name == "file"


def test_put_sync_without_sqlalchemy_session_raises_and_does_not_call_store() -> None:
store_ops = FakeStoreOps()
_configure_store(store_ops)
Expand All @@ -154,62 +120,6 @@ def test_put_sync_without_sqlalchemy_session_raises_and_does_not_call_store() ->
assert model.file is None


def test_put_sync_flush_populates_uuidv7_id_before_key_generation(
engine: Engine,
monkeypatch: pytest.MonkeyPatch,
) -> None:
store_ops = FakeStoreOps()
_configure_uuidv7_store(store_ops)

with Session(engine) as session:
model = UUIDv7FileModel()
session.add(model)
assigned_id = uuid4()
flush_calls = 0

def fake_flush() -> None:
nonlocal flush_calls
flush_calls += 1
if model.id is None:
model.id = assigned_id

monkeypatch.setattr(session, "flush", fake_flush)

put_result = _uuidv7_file_handle(model).put(
b"hello",
content_type="text/plain",
session=session,
flush=True,
)

assert put_result == {"e_tag": None, "version": None}
assert flush_calls == 2
assert model.id == assigned_id
assert model.file is not None
assert model.file.key == f"{assigned_id}/file"
assert model.file.status == "pending"
assert store_ops.calls == []


def test_put_sync_rejects_uuidv7_model_without_flush(engine: Engine) -> None:
store_ops = FakeStoreOps()
_configure_uuidv7_store(store_ops)

with Session(engine) as session:
model = UUIDv7FileModel()
session.add(model)

with pytest.raises(ValueError, match=r"Pass flush=True or flush the model before calling put"):
_uuidv7_file_handle(model).put(
b"hello",
content_type="text/plain",
session=session,
)

assert model.file is None
assert store_ops.calls == []


def test_put_sync_defers_and_commits_when_sqlalchemy_session_is_attached(engine: Engine) -> None:
store_ops = FakeStoreOps()
_configure_store(store_ops)
Expand Down Expand Up @@ -346,58 +256,6 @@ async def test_put_rejects_invalid_model_id_type(engine: Engine) -> None:
await _file_handle(model).put_async(b"data")


@pytest.mark.asyncio
async def test_put_async_flush_populates_uuidv7_id_before_key_generation(engine: Engine) -> None:
store_ops = FakeStoreOps()
_configure_uuidv7_store(store_ops)

with Session(engine) as session:
model = UUIDv7FileModel()
session.add(model)
assigned_id = uuid4()
flush_calls = 0

def on_flush() -> None:
nonlocal flush_calls
flush_calls += 1
if model.id is None:
model.id = assigned_id

await _uuidv7_file_handle(model).put_async(
b"data",
content_type="text/plain",
session=cast("AsyncSession", AsyncSessionShim(session, on_flush=on_flush)),
flush=True,
)

assert flush_calls == 2
assert model.id == assigned_id
assert model.file is not None
assert model.file.key == f"{assigned_id}/file"
assert model.file.status == "pending"
assert store_ops.calls == []


@pytest.mark.asyncio
async def test_put_async_rejects_uuidv7_model_without_flush(engine: Engine) -> None:
store_ops = FakeStoreOps()
_configure_uuidv7_store(store_ops)

with Session(engine) as session:
model = UUIDv7FileModel()
session.add(model)

with pytest.raises(ValueError, match=r"Pass flush=True or flush the model before calling put_async"):
await _uuidv7_file_handle(model).put_async(
b"data",
content_type="text/plain",
session=session,
)

assert model.file is None
assert store_ops.calls == []


@pytest.mark.asyncio
async def test_put_allows_uuid_model_id(engine: Engine) -> None:
store_ops = FakeStoreOps()
Expand Down
2 changes: 1 addition & 1 deletion src/brussels/__tests__/types/file/test_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ def test_process_bind_param_accepts_dict_payload() -> None:

def test_process_bind_param_rejects_invalid_value_type() -> None:
with pytest.raises(ValueError, match="RemoteStorage RemoteMetadata is invalid"):
RemoteStorage(store=MemoryStore()).process_bind_param("bad-value", None) # type: ignore[arg-type]
RemoteStorage(store=MemoryStore()).process_bind_param("bad-value", None) # ty: ignore[invalid-argument-type]


def test_process_result_value_none_returns_none() -> None:
Expand Down
2 changes: 1 addition & 1 deletion src/brussels/__tests__/types/test_datetime_utc.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def test_process_bind_param_returns_none() -> None:
@pytest.mark.parametrize("value", [date(2024, 1, 1), object()])
def test_process_bind_param_rejects_non_datetime(value: object) -> None:
with pytest.raises(TypeError) as excinfo:
DateTimeUTC().process_bind_param(value, None) # type: ignore[arg-type]
DateTimeUTC().process_bind_param(value, None) # ty: ignore[invalid-argument-type]

message = str(excinfo.value)
assert "DateTimeUTC requires datetime object" in message
Expand Down
4 changes: 2 additions & 2 deletions src/brussels/__tests__/types/test_encrypted_string.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ def test_constructor_accepts_bytes_key() -> None:

def test_constructor_rejects_non_key_type() -> None:
with pytest.raises(TypeError, match="key must be str or bytes"):
EncryptedString(key=123) # type: ignore[arg-type]
EncryptedString(key=123) # ty: ignore[invalid-argument-type]


def test_constructor_rejects_malformed_key() -> None:
Expand All @@ -67,7 +67,7 @@ def test_process_bind_param_returns_none() -> None:
def test_process_bind_param_rejects_non_str_value() -> None:
encrypted = EncryptedString(key=MODEL_KEY)
with pytest.raises(TypeError, match="requires str value"):
encrypted.process_bind_param(1, None) # type: ignore[arg-type]
encrypted.process_bind_param(1, None) # ty: ignore[invalid-argument-type]


def test_process_bind_param_encrypts_plaintext() -> None:
Expand Down
4 changes: 2 additions & 2 deletions src/brussels/mixins/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from brussels.mixins.ordered import OrderedMixin
from brussels.mixins.primary_key import PrimaryKeyMixin, UUIDv7PrimaryKeyMixin
from brussels.mixins.primary_key import PrimaryKeyMixin
from brussels.mixins.timestamp import TimestampMixin

__all__ = ["OrderedMixin", "PrimaryKeyMixin", "TimestampMixin", "UUIDv7PrimaryKeyMixin"]
__all__ = ["OrderedMixin", "PrimaryKeyMixin", "TimestampMixin"]
Loading