rebase
This commit is contained in:
1
app/migrations/__init__.py
Normal file
1
app/migrations/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Migrations package."""
|
||||
14
app/migrations/init_db.py
Normal file
14
app/migrations/init_db.py
Normal file
@@ -0,0 +1,14 @@
|
||||
"""Database initialization utilities — used by alembic env and CLI."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.settings_service import seed_default_settings
|
||||
from app.migrations.seed import seed_builtin_presets
|
||||
|
||||
|
||||
async def init_db(session: AsyncSession) -> None:
|
||||
"""Seed settings + builtin presets. Idempotent."""
|
||||
await seed_default_settings(session)
|
||||
await seed_builtin_presets(session)
|
||||
11
app/migrations/init_qdrant.py
Normal file
11
app/migrations/init_qdrant.py
Normal file
@@ -0,0 +1,11 @@
|
||||
"""Qdrant collection initializer — runs on application startup.
|
||||
|
||||
Creates the `entities` and `story_entries` collections with payload indexes
|
||||
on `world_id` (and per-collection secondary indexes).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from app.core.qdrant_client import init_qdrant_collections
|
||||
|
||||
__all__ = ["init_qdrant_collections"]
|
||||
273
app/migrations/seed.py
Normal file
273
app/migrations/seed.py
Normal file
@@ -0,0 +1,273 @@
|
||||
"""Seed builtin world presets (fantasy + sci-fi).
|
||||
|
||||
Idempotent: skips presets whose name already exists.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models import WorldPreset
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Fantasy preset
|
||||
# --------------------------------------------------------------------------- #
|
||||
FANTASY_PRESET = {
|
||||
"name": "Classic Fantasy",
|
||||
"description": (
|
||||
"A high-fantasy world with taverns, dungeons, magic, and monsters. "
|
||||
"Standard d20-style stats. Default setting for new players."
|
||||
),
|
||||
"language": "en",
|
||||
"rules": [
|
||||
"Magic requires mana; mana regenerates with sleep.",
|
||||
"Combat is turn-based; stats.health is the HP pool.",
|
||||
"NPCs remember their relationship to the player across sessions.",
|
||||
"Death is permanent unless a resurrection scroll is used.",
|
||||
],
|
||||
"time_schema": {"hours_in_day": 24, "initial_date": "day_1_hour_8"},
|
||||
"schemas": [
|
||||
{
|
||||
"type": "character",
|
||||
"verbose": "Character",
|
||||
"plural": "characters",
|
||||
"properties": [
|
||||
{"name": "name", "type": "string", "required": True},
|
||||
{"name": "description", "type": "string", "required": False},
|
||||
{"name": "stats", "type": "object", "required": True, "properties": [
|
||||
{"name": "health", "type": "integer", "required": True, "min": 0, "max": 100},
|
||||
{"name": "mana", "type": "integer", "required": False, "min": 0, "max": 100},
|
||||
{"name": "strength", "type": "integer", "required": True, "min": 1, "max": 20},
|
||||
{"name": "dexterity", "type": "integer", "required": False, "min": 1, "max": 20},
|
||||
{"name": "intelligence", "type": "integer", "required": False, "min": 1, "max": 20},
|
||||
]},
|
||||
{"name": "inventory", "type": "array", "required": False, "items": {"type": "object"}},
|
||||
{"name": "relationship", "type": "string", "required": False},
|
||||
],
|
||||
},
|
||||
{
|
||||
"type": "item",
|
||||
"verbose": "Item",
|
||||
"plural": "items",
|
||||
"properties": [
|
||||
{"name": "name", "type": "string", "required": True},
|
||||
{"name": "description", "type": "string", "required": False},
|
||||
{"name": "qty", "type": "integer", "required": False, "min": 1, "max": 9999},
|
||||
{"name": "value", "type": "integer", "required": False, "min": 0},
|
||||
],
|
||||
},
|
||||
{
|
||||
"type": "location",
|
||||
"verbose": "Location",
|
||||
"plural": "locations",
|
||||
"properties": [
|
||||
{"name": "name", "type": "string", "required": True},
|
||||
{"name": "description", "type": "string", "required": True},
|
||||
{"name": "exits", "type": "array", "required": False, "items": {"type": "string"}},
|
||||
{"name": "is_safe", "type": "boolean", "required": False},
|
||||
],
|
||||
},
|
||||
{
|
||||
"type": "faction",
|
||||
"verbose": "Faction",
|
||||
"plural": "factions",
|
||||
"properties": [
|
||||
{"name": "name", "type": "string", "required": True},
|
||||
{"name": "description", "type": "string", "required": False},
|
||||
{"name": "alignment", "type": "string", "required": False},
|
||||
],
|
||||
},
|
||||
],
|
||||
"environment_schema": [
|
||||
{
|
||||
"name": "player", "type": "object", "required": True,
|
||||
"properties": [
|
||||
{"name": "name", "type": "string", "required": True},
|
||||
{"name": "stats", "type": "object", "required": True, "properties": [
|
||||
{"name": "health", "type": "integer", "required": True, "min": 0, "max": 100},
|
||||
{"name": "mana", "type": "integer", "required": False, "min": 0, "max": 100},
|
||||
{"name": "strength", "type": "integer", "required": True, "min": 1, "max": 20},
|
||||
]},
|
||||
{"name": "inventory", "type": "array", "required": False},
|
||||
{"name": "backstory", "type": "string", "required": False},
|
||||
],
|
||||
},
|
||||
{"name": "current_location", "type": "string", "required": True},
|
||||
{
|
||||
"name": "plot_rails", "type": "object", "required": True,
|
||||
"properties": [
|
||||
{"name": "hooks", "type": "array", "required": True},
|
||||
{"name": "current_goals", "type": "array", "required": True},
|
||||
{"name": "completed_goals", "type": "array", "required": False},
|
||||
],
|
||||
},
|
||||
],
|
||||
"environment_initial": {
|
||||
"player": {
|
||||
"name": "Hero",
|
||||
"stats": {"health": 100, "mana": 10, "strength": 10},
|
||||
"inventory": [],
|
||||
"backstory": "A wanderer with a mysterious past.",
|
||||
},
|
||||
"current_location": "The Rusty Tankard Tavern",
|
||||
"plot_rails": {
|
||||
"hooks": [
|
||||
"Strange travelers have been seen near the old ruins.",
|
||||
"The tavern keeper is looking for someone to deliver a package.",
|
||||
],
|
||||
"current_goals": ["Find lodging for the night and learn local rumors."],
|
||||
"completed_goals": [],
|
||||
},
|
||||
},
|
||||
"is_public": True,
|
||||
"status": "ready",
|
||||
}
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Sci-fi preset
|
||||
# --------------------------------------------------------------------------- #
|
||||
SCI_FI_PRESET = {
|
||||
"name": "Deep Space Outpost",
|
||||
"description": (
|
||||
"A sci-fi setting on a remote space station. The player is a junior officer "
|
||||
"investigating strange signals from the outer rim. Resource management and "
|
||||
"social deduction blend with exploration."
|
||||
),
|
||||
"language": "en",
|
||||
"rules": [
|
||||
"Oxygen and power are limited resources; track them via env_update.",
|
||||
"The station's AI is an NPC with its own agenda.",
|
||||
"Combat is lethal — avoid open conflict when possible.",
|
||||
"Distress signals from other ships may be traps.",
|
||||
],
|
||||
"time_schema": {"hours_in_day": 24, "initial_date": "day_1_hour_8"},
|
||||
"schemas": [
|
||||
{
|
||||
"type": "character",
|
||||
"verbose": "Character",
|
||||
"plural": "characters",
|
||||
"properties": [
|
||||
{"name": "name", "type": "string", "required": True},
|
||||
{"name": "role", "type": "string", "required": False},
|
||||
{"name": "stats", "type": "object", "required": True, "properties": [
|
||||
{"name": "health", "type": "integer", "required": True, "min": 0, "max": 100},
|
||||
{"name": "oxygen", "type": "integer", "required": True, "min": 0, "max": 100},
|
||||
{"name": "tech_skill", "type": "integer", "required": False, "min": 1, "max": 20},
|
||||
]},
|
||||
{"name": "inventory", "type": "array", "required": False},
|
||||
{"name": "loyalty", "type": "string", "required": False},
|
||||
],
|
||||
},
|
||||
{
|
||||
"type": "item",
|
||||
"verbose": "Item",
|
||||
"plural": "items",
|
||||
"properties": [
|
||||
{"name": "name", "type": "string", "required": True},
|
||||
{"name": "description", "type": "string", "required": False},
|
||||
{"name": "qty", "type": "integer", "required": False, "min": 1, "max": 9999},
|
||||
],
|
||||
},
|
||||
{
|
||||
"type": "location",
|
||||
"verbose": "Location",
|
||||
"plural": "locations",
|
||||
"properties": [
|
||||
{"name": "name", "type": "string", "required": True},
|
||||
{"name": "description", "type": "string", "required": True},
|
||||
{"name": "exits", "type": "array", "required": False},
|
||||
{"name": "is_sealed", "type": "boolean", "required": False},
|
||||
],
|
||||
},
|
||||
{
|
||||
"type": "faction",
|
||||
"verbose": "Faction",
|
||||
"plural": "factions",
|
||||
"properties": [
|
||||
{"name": "name", "type": "string", "required": True},
|
||||
{"name": "description", "type": "string", "required": False},
|
||||
{"name": "allegiance", "type": "string", "required": False},
|
||||
],
|
||||
},
|
||||
],
|
||||
"environment_schema": [
|
||||
{
|
||||
"name": "player", "type": "object", "required": True,
|
||||
"properties": [
|
||||
{"name": "name", "type": "string", "required": True},
|
||||
{"name": "role", "type": "string", "required": False},
|
||||
{"name": "stats", "type": "object", "required": True, "properties": [
|
||||
{"name": "health", "type": "integer", "required": True, "min": 0, "max": 100},
|
||||
{"name": "oxygen", "type": "integer", "required": True, "min": 0, "max": 100},
|
||||
{"name": "tech_skill", "type": "integer", "required": False, "min": 1, "max": 20},
|
||||
]},
|
||||
{"name": "inventory", "type": "array", "required": False},
|
||||
{"name": "backstory", "type": "string", "required": False},
|
||||
],
|
||||
},
|
||||
{"name": "current_location", "type": "string", "required": True},
|
||||
{
|
||||
"name": "plot_rails", "type": "object", "required": True,
|
||||
"properties": [
|
||||
{"name": "hooks", "type": "array", "required": True},
|
||||
{"name": "current_goals", "type": "array", "required": True},
|
||||
{"name": "completed_goals", "type": "array", "required": False},
|
||||
],
|
||||
},
|
||||
],
|
||||
"environment_initial": {
|
||||
"player": {
|
||||
"name": "Operative",
|
||||
"role": "Junior Officer",
|
||||
"stats": {"health": 100, "oxygen": 100, "tech_skill": 8},
|
||||
"inventory": [],
|
||||
"backstory": "Fresh out of the academy, assigned to the Outer Rim Station.",
|
||||
},
|
||||
"current_location": "Station Command Module",
|
||||
"plot_rails": {
|
||||
"hooks": [
|
||||
"Anomalous signal detected from sector 7G.",
|
||||
"The station AI has been unusually quiet lately.",
|
||||
],
|
||||
"current_goals": ["Report to the commanding officer and check the signal log."],
|
||||
"completed_goals": [],
|
||||
},
|
||||
},
|
||||
"is_public": True,
|
||||
"status": "ready",
|
||||
}
|
||||
|
||||
|
||||
BUILTIN_PRESETS = [FANTASY_PRESET, SCI_FI_PRESET]
|
||||
|
||||
|
||||
async def seed_builtin_presets(session: AsyncSession) -> None:
|
||||
"""Insert builtin presets if they don't yet exist. Owned by the first admin (or a system sentinel)."""
|
||||
for preset_data in BUILTIN_PRESETS:
|
||||
existing = (
|
||||
await session.execute(
|
||||
select(WorldPreset).where(WorldPreset.name == preset_data["name"])
|
||||
)
|
||||
).scalar_one_or_none()
|
||||
if existing is not None:
|
||||
continue
|
||||
# Find any admin to own the preset, or use a sentinel UUID
|
||||
from app.models import User
|
||||
from sqlalchemy import func
|
||||
|
||||
admin = (
|
||||
await session.execute(
|
||||
select(User).where(User.is_admin.is_(True)).limit(1)
|
||||
)
|
||||
).scalar_one_or_none()
|
||||
owner_id = admin.id if admin else uuid.UUID("00000000-0000-0000-0000-000000000001")
|
||||
preset = WorldPreset(
|
||||
owner_id=owner_id,
|
||||
**preset_data,
|
||||
version=1,
|
||||
)
|
||||
session.add(preset)
|
||||
await session.commit()
|
||||
29
app/migrations/versions/001_initial_schema.py
Normal file
29
app/migrations/versions/001_initial_schema.py
Normal file
@@ -0,0 +1,29 @@
|
||||
"""Initial schema migration — creates all tables.
|
||||
|
||||
This is a hand-rolled async migration that creates all tables defined in
|
||||
`app.models` via SQLAlchemy `Base.metadata.create_all`. It is the equivalent
|
||||
of alembic migration 001.
|
||||
|
||||
For real-world deployments the project includes an `alembic.ini` and
|
||||
`alembic env.py` so that incremental migrations can be added — but for the
|
||||
MVP we use this single idempotent script.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncEngine
|
||||
|
||||
from app.db import Base
|
||||
from app.models import * # noqa: F401,F403 — ensure all models are imported
|
||||
|
||||
|
||||
async def create_all_tables(engine: AsyncEngine) -> None:
|
||||
"""Create all tables defined on Base.metadata. Idempotent."""
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
|
||||
|
||||
async def drop_all_tables(engine: AsyncEngine) -> None:
|
||||
"""Drop all tables. Used in tests."""
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.drop_all)
|
||||
Reference in New Issue
Block a user