From 5cb44b2c65afda278ec697c771a83baef3946e00 Mon Sep 17 00:00:00 2001 From: Piotr Oleszczyk Date: Sun, 1 Mar 2026 17:27:07 +0100 Subject: [PATCH] fix(backend): apply black/isort formatting and fix ruff noqa annotations Co-Authored-By: Claude Sonnet 4.6 --- backend/alembic/env.py | 5 +- .../versions/c2d626a2b36c_initial_schema.py | 642 ++++++++++++------ backend/innercontext/api/routines.py | 66 +- backend/innercontext/api/skincare.py | 12 +- backend/innercontext/llm.py | 1 - backend/innercontext/mcp_server.py | 19 +- 6 files changed, 515 insertions(+), 230 deletions(-) diff --git a/backend/alembic/env.py b/backend/alembic/env.py index 76f7495..4253c54 100644 --- a/backend/alembic/env.py +++ b/backend/alembic/env.py @@ -1,15 +1,16 @@ import os from logging.config import fileConfig -from alembic import context from dotenv import load_dotenv from sqlalchemy import engine_from_config, pool from sqlmodel import SQLModel +from alembic import context + load_dotenv() # Import all models so their tables are registered in SQLModel.metadata -import innercontext.models # noqa: F401 +import innercontext.models # noqa: F401, E402 config = context.config diff --git a/backend/alembic/versions/c2d626a2b36c_initial_schema.py b/backend/alembic/versions/c2d626a2b36c_initial_schema.py index 093aca0..7407207 100644 --- a/backend/alembic/versions/c2d626a2b36c_initial_schema.py +++ b/backend/alembic/versions/c2d626a2b36c_initial_schema.py @@ -1,19 +1,20 @@ """initial_schema Revision ID: c2d626a2b36c -Revises: +Revises: Create Date: 2026-02-28 20:13:55.499494 """ + from typing import Sequence, Union -from alembic import op import sqlalchemy as sa import sqlmodel.sql.sqltypes +from alembic import op # revision identifiers, used by Alembic. -revision: str = 'c2d626a2b36c' +revision: str = "c2d626a2b36c" down_revision: Union[str, Sequence[str], None] = None branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None @@ -22,217 +23,456 @@ depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: """Upgrade schema.""" # ### commands auto generated by Alembic - please adjust! ### - op.create_table('grooming_schedule', - sa.Column('id', sa.Uuid(), nullable=False), - sa.Column('day_of_week', sa.Integer(), nullable=False), - sa.Column('action', sa.Enum('SHAVING_RAZOR', 'SHAVING_ONEBLADE', 'DERMAROLLING', name='groomingaction'), nullable=False), - sa.Column('notes', sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.PrimaryKeyConstraint('id') + op.create_table( + "grooming_schedule", + sa.Column("id", sa.Uuid(), nullable=False), + sa.Column("day_of_week", sa.Integer(), nullable=False), + sa.Column( + "action", + sa.Enum( + "SHAVING_RAZOR", + "SHAVING_ONEBLADE", + "DERMAROLLING", + name="groomingaction", + ), + nullable=False, + ), + sa.Column("notes", sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.PrimaryKeyConstraint("id"), ) - op.create_index(op.f('ix_grooming_schedule_day_of_week'), 'grooming_schedule', ['day_of_week'], unique=False) - op.create_table('lab_results', - sa.Column('record_id', sa.Uuid(), nullable=False), - sa.Column('collected_at', sa.DateTime(), nullable=False), - sa.Column('test_code', sqlmodel.sql.sqltypes.AutoString(), nullable=False), - sa.Column('test_name_original', sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column('test_name_loinc', sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column('value_num', sa.Float(), nullable=True), - sa.Column('value_text', sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column('value_bool', sa.Boolean(), nullable=True), - sa.Column('unit_original', sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column('unit_ucum', sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column('ref_low', sa.Float(), nullable=True), - sa.Column('ref_high', sa.Float(), nullable=True), - sa.Column('ref_text', sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column('flag', sa.Enum('NORMAL', 'ABNORMAL', 'POSITIVE', 'NEGATIVE', 'LOW', 'HIGH', name='resultflag'), nullable=True), - sa.Column('lab', sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column('source_file', sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column('notes', sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column('created_at', sa.DateTime(), nullable=False), - sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False), - sa.PrimaryKeyConstraint('record_id') + op.create_index( + op.f("ix_grooming_schedule_day_of_week"), + "grooming_schedule", + ["day_of_week"], + unique=False, ) - op.create_index(op.f('ix_lab_results_collected_at'), 'lab_results', ['collected_at'], unique=False) - op.create_index(op.f('ix_lab_results_flag'), 'lab_results', ['flag'], unique=False) - op.create_index(op.f('ix_lab_results_lab'), 'lab_results', ['lab'], unique=False) - op.create_index(op.f('ix_lab_results_test_code'), 'lab_results', ['test_code'], unique=False) - op.create_table('medication_entries', - sa.Column('record_id', sa.Uuid(), nullable=False), - sa.Column('kind', sa.Enum('PRESCRIPTION', 'OTC', 'SUPPLEMENT', 'HERBAL', 'OTHER', name='medicationkind'), nullable=False), - sa.Column('product_name', sqlmodel.sql.sqltypes.AutoString(), nullable=False), - sa.Column('active_substance', sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column('formulation', sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column('route', sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column('source_file', sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column('notes', sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column('created_at', sa.DateTime(), nullable=False), - sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False), - sa.PrimaryKeyConstraint('record_id') + op.create_table( + "lab_results", + sa.Column("record_id", sa.Uuid(), nullable=False), + sa.Column("collected_at", sa.DateTime(), nullable=False), + sa.Column("test_code", sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column( + "test_name_original", sqlmodel.sql.sqltypes.AutoString(), nullable=True + ), + sa.Column("test_name_loinc", sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column("value_num", sa.Float(), nullable=True), + sa.Column("value_text", sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column("value_bool", sa.Boolean(), nullable=True), + sa.Column("unit_original", sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column("unit_ucum", sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column("ref_low", sa.Float(), nullable=True), + sa.Column("ref_high", sa.Float(), nullable=True), + sa.Column("ref_text", sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column( + "flag", + sa.Enum( + "NORMAL", + "ABNORMAL", + "POSITIVE", + "NEGATIVE", + "LOW", + "HIGH", + name="resultflag", + ), + nullable=True, + ), + sa.Column("lab", sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column("source_file", sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column("notes", sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column("created_at", sa.DateTime(), nullable=False), + sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False), + sa.PrimaryKeyConstraint("record_id"), ) - op.create_index(op.f('ix_medication_entries_active_substance'), 'medication_entries', ['active_substance'], unique=False) - op.create_index(op.f('ix_medication_entries_kind'), 'medication_entries', ['kind'], unique=False) - op.create_index(op.f('ix_medication_entries_product_name'), 'medication_entries', ['product_name'], unique=False) - op.create_table('products', - sa.Column('name', sqlmodel.sql.sqltypes.AutoString(), nullable=False), - sa.Column('brand', sqlmodel.sql.sqltypes.AutoString(), nullable=False), - sa.Column('line_name', sqlmodel.sql.sqltypes.AutoString(length=128), nullable=True), - sa.Column('sku', sqlmodel.sql.sqltypes.AutoString(length=64), nullable=True), - sa.Column('url', sqlmodel.sql.sqltypes.AutoString(length=512), nullable=True), - sa.Column('barcode', sqlmodel.sql.sqltypes.AutoString(length=64), nullable=True), - sa.Column('category', sa.Enum('CLEANSER', 'TONER', 'ESSENCE', 'SERUM', 'MOISTURIZER', 'SPF', 'MASK', 'EXFOLIANT', 'HAIR_TREATMENT', 'TOOL', 'SPOT_TREATMENT', 'OIL', name='productcategory'), nullable=False), - sa.Column('recommended_time', sa.Enum('AM', 'PM', 'BOTH', name='daytime'), nullable=False), - sa.Column('texture', sa.Enum('WATERY', 'GEL', 'EMULSION', 'CREAM', 'OIL', 'BALM', 'FOAM', 'FLUID', name='texturetype'), nullable=True), - sa.Column('absorption_speed', sa.Enum('VERY_FAST', 'FAST', 'MODERATE', 'SLOW', 'VERY_SLOW', name='absorptionspeed'), nullable=True), - sa.Column('leave_on', sa.Boolean(), nullable=False), - sa.Column('size_ml', sa.Float(), nullable=True), - sa.Column('full_weight_g', sa.Float(), nullable=True), - sa.Column('empty_weight_g', sa.Float(), nullable=True), - sa.Column('pao_months', sa.Integer(), nullable=True), - sa.Column('price_tier', sa.Enum('BUDGET', 'MID', 'PREMIUM', 'LUXURY', name='pricetier'), nullable=True), - sa.Column('inci', sa.JSON(), nullable=False), - sa.Column('actives', sa.JSON(), nullable=True), - sa.Column('recommended_for', sa.JSON(), nullable=False), - sa.Column('targets', sa.JSON(), nullable=False), - sa.Column('contraindications', sa.JSON(), nullable=False), - sa.Column('usage_notes', sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column('fragrance_free', sa.Boolean(), nullable=True), - sa.Column('essential_oils_free', sa.Boolean(), nullable=True), - sa.Column('alcohol_denat_free', sa.Boolean(), nullable=True), - sa.Column('pregnancy_safe', sa.Boolean(), nullable=True), - sa.Column('product_effect_profile', sa.JSON(), nullable=False), - sa.Column('ph_min', sa.Float(), nullable=True), - sa.Column('ph_max', sa.Float(), nullable=True), - sa.Column('incompatible_with', sa.JSON(), nullable=True), - sa.Column('synergizes_with', sa.JSON(), nullable=True), - sa.Column('context_rules', sa.JSON(), nullable=True), - sa.Column('min_interval_hours', sa.Integer(), nullable=True), - sa.Column('max_frequency_per_week', sa.Integer(), nullable=True), - sa.Column('is_medication', sa.Boolean(), nullable=False), - sa.Column('is_tool', sa.Boolean(), nullable=False), - sa.Column('needle_length_mm', sa.Float(), nullable=True), - sa.Column('personal_tolerance_notes', sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column('personal_repurchase_intent', sa.Boolean(), nullable=True), - sa.Column('id', sa.Uuid(), nullable=False), - sa.Column('created_at', sa.DateTime(), nullable=False), - sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False), - sa.PrimaryKeyConstraint('id') + op.create_index( + op.f("ix_lab_results_collected_at"), + "lab_results", + ["collected_at"], + unique=False, ) - op.create_index(op.f('ix_products_price_tier'), 'products', ['price_tier'], unique=False) - op.create_table('routines', - sa.Column('id', sa.Uuid(), nullable=False), - sa.Column('routine_date', sa.Date(), nullable=False), - sa.Column('part_of_day', sa.Enum('AM', 'PM', name='partofday'), nullable=False), - sa.Column('notes', sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column('created_at', sa.DateTime(), nullable=False), - sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('routine_date', 'part_of_day', name='uq_routine_date_part_of_day') + op.create_index(op.f("ix_lab_results_flag"), "lab_results", ["flag"], unique=False) + op.create_index(op.f("ix_lab_results_lab"), "lab_results", ["lab"], unique=False) + op.create_index( + op.f("ix_lab_results_test_code"), "lab_results", ["test_code"], unique=False ) - op.create_index(op.f('ix_routines_part_of_day'), 'routines', ['part_of_day'], unique=False) - op.create_index(op.f('ix_routines_routine_date'), 'routines', ['routine_date'], unique=False) - op.create_table('skin_condition_snapshots', - sa.Column('snapshot_date', sa.Date(), nullable=False), - sa.Column('overall_state', sa.Enum('EXCELLENT', 'GOOD', 'FAIR', 'POOR', name='overallskinstate'), nullable=True), - sa.Column('skin_type', sa.Enum('DRY', 'OILY', 'COMBINATION', 'SENSITIVE', 'NORMAL', 'ACNE_PRONE', name='skintype'), nullable=True), - sa.Column('texture', sa.Enum('SMOOTH', 'ROUGH', 'FLAKY', 'BUMPY', name='skintexture'), nullable=True), - sa.Column('hydration_level', sa.Integer(), nullable=True), - sa.Column('sebum_tzone', sa.Integer(), nullable=True), - sa.Column('sebum_cheeks', sa.Integer(), nullable=True), - sa.Column('sensitivity_level', sa.Integer(), nullable=True), - sa.Column('barrier_state', sa.Enum('INTACT', 'MILDLY_COMPROMISED', 'COMPROMISED', name='barrierstate'), nullable=True), - sa.Column('active_concerns', sa.JSON(), nullable=False), - sa.Column('risks', sa.JSON(), nullable=False), - sa.Column('priorities', sa.JSON(), nullable=False), - sa.Column('notes', sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column('id', sa.Uuid(), nullable=False), - sa.Column('created_at', sa.DateTime(), nullable=False), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('snapshot_date', name='uq_skin_snapshot_date') + op.create_table( + "medication_entries", + sa.Column("record_id", sa.Uuid(), nullable=False), + sa.Column( + "kind", + sa.Enum( + "PRESCRIPTION", + "OTC", + "SUPPLEMENT", + "HERBAL", + "OTHER", + name="medicationkind", + ), + nullable=False, + ), + sa.Column("product_name", sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column( + "active_substance", sqlmodel.sql.sqltypes.AutoString(), nullable=True + ), + sa.Column("formulation", sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column("route", sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column("source_file", sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column("notes", sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column("created_at", sa.DateTime(), nullable=False), + sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False), + sa.PrimaryKeyConstraint("record_id"), ) - op.create_index(op.f('ix_skin_condition_snapshots_snapshot_date'), 'skin_condition_snapshots', ['snapshot_date'], unique=False) - op.create_table('medication_usages', - sa.Column('record_id', sa.Uuid(), nullable=False), - sa.Column('medication_record_id', sa.Uuid(), nullable=False), - sa.Column('dose_value', sa.Float(), nullable=True), - sa.Column('dose_unit', sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column('frequency', sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column('schedule_text', sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column('as_needed', sa.Boolean(), nullable=False), - sa.Column('valid_from', sa.DateTime(), nullable=False), - sa.Column('valid_to', sa.DateTime(), nullable=True), - sa.Column('source_file', sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column('notes', sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column('created_at', sa.DateTime(), nullable=False), - sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False), - sa.ForeignKeyConstraint(['medication_record_id'], ['medication_entries.record_id'], ), - sa.PrimaryKeyConstraint('record_id') + op.create_index( + op.f("ix_medication_entries_active_substance"), + "medication_entries", + ["active_substance"], + unique=False, ) - op.create_index(op.f('ix_medication_usages_as_needed'), 'medication_usages', ['as_needed'], unique=False) - op.create_index(op.f('ix_medication_usages_medication_record_id'), 'medication_usages', ['medication_record_id'], unique=False) - op.create_index(op.f('ix_medication_usages_valid_from'), 'medication_usages', ['valid_from'], unique=False) - op.create_index(op.f('ix_medication_usages_valid_to'), 'medication_usages', ['valid_to'], unique=False) - op.create_table('product_inventory', - sa.Column('id', sa.Uuid(), nullable=False), - sa.Column('product_id', sa.Uuid(), nullable=False), - sa.Column('is_opened', sa.Boolean(), nullable=False), - sa.Column('opened_at', sa.Date(), nullable=True), - sa.Column('finished_at', sa.Date(), nullable=True), - sa.Column('expiry_date', sa.Date(), nullable=True), - sa.Column('current_weight_g', sa.Float(), nullable=True), - sa.Column('last_weighed_at', sa.Date(), nullable=True), - sa.Column('notes', sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column('created_at', sa.DateTime(), nullable=False), - sa.ForeignKeyConstraint(['product_id'], ['products.id'], ), - sa.PrimaryKeyConstraint('id') + op.create_index( + op.f("ix_medication_entries_kind"), "medication_entries", ["kind"], unique=False ) - op.create_index(op.f('ix_product_inventory_product_id'), 'product_inventory', ['product_id'], unique=False) - op.create_table('routine_steps', - sa.Column('id', sa.Uuid(), nullable=False), - sa.Column('routine_id', sa.Uuid(), nullable=False), - sa.Column('product_id', sa.Uuid(), nullable=True), - sa.Column('order_index', sa.Integer(), nullable=False), - sa.Column('action_type', sa.Enum('SHAVING_RAZOR', 'SHAVING_ONEBLADE', 'DERMAROLLING', name='groomingaction'), nullable=True), - sa.Column('action_notes', sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column('dose', sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column('region', sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.ForeignKeyConstraint(['product_id'], ['products.id'], ), - sa.ForeignKeyConstraint(['routine_id'], ['routines.id'], ), - sa.PrimaryKeyConstraint('id') + op.create_index( + op.f("ix_medication_entries_product_name"), + "medication_entries", + ["product_name"], + unique=False, + ) + op.create_table( + "products", + sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column("brand", sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column( + "line_name", sqlmodel.sql.sqltypes.AutoString(length=128), nullable=True + ), + sa.Column("sku", sqlmodel.sql.sqltypes.AutoString(length=64), nullable=True), + sa.Column("url", sqlmodel.sql.sqltypes.AutoString(length=512), nullable=True), + sa.Column( + "barcode", sqlmodel.sql.sqltypes.AutoString(length=64), nullable=True + ), + sa.Column( + "category", + sa.Enum( + "CLEANSER", + "TONER", + "ESSENCE", + "SERUM", + "MOISTURIZER", + "SPF", + "MASK", + "EXFOLIANT", + "HAIR_TREATMENT", + "TOOL", + "SPOT_TREATMENT", + "OIL", + name="productcategory", + ), + nullable=False, + ), + sa.Column( + "recommended_time", + sa.Enum("AM", "PM", "BOTH", name="daytime"), + nullable=False, + ), + sa.Column( + "texture", + sa.Enum( + "WATERY", + "GEL", + "EMULSION", + "CREAM", + "OIL", + "BALM", + "FOAM", + "FLUID", + name="texturetype", + ), + nullable=True, + ), + sa.Column( + "absorption_speed", + sa.Enum( + "VERY_FAST", + "FAST", + "MODERATE", + "SLOW", + "VERY_SLOW", + name="absorptionspeed", + ), + nullable=True, + ), + sa.Column("leave_on", sa.Boolean(), nullable=False), + sa.Column("size_ml", sa.Float(), nullable=True), + sa.Column("full_weight_g", sa.Float(), nullable=True), + sa.Column("empty_weight_g", sa.Float(), nullable=True), + sa.Column("pao_months", sa.Integer(), nullable=True), + sa.Column( + "price_tier", + sa.Enum("BUDGET", "MID", "PREMIUM", "LUXURY", name="pricetier"), + nullable=True, + ), + sa.Column("inci", sa.JSON(), nullable=False), + sa.Column("actives", sa.JSON(), nullable=True), + sa.Column("recommended_for", sa.JSON(), nullable=False), + sa.Column("targets", sa.JSON(), nullable=False), + sa.Column("contraindications", sa.JSON(), nullable=False), + sa.Column("usage_notes", sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column("fragrance_free", sa.Boolean(), nullable=True), + sa.Column("essential_oils_free", sa.Boolean(), nullable=True), + sa.Column("alcohol_denat_free", sa.Boolean(), nullable=True), + sa.Column("pregnancy_safe", sa.Boolean(), nullable=True), + sa.Column("product_effect_profile", sa.JSON(), nullable=False), + sa.Column("ph_min", sa.Float(), nullable=True), + sa.Column("ph_max", sa.Float(), nullable=True), + sa.Column("incompatible_with", sa.JSON(), nullable=True), + sa.Column("synergizes_with", sa.JSON(), nullable=True), + sa.Column("context_rules", sa.JSON(), nullable=True), + sa.Column("min_interval_hours", sa.Integer(), nullable=True), + sa.Column("max_frequency_per_week", sa.Integer(), nullable=True), + sa.Column("is_medication", sa.Boolean(), nullable=False), + sa.Column("is_tool", sa.Boolean(), nullable=False), + sa.Column("needle_length_mm", sa.Float(), nullable=True), + sa.Column( + "personal_tolerance_notes", + sqlmodel.sql.sqltypes.AutoString(), + nullable=True, + ), + sa.Column("personal_repurchase_intent", sa.Boolean(), nullable=True), + sa.Column("id", sa.Uuid(), nullable=False), + sa.Column("created_at", sa.DateTime(), nullable=False), + sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index( + op.f("ix_products_price_tier"), "products", ["price_tier"], unique=False + ) + op.create_table( + "routines", + sa.Column("id", sa.Uuid(), nullable=False), + sa.Column("routine_date", sa.Date(), nullable=False), + sa.Column("part_of_day", sa.Enum("AM", "PM", name="partofday"), nullable=False), + sa.Column("notes", sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column("created_at", sa.DateTime(), nullable=False), + sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint( + "routine_date", "part_of_day", name="uq_routine_date_part_of_day" + ), + ) + op.create_index( + op.f("ix_routines_part_of_day"), "routines", ["part_of_day"], unique=False + ) + op.create_index( + op.f("ix_routines_routine_date"), "routines", ["routine_date"], unique=False + ) + op.create_table( + "skin_condition_snapshots", + sa.Column("snapshot_date", sa.Date(), nullable=False), + sa.Column( + "overall_state", + sa.Enum("EXCELLENT", "GOOD", "FAIR", "POOR", name="overallskinstate"), + nullable=True, + ), + sa.Column( + "skin_type", + sa.Enum( + "DRY", + "OILY", + "COMBINATION", + "SENSITIVE", + "NORMAL", + "ACNE_PRONE", + name="skintype", + ), + nullable=True, + ), + sa.Column( + "texture", + sa.Enum("SMOOTH", "ROUGH", "FLAKY", "BUMPY", name="skintexture"), + nullable=True, + ), + sa.Column("hydration_level", sa.Integer(), nullable=True), + sa.Column("sebum_tzone", sa.Integer(), nullable=True), + sa.Column("sebum_cheeks", sa.Integer(), nullable=True), + sa.Column("sensitivity_level", sa.Integer(), nullable=True), + sa.Column( + "barrier_state", + sa.Enum("INTACT", "MILDLY_COMPROMISED", "COMPROMISED", name="barrierstate"), + nullable=True, + ), + sa.Column("active_concerns", sa.JSON(), nullable=False), + sa.Column("risks", sa.JSON(), nullable=False), + sa.Column("priorities", sa.JSON(), nullable=False), + sa.Column("notes", sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column("id", sa.Uuid(), nullable=False), + sa.Column("created_at", sa.DateTime(), nullable=False), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("snapshot_date", name="uq_skin_snapshot_date"), + ) + op.create_index( + op.f("ix_skin_condition_snapshots_snapshot_date"), + "skin_condition_snapshots", + ["snapshot_date"], + unique=False, + ) + op.create_table( + "medication_usages", + sa.Column("record_id", sa.Uuid(), nullable=False), + sa.Column("medication_record_id", sa.Uuid(), nullable=False), + sa.Column("dose_value", sa.Float(), nullable=True), + sa.Column("dose_unit", sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column("frequency", sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column("schedule_text", sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column("as_needed", sa.Boolean(), nullable=False), + sa.Column("valid_from", sa.DateTime(), nullable=False), + sa.Column("valid_to", sa.DateTime(), nullable=True), + sa.Column("source_file", sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column("notes", sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column("created_at", sa.DateTime(), nullable=False), + sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False), + sa.ForeignKeyConstraint( + ["medication_record_id"], + ["medication_entries.record_id"], + ), + sa.PrimaryKeyConstraint("record_id"), + ) + op.create_index( + op.f("ix_medication_usages_as_needed"), + "medication_usages", + ["as_needed"], + unique=False, + ) + op.create_index( + op.f("ix_medication_usages_medication_record_id"), + "medication_usages", + ["medication_record_id"], + unique=False, + ) + op.create_index( + op.f("ix_medication_usages_valid_from"), + "medication_usages", + ["valid_from"], + unique=False, + ) + op.create_index( + op.f("ix_medication_usages_valid_to"), + "medication_usages", + ["valid_to"], + unique=False, + ) + op.create_table( + "product_inventory", + sa.Column("id", sa.Uuid(), nullable=False), + sa.Column("product_id", sa.Uuid(), nullable=False), + sa.Column("is_opened", sa.Boolean(), nullable=False), + sa.Column("opened_at", sa.Date(), nullable=True), + sa.Column("finished_at", sa.Date(), nullable=True), + sa.Column("expiry_date", sa.Date(), nullable=True), + sa.Column("current_weight_g", sa.Float(), nullable=True), + sa.Column("last_weighed_at", sa.Date(), nullable=True), + sa.Column("notes", sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column("created_at", sa.DateTime(), nullable=False), + sa.ForeignKeyConstraint( + ["product_id"], + ["products.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index( + op.f("ix_product_inventory_product_id"), + "product_inventory", + ["product_id"], + unique=False, + ) + op.create_table( + "routine_steps", + sa.Column("id", sa.Uuid(), nullable=False), + sa.Column("routine_id", sa.Uuid(), nullable=False), + sa.Column("product_id", sa.Uuid(), nullable=True), + sa.Column("order_index", sa.Integer(), nullable=False), + sa.Column( + "action_type", + sa.Enum( + "SHAVING_RAZOR", + "SHAVING_ONEBLADE", + "DERMAROLLING", + name="groomingaction", + ), + nullable=True, + ), + sa.Column("action_notes", sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column("dose", sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column("region", sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.ForeignKeyConstraint( + ["product_id"], + ["products.id"], + ), + sa.ForeignKeyConstraint( + ["routine_id"], + ["routines.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index( + op.f("ix_routine_steps_product_id"), + "routine_steps", + ["product_id"], + unique=False, + ) + op.create_index( + op.f("ix_routine_steps_routine_id"), + "routine_steps", + ["routine_id"], + unique=False, ) - op.create_index(op.f('ix_routine_steps_product_id'), 'routine_steps', ['product_id'], unique=False) - op.create_index(op.f('ix_routine_steps_routine_id'), 'routine_steps', ['routine_id'], unique=False) # ### end Alembic commands ### def downgrade() -> None: """Downgrade schema.""" # ### commands auto generated by Alembic - please adjust! ### - op.drop_index(op.f('ix_routine_steps_routine_id'), table_name='routine_steps') - op.drop_index(op.f('ix_routine_steps_product_id'), table_name='routine_steps') - op.drop_table('routine_steps') - op.drop_index(op.f('ix_product_inventory_product_id'), table_name='product_inventory') - op.drop_table('product_inventory') - op.drop_index(op.f('ix_medication_usages_valid_to'), table_name='medication_usages') - op.drop_index(op.f('ix_medication_usages_valid_from'), table_name='medication_usages') - op.drop_index(op.f('ix_medication_usages_medication_record_id'), table_name='medication_usages') - op.drop_index(op.f('ix_medication_usages_as_needed'), table_name='medication_usages') - op.drop_table('medication_usages') - op.drop_index(op.f('ix_skin_condition_snapshots_snapshot_date'), table_name='skin_condition_snapshots') - op.drop_table('skin_condition_snapshots') - op.drop_index(op.f('ix_routines_routine_date'), table_name='routines') - op.drop_index(op.f('ix_routines_part_of_day'), table_name='routines') - op.drop_table('routines') - op.drop_index(op.f('ix_products_price_tier'), table_name='products') - op.drop_table('products') - op.drop_index(op.f('ix_medication_entries_product_name'), table_name='medication_entries') - op.drop_index(op.f('ix_medication_entries_kind'), table_name='medication_entries') - op.drop_index(op.f('ix_medication_entries_active_substance'), table_name='medication_entries') - op.drop_table('medication_entries') - op.drop_index(op.f('ix_lab_results_test_code'), table_name='lab_results') - op.drop_index(op.f('ix_lab_results_lab'), table_name='lab_results') - op.drop_index(op.f('ix_lab_results_flag'), table_name='lab_results') - op.drop_index(op.f('ix_lab_results_collected_at'), table_name='lab_results') - op.drop_table('lab_results') - op.drop_index(op.f('ix_grooming_schedule_day_of_week'), table_name='grooming_schedule') - op.drop_table('grooming_schedule') + op.drop_index(op.f("ix_routine_steps_routine_id"), table_name="routine_steps") + op.drop_index(op.f("ix_routine_steps_product_id"), table_name="routine_steps") + op.drop_table("routine_steps") + op.drop_index( + op.f("ix_product_inventory_product_id"), table_name="product_inventory" + ) + op.drop_table("product_inventory") + op.drop_index(op.f("ix_medication_usages_valid_to"), table_name="medication_usages") + op.drop_index( + op.f("ix_medication_usages_valid_from"), table_name="medication_usages" + ) + op.drop_index( + op.f("ix_medication_usages_medication_record_id"), + table_name="medication_usages", + ) + op.drop_index( + op.f("ix_medication_usages_as_needed"), table_name="medication_usages" + ) + op.drop_table("medication_usages") + op.drop_index( + op.f("ix_skin_condition_snapshots_snapshot_date"), + table_name="skin_condition_snapshots", + ) + op.drop_table("skin_condition_snapshots") + op.drop_index(op.f("ix_routines_routine_date"), table_name="routines") + op.drop_index(op.f("ix_routines_part_of_day"), table_name="routines") + op.drop_table("routines") + op.drop_index(op.f("ix_products_price_tier"), table_name="products") + op.drop_table("products") + op.drop_index( + op.f("ix_medication_entries_product_name"), table_name="medication_entries" + ) + op.drop_index(op.f("ix_medication_entries_kind"), table_name="medication_entries") + op.drop_index( + op.f("ix_medication_entries_active_substance"), table_name="medication_entries" + ) + op.drop_table("medication_entries") + op.drop_index(op.f("ix_lab_results_test_code"), table_name="lab_results") + op.drop_index(op.f("ix_lab_results_lab"), table_name="lab_results") + op.drop_index(op.f("ix_lab_results_flag"), table_name="lab_results") + op.drop_index(op.f("ix_lab_results_collected_at"), table_name="lab_results") + op.drop_table("lab_results") + op.drop_index( + op.f("ix_grooming_schedule_day_of_week"), table_name="grooming_schedule" + ) + op.drop_table("grooming_schedule") # ### end Alembic commands ### diff --git a/backend/innercontext/api/routines.py b/backend/innercontext/api/routines.py index 5cc129f..98b03f1 100644 --- a/backend/innercontext/api/routines.py +++ b/backend/innercontext/api/routines.py @@ -11,7 +11,13 @@ from sqlmodel import Session, SQLModel, col, select from db import get_session from innercontext.api.utils import get_or_404 from innercontext.llm import get_gemini_client -from innercontext.models import GroomingSchedule, Product, Routine, RoutineStep, SkinConditionSnapshot +from innercontext.models import ( + GroomingSchedule, + Product, + Routine, + RoutineStep, + SkinConditionSnapshot, +) from innercontext.models.enums import GroomingAction, PartOfDay router = APIRouter() @@ -135,16 +141,30 @@ class _BatchOut(PydanticBase): # Prompt helpers # --------------------------------------------------------------------------- -_DAY_NAMES = ["poniedziałek", "wtorek", "środa", "czwartek", "piątek", "sobota", "niedziela"] +_DAY_NAMES = [ + "poniedziałek", + "wtorek", + "środa", + "czwartek", + "piątek", + "sobota", + "niedziela", +] def _ev(v: object) -> str: - return v.value if v is not None and hasattr(v, "value") else str(v) if v is not None else "" + return ( + v.value + if v is not None and hasattr(v, "value") + else str(v) if v is not None else "" + ) def _build_skin_context(session: Session) -> str: snapshot = session.exec( - select(SkinConditionSnapshot).order_by(col(SkinConditionSnapshot.snapshot_date).desc()) + select(SkinConditionSnapshot).order_by( + col(SkinConditionSnapshot.snapshot_date).desc() + ) ).first() if snapshot is None: return "STAN SKÓRY: brak danych\n" @@ -160,16 +180,24 @@ def _build_skin_context(session: Session) -> str: ) -def _build_grooming_context(session: Session, weekdays: Optional[list[int]] = None) -> str: - entries = session.exec(select(GroomingSchedule).order_by(GroomingSchedule.day_of_week)).all() +def _build_grooming_context( + session: Session, weekdays: Optional[list[int]] = None +) -> str: + entries = session.exec( + select(GroomingSchedule).order_by(GroomingSchedule.day_of_week) + ).all() if not entries: return "HARMONOGRAM PIELĘGNACJI: brak\n" lines = ["HARMONOGRAM PIELĘGNACJI:"] for e in entries: if weekdays is not None and e.day_of_week not in weekdays: continue - day_name = _DAY_NAMES[e.day_of_week] if 0 <= e.day_of_week <= 6 else str(e.day_of_week) - lines.append(f" {day_name}: {_ev(e.action)}" + (f" ({e.notes})" if e.notes else "")) + day_name = ( + _DAY_NAMES[e.day_of_week] if 0 <= e.day_of_week <= 6 else str(e.day_of_week) + ) + lines.append( + f" {day_name}: {_ev(e.action)}" + (f" ({e.notes})" if e.notes else "") + ) if len(lines) == 1: lines.append(" (brak wpisów dla podanych dni)") return "\n".join(lines) + "\n" @@ -198,12 +226,18 @@ def _build_recent_history(session: Session) -> str: step_names.append(p.name if p else str(s.product_id)) elif s.action_type: step_names.append(_ev(s.action_type)) - lines.append(f" {r.routine_date} {_ev(r.part_of_day).upper()}: {', '.join(step_names)}") + lines.append( + f" {r.routine_date} {_ev(r.part_of_day).upper()}: {', '.join(step_names)}" + ) return "\n".join(lines) + "\n" def _build_products_context(session: Session, time_filter: Optional[str] = None) -> str: - stmt = select(Product).where(Product.is_medication == False).where(Product.is_tool == False) # noqa: E712 + stmt = ( + select(Product) + .where(Product.is_medication == False) # noqa: E712 + .where(Product.is_tool == False) # noqa: E712 + ) products = session.exec(stmt).all() lines = ["DOSTĘPNE PRODUKTY:"] for p in products: @@ -353,13 +387,17 @@ def suggest_batch( ): delta = (data.to_date - data.from_date).days + 1 if delta > 14: - raise HTTPException(status_code=400, detail="Date range must not exceed 14 days.") + raise HTTPException( + status_code=400, detail="Date range must not exceed 14 days." + ) if data.from_date > data.to_date: raise HTTPException(status_code=400, detail="from_date must be <= to_date.") client, model = get_gemini_client() - weekdays = list({(data.from_date + timedelta(days=i)).weekday() for i in range(delta)}) + weekdays = list( + {(data.from_date + timedelta(days=i)).weekday() for i in range(delta)} + ) skin_ctx = _build_skin_context(session) grooming_ctx = _build_grooming_context(session, weekdays=weekdays) history_ctx = _build_recent_history(session) @@ -437,7 +475,9 @@ def suggest_batch( ) ) - return BatchSuggestion(days=days, overall_reasoning=parsed.get("overall_reasoning", "")) + return BatchSuggestion( + days=days, overall_reasoning=parsed.get("overall_reasoning", "") + ) # Grooming-schedule GET must appear before /{routine_id} to avoid being shadowed diff --git a/backend/innercontext/api/skincare.py b/backend/innercontext/api/skincare.py index 57b60b1..2f70a30 100644 --- a/backend/innercontext/api/skincare.py +++ b/backend/innercontext/api/skincare.py @@ -146,11 +146,17 @@ async def analyze_skin_photos( parts: list[genai_types.Part] = [] for photo in photos: if photo.content_type not in allowed: - raise HTTPException(status_code=422, detail=f"Unsupported type: {photo.content_type}") + raise HTTPException( + status_code=422, detail=f"Unsupported type: {photo.content_type}" + ) data = await photo.read() if len(data) > MAX_IMAGE_BYTES: - raise HTTPException(status_code=413, detail=f"{photo.filename} exceeds 5 MB.") - parts.append(genai_types.Part.from_bytes(data=data, mime_type=photo.content_type)) + raise HTTPException( + status_code=413, detail=f"{photo.filename} exceeds 5 MB." + ) + parts.append( + genai_types.Part.from_bytes(data=data, mime_type=photo.content_type) + ) parts.append( genai_types.Part.from_text( text="Analyze the skin condition visible in the above photo(s) and return the JSON assessment." diff --git a/backend/innercontext/llm.py b/backend/innercontext/llm.py index 428b744..dd0ea99 100644 --- a/backend/innercontext/llm.py +++ b/backend/innercontext/llm.py @@ -5,7 +5,6 @@ import os from fastapi import HTTPException from google import genai - _DEFAULT_MODEL = "gemini-flash-latest" diff --git a/backend/innercontext/mcp_server.py b/backend/innercontext/mcp_server.py index 618f267..01f3826 100644 --- a/backend/innercontext/mcp_server.py +++ b/backend/innercontext/mcp_server.py @@ -287,9 +287,9 @@ def get_medications() -> list[dict]: "frequency": u.frequency, "schedule_text": u.schedule_text, "as_needed": u.as_needed, - "valid_from": u.valid_from.isoformat() - if u.valid_from - else None, + "valid_from": ( + u.valid_from.isoformat() if u.valid_from else None + ), "valid_to": u.valid_to.isoformat() if u.valid_to else None, } for u in usages @@ -323,9 +323,9 @@ def get_expiring_inventory(days: int = 30) -> list[dict]: "product_name": product.name, "brand": product.brand, "expiry_date": inv.expiry_date.isoformat() if inv.expiry_date else None, - "days_remaining": (inv.expiry_date - today).days - if inv.expiry_date - else None, + "days_remaining": ( + (inv.expiry_date - today).days if inv.expiry_date else None + ), "current_weight_g": inv.current_weight_g, } for inv, product in rows @@ -384,9 +384,7 @@ def get_recent_lab_results(limit: int = 30) -> list[dict]: """Get the most recent lab results sorted by collection date descending.""" with Session(engine) as session: results = session.exec( - select(LabResult) - .order_by(col(LabResult.collected_at).desc()) - .limit(limit) + select(LabResult).order_by(col(LabResult.collected_at).desc()).limit(limit) ).all() return [_lab_result_to_dict(r) for r in results] @@ -394,7 +392,8 @@ def get_recent_lab_results(limit: int = 30) -> list[dict]: @mcp.tool() def get_available_lab_tests() -> list[dict]: """List all distinct lab tests ever performed, grouped by LOINC test_code. - Returns test_code, LOINC name, original lab names, result count, and last collection date.""" + Returns test_code, LOINC name, original lab names, result count, and last collection date. + """ with Session(engine) as session: results = session.exec(select(LabResult)).all() tests: dict[str, dict] = {}