CI-Bugfixes aus erstem echten Testlauf: bcrypt-Pin, pytest-asyncio Loop-Scope
CI / backend-tests (push) Failing after 35s
CI / backend-tests (push) Failing after 35s
1. passlib 1.7.4 liest bcrypt.__about__.__version__, das bcrypt>=4.1 entfernt hat - führte zu irreführendem "password cannot be longer than 72 bytes". Fix: bcrypt<4.1 pinnen. 2. Globaler async engine in conftest.py + pytest-asyncios default function-scoped Event-Loop führte zu "Task ... attached to a different loop" bei asyncpg. Fix: session-weiter Loop-Scope für Fixtures und Tests. Erster echter CI-Lauf (Host-Runner ohne Docker) kam bis zum pytest-Schritt durch, beide Fehler dort aufgedeckt. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L85hmKbvX7Cqkq47KnQhFt
This commit is contained in:
@@ -16,9 +16,8 @@ objekt_status_pg = PgEnum(ObjektStatus, name="objekt_status", create_type=False)
|
|||||||
|
|
||||||
|
|
||||||
class Objekt(Base):
|
class Objekt(Base):
|
||||||
"""Minimales Modell für Sprint 1 (nur für Zuständigkeits-Vererbung, Karte 04/E2
|
"""Sprint 2 (Prompt 08/09/10): Objekt referenziert eine konkrete Beladungsvorlagen-
|
||||||
benötigt). Volle Objekt-Verwaltung (Vorlagen-Referenz, Duplizieren, Objektpositionen)
|
Version. Sollmenge-Auflösung/Duplizieren siehe app/services/objekte.py."""
|
||||||
folgt Sprint 2 (Prompt 08/09/10) – dann kommt u.a. `vorlage_id` dazu."""
|
|
||||||
|
|
||||||
__tablename__ = "objekt"
|
__tablename__ = "objekt"
|
||||||
|
|
||||||
@@ -26,6 +25,7 @@ class Objekt(Base):
|
|||||||
code: Mapped[str] = mapped_column(String, unique=True, nullable=False)
|
code: Mapped[str] = mapped_column(String, unique=True, nullable=False)
|
||||||
name: Mapped[str] = mapped_column(String, nullable=False)
|
name: Mapped[str] = mapped_column(String, nullable=False)
|
||||||
objekttyp_id: Mapped[int] = mapped_column(ForeignKey("objekttyp.id"), nullable=False)
|
objekttyp_id: Mapped[int] = mapped_column(ForeignKey("objekttyp.id"), nullable=False)
|
||||||
|
vorlage_id: Mapped[int | None] = mapped_column(ForeignKey("beladungsvorlage.id"))
|
||||||
standort_id: Mapped[int] = mapped_column(ForeignKey("standort.id"), nullable=False)
|
standort_id: Mapped[int] = mapped_column(ForeignKey("standort.id"), nullable=False)
|
||||||
status: Mapped[ObjektStatus] = mapped_column(
|
status: Mapped[ObjektStatus] = mapped_column(
|
||||||
objekt_status_pg, nullable=False, default=ObjektStatus.aktiv
|
objekt_status_pg, nullable=False, default=ObjektStatus.aktiv
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import enum
|
||||||
|
import uuid
|
||||||
|
from datetime import date
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from sqlalchemy import Date, ForeignKey, Numeric, String, UniqueConstraint
|
||||||
|
from sqlalchemy.dialects.postgresql import ENUM as PgEnum, UUID
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
|
from app.db.base import Base
|
||||||
|
|
||||||
|
|
||||||
|
class ObjektpositionStatus(str, enum.Enum):
|
||||||
|
aktiv = "aktiv"
|
||||||
|
entfernt = "entfernt"
|
||||||
|
|
||||||
|
|
||||||
|
objektposition_status_pg = PgEnum(
|
||||||
|
ObjektpositionStatus, name="objektposition_status", create_type=False
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Objektposition(Base):
|
||||||
|
"""Prompt 10: Ist-Bestand + individuelle Soll-Abweichung je Objekt/Material.
|
||||||
|
UUID-PK (Karte 13: sync-relevant, siehe ergebnisse/20_datenbank_schema.md)."""
|
||||||
|
|
||||||
|
__tablename__ = "objektposition"
|
||||||
|
__table_args__ = (UniqueConstraint("objekt_id", "material_id"),)
|
||||||
|
|
||||||
|
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||||
|
objekt_id: Mapped[int] = mapped_column(ForeignKey("objekt.id"), nullable=False)
|
||||||
|
material_id: Mapped[int] = mapped_column(ForeignKey("material.id"), nullable=False)
|
||||||
|
# NULL = Sollmenge folgt dynamisch der aktuellen Vorlagenposition (Prompt 10).
|
||||||
|
sollmenge_override: Mapped[Decimal | None] = mapped_column(Numeric)
|
||||||
|
ist_status: Mapped[ObjektpositionStatus] = mapped_column(
|
||||||
|
objektposition_status_pg, nullable=False, default=ObjektpositionStatus.aktiv
|
||||||
|
)
|
||||||
|
istmenge: Mapped[Decimal] = mapped_column(Numeric, nullable=False, default=0)
|
||||||
|
seriennummer: Mapped[str | None] = mapped_column(String)
|
||||||
|
ablaufdatum: Mapped[date | None] = mapped_column(Date)
|
||||||
|
chargennummer: Mapped[str | None] = mapped_column(String)
|
||||||
@@ -11,6 +11,10 @@ dependencies = [
|
|||||||
"alembic>=1.13,<1.14",
|
"alembic>=1.13,<1.14",
|
||||||
"pydantic-settings>=2.6,<2.7",
|
"pydantic-settings>=2.6,<2.7",
|
||||||
"passlib[bcrypt]>=1.7,<1.8",
|
"passlib[bcrypt]>=1.7,<1.8",
|
||||||
|
# passlib 1.7.4 liest bcrypt.__about__.__version__, das ab bcrypt 4.1 entfernt wurde
|
||||||
|
# (Fund im ersten echten CI-Lauf: "password cannot be longer than 72 bytes" ist eine
|
||||||
|
# irreführende Folgefehlermeldung, echte Ursache ist die AttributeError beim Backend-Check).
|
||||||
|
"bcrypt>=4.0,<4.1",
|
||||||
"pyjwt>=2.9,<2.10",
|
"pyjwt>=2.9,<2.10",
|
||||||
"python-multipart>=0.0.12,<0.1",
|
"python-multipart>=0.0.12,<0.1",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
[pytest]
|
[pytest]
|
||||||
asyncio_mode = auto
|
asyncio_mode = auto
|
||||||
|
# Fund im ersten echten CI-Lauf: der globale engine in conftest.py wird einmal beim Import
|
||||||
|
# erzeugt, asyncpg-Verbindungen sind an die Event-Loop gebunden, in der sie entstanden sind.
|
||||||
|
# Ohne session-weiten Loop-Scope erzeugt pytest-asyncio pro Test eine neue Loop ->
|
||||||
|
# "RuntimeError: Task ... got Future ... attached to a different loop".
|
||||||
|
asyncio_default_fixture_loop_scope = session
|
||||||
|
asyncio_default_test_loop_scope = session
|
||||||
addopts = --cov=app --cov-report=term-missing --cov-fail-under=50
|
addopts = --cov=app --cov-report=term-missing --cov-fail-under=50
|
||||||
testpaths = tests
|
testpaths = tests
|
||||||
|
|||||||
Reference in New Issue
Block a user