Compare commits

...
4 Commits
6 changed files with 71 additions and 7 deletions

No files matched your search

@@ -0,0 +1,37 @@
"""manufacturers_url_nullable
Revision ID: 0fec09fb6ae6
Revises: 9a1c2f6e0b3d
Create Date: 2026-09-04 23:05:18.990806
"""
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision: str = "0fec09fb6ae6"
down_revision: str | None = "9a1c2f6e0b3d"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def upgrade() -> None:
with op.batch_alter_table("manufacturers", schema=None) as batch_op:
batch_op.alter_column(
"url",
existing_type=sa.String(),
nullable=True,
)
def downgrade() -> None:
op.execute("UPDATE manufacturers SET url = '' WHERE url IS NULL")
with op.batch_alter_table("manufacturers", schema=None) as batch_op:
batch_op.alter_column(
"url",
existing_type=sa.String(),
nullable=False,
)
@@ -28,12 +28,19 @@ def upgrade() -> None:
sa.Column("logo_url", sa.String(), nullable=True), sa.Column("logo_url", sa.String(), nullable=True),
sa.PrimaryKeyConstraint("id"), sa.PrimaryKeyConstraint("id"),
) )
# batch_alter_table's alter_column doesn't emit CREATE TYPE for the new
# Enum on its own (unlike create_table, which ties enum creation to the
# table-create DDL event) - create it explicitly first, or Postgres fails
# with `type "uncseries" does not exist`. checkfirst / no-op on SQLite.
uncseries = sa.Enum("UNC", "UNF", "UNEF", "UN", "UNS", name="uncseries")
uncseries.create(op.get_bind(), checkfirst=True)
with op.batch_alter_table("unc_threads", schema=None) as batch_op: with op.batch_alter_table("unc_threads", schema=None) as batch_op:
batch_op.alter_column( batch_op.alter_column(
"series", "series",
existing_type=sa.VARCHAR(length=8), existing_type=sa.VARCHAR(length=8),
type_=sa.Enum("UNC", "UNF", "UNEF", "UN", "UNS", name="uncseries"), type_=uncseries,
existing_nullable=False, existing_nullable=False,
postgresql_using="series::uncseries",
) )
# ### end Alembic commands ### # ### end Alembic commands ###
@@ -47,7 +54,9 @@ def downgrade() -> None:
existing_type=sa.Enum("UNC", "UNF", "UNEF", "UN", "UNS", name="uncseries"), existing_type=sa.Enum("UNC", "UNF", "UNEF", "UN", "UNS", name="uncseries"),
type_=sa.VARCHAR(length=8), type_=sa.VARCHAR(length=8),
existing_nullable=False, existing_nullable=False,
postgresql_using="series::varchar(8)",
) )
sa.Enum(name="uncseries").drop(op.get_bind(), checkfirst=True)
op.drop_table("manufacturers") op.drop_table("manufacturers")
# ### end Alembic commands ### # ### end Alembic commands ###
+1 -1
View File
@@ -26,7 +26,7 @@ async def create(
db: AsyncSession, db: AsyncSession,
*, *,
name: str, name: str,
url: str, url: str | None = None,
logo_url: str | None = None, logo_url: str | None = None,
) -> Manufacturer: ) -> Manufacturer:
m = Manufacturer(name=name, url=url, logo_url=logo_url) m = Manufacturer(name=name, url=url, logo_url=logo_url)
+1 -1
View File
@@ -8,5 +8,5 @@ class Manufacturer(Base):
id: Mapped[int] = mapped_column(primary_key=True) id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(nullable=False) name: Mapped[str] = mapped_column(nullable=False)
url: Mapped[str] = mapped_column(nullable=False) url: Mapped[str | None] = mapped_column(nullable=True)
logo_url: Mapped[str | None] = mapped_column(nullable=True, default=None) logo_url: Mapped[str | None] = mapped_column(nullable=True, default=None)
+16 -2
View File
@@ -1,11 +1,21 @@
from pydantic import BaseModel, ConfigDict, HttpUrl from pydantic import BaseModel, ConfigDict, HttpUrl, field_validator
def _blank_to_none(value: str | None) -> str | None:
if isinstance(value, str) and not value.strip():
return None
return value
class ManufacturerBase(BaseModel): class ManufacturerBase(BaseModel):
name: str name: str
url: HttpUrl url: HttpUrl | None = None
logo_url: HttpUrl | None = None logo_url: HttpUrl | None = None
_normalize_url = field_validator("url", "logo_url", mode="before")(
_blank_to_none
)
class ManufacturerCreate(ManufacturerBase): class ManufacturerCreate(ManufacturerBase):
pass pass
@@ -16,6 +26,10 @@ class ManufacturerUpdate(BaseModel):
url: HttpUrl | None = None url: HttpUrl | None = None
logo_url: HttpUrl | None = None logo_url: HttpUrl | None = None
_normalize_url = field_validator("url", "logo_url", mode="before")(
_blank_to_none
)
class ManufacturerRead(ManufacturerBase): class ManufacturerRead(ManufacturerBase):
model_config = ConfigDict(from_attributes=True) model_config = ConfigDict(from_attributes=True)
+6 -2
View File
@@ -728,7 +728,7 @@ async def create_manufacturer(
created = await manufacturer_crud.create( created = await manufacturer_crud.create(
db, db,
name=name, name=name,
url=url, url=url.strip() or None,
logo_url=logo_url.strip() or None, logo_url=logo_url.strip() or None,
) )
await audit_crud.log( await audit_crud.log(
@@ -789,7 +789,11 @@ async def update_manufacturer(
m = await manufacturer_crud.get(db, manufacturer_id) m = await manufacturer_crud.get(db, manufacturer_id)
if m is None: if m is None:
raise HTTPException(status_code=404, detail="Manufacturer not found") raise HTTPException(status_code=404, detail="Manufacturer not found")
changes = {"name": name, "url": url, "logo_url": logo_url.strip() or None} changes = {
"name": name,
"url": url.strip() or None,
"logo_url": logo_url.strip() or None,
}
await manufacturer_crud.update(db, m, changes) await manufacturer_crud.update(db, m, changes)
await audit_crud.log( await audit_crud.log(
db, db,