fix(backend): apply black/isort formatting and fix ruff noqa annotations

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Piotr Oleszczyk 2026-03-01 17:27:07 +01:00
parent 4b0fedde35
commit 5cb44b2c65
6 changed files with 515 additions and 230 deletions

View file

@ -1,15 +1,16 @@
import os import os
from logging.config import fileConfig from logging.config import fileConfig
from alembic import context
from dotenv import load_dotenv from dotenv import load_dotenv
from sqlalchemy import engine_from_config, pool from sqlalchemy import engine_from_config, pool
from sqlmodel import SQLModel from sqlmodel import SQLModel
from alembic import context
load_dotenv() load_dotenv()
# Import all models so their tables are registered in SQLModel.metadata # 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 config = context.config

View file

@ -5,15 +5,16 @@ Revises:
Create Date: 2026-02-28 20:13:55.499494 Create Date: 2026-02-28 20:13:55.499494
""" """
from typing import Sequence, Union from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa import sqlalchemy as sa
import sqlmodel.sql.sqltypes import sqlmodel.sql.sqltypes
from alembic import op
# revision identifiers, used by Alembic. # revision identifiers, used by Alembic.
revision: str = 'c2d626a2b36c' revision: str = "c2d626a2b36c"
down_revision: Union[str, Sequence[str], None] = None down_revision: Union[str, Sequence[str], None] = None
branch_labels: Union[str, Sequence[str], None] = None branch_labels: Union[str, Sequence[str], None] = None
depends_on: 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: def upgrade() -> None:
"""Upgrade schema.""" """Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ### # ### commands auto generated by Alembic - please adjust! ###
op.create_table('grooming_schedule', op.create_table(
sa.Column('id', sa.Uuid(), nullable=False), "grooming_schedule",
sa.Column('day_of_week', sa.Integer(), nullable=False), sa.Column("id", sa.Uuid(), nullable=False),
sa.Column('action', sa.Enum('SHAVING_RAZOR', 'SHAVING_ONEBLADE', 'DERMAROLLING', name='groomingaction'), nullable=False), sa.Column("day_of_week", sa.Integer(), nullable=False),
sa.Column('notes', sqlmodel.sql.sqltypes.AutoString(), nullable=True), sa.Column(
sa.PrimaryKeyConstraint('id') "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_index(
op.create_table('lab_results', op.f("ix_grooming_schedule_day_of_week"),
sa.Column('record_id', sa.Uuid(), nullable=False), "grooming_schedule",
sa.Column('collected_at', sa.DateTime(), nullable=False), ["day_of_week"],
sa.Column('test_code', sqlmodel.sql.sqltypes.AutoString(), nullable=False), unique=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_lab_results_collected_at'), 'lab_results', ['collected_at'], unique=False) op.create_table(
op.create_index(op.f('ix_lab_results_flag'), 'lab_results', ['flag'], unique=False) "lab_results",
op.create_index(op.f('ix_lab_results_lab'), 'lab_results', ['lab'], unique=False) sa.Column("record_id", sa.Uuid(), nullable=False),
op.create_index(op.f('ix_lab_results_test_code'), 'lab_results', ['test_code'], unique=False) sa.Column("collected_at", sa.DateTime(), nullable=False),
op.create_table('medication_entries', sa.Column("test_code", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column('record_id', sa.Uuid(), nullable=False), sa.Column(
sa.Column('kind', sa.Enum('PRESCRIPTION', 'OTC', 'SUPPLEMENT', 'HERBAL', 'OTHER', name='medicationkind'), nullable=False), "test_name_original", sqlmodel.sql.sqltypes.AutoString(), nullable=True
sa.Column('product_name', sqlmodel.sql.sqltypes.AutoString(), nullable=False), ),
sa.Column('active_substance', sqlmodel.sql.sqltypes.AutoString(), nullable=True), sa.Column("test_name_loinc", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
sa.Column('formulation', sqlmodel.sql.sqltypes.AutoString(), nullable=True), sa.Column("value_num", sa.Float(), nullable=True),
sa.Column('route', sqlmodel.sql.sqltypes.AutoString(), nullable=True), sa.Column("value_text", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
sa.Column('source_file', sqlmodel.sql.sqltypes.AutoString(), nullable=True), sa.Column("value_bool", sa.Boolean(), nullable=True),
sa.Column('notes', sqlmodel.sql.sqltypes.AutoString(), nullable=True), sa.Column("unit_original", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=False), sa.Column("unit_ucum", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False), sa.Column("ref_low", sa.Float(), nullable=True),
sa.PrimaryKeyConstraint('record_id') 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.create_index(op.f('ix_medication_entries_kind'), 'medication_entries', ['kind'], unique=False) op.f("ix_lab_results_collected_at"),
op.create_index(op.f('ix_medication_entries_product_name'), 'medication_entries', ['product_name'], unique=False) "lab_results",
op.create_table('products', ["collected_at"],
sa.Column('name', sqlmodel.sql.sqltypes.AutoString(), nullable=False), unique=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_index(op.f("ix_lab_results_flag"), "lab_results", ["flag"], unique=False)
op.create_table('routines', op.create_index(op.f("ix_lab_results_lab"), "lab_results", ["lab"], unique=False)
sa.Column('id', sa.Uuid(), nullable=False), op.create_index(
sa.Column('routine_date', sa.Date(), nullable=False), op.f("ix_lab_results_test_code"), "lab_results", ["test_code"], unique=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_table(
op.create_index(op.f('ix_routines_routine_date'), 'routines', ['routine_date'], unique=False) "medication_entries",
op.create_table('skin_condition_snapshots', sa.Column("record_id", sa.Uuid(), nullable=False),
sa.Column('snapshot_date', sa.Date(), nullable=False), sa.Column(
sa.Column('overall_state', sa.Enum('EXCELLENT', 'GOOD', 'FAIR', 'POOR', name='overallskinstate'), nullable=True), "kind",
sa.Column('skin_type', sa.Enum('DRY', 'OILY', 'COMBINATION', 'SENSITIVE', 'NORMAL', 'ACNE_PRONE', name='skintype'), nullable=True), sa.Enum(
sa.Column('texture', sa.Enum('SMOOTH', 'ROUGH', 'FLAKY', 'BUMPY', name='skintexture'), nullable=True), "PRESCRIPTION",
sa.Column('hydration_level', sa.Integer(), nullable=True), "OTC",
sa.Column('sebum_tzone', sa.Integer(), nullable=True), "SUPPLEMENT",
sa.Column('sebum_cheeks', sa.Integer(), nullable=True), "HERBAL",
sa.Column('sensitivity_level', sa.Integer(), nullable=True), "OTHER",
sa.Column('barrier_state', sa.Enum('INTACT', 'MILDLY_COMPROMISED', 'COMPROMISED', name='barrierstate'), nullable=True), name="medicationkind",
sa.Column('active_concerns', sa.JSON(), nullable=False), ),
sa.Column('risks', sa.JSON(), nullable=False), nullable=False,
sa.Column('priorities', sa.JSON(), nullable=False), ),
sa.Column('notes', sqlmodel.sql.sqltypes.AutoString(), nullable=True), sa.Column("product_name", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column('id', sa.Uuid(), nullable=False), sa.Column(
sa.Column('created_at', sa.DateTime(), nullable=False), "active_substance", sqlmodel.sql.sqltypes.AutoString(), nullable=True
sa.PrimaryKeyConstraint('id'), ),
sa.UniqueConstraint('snapshot_date', name='uq_skin_snapshot_date') 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_index(
op.create_table('medication_usages', op.f("ix_medication_entries_active_substance"),
sa.Column('record_id', sa.Uuid(), nullable=False), "medication_entries",
sa.Column('medication_record_id', sa.Uuid(), nullable=False), ["active_substance"],
sa.Column('dose_value', sa.Float(), nullable=True), unique=False,
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.create_index(op.f('ix_medication_usages_medication_record_id'), 'medication_usages', ['medication_record_id'], unique=False) op.f("ix_medication_entries_kind"), "medication_entries", ["kind"], 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_index(
op.create_table('routine_steps', op.f("ix_medication_entries_product_name"),
sa.Column('id', sa.Uuid(), nullable=False), "medication_entries",
sa.Column('routine_id', sa.Uuid(), nullable=False), ["product_name"],
sa.Column('product_id', sa.Uuid(), nullable=True), unique=False,
sa.Column('order_index', sa.Integer(), nullable=False), )
sa.Column('action_type', sa.Enum('SHAVING_RAZOR', 'SHAVING_ONEBLADE', 'DERMAROLLING', name='groomingaction'), nullable=True), op.create_table(
sa.Column('action_notes', sqlmodel.sql.sqltypes.AutoString(), nullable=True), "products",
sa.Column('dose', sqlmodel.sql.sqltypes.AutoString(), nullable=True), sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column('region', sqlmodel.sql.sqltypes.AutoString(), nullable=True), sa.Column("brand", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.ForeignKeyConstraint(['product_id'], ['products.id'], ), sa.Column(
sa.ForeignKeyConstraint(['routine_id'], ['routines.id'], ), "line_name", sqlmodel.sql.sqltypes.AutoString(length=128), nullable=True
sa.PrimaryKeyConstraint('id') ),
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 ### # ### end Alembic commands ###
def downgrade() -> None: def downgrade() -> None:
"""Downgrade schema.""" """Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ### # ### 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_routine_id"), table_name="routine_steps")
op.drop_index(op.f('ix_routine_steps_product_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_table("routine_steps")
op.drop_index(op.f('ix_product_inventory_product_id'), table_name='product_inventory') op.drop_index(
op.drop_table('product_inventory') op.f("ix_product_inventory_product_id"), table_name="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_table("product_inventory")
op.drop_index(op.f('ix_medication_usages_medication_record_id'), table_name='medication_usages') op.drop_index(op.f("ix_medication_usages_valid_to"), table_name="medication_usages")
op.drop_index(op.f('ix_medication_usages_as_needed'), table_name='medication_usages') op.drop_index(
op.drop_table('medication_usages') op.f("ix_medication_usages_valid_from"), table_name="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.drop_index(op.f('ix_routines_routine_date'), table_name='routines') op.f("ix_medication_usages_medication_record_id"),
op.drop_index(op.f('ix_routines_part_of_day'), table_name='routines') table_name="medication_usages",
op.drop_table('routines') )
op.drop_index(op.f('ix_products_price_tier'), table_name='products') op.drop_index(
op.drop_table('products') op.f("ix_medication_usages_as_needed"), table_name="medication_usages"
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_table("medication_usages")
op.drop_index(op.f('ix_medication_entries_active_substance'), table_name='medication_entries') op.drop_index(
op.drop_table('medication_entries') op.f("ix_skin_condition_snapshots_snapshot_date"),
op.drop_index(op.f('ix_lab_results_test_code'), table_name='lab_results') table_name="skin_condition_snapshots",
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_table("skin_condition_snapshots")
op.drop_index(op.f('ix_lab_results_collected_at'), table_name='lab_results') op.drop_index(op.f("ix_routines_routine_date"), table_name="routines")
op.drop_table('lab_results') op.drop_index(op.f("ix_routines_part_of_day"), table_name="routines")
op.drop_index(op.f('ix_grooming_schedule_day_of_week'), table_name='grooming_schedule') op.drop_table("routines")
op.drop_table('grooming_schedule') 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 ### # ### end Alembic commands ###

View file

@ -11,7 +11,13 @@ from sqlmodel import Session, SQLModel, col, select
from db import get_session from db import get_session
from innercontext.api.utils import get_or_404 from innercontext.api.utils import get_or_404
from innercontext.llm import get_gemini_client 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 from innercontext.models.enums import GroomingAction, PartOfDay
router = APIRouter() router = APIRouter()
@ -135,16 +141,30 @@ class _BatchOut(PydanticBase):
# Prompt helpers # 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: 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: def _build_skin_context(session: Session) -> str:
snapshot = session.exec( snapshot = session.exec(
select(SkinConditionSnapshot).order_by(col(SkinConditionSnapshot.snapshot_date).desc()) select(SkinConditionSnapshot).order_by(
col(SkinConditionSnapshot.snapshot_date).desc()
)
).first() ).first()
if snapshot is None: if snapshot is None:
return "STAN SKÓRY: brak danych\n" 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: def _build_grooming_context(
entries = session.exec(select(GroomingSchedule).order_by(GroomingSchedule.day_of_week)).all() session: Session, weekdays: Optional[list[int]] = None
) -> str:
entries = session.exec(
select(GroomingSchedule).order_by(GroomingSchedule.day_of_week)
).all()
if not entries: if not entries:
return "HARMONOGRAM PIELĘGNACJI: brak\n" return "HARMONOGRAM PIELĘGNACJI: brak\n"
lines = ["HARMONOGRAM PIELĘGNACJI:"] lines = ["HARMONOGRAM PIELĘGNACJI:"]
for e in entries: for e in entries:
if weekdays is not None and e.day_of_week not in weekdays: if weekdays is not None and e.day_of_week not in weekdays:
continue continue
day_name = _DAY_NAMES[e.day_of_week] if 0 <= e.day_of_week <= 6 else str(e.day_of_week) day_name = (
lines.append(f" {day_name}: {_ev(e.action)}" + (f" ({e.notes})" if e.notes else "")) _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: if len(lines) == 1:
lines.append(" (brak wpisów dla podanych dni)") lines.append(" (brak wpisów dla podanych dni)")
return "\n".join(lines) + "\n" 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)) step_names.append(p.name if p else str(s.product_id))
elif s.action_type: elif s.action_type:
step_names.append(_ev(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" return "\n".join(lines) + "\n"
def _build_products_context(session: Session, time_filter: Optional[str] = None) -> str: 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() products = session.exec(stmt).all()
lines = ["DOSTĘPNE PRODUKTY:"] lines = ["DOSTĘPNE PRODUKTY:"]
for p in products: for p in products:
@ -353,13 +387,17 @@ def suggest_batch(
): ):
delta = (data.to_date - data.from_date).days + 1 delta = (data.to_date - data.from_date).days + 1
if delta > 14: 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: if data.from_date > data.to_date:
raise HTTPException(status_code=400, detail="from_date must be <= to_date.") raise HTTPException(status_code=400, detail="from_date must be <= to_date.")
client, model = get_gemini_client() 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) skin_ctx = _build_skin_context(session)
grooming_ctx = _build_grooming_context(session, weekdays=weekdays) grooming_ctx = _build_grooming_context(session, weekdays=weekdays)
history_ctx = _build_recent_history(session) 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 # Grooming-schedule GET must appear before /{routine_id} to avoid being shadowed

View file

@ -146,11 +146,17 @@ async def analyze_skin_photos(
parts: list[genai_types.Part] = [] parts: list[genai_types.Part] = []
for photo in photos: for photo in photos:
if photo.content_type not in allowed: 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() data = await photo.read()
if len(data) > MAX_IMAGE_BYTES: if len(data) > MAX_IMAGE_BYTES:
raise HTTPException(status_code=413, detail=f"{photo.filename} exceeds 5 MB.") raise HTTPException(
parts.append(genai_types.Part.from_bytes(data=data, mime_type=photo.content_type)) 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( parts.append(
genai_types.Part.from_text( genai_types.Part.from_text(
text="Analyze the skin condition visible in the above photo(s) and return the JSON assessment." text="Analyze the skin condition visible in the above photo(s) and return the JSON assessment."

View file

@ -5,7 +5,6 @@ import os
from fastapi import HTTPException from fastapi import HTTPException
from google import genai from google import genai
_DEFAULT_MODEL = "gemini-flash-latest" _DEFAULT_MODEL = "gemini-flash-latest"

View file

@ -287,9 +287,9 @@ def get_medications() -> list[dict]:
"frequency": u.frequency, "frequency": u.frequency,
"schedule_text": u.schedule_text, "schedule_text": u.schedule_text,
"as_needed": u.as_needed, "as_needed": u.as_needed,
"valid_from": u.valid_from.isoformat() "valid_from": (
if u.valid_from u.valid_from.isoformat() if u.valid_from else None
else None, ),
"valid_to": u.valid_to.isoformat() if u.valid_to else None, "valid_to": u.valid_to.isoformat() if u.valid_to else None,
} }
for u in usages for u in usages
@ -323,9 +323,9 @@ def get_expiring_inventory(days: int = 30) -> list[dict]:
"product_name": product.name, "product_name": product.name,
"brand": product.brand, "brand": product.brand,
"expiry_date": inv.expiry_date.isoformat() if inv.expiry_date else None, "expiry_date": inv.expiry_date.isoformat() if inv.expiry_date else None,
"days_remaining": (inv.expiry_date - today).days "days_remaining": (
if inv.expiry_date (inv.expiry_date - today).days if inv.expiry_date else None
else None, ),
"current_weight_g": inv.current_weight_g, "current_weight_g": inv.current_weight_g,
} }
for inv, product in rows 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.""" """Get the most recent lab results sorted by collection date descending."""
with Session(engine) as session: with Session(engine) as session:
results = session.exec( results = session.exec(
select(LabResult) select(LabResult).order_by(col(LabResult.collected_at).desc()).limit(limit)
.order_by(col(LabResult.collected_at).desc())
.limit(limit)
).all() ).all()
return [_lab_result_to_dict(r) for r in results] 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() @mcp.tool()
def get_available_lab_tests() -> list[dict]: def get_available_lab_tests() -> list[dict]:
"""List all distinct lab tests ever performed, grouped by LOINC test_code. """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: with Session(engine) as session:
results = session.exec(select(LabResult)).all() results = session.exec(select(LabResult)).all()
tests: dict[str, dict] = {} tests: dict[str, dict] = {}