diff --git a/README.md b/README.md index 0cafbf3..220ac8d 100644 --- a/README.md +++ b/README.md @@ -138,7 +138,7 @@ Before you begin, ensure you have the following installed: Or with uvicorn: ```bash - uvicorn app:app --reload --host 0.0.0.0 --port 8000 + uvicorn backend.app:app --reload --host 0.0.0.0 --port 8000 ``` 5. **Access the API:** @@ -478,4 +478,3 @@ Using new technology can be tricky, but don't worry. Help is always available. **Made with ❤️ for The Villages Community** *Stay connected, stay engaged, stay active!* - diff --git a/backend/README.md b/backend/README.md index 9e427c6..77a42cf 100644 --- a/backend/README.md +++ b/backend/README.md @@ -36,7 +36,7 @@ pip install -r requirements.txt python app.py # Or using uvicorn -uvicorn app:app --reload --host 0.0.0.0 --port 8000 +uvicorn backend.app:app --reload --host 0.0.0.0 --port 8000 ``` The server will start on `http://localhost:8000` @@ -70,14 +70,30 @@ backend/ ### Health Check ``` GET / +GET /health GET /api/v1/health ``` -### Events (Placeholder) +### Events (Mock data) ``` +GET /events GET /api/v1/events ``` +### Recreation Centers (Mock data) +``` +GET /rec_centers +``` + +### Villages (Mock data) +``` +GET /villages +``` + +### Example Fixtures + +Sample JSON fixtures live in `docs/examples/`, with JSON schema definitions in `docs/schemas/`. + *More endpoints will be added as features are implemented.* ## Development @@ -136,7 +152,7 @@ For production deployment: 5. Enable HTTPS/SSL ```bash -uvicorn app:app --host 0.0.0.0 --port 8000 --workers 4 +uvicorn backend.app:app --host 0.0.0.0 --port 8000 --workers 4 ``` ## Contributing diff --git a/backend/alembic.ini b/backend/alembic.ini new file mode 100644 index 0000000..da497cc --- /dev/null +++ b/backend/alembic.ini @@ -0,0 +1,36 @@ +[alembic] +script_location = migrations +prepend_sys_path = . +sqlalchemy.url = sqlite:///./village_connect.db + +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s diff --git a/backend/app.py b/backend/app.py index d4d5952..0646d7d 100644 --- a/backend/app.py +++ b/backend/app.py @@ -1,15 +1,21 @@ -""" -Village Connect API Server -FastAPI application for serving community data -""" +"""Village Connect API Server.""" + +import json +from pathlib import Path from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware +FIXTURES_DIR = Path(__file__).resolve().parent / "fixtures" +EVENTS_FIXTURE = FIXTURES_DIR / "events.json" +REC_CENTERS_FIXTURE = FIXTURES_DIR / "rec_centers.json" +VILLAGES_FIXTURE = FIXTURES_DIR / "villages.json" + + app = FastAPI( title="Village Connect API", description="API for community events, residents, and services", - version="0.1.0" + version="0.1.0", ) # CORS middleware for mobile app access @@ -22,29 +28,54 @@ ) +def load_fixture(path: Path) -> list[dict]: + if not path.exists(): + return [] + with path.open(encoding="utf-8") as file: + return json.load(file) + + +def health_payload() -> dict: + return {"status": "ok"} + + @app.get("/") -async def root(): - """Health check endpoint""" +async def root() -> dict: return { "status": "ok", "message": "Village Connect API is running", - "version": "0.1.0" + "version": "0.1.0", } +@app.get("/health") +async def health() -> dict: + return health_payload() + + +@app.get("/events") +async def get_events() -> list[dict]: + return load_fixture(EVENTS_FIXTURE) + + +@app.get("/rec_centers") +async def get_rec_centers() -> list[dict]: + return load_fixture(REC_CENTERS_FIXTURE) + + +@app.get("/villages") +async def get_villages() -> list[dict]: + return load_fixture(VILLAGES_FIXTURE) + + @app.get("/api/v1/events") -async def get_events(): - """Get community events - placeholder endpoint""" - return { - "events": [], - "message": "Events endpoint - to be implemented" - } +async def legacy_events() -> list[dict]: + return load_fixture(EVENTS_FIXTURE) @app.get("/api/v1/health") -async def health_check(): - """API health check""" - return {"status": "healthy"} +async def legacy_health() -> dict: + return health_payload() if __name__ == "__main__": diff --git a/backend/database.py b/backend/database.py new file mode 100644 index 0000000..38ec98c --- /dev/null +++ b/backend/database.py @@ -0,0 +1,14 @@ +"""Database configuration and session management.""" + +import os + +from sqlalchemy import create_engine +from sqlalchemy.orm import declarative_base, sessionmaker + +DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///./village_connect.db") + +connect_args = {"check_same_thread": False} if DATABASE_URL.startswith("sqlite") else {} +engine = create_engine(DATABASE_URL, connect_args=connect_args, future=True) + +SessionLocal = sessionmaker(bind=engine, autocommit=False, autoflush=False, future=True) +Base = declarative_base() diff --git a/backend/fixtures/events.json b/backend/fixtures/events.json new file mode 100644 index 0000000..07472dc --- /dev/null +++ b/backend/fixtures/events.json @@ -0,0 +1,20 @@ +[ + { + "id": 1, + "title": "Sunrise Yoga", + "description": "Start your day with a gentle yoga session.", + "start_time": "2026-01-21T07:30:00", + "end_time": "2026-01-21T08:30:00", + "location": "Lakeview Lawn", + "rec_center_id": 101 + }, + { + "id": 2, + "title": "Community Potluck", + "description": "Bring a dish to share and meet neighbors.", + "start_time": "2026-01-22T18:00:00", + "end_time": "2026-01-22T20:00:00", + "location": "Harbor Hall", + "rec_center_id": 102 + } +] diff --git a/backend/fixtures/rec_centers.json b/backend/fixtures/rec_centers.json new file mode 100644 index 0000000..f8adb24 --- /dev/null +++ b/backend/fixtures/rec_centers.json @@ -0,0 +1,16 @@ +[ + { + "id": 101, + "name": "Lakeview Recreation Center", + "address": "123 Lakeview Drive, The Villages, FL", + "phone": "352-555-0101", + "village_id": 201 + }, + { + "id": 102, + "name": "Harbor Recreation Center", + "address": "456 Harbor Way, The Villages, FL", + "phone": "352-555-0102", + "village_id": 202 + } +] diff --git a/backend/fixtures/villages.json b/backend/fixtures/villages.json new file mode 100644 index 0000000..0754373 --- /dev/null +++ b/backend/fixtures/villages.json @@ -0,0 +1,16 @@ +[ + { + "id": 201, + "name": "Lakeview", + "county": "Sumter", + "state": "FL", + "description": "Lakeside neighborhood with walking paths." + }, + { + "id": 202, + "name": "Harbor", + "county": "Marion", + "state": "FL", + "description": "Harbor-side village with scenic views." + } +] diff --git a/backend/migrations/README b/backend/migrations/README new file mode 100644 index 0000000..5bff13e --- /dev/null +++ b/backend/migrations/README @@ -0,0 +1 @@ +Alembic migration scripts live in this directory. diff --git a/backend/migrations/env.py b/backend/migrations/env.py new file mode 100644 index 0000000..3db9d29 --- /dev/null +++ b/backend/migrations/env.py @@ -0,0 +1,52 @@ +"""Alembic environment configuration.""" + +from logging.config import fileConfig +import os +import sys + +from alembic import context +from sqlalchemy import engine_from_config, pool + +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) + +config = context.config + +if config.config_file_name is not None: + fileConfig(config.config_file_name) + +from backend.database import Base # noqa: E402 + +target_metadata = Base.metadata + + +def run_migrations_offline() -> None: + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online() -> None: + connectable = engine_from_config( + config.get_section(config.config_ini_section, {}), + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + + with connectable.connect() as connection: + context.configure(connection=connection, target_metadata=target_metadata) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/backend/migrations/script.py.mako b/backend/migrations/script.py.mako new file mode 100644 index 0000000..70fbaf8 --- /dev/null +++ b/backend/migrations/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} +""" + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade() -> None: + ${upgrades if upgrades else "pass"} + + +def downgrade() -> None: + ${downgrades if downgrades else "pass"} diff --git a/backend/migrations/versions/.gitkeep b/backend/migrations/versions/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/backend/models/__init__.py b/backend/models/__init__.py index d247751..a8ee38c 100644 --- a/backend/models/__init__.py +++ b/backend/models/__init__.py @@ -1,3 +1,3 @@ -""" -Data models and schemas -""" +"""Data models.""" + +from .base import Base # noqa: F401 diff --git a/backend/models/base.py b/backend/models/base.py new file mode 100644 index 0000000..274aa80 --- /dev/null +++ b/backend/models/base.py @@ -0,0 +1,5 @@ +"""Base SQLAlchemy model.""" + +from backend.database import Base + +__all__ = ["Base"] diff --git a/backend/pytest.ini b/backend/pytest.ini new file mode 100644 index 0000000..dea19c7 --- /dev/null +++ b/backend/pytest.ini @@ -0,0 +1,6 @@ +[pytest] +testpaths = tests +addopts = --cov=backend --cov-report=term-missing +pythonpath = + . + .. diff --git a/backend/requirements.txt b/backend/requirements.txt index 5102bb4..e689ed1 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -19,7 +19,7 @@ python-multipart==0.0.6 # Testing pytest==7.4.4 pytest-asyncio==0.23.3 -httpx==0.26.0 +pytest-cov==4.1.0 # Code Quality black==23.12.1 diff --git a/backend/schemas/__init__.py b/backend/schemas/__init__.py new file mode 100644 index 0000000..ef3614d --- /dev/null +++ b/backend/schemas/__init__.py @@ -0,0 +1,5 @@ +"""Pydantic schemas for API payloads.""" + +from .event import Event # noqa: F401 +from .rec_center import RecCenter # noqa: F401 +from .village import Village # noqa: F401 diff --git a/backend/schemas/event.py b/backend/schemas/event.py new file mode 100644 index 0000000..0f5b2b4 --- /dev/null +++ b/backend/schemas/event.py @@ -0,0 +1,18 @@ +"""Event schema definitions.""" + +from datetime import datetime +from typing import Optional + +from pydantic import BaseModel, ConfigDict + + +class Event(BaseModel): + model_config = ConfigDict(from_attributes=True) + + id: int + title: str + description: Optional[str] = None + start_time: datetime + end_time: Optional[datetime] = None + location: str + rec_center_id: int diff --git a/backend/schemas/rec_center.py b/backend/schemas/rec_center.py new file mode 100644 index 0000000..f94962a --- /dev/null +++ b/backend/schemas/rec_center.py @@ -0,0 +1,15 @@ +"""Recreation center schema definitions.""" + +from typing import Optional + +from pydantic import BaseModel, ConfigDict + + +class RecCenter(BaseModel): + model_config = ConfigDict(from_attributes=True) + + id: int + name: str + address: str + phone: Optional[str] = None + village_id: int diff --git a/backend/schemas/village.py b/backend/schemas/village.py new file mode 100644 index 0000000..cb4159b --- /dev/null +++ b/backend/schemas/village.py @@ -0,0 +1,15 @@ +"""Village schema definitions.""" + +from typing import Optional + +from pydantic import BaseModel, ConfigDict + + +class Village(BaseModel): + model_config = ConfigDict(from_attributes=True) + + id: int + name: str + county: str + state: str + description: Optional[str] = None diff --git a/backend/tests/test_app.py b/backend/tests/test_app.py new file mode 100644 index 0000000..58813f4 --- /dev/null +++ b/backend/tests/test_app.py @@ -0,0 +1,45 @@ +from fastapi.testclient import TestClient + +from backend.app import app + + +client = TestClient(app) + + +def test_health() -> None: + response = client.get("/health") + + assert response.status_code == 200 + assert response.json() == {"status": "ok"} + + +def test_events() -> None: + response = client.get("/events") + + assert response.status_code == 200 + payload = response.json() + assert isinstance(payload, list) + assert payload + assert {"id", "title", "start_time", "location", "rec_center_id"}.issubset( + payload[0].keys() + ) + + +def test_rec_centers() -> None: + response = client.get("/rec_centers") + + assert response.status_code == 200 + payload = response.json() + assert isinstance(payload, list) + assert payload + assert {"id", "name", "address", "village_id"}.issubset(payload[0].keys()) + + +def test_villages() -> None: + response = client.get("/villages") + + assert response.status_code == 200 + payload = response.json() + assert isinstance(payload, list) + assert payload + assert {"id", "name", "county", "state"}.issubset(payload[0].keys()) diff --git a/docs/TASKS.md b/docs/TASKS.md index 79cdc2a..b4a24e9 100644 --- a/docs/TASKS.md +++ b/docs/TASKS.md @@ -9,6 +9,7 @@ This document tracks ongoing development tasks and issues for the Village Connec ### Completed - [x] Repository initialization and monorepo structure setup (#1) +- [x] Added API schemas, fixtures, and mock endpoints (#2, #3) ## Backlog diff --git a/docs/examples/events.json b/docs/examples/events.json new file mode 100644 index 0000000..07472dc --- /dev/null +++ b/docs/examples/events.json @@ -0,0 +1,20 @@ +[ + { + "id": 1, + "title": "Sunrise Yoga", + "description": "Start your day with a gentle yoga session.", + "start_time": "2026-01-21T07:30:00", + "end_time": "2026-01-21T08:30:00", + "location": "Lakeview Lawn", + "rec_center_id": 101 + }, + { + "id": 2, + "title": "Community Potluck", + "description": "Bring a dish to share and meet neighbors.", + "start_time": "2026-01-22T18:00:00", + "end_time": "2026-01-22T20:00:00", + "location": "Harbor Hall", + "rec_center_id": 102 + } +] diff --git a/docs/examples/rec_centers.json b/docs/examples/rec_centers.json new file mode 100644 index 0000000..f8adb24 --- /dev/null +++ b/docs/examples/rec_centers.json @@ -0,0 +1,16 @@ +[ + { + "id": 101, + "name": "Lakeview Recreation Center", + "address": "123 Lakeview Drive, The Villages, FL", + "phone": "352-555-0101", + "village_id": 201 + }, + { + "id": 102, + "name": "Harbor Recreation Center", + "address": "456 Harbor Way, The Villages, FL", + "phone": "352-555-0102", + "village_id": 202 + } +] diff --git a/docs/examples/villages.json b/docs/examples/villages.json new file mode 100644 index 0000000..0754373 --- /dev/null +++ b/docs/examples/villages.json @@ -0,0 +1,16 @@ +[ + { + "id": 201, + "name": "Lakeview", + "county": "Sumter", + "state": "FL", + "description": "Lakeside neighborhood with walking paths." + }, + { + "id": 202, + "name": "Harbor", + "county": "Marion", + "state": "FL", + "description": "Harbor-side village with scenic views." + } +] diff --git a/docs/schemas/event.json b/docs/schemas/event.json new file mode 100644 index 0000000..cf3f156 --- /dev/null +++ b/docs/schemas/event.json @@ -0,0 +1,47 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Event", + "type": "object", + "required": [ + "id", + "title", + "start_time", + "location", + "rec_center_id" + ], + "properties": { + "id": { + "type": "integer" + }, + "title": { + "type": "string" + }, + "description": { + "type": [ + "string", + "null" + ] + }, + "start_time": { + "type": "string", + "format": "date-time" + }, + "end_time": { + "oneOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ] + }, + "location": { + "type": "string" + }, + "rec_center_id": { + "type": "integer" + } + } +} diff --git a/docs/schemas/rec_center.json b/docs/schemas/rec_center.json new file mode 100644 index 0000000..ed9267d --- /dev/null +++ b/docs/schemas/rec_center.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "RecCenter", + "type": "object", + "required": [ + "id", + "name", + "address", + "village_id" + ], + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "address": { + "type": "string" + }, + "phone": { + "type": [ + "string", + "null" + ] + }, + "village_id": { + "type": "integer" + } + } +} diff --git a/docs/schemas/village.json b/docs/schemas/village.json new file mode 100644 index 0000000..2efcb31 --- /dev/null +++ b/docs/schemas/village.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Village", + "type": "object", + "required": [ + "id", + "name", + "county", + "state" + ], + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "county": { + "type": "string" + }, + "state": { + "type": "string" + }, + "description": { + "type": [ + "string", + "null" + ] + } + } +} diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 2dffaaa..0fdfc88 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -19,7 +19,7 @@ dependencies: firebase_messaging: ^16.0.4 flutter_local_notifications: ^19.5.0 timezone: ^0.10.1 - flutter_timezone: ^5.0.1 + flutter_timezone: ^4.1.1 flutter_tts: ^4.0.2 accessibility_tools: ^2.0.0 hive: ^2.2.3 @@ -53,4 +53,4 @@ flutter: - asset: fonts/Roboto-Bold.ttf weight: 700 - asset: fonts/Roboto-Medium.ttf - weight: 500 \ No newline at end of file + weight: 500