diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..49639e2 --- /dev/null +++ b/.env.example @@ -0,0 +1,16 @@ +# BridgeStack Environment Configuration +# Copy this file to .env and modify as needed. +# All variables are prefixed with BRIDGE_ to avoid conflicts. + +# Database connection string (SQLite default; can be changed to PostgreSQL) +BRIDGE_DATABASE_URL=sqlite:///./rootstack.db + +# Enable debug mode (true/false) +BRIDGE_DEBUG=false + +# Allowed CORS origins (JSON array) +# Use ["*"] for development, restrict in production +BRIDGE_CORS_ORIGINS=["http://localhost:3000","http://localhost:5173"] + +# Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL) +BRIDGE_LOG_LEVEL=INFO diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7863add..8dd4927 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,15 +7,48 @@ on: branches: [main] jobs: - validate: + lint: + name: Lint & Format runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - python-version: '3.12' - - name: Check Python syntax - run: python -m py_compile $(find . -name "*.py" -not -path "./.git/*") || true + python-version: "3.12" + cache: pip + - run: pip install ruff>=0.11.0 + - name: Check formatting + run: ruff format --check . + - name: Check linting + run: ruff check . + + test: + name: Test (Python ${{ matrix.python-version }}) + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.11", "3.12", "3.13"] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: pip + - run: pip install -r requirements.txt + - name: Run tests with coverage + run: pytest --cov=app --cov-report=term-missing --cov-report=xml + - name: Upload coverage + if: matrix.python-version == '3.12' + uses: codecov/codecov-action@v5 + with: + files: coverage.xml + fail_ci_if_error: false + + links: + name: Check Links + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 - name: Check links uses: lycheeverse/lychee-action@v2 with: diff --git a/CITATION.cff b/CITATION.cff index 892675b..0077404 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -6,8 +6,8 @@ abstract: "FastAPI backend connecting RootStack database to the OpenStacks front authors: - family-names: "Sri Raman" given-names: "Varna" -version: 0.2.0 -date-released: "2025-04-28" +version: 0.3.0 +date-released: "2026-04-04" license: MIT url: "https://openstacks.dev" repository-code: "https://github.com/Varnasr/BridgeStack" diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..82b6596 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,34 @@ +# Code of Conduct + +## Our Pledge + +We are committed to making participation in BridgeStack and the OpenStacks ecosystem a welcoming, inclusive, and harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +**Positive behaviours include:** + +- Using welcoming and inclusive language +- Respecting differing viewpoints and experiences +- Accepting constructive criticism gracefully +- Focusing on what is best for the community +- Showing empathy towards other community members + +**Unacceptable behaviours include:** + +- Trolling, insulting/derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information without explicit permission +- Other conduct which could reasonably be considered inappropriate in a professional setting + +## Scope + +This Code of Conduct applies within all project spaces — issues, pull requests, discussions, and any other channel associated with BridgeStack or OpenStacks. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behaviour may be reported by contacting the project team at **hello@impactmojo.in**. All complaints will be reviewed and investigated promptly and fairly. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 2.1. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..302e6ec --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,125 @@ +# Contributing to BridgeStack + +Thank you for your interest in contributing to BridgeStack! This project is part of the [OpenStacks](https://openstacks.dev) ecosystem, and we welcome contributions of all kinds. + +## Getting Started + +### Prerequisites + +- Python 3.11+ +- Git + +### Setup + +```bash +git clone https://github.com/Varnasr/BridgeStack.git +cd BridgeStack +python -m venv .venv +source .venv/bin/activate # Windows: .venv\Scripts\activate +pip install -r requirements.txt +``` + +### Running Locally + +```bash +uvicorn app.main:app --reload +``` + +API docs will be at [http://localhost:8000/docs](http://localhost:8000/docs). + +### Running Tests + +```bash +pytest # run all tests +pytest --cov=app # with coverage +pytest -k "test_geography" # run specific test class +``` + +### Linting & Formatting + +```bash +ruff check . # check for lint issues +ruff check --fix . # auto-fix lint issues +ruff format . # format code +``` + +## How to Contribute + +### Reporting Bugs + +Open an issue using the [bug report template](https://github.com/Varnasr/BridgeStack/issues/new?template=bug_report.md). Include: + +- Steps to reproduce +- Expected vs actual behaviour +- Python version and OS + +### Suggesting Features + +Open an issue using the [feature request template](https://github.com/Varnasr/BridgeStack/issues/new?template=feature_request.md). + +### Submitting Code + +1. Fork the repository +2. Create a feature branch: `git checkout -b feat/your-feature` +3. Make your changes +4. Run tests and linting: + ```bash + pytest --cov=app + ruff check . + ruff format --check . + ``` +5. Commit using the [commit conventions](#commit-conventions) +6. Push and open a pull request + +### Commit Conventions + +All commit messages must start with one of these prefixes: + +| Prefix | Use for | +|--------|---------| +| `Add:` | New features or files | +| `Fix:` | Bug fixes | +| `Update:` | Enhancements to existing features | +| `Docs:` | Documentation changes | +| `Refactor:` | Code restructuring (no behaviour change) | +| `Test:` | Adding or updating tests | +| `CI:` | CI/CD pipeline changes | +| `Chore:` | Maintenance tasks | + +Example: `Add: pagination support for indicator values endpoint` + +### Pull Request Guidelines + +- Keep PRs focused — one feature or fix per PR +- Include tests for new functionality +- Update documentation if you change API behaviour +- Fill in the PR template completely + +## Project Structure + +``` +app/ +├── core/ # Config, database setup, shared utilities +├── models/ # SQLAlchemy ORM models +├── schemas/ # Pydantic request/response schemas +└── routes/ # FastAPI route handlers +tests/ # Pytest test suite +docs/ # Project documentation +``` + +## Areas Where Help Is Welcome + +- Additional query endpoints and aggregations +- Pagination and rate limiting +- Response caching +- PostgreSQL adapter +- Test coverage improvements +- Documentation and examples + +## Code of Conduct + +Please read our [Code of Conduct](CODE_OF_CONDUCT.md) before contributing. + +## Questions? + +Open a [discussion](https://github.com/Varnasr/BridgeStack/issues) or reach out at hello@impactmojo.in. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..43c202c --- /dev/null +++ b/Makefile @@ -0,0 +1,41 @@ +.PHONY: install dev test lint format run docker-up docker-down clean + +install: ## Install production dependencies + pip install -r requirements.txt + +dev: ## Install all dependencies (including dev tools) + pip install -e ".[dev]" + +test: ## Run tests with coverage + pytest --cov=app --cov-report=term-missing + +lint: ## Run linter + ruff check . + +format: ## Format code + ruff format . + ruff check --fix . + +check: ## Run all checks (lint + test) + ruff format --check . + ruff check . + pytest --cov=app + +run: ## Start the development server + uvicorn app.main:app --reload --host 0.0.0.0 --port 8000 + +docker-up: ## Start with Docker Compose + docker compose up --build -d + +docker-down: ## Stop Docker Compose + docker compose down + +clean: ## Remove build artifacts and caches + find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true + find . -type d -name .pytest_cache -exec rm -rf {} + 2>/dev/null || true + find . -type d -name .ruff_cache -exec rm -rf {} + 2>/dev/null || true + rm -f .coverage coverage.xml + rm -f test.db + +help: ## Show this help + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-15s\033[0m %s\n", $$1, $$2}' diff --git a/README.md b/README.md index 842a6a8..71b8dc1 100644 --- a/README.md +++ b/README.md @@ -2,34 +2,41 @@ **API backend bridging OpenStacks data layers.** +[![CI](https://github.com/Varnasr/BridgeStack/actions/workflows/ci.yml/badge.svg)](https://github.com/Varnasr/BridgeStack/actions/workflows/ci.yml) [![Part of OpenStacks](https://img.shields.io/badge/Part%20of-OpenStacks-blue)](https://openstacks.dev) [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE) [![Python 3.11+](https://img.shields.io/badge/Python-3.11%2B-blue)]() -[![FastAPI](https://img.shields.io/badge/FastAPI-0.104%2B-009688)]() +[![FastAPI](https://img.shields.io/badge/FastAPI-0.115%2B-009688)]() +[![Code style: Ruff](https://img.shields.io/badge/code%20style-ruff-000000.svg)](https://docs.astral.sh/ruff/) -> The API layer for OpenStacks — connecting database to frontend. +> The API layer for [OpenStacks](https://openstacks.dev) — connecting database to frontend. --- ## Quick Start ```bash -# Clone and install git clone https://github.com/Varnasr/BridgeStack.git cd BridgeStack pip install -r requirements.txt - -# Run the API uvicorn app.main:app --reload - -# Open docs -# http://localhost:8000/docs ``` +Open [http://localhost:8000/docs](http://localhost:8000/docs) for interactive API docs. + ### With Docker ```bash -docker compose up +docker compose up --build +``` + +### With Make + +```bash +make dev # install with dev tools +make run # start the server +make test # run tests with coverage +make check # lint + test ``` ## Architecture @@ -52,54 +59,53 @@ BridgeStack serves as the middleware connecting [RootStack](https://github.com/V ## API Endpoints -All endpoints are prefixed with `/api/v1`. +All endpoints are prefixed with `/api/v1`. Full reference: [docs/api-reference.md](docs/api-reference.md) -### Geography +| Domain | Endpoints | Filters | +|--------|-----------|---------| +| **Geography** | `states`, `districts` | `region`, `state_id`, `tier` | +| **Sectors** | `sectors` | — | +| **Indicators** | `indicators`, `values` | `sector_id`, `source`, `state_id`, `year` | +| **Policies** | `schemes`, `budgets`, `coverage` | `sector_id`, `status`, `level`, `fiscal_year` | +| **Tools** | `tools` | `stack`, `language`, `tool_type`, `difficulty` | +| **Health** | `/`, `/health` | — | -| Method | Endpoint | Description | -|--------|----------|-------------| -| GET | `/geography/states` | List all states (filter: `?region=`) | -| GET | `/geography/states/{id}` | State detail with districts | -| GET | `/geography/districts` | List districts (filter: `?state_id=`, `?tier=`) | -| GET | `/geography/districts/{id}` | District detail | +## Documentation -### Sectors +Full documentation is in the [`docs/`](docs/) directory: -| Method | Endpoint | Description | -|--------|----------|-------------| -| GET | `/sectors/` | List all development sectors | -| GET | `/sectors/{id}` | Sector detail | +- [Getting Started](docs/getting-started.md) — Installation and first request +- [API Reference](docs/api-reference.md) — Complete endpoint docs +- [Architecture](docs/architecture.md) — System design and data model +- [Configuration](docs/configuration.md) — Environment variables +- [Deployment](docs/deployment.md) — Docker, production, monitoring -### Indicators +## Configuration -| Method | Endpoint | Description | -|--------|----------|-------------| -| GET | `/indicators/` | List indicators (filter: `?sector_id=`, `?source=`) | -| GET | `/indicators/{id}` | Indicator detail with values | -| GET | `/indicators/values/` | Query data points (filter: `?indicator_id=`, `?state_id=`, `?year=`) | +Environment variables (prefix `BRIDGE_`): -### Policies +| Variable | Default | Description | +|----------|---------|-------------| +| `BRIDGE_DATABASE_URL` | `sqlite:///./rootstack.db` | Database connection string | +| `BRIDGE_DEBUG` | `false` | Enable debug mode | +| `BRIDGE_CORS_ORIGINS` | `["*"]` | Allowed CORS origins | +| `BRIDGE_LOG_LEVEL` | `INFO` | Logging level | -| Method | Endpoint | Description | -|--------|----------|-------------| -| GET | `/policies/schemes` | List schemes (filter: `?sector_id=`, `?status=`, `?level=`) | -| GET | `/policies/schemes/{id}` | Scheme detail with budgets & coverage | -| GET | `/policies/budgets` | Budget data (filter: `?scheme_id=`, `?fiscal_year=`) | -| GET | `/policies/coverage` | Coverage data (filter: `?scheme_id=`, `?state_id=`) | +See [.env.example](.env.example) for a starter configuration. -### Tools +## Development -| Method | Endpoint | Description | -|--------|----------|-------------| -| GET | `/tools/` | List tools (filter: `?stack=`, `?language=`, `?tool_type=`, `?difficulty=`) | -| GET | `/tools/{id}` | Tool detail | +```bash +# Install dev dependencies +pip install -e ".[dev]" -### Health +# Run tests +pytest --cov=app -| Method | Endpoint | Description | -|--------|----------|-------------| -| GET | `/` | API info and ecosystem map | -| GET | `/health` | Health check | +# Lint & format +ruff check . +ruff format . +``` ## Project Structure @@ -109,73 +115,30 @@ BridgeStack/ │ ├── main.py # FastAPI app entry point │ ├── core/ │ │ ├── config.py # Settings (env-configurable) -│ │ └── database.py # SQLite/SQLAlchemy setup +│ │ └── database.py # SQLAlchemy setup │ ├── models/ # SQLAlchemy ORM models -│ │ ├── geography.py # States, Districts -│ │ ├── sectors.py # Development sectors -│ │ ├── indicators.py # Indicators & values -│ │ ├── policies.py # Schemes, budgets, coverage -│ │ └── tools.py # OpenStacks tool catalog │ ├── schemas/ # Pydantic response schemas -│ │ ├── geography.py -│ │ ├── sectors.py -│ │ ├── indicators.py -│ │ ├── policies.py -│ │ └── tools.py │ └── routes/ # API route handlers -│ ├── geography.py -│ ├── sectors.py -│ ├── indicators.py -│ ├── policies.py -│ └── tools.py -├── tests/ -│ └── test_api.py # 14 endpoint tests -├── requirements.txt -├── Dockerfile -└── docker-compose.yml -``` - -## Configuration - -Environment variables (prefix `BRIDGE_`): - -| Variable | Default | Description | -|----------|---------|-------------| -| `BRIDGE_DATABASE_URL` | `sqlite:///./rootstack.db` | Database connection string | -| `BRIDGE_DEBUG` | `false` | Enable debug mode | -| `BRIDGE_CORS_ORIGINS` | `["*"]` | Allowed CORS origins | - -## Using with RootStack - -To populate the database, clone and run [RootStack](https://github.com/Varnasr/RootStack) setup, then point BridgeStack at the generated SQLite file: - -```bash -# In RootStack directory -bash scripts/setup.sh - -# Copy the database to BridgeStack -cp rootstack.db ../BridgeStack/ - -# Start the API -cd ../BridgeStack -uvicorn app.main:app --reload -``` - -## Running Tests - -```bash -pytest tests/ -v +├── tests/ # Pytest test suite +├── docs/ # Project documentation +├── pyproject.toml # Project metadata & tool config +├── requirements.txt # Python dependencies +├── Makefile # Development commands +├── Dockerfile # Container image +└── docker-compose.yml # Local orchestration ``` -## How to Contribute +## Contributing See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines. Areas where contributions are welcome: - Additional query endpoints and aggregations -- Authentication for write operations - Pagination and rate limiting -- WebSocket support for real-time data +- Response caching - PostgreSQL adapter +- WebSocket support for real-time data + +Please read our [Code of Conduct](CODE_OF_CONDUCT.md) before participating. ## License @@ -184,3 +147,5 @@ MIT — free to use, modify, and share. See [LICENSE](LICENSE). --- **Created by [Varna Sri Raman](https://github.com/Varnasr)** — Development Economist & Social Researcher + +Part of the [OpenStacks](https://openstacks.dev) ecosystem by [ImpactMojo](https://impactmojo.in). diff --git a/ROADMAP.md b/ROADMAP.md index 640bf08..19da1c0 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,14 +1,42 @@ -# 🛣️ Roadmap +# Roadmap -This document outlines the planned future development for this stack. +This document outlines the planned future development for BridgeStack. -## Planned Enhancements -- Expand and optimize schemas/queries/APIs/UI components. -- Integrate advanced features like authentication, search, dynamic dashboards. -- Improve documentation with visual diagrams and interactive tutorials. -- Expand multilingual and multi-database support where relevant. -- Integrate broader OpenStacks ecosystem capabilities. +## v0.4 — Query Enhancements + +- [ ] Pagination (`limit` / `offset` query params) for all list endpoints +- [ ] Rate limiting with configurable thresholds +- [ ] Response caching with `Cache-Control` / `ETag` headers +- [ ] Sorting support (`?sort=name`, `?order=desc`) + +## v0.5 — Data & Search + +- [ ] Full-text search across indicators and schemes +- [ ] Aggregation endpoints (state-level summaries, sector rollups) +- [ ] Bulk data export (CSV/JSON download) +- [ ] Cross-domain queries (indicators + schemes by state) + +## v0.6 — Production Hardening + +- [ ] PostgreSQL adapter (configurable via `DATABASE_URL`) +- [ ] Authentication for write operations (API key / OAuth2) +- [ ] Structured JSON logging for production monitoring +- [ ] OpenTelemetry tracing integration + +## v0.7 — Real-Time & Integration + +- [ ] WebSocket support for real-time data subscriptions +- [ ] Webhook notifications for data updates +- [ ] GraphQL endpoint alongside REST +- [ ] Multilingual response support + +## Ongoing + +- Expand and optimise schemas, queries, and API surface +- Improve documentation with visual diagrams and interactive tutorials +- Integrate broader OpenStacks ecosystem capabilities +- Community contributions and feedback incorporation --- -*This roadmap evolves as the project grows.* +*This roadmap evolves as the project grows. Suggestions welcome via [GitHub Issues](https://github.com/Varnasr/BridgeStack/issues).* diff --git a/app/core/config.py b/app/core/config.py index 1bf739d..b75f07a 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -1,15 +1,25 @@ +import logging + from pydantic_settings import BaseSettings class Settings(BaseSettings): app_name: str = "BridgeStack API" - app_version: str = "0.2.0" + app_version: str = "0.3.0" app_description: str = "API backend bridging OpenStacks data layers" database_url: str = "sqlite:///./rootstack.db" debug: bool = False cors_origins: list[str] = ["*"] + log_level: str = "INFO" model_config = {"env_prefix": "BRIDGE_"} settings = Settings() + +logging.basicConfig( + level=getattr(logging, settings.log_level.upper(), logging.INFO), + format="%(asctime)s | %(levelname)-8s | %(name)s | %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", +) +logger = logging.getLogger("bridgestack") diff --git a/app/main.py b/app/main.py index 4a4c230..a7b2044 100644 --- a/app/main.py +++ b/app/main.py @@ -1,8 +1,9 @@ from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware +from sqlalchemy import text -from app.core.config import settings -from app.core.database import Base, engine +from app.core.config import logger, settings +from app.core.database import Base, engine, get_db from app.routes import geography, indicators, policies, sectors, tools Base.metadata.create_all(bind=engine) @@ -29,6 +30,8 @@ app.include_router(policies.router, prefix="/api/v1") app.include_router(tools.router, prefix="/api/v1") +logger.info("BridgeStack %s started", settings.app_version) + @app.get("/", tags=["Health"]) def root(): @@ -50,4 +53,19 @@ def root(): @app.get("/health", tags=["Health"]) def health_check(): - return {"status": "healthy"} + db = next(get_db()) + try: + db.execute(text("SELECT 1")) + db_status = "connected" + except Exception: + db_status = "unavailable" + logger.warning("Health check: database unreachable") + finally: + db.close() + + status = "healthy" if db_status == "connected" else "degraded" + return { + "status": status, + "version": settings.app_version, + "database": db_status, + } diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..6497403 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,22 @@ +# BridgeStack Documentation + +Welcome to the BridgeStack documentation. BridgeStack is the REST API layer for [OpenStacks](https://openstacks.dev) — connecting RootStack's database to all consumer applications. + +## Table of Contents + +- [Getting Started](getting-started.md) — Installation, setup, and first request +- [API Reference](api-reference.md) — Complete endpoint documentation +- [Architecture](architecture.md) — System design and data flow +- [Configuration](configuration.md) — Environment variables and settings +- [Deployment](deployment.md) — Docker, production setup, and monitoring +- [Contributing](../CONTRIBUTING.md) — How to contribute + +## Quick Links + +| Resource | URL | +|----------|-----| +| Live API Docs | `/docs` (Swagger UI) | +| ReDoc | `/redoc` | +| Health Check | `/health` | +| GitHub | [Varnasr/BridgeStack](https://github.com/Varnasr/BridgeStack) | +| OpenStacks | [openstacks.dev](https://openstacks.dev) | diff --git a/docs/api-reference.md b/docs/api-reference.md new file mode 100644 index 0000000..8145417 --- /dev/null +++ b/docs/api-reference.md @@ -0,0 +1,199 @@ +# API Reference + +All endpoints are read-only (GET) and prefixed with `/api/v1`. The API returns JSON and follows RESTful conventions. + +Interactive documentation is available at `/docs` (Swagger UI) and `/redoc` (ReDoc) when the server is running. + +--- + +## Health + +### `GET /` + +Returns API metadata and the OpenStacks ecosystem map. + +**Response:** +```json +{ + "name": "BridgeStack API", + "version": "0.3.0", + "docs": "/docs", + "stacks": { + "data": "RootStack", + "api": "BridgeStack", + "frontend": "ViewStack", + "analysis": "EquityStack", + "fieldwork": "FieldStack", + "mel": "InsightStack", + "content": "SignalStack" + } +} +``` + +### `GET /health` + +Returns health status including database connectivity. + +**Response:** +```json +{ + "status": "healthy", + "version": "0.3.0", + "database": "connected" +} +``` + +Possible `status` values: `healthy`, `degraded`. + +--- + +## Geography + +### `GET /api/v1/geography/states` + +List all states. + +| Parameter | Type | Description | +|-----------|------|-------------| +| `region` | string | Filter by region (e.g. `South`, `West`, `North`, `East`, `Northeast`) | + +**Example:** `GET /api/v1/geography/states?region=South` + +### `GET /api/v1/geography/states/{state_id}` + +Get a state with its districts. + +**Response includes:** `state_id`, `state_name`, `region`, `state_type`, `capital`, `area_sq_km`, `census_2011_pop`, `districts[]` + +### `GET /api/v1/geography/districts` + +List all districts. + +| Parameter | Type | Description | +|-----------|------|-------------| +| `state_id` | string | Filter by parent state | +| `tier` | string | Filter by tier (e.g. `Tier-1`, `Tier-2`, `Tier-3`) | + +### `GET /api/v1/geography/districts/{district_id}` + +Get a single district by ID. + +--- + +## Sectors + +### `GET /api/v1/sectors/` + +List all development sectors. Sectors are hierarchical — child sectors have a `parent_id`. + +### `GET /api/v1/sectors/{sector_id}` + +Get a single sector by ID. + +--- + +## Indicators + +### `GET /api/v1/indicators/` + +List indicators. + +| Parameter | Type | Description | +|-----------|------|-------------| +| `sector_id` | string | Filter by sector | +| `source` | string | Filter by data source (partial match) | + +### `GET /api/v1/indicators/{indicator_id}` + +Get an indicator with all its data values. + +**Response includes:** indicator metadata + `values[]` array with `state_id`, `year`, `value` entries. + +### `GET /api/v1/indicators/values/` + +Query indicator data points directly. + +| Parameter | Type | Description | +|-----------|------|-------------| +| `indicator_id` | string | Filter by indicator | +| `state_id` | string | Filter by state | +| `year` | integer | Filter by year | + +**Example:** `GET /api/v1/indicators/values/?indicator_id=IMR&state_id=KA&year=2020` + +--- + +## Policies + +### `GET /api/v1/policies/schemes` + +List government schemes. + +| Parameter | Type | Description | +|-----------|------|-------------| +| `sector_id` | string | Filter by sector | +| `status` | string | Filter by status (e.g. `Active`, `Closed`) | +| `level` | string | Filter by funding level (e.g. `Central`, `State`) | + +### `GET /api/v1/policies/schemes/{scheme_id}` + +Get a scheme with its budgets and coverage data. + +**Response includes:** scheme metadata + `budgets[]` + `coverage[]` + +### `GET /api/v1/policies/budgets` + +Query scheme budget allocations. + +| Parameter | Type | Description | +|-----------|------|-------------| +| `scheme_id` | string | Filter by scheme | +| `fiscal_year` | string | Filter by fiscal year (e.g. `2022-23`) | + +### `GET /api/v1/policies/coverage` + +Query scheme coverage (beneficiaries reached). + +| Parameter | Type | Description | +|-----------|------|-------------| +| `scheme_id` | string | Filter by scheme | +| `state_id` | string | Filter by state | + +--- + +## Tools + +### `GET /api/v1/tools/` + +List tools from the OpenStacks tool catalog. + +| Parameter | Type | Description | +|-----------|------|-------------| +| `stack` | string | Filter by stack (e.g. `ViewStack`, `EquityStack`) | +| `language` | string | Filter by language (e.g. `Python`, `JavaScript`, `R`) | +| `tool_type` | string | Filter by type (e.g. `dashboard`, `notebook`, `script`) | +| `difficulty` | string | Filter by difficulty (`beginner`, `intermediate`, `advanced`) | + +### `GET /api/v1/tools/{tool_id}` + +Get a single tool by numeric ID. + +--- + +## Error Responses + +All endpoints return standard HTTP error codes: + +| Code | Meaning | +|------|---------| +| `200` | Success | +| `404` | Resource not found | +| `422` | Validation error (invalid query parameters) | +| `500` | Internal server error | + +Error response format: +```json +{ + "detail": "State not found" +} +``` diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..92d045e --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,105 @@ +# Architecture + +## System Overview + +BridgeStack is the API middleware in the OpenStacks ecosystem. It reads from a RootStack SQLite database and exposes RESTful endpoints consumed by multiple frontend and analysis Stacks. + +``` +┌─────────────┐ ┌──────────────┐ ┌─────────────────────────────┐ +│ RootStack │────▶│ BridgeStack │────▶│ Consumer Stacks │ +│ (SQLite DB) │ │ (FastAPI) │ │ │ +│ │ │ │ │ ViewStack (dashboards) │ +│ - States │ │ /api/v1/ │ │ EquityStack (analysis) │ +│ - Districts │ │ │ │ FieldStack (fieldwork) │ +│ - Sectors │ │ GET-only │ │ InsightStack (MEL) │ +│ - Indicators│ │ JSON API │ │ SignalStack (content) │ +│ - Schemes │ │ │ │ │ +│ - Tools │ │ │ │ │ +└─────────────┘ └──────────────┘ └─────────────────────────────┘ +``` + +## Technology Stack + +| Layer | Technology | Purpose | +|-------|-----------|---------| +| Framework | FastAPI 0.115+ | Async-capable REST framework with auto-generated OpenAPI docs | +| ORM | SQLAlchemy 2.0+ | Database abstraction with relationship mapping | +| Validation | Pydantic 2.11+ | Request/response schema validation | +| Database | SQLite | Lightweight, file-based storage (PostgreSQL planned) | +| Server | Uvicorn 0.34+ | ASGI server for production and development | + +## Application Structure + +``` +app/ +├── main.py # FastAPI app, middleware, health endpoints +├── core/ +│ ├── config.py # Pydantic settings (env-configurable) +│ └── database.py # Engine, session factory, dependency injection +├── models/ # SQLAlchemy ORM models (5 domains) +│ ├── geography.py # State, District +│ ├── sectors.py # Sector (hierarchical) +│ ├── indicators.py # Indicator, IndicatorValue +│ ├── policies.py # Scheme, SchemeBudget, SchemeCoverage +│ └── tools.py # Tool +├── schemas/ # Pydantic response models +│ ├── geography.py # StateBase, StateDetail, DistrictBase +│ ├── sectors.py # SectorBase, SectorTree +│ ├── indicators.py # IndicatorBase, IndicatorDetail, IndicatorValueBase +│ ├── policies.py # SchemeBase, SchemeDetail, SchemeBudgetBase, SchemeCoverageBase +│ └── tools.py # ToolBase +└── routes/ # API endpoint handlers + ├── geography.py # 4 endpoints + ├── sectors.py # 2 endpoints + ├── indicators.py # 3 endpoints + ├── policies.py # 4 endpoints + └── tools.py # 2 endpoints +``` + +## Data Model + +### Entity Relationships + +``` +sectors ◄──────────── indicators ──────────► indicator_values + ▲ │ + │ │ + └── schemes ──┬── scheme_budgets states ◄──── districts + └── scheme_coverage ────────►│ +``` + +### Key Design Decisions + +1. **Read-only API**: BridgeStack exposes only GET endpoints. Data is managed through RootStack. + +2. **Eager loading**: Detail endpoints use `joinedload()` to fetch related records in a single query, avoiding N+1 problems. + +3. **SQLite foreign keys**: Explicitly enabled via SQLAlchemy event listener (`PRAGMA foreign_keys=ON`) since SQLite disables them by default. + +4. **Dependency injection**: Database sessions are managed through FastAPI's `Depends(get_db)` pattern, ensuring proper cleanup. + +5. **Schema separation**: SQLAlchemy models (database) and Pydantic schemas (API) are kept separate, allowing the API contract to evolve independently of the database schema. + +## Request Flow + +``` +HTTP Request + │ + ▼ +FastAPI Router (routes/*.py) + │ + ├── Query parameter validation (Pydantic) + ├── Database session injection (Depends) + │ + ▼ +SQLAlchemy Query (models/*.py) + │ + ├── Filters applied from query params + ├── Eager loading for detail endpoints + │ + ▼ +Pydantic Serialization (schemas/*.py) + │ + ▼ +JSON Response +``` diff --git a/docs/configuration.md b/docs/configuration.md new file mode 100644 index 0000000..a97fc58 --- /dev/null +++ b/docs/configuration.md @@ -0,0 +1,72 @@ +# Configuration + +BridgeStack is configured through environment variables. All variables use the `BRIDGE_` prefix to avoid conflicts with other applications. + +## Environment Variables + +| Variable | Default | Description | +|----------|---------|-------------| +| `BRIDGE_DATABASE_URL` | `sqlite:///./rootstack.db` | Database connection string | +| `BRIDGE_DEBUG` | `false` | Enable debug mode (verbose errors) | +| `BRIDGE_CORS_ORIGINS` | `["*"]` | JSON array of allowed CORS origins | +| `BRIDGE_LOG_LEVEL` | `INFO` | Logging level (`DEBUG`, `INFO`, `WARNING`, `ERROR`) | + +## Setup + +Copy the example environment file and modify as needed: + +```bash +cp .env.example .env +``` + +## Configuration Examples + +### Development + +```env +BRIDGE_DATABASE_URL=sqlite:///./rootstack.db +BRIDGE_DEBUG=true +BRIDGE_CORS_ORIGINS=["*"] +BRIDGE_LOG_LEVEL=DEBUG +``` + +### Production + +```env +BRIDGE_DATABASE_URL=sqlite:///./data/rootstack.db +BRIDGE_DEBUG=false +BRIDGE_CORS_ORIGINS=["https://yourdomain.com","https://app.yourdomain.com"] +BRIDGE_LOG_LEVEL=WARNING +``` + +### Docker + +Environment variables are set in `docker-compose.yml`: + +```yaml +services: + api: + build: . + ports: + - "8000:8000" + volumes: + - ./rootstack.db:/app/rootstack.db + environment: + - BRIDGE_DATABASE_URL=sqlite:///./rootstack.db + - BRIDGE_DEBUG=false + - BRIDGE_LOG_LEVEL=INFO +``` + +## CORS Configuration + +By default, CORS allows all origins (`["*"]`). For production, restrict this to known consumer applications: + +```env +BRIDGE_CORS_ORIGINS=["https://viewstack.openstacks.dev","https://equitystack.openstacks.dev"] +``` + +## Database + +BridgeStack currently supports **SQLite** as its database backend. The database file is created by [RootStack](https://github.com/Varnasr/RootStack) and contains all seed data. + +PostgreSQL support is planned — see the [Roadmap](../ROADMAP.md). diff --git a/docs/deployment.md b/docs/deployment.md new file mode 100644 index 0000000..5bf1664 --- /dev/null +++ b/docs/deployment.md @@ -0,0 +1,89 @@ +# Deployment + +## Docker (Recommended) + +### Quick Start + +```bash +docker compose up --build -d +``` + +This builds the image and starts the API on port 8000. + +### Docker Compose + +```yaml +services: + api: + build: . + ports: + - "8000:8000" + volumes: + - ./rootstack.db:/app/rootstack.db + environment: + - BRIDGE_DATABASE_URL=sqlite:///./rootstack.db + - BRIDGE_DEBUG=false + - BRIDGE_LOG_LEVEL=INFO + restart: unless-stopped + healthcheck: + test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"] + interval: 30s + timeout: 5s + retries: 3 +``` + +### Building Manually + +```bash +docker build -t bridgestack:latest . +docker run -p 8000:8000 -v ./rootstack.db:/app/rootstack.db bridgestack:latest +``` + +## Direct Deployment + +### With Uvicorn + +```bash +# Development +uvicorn app.main:app --reload --host 0.0.0.0 --port 8000 + +# Production (multiple workers) +uvicorn app.main:app --host 0.0.0.0 --port 8000 --workers 4 +``` + +### With Gunicorn + Uvicorn Workers + +```bash +pip install gunicorn +gunicorn app.main:app -w 4 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:8000 +``` + +## Health Monitoring + +The `/health` endpoint checks database connectivity: + +```bash +curl http://localhost:8000/health +``` + +**Healthy response:** +```json +{"status": "healthy", "version": "0.3.0", "database": "connected"} +``` + +**Degraded response** (database unreachable): +```json +{"status": "degraded", "version": "0.3.0", "database": "unavailable"} +``` + +Use this endpoint for container orchestration health checks (Docker, Kubernetes). + +## Production Checklist + +- [ ] Set `BRIDGE_DEBUG=false` +- [ ] Restrict `BRIDGE_CORS_ORIGINS` to known domains +- [ ] Set `BRIDGE_LOG_LEVEL=WARNING` or `INFO` +- [ ] Mount the RootStack database file as a volume +- [ ] Run behind a reverse proxy (nginx, Caddy) for HTTPS +- [ ] Set up log aggregation for structured logs +- [ ] Configure container health checks diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 0000000..1178f5f --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,101 @@ +# Getting Started + +## Prerequisites + +- **Python 3.11+** (3.12 recommended) +- **Git** +- **RootStack database** (optional — the API works without data, but endpoints will return empty results) + +## Installation + +### From Source + +```bash +# Clone the repository +git clone https://github.com/Varnasr/BridgeStack.git +cd BridgeStack + +# Create a virtual environment +python -m venv .venv +source .venv/bin/activate # Windows: .venv\Scripts\activate + +# Install dependencies +pip install -r requirements.txt +``` + +### With Docker + +```bash +docker compose up --build +``` + +The API will be available at `http://localhost:8000`. + +## Running the API + +```bash +uvicorn app.main:app --reload +``` + +Open [http://localhost:8000/docs](http://localhost:8000/docs) for the interactive Swagger UI. + +## Your First Request + +```bash +# Check API health +curl http://localhost:8000/health + +# List all states +curl http://localhost:8000/api/v1/geography/states + +# Filter states by region +curl "http://localhost:8000/api/v1/geography/states?region=South" + +# Get indicator details +curl http://localhost:8000/api/v1/indicators/IMR +``` + +## Populating the Database + +BridgeStack reads from a RootStack SQLite database. To populate it: + +```bash +# Clone RootStack +git clone https://github.com/Varnasr/RootStack.git +cd RootStack +bash scripts/setup.sh + +# Copy the database to BridgeStack +cp rootstack.db ../BridgeStack/ +``` + +Then restart the API — all endpoints will return data. + +## Development Setup + +For contributors, install the dev tools: + +```bash +pip install -e ".[dev]" + +# Or use Make +make dev +``` + +This installs `ruff` (linter/formatter) and `pytest` with coverage support. + +### Useful Make Commands + +```bash +make test # Run tests with coverage +make lint # Check for lint issues +make format # Auto-format code +make check # Run all checks (lint + test) +make run # Start dev server +``` + +## Next Steps + +- [API Reference](api-reference.md) — Explore all available endpoints +- [Configuration](configuration.md) — Customize BridgeStack settings +- [Architecture](architecture.md) — Understand the system design diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..906041b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,81 @@ +[project] +name = "bridgestack" +version = "0.3.0" +description = "API backend bridging OpenStacks data layers using FastAPI and SQLite" +readme = "README.md" +license = {text = "MIT"} +requires-python = ">=3.11" +authors = [ + {name = "Varna Sri Raman", email = "hello@impactmojo.in"}, +] +keywords = ["fastapi", "rest-api", "development-data", "openstacks"] +classifiers = [ + "Development Status :: 4 - Beta", + "Framework :: FastAPI", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", +] + +dependencies = [ + "fastapi>=0.115.0", + "uvicorn[standard]>=0.34.0", + "sqlalchemy>=2.0.40", + "pydantic>=2.11.0", + "pydantic-settings>=2.9.0", +] + +[project.optional-dependencies] +dev = [ + "httpx>=0.28.0", + "pytest>=8.3.0", + "pytest-cov>=6.1.0", + "ruff>=0.11.0", +] + +[project.urls] +Homepage = "https://openstacks.dev" +Repository = "https://github.com/Varnasr/BridgeStack" +Documentation = "https://github.com/Varnasr/BridgeStack/tree/main/docs" +Issues = "https://github.com/Varnasr/BridgeStack/issues" + +[tool.ruff] +target-version = "py311" +line-length = 100 + +[tool.ruff.lint] +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "I", # isort + "N", # pep8-naming + "UP", # pyupgrade + "B", # flake8-bugbear + "SIM", # flake8-simplify + "T20", # flake8-print +] + +[tool.ruff.lint.per-file-ignores] +"app/routes/*.py" = ["B008"] # Depends() in defaults is standard FastAPI + +[tool.ruff.lint.isort] +known-first-party = ["app"] + +[tool.pytest.ini_options] +testpaths = ["tests"] +pythonpath = ["."] +addopts = "-v --tb=short --strict-markers" +markers = [ + "integration: marks tests that require database fixtures", +] + +[tool.coverage.run] +source = ["app"] +omit = ["tests/*"] + +[tool.coverage.report] +show_missing = true +fail_under = 70 diff --git a/requirements.txt b/requirements.txt index 7147525..ea7afee 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,9 @@ -fastapi>=0.104.0 -uvicorn[standard]>=0.24.0 -sqlalchemy>=2.0.0 -pydantic>=2.0.0 -pydantic-settings>=2.0.0 -httpx>=0.25.0 -pytest>=7.4.0 -pytest-asyncio>=0.21.0 +fastapi>=0.115.0 +uvicorn[standard]>=0.34.0 +sqlalchemy>=2.0.40 +pydantic>=2.11.0 +pydantic-settings>=2.9.0 +httpx>=0.28.0 +pytest>=8.3.0 +pytest-cov>=6.1.0 +ruff>=0.11.0 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..4fcb90b --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,258 @@ +import pytest +from fastapi.testclient import TestClient +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker +from sqlalchemy.pool import StaticPool + +from app.core.database import Base, get_db +from app.main import app +from app.models.geography import District, State +from app.models.indicators import Indicator, IndicatorValue +from app.models.policies import Scheme, SchemeBudget, SchemeCoverage +from app.models.sectors import Sector +from app.models.tools import Tool + +engine = create_engine( + "sqlite:///:memory:", + connect_args={"check_same_thread": False}, + poolclass=StaticPool, +) +TestSession = sessionmaker(bind=engine) + + +def override_get_db(): + db = TestSession() + try: + yield db + finally: + db.close() + + +app.dependency_overrides[get_db] = override_get_db + + +@pytest.fixture() +def client(): + Base.metadata.create_all(bind=engine) + yield TestClient(app) + Base.metadata.drop_all(bind=engine) + + +@pytest.fixture() +def db(): + Base.metadata.create_all(bind=engine) + session = TestSession() + yield session + session.close() + Base.metadata.drop_all(bind=engine) + + +@pytest.fixture() +def seed_data(db): + """Populate the test database with representative sample data.""" + # Sectors + health = Sector(sector_id="health", sector_name="Health") + education = Sector(sector_id="education", sector_name="Education") + nutrition = Sector(sector_id="health-nutrition", sector_name="Nutrition", parent_id="health") + db.add_all([health, education, nutrition]) + + # States + karnataka = State( + state_id="KA", + state_name="Karnataka", + region="South", + state_type="State", + capital="Bengaluru", + area_sq_km=191791.0, + census_2011_pop=61095297, + ) + maharashtra = State( + state_id="MH", + state_name="Maharashtra", + region="West", + state_type="State", + capital="Mumbai", + area_sq_km=307713.0, + census_2011_pop=112374333, + ) + db.add_all([karnataka, maharashtra]) + + # Districts + bengaluru = District( + district_id="KA-BLR", + district_name="Bengaluru Urban", + state_id="KA", + tier="Tier-1", + census_2011_pop=9621551, + area_sq_km=2196.0, + latitude=12.97, + longitude=77.59, + ) + mysuru = District( + district_id="KA-MYS", + district_name="Mysuru", + state_id="KA", + tier="Tier-2", + census_2011_pop=3001127, + area_sq_km=6854.0, + latitude=12.30, + longitude=76.64, + ) + mumbai = District( + district_id="MH-MUM", + district_name="Mumbai", + state_id="MH", + tier="Tier-1", + census_2011_pop=12442373, + area_sq_km=603.0, + latitude=19.08, + longitude=72.88, + ) + db.add_all([bengaluru, mysuru, mumbai]) + + # Indicators + imr = Indicator( + indicator_id="IMR", + indicator_name="Infant Mortality Rate", + sector_id="health", + unit="per 1000 live births", + direction="lower_is_better", + source="SRS", + frequency="Annual", + description="Deaths of infants under one year per 1000 live births", + ) + literacy = Indicator( + indicator_id="LIT", + indicator_name="Literacy Rate", + sector_id="education", + unit="percentage", + direction="higher_is_better", + source="Census", + frequency="Decennial", + description="Percentage of population aged 7+ that is literate", + ) + db.add_all([imr, literacy]) + + # Indicator values + db.add_all( + [ + IndicatorValue(indicator_id="IMR", state_id="KA", year=2020, value=25.0), + IndicatorValue(indicator_id="IMR", state_id="MH", year=2020, value=19.0), + IndicatorValue(indicator_id="IMR", state_id="KA", year=2021, value=23.0), + IndicatorValue(indicator_id="LIT", state_id="KA", year=2011, value=75.4), + IndicatorValue(indicator_id="LIT", state_id="MH", year=2011, value=82.3), + ] + ) + + # Schemes + nrhm = Scheme( + scheme_id="NRHM", + scheme_name="National Rural Health Mission", + ministry="Ministry of Health", + sector_id="health", + level="Central", + launch_year=2005, + status="Active", + beneficiary_type="Rural population", + website="https://nhm.gov.in", + ) + mdm = Scheme( + scheme_id="MDM", + scheme_name="Mid-Day Meal Scheme", + ministry="Ministry of Education", + sector_id="education", + level="Central", + launch_year=1995, + status="Active", + beneficiary_type="School children", + ) + db.add_all([nrhm, mdm]) + + # Budgets + db.add_all( + [ + SchemeBudget( + scheme_id="NRHM", + fiscal_year="2022-23", + allocated_crores=37800.0, + revised_crores=35200.0, + spent_crores=33100.0, + ), + SchemeBudget( + scheme_id="NRHM", + fiscal_year="2023-24", + allocated_crores=40000.0, + revised_crores=38500.0, + spent_crores=36200.0, + ), + SchemeBudget( + scheme_id="MDM", + fiscal_year="2022-23", + allocated_crores=10234.0, + revised_crores=9800.0, + spent_crores=9500.0, + ), + ] + ) + + # Coverage + db.add_all( + [ + SchemeCoverage( + scheme_id="NRHM", + state_id="KA", + year=2022, + beneficiaries=4500000, + target=5000000, + achievement_pct=90.0, + ), + SchemeCoverage( + scheme_id="NRHM", + state_id="MH", + year=2022, + beneficiaries=8200000, + target=9000000, + achievement_pct=91.1, + ), + ] + ) + + # Tools + db.add_all( + [ + Tool( + tool_name="District Health Dashboard", + stack="ViewStack", + directory="dashboards/health", + description="Interactive health indicator explorer", + sector="health", + language="JavaScript", + tool_type="dashboard", + difficulty="intermediate", + tags="health,dashboard,d3", + file_count=12, + ), + Tool( + tool_name="Equity Analysis Notebook", + stack="EquityStack", + directory="notebooks/equity", + description="Jupyter notebook for equity gap analysis", + sector="education", + language="Python", + tool_type="notebook", + difficulty="advanced", + tags="equity,analysis,jupyter", + file_count=5, + ), + ] + ) + + db.commit() + + return { + "states": [karnataka, maharashtra], + "districts": [bengaluru, mysuru, mumbai], + "sectors": [health, education, nutrition], + "indicators": [imr, literacy], + "schemes": [nrhm, mdm], + } diff --git a/tests/test_api.py b/tests/test_api.py index 04eee2f..a63427b 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,112 +1,241 @@ -import pytest -from fastapi.testclient import TestClient -from sqlalchemy import create_engine -from sqlalchemy.orm import sessionmaker - -from app.core.database import Base, get_db -from app.main import app - -engine = create_engine( - "sqlite:///./test.db", connect_args={"check_same_thread": False} -) -TestSession = sessionmaker(bind=engine) - - -def override_get_db(): - db = TestSession() - try: - yield db - finally: - db.close() - - -app.dependency_overrides[get_db] = override_get_db -client = TestClient(app) - - -@pytest.fixture(autouse=True) -def setup_db(): - Base.metadata.create_all(bind=engine) - yield - Base.metadata.drop_all(bind=engine) - - -def test_root(): - response = client.get("/") - assert response.status_code == 200 - data = response.json() - assert data["name"] == "BridgeStack API" - assert "stacks" in data - - -def test_health(): - response = client.get("/health") - assert response.status_code == 200 - assert response.json()["status"] == "healthy" - - -def test_list_states_empty(): - response = client.get("/api/v1/geography/states") - assert response.status_code == 200 - assert response.json() == [] - - -def test_list_sectors_empty(): - response = client.get("/api/v1/sectors/") - assert response.status_code == 200 - assert response.json() == [] - - -def test_list_indicators_empty(): - response = client.get("/api/v1/indicators/") - assert response.status_code == 200 - assert response.json() == [] - - -def test_list_schemes_empty(): - response = client.get("/api/v1/policies/schemes") - assert response.status_code == 200 - assert response.json() == [] - - -def test_list_tools_empty(): - response = client.get("/api/v1/tools/") - assert response.status_code == 200 - assert response.json() == [] - - -def test_state_not_found(): - response = client.get("/api/v1/geography/states/nonexistent") - assert response.status_code == 404 - - -def test_district_not_found(): - response = client.get("/api/v1/geography/districts/nonexistent") - assert response.status_code == 404 - - -def test_indicator_not_found(): - response = client.get("/api/v1/indicators/nonexistent") - assert response.status_code == 404 - - -def test_scheme_not_found(): - response = client.get("/api/v1/policies/schemes/nonexistent") - assert response.status_code == 404 - - -def test_tool_not_found(): - response = client.get("/api/v1/tools/999") - assert response.status_code == 404 - - -def test_docs_available(): - response = client.get("/docs") - assert response.status_code == 200 - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200 - schema = response.json() - assert schema["info"]["title"] == "BridgeStack API" +class TestHealthEndpoints: + def test_root(self, client): + response = client.get("/") + assert response.status_code == 200 + data = response.json() + assert data["name"] == "BridgeStack API" + assert "stacks" in data + + def test_health(self, client): + response = client.get("/health") + assert response.status_code == 200 + body = response.json() + assert body["status"] == "healthy" + assert body["database"] == "connected" + assert "version" in body + + def test_docs_available(self, client): + response = client.get("/docs") + assert response.status_code == 200 + + def test_openapi_schema(self, client): + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json()["info"]["title"] == "BridgeStack API" + + +class TestGeography: + def test_list_states_empty(self, client): + response = client.get("/api/v1/geography/states") + assert response.status_code == 200 + assert response.json() == [] + + def test_list_states(self, client, seed_data): + response = client.get("/api/v1/geography/states") + assert response.status_code == 200 + states = response.json() + assert len(states) == 2 + assert states[0]["state_name"] == "Karnataka" + + def test_filter_states_by_region(self, client, seed_data): + response = client.get("/api/v1/geography/states?region=South") + states = response.json() + assert len(states) == 1 + assert states[0]["state_id"] == "KA" + + def test_get_state_detail(self, client, seed_data): + response = client.get("/api/v1/geography/states/KA") + assert response.status_code == 200 + state = response.json() + assert state["state_name"] == "Karnataka" + assert len(state["districts"]) == 2 + + def test_state_not_found(self, client): + response = client.get("/api/v1/geography/states/NONEXISTENT") + assert response.status_code == 404 + + def test_list_districts(self, client, seed_data): + response = client.get("/api/v1/geography/districts") + assert response.status_code == 200 + assert len(response.json()) == 3 + + def test_filter_districts_by_state(self, client, seed_data): + response = client.get("/api/v1/geography/districts?state_id=KA") + districts = response.json() + assert len(districts) == 2 + assert all(d["state_id"] == "KA" for d in districts) + + def test_filter_districts_by_tier(self, client, seed_data): + response = client.get("/api/v1/geography/districts?tier=Tier-1") + districts = response.json() + assert len(districts) == 2 + + def test_district_not_found(self, client): + response = client.get("/api/v1/geography/districts/NONEXISTENT") + assert response.status_code == 404 + + +class TestSectors: + def test_list_sectors_empty(self, client): + response = client.get("/api/v1/sectors/") + assert response.status_code == 200 + assert response.json() == [] + + def test_list_sectors(self, client, seed_data): + response = client.get("/api/v1/sectors/") + sectors = response.json() + assert len(sectors) == 3 + + def test_get_sector(self, client, seed_data): + response = client.get("/api/v1/sectors/health") + assert response.status_code == 200 + assert response.json()["sector_name"] == "Health" + + def test_sector_not_found(self, client): + response = client.get("/api/v1/sectors/nonexistent") + assert response.status_code == 404 + + +class TestIndicators: + def test_list_indicators_empty(self, client): + response = client.get("/api/v1/indicators/") + assert response.status_code == 200 + assert response.json() == [] + + def test_list_indicators(self, client, seed_data): + response = client.get("/api/v1/indicators/") + assert len(response.json()) == 2 + + def test_filter_indicators_by_sector(self, client, seed_data): + response = client.get("/api/v1/indicators/?sector_id=health") + indicators = response.json() + assert len(indicators) == 1 + assert indicators[0]["indicator_id"] == "IMR" + + def test_filter_indicators_by_source(self, client, seed_data): + response = client.get("/api/v1/indicators/?source=SRS") + indicators = response.json() + assert len(indicators) == 1 + + def test_get_indicator_detail(self, client, seed_data): + response = client.get("/api/v1/indicators/IMR") + assert response.status_code == 200 + indicator = response.json() + assert indicator["indicator_name"] == "Infant Mortality Rate" + assert len(indicator["values"]) == 3 + + def test_indicator_not_found(self, client): + response = client.get("/api/v1/indicators/NONEXISTENT") + assert response.status_code == 404 + + def test_list_indicator_values(self, client, seed_data): + response = client.get("/api/v1/indicators/values/") + assert response.status_code == 200 + assert len(response.json()) == 5 + + def test_filter_values_by_indicator(self, client, seed_data): + response = client.get("/api/v1/indicators/values/?indicator_id=IMR") + values = response.json() + assert len(values) == 3 + assert all(v["indicator_id"] == "IMR" for v in values) + + def test_filter_values_by_state(self, client, seed_data): + response = client.get("/api/v1/indicators/values/?state_id=KA") + values = response.json() + assert len(values) == 3 + + def test_filter_values_by_year(self, client, seed_data): + response = client.get("/api/v1/indicators/values/?year=2020") + values = response.json() + assert len(values) == 2 + + +class TestPolicies: + def test_list_schemes_empty(self, client): + response = client.get("/api/v1/policies/schemes") + assert response.status_code == 200 + assert response.json() == [] + + def test_list_schemes(self, client, seed_data): + response = client.get("/api/v1/policies/schemes") + assert len(response.json()) == 2 + + def test_filter_schemes_by_sector(self, client, seed_data): + response = client.get("/api/v1/policies/schemes?sector_id=health") + schemes = response.json() + assert len(schemes) == 1 + assert schemes[0]["scheme_id"] == "NRHM" + + def test_filter_schemes_by_status(self, client, seed_data): + response = client.get("/api/v1/policies/schemes?status=Active") + assert len(response.json()) == 2 + + def test_filter_schemes_by_level(self, client, seed_data): + response = client.get("/api/v1/policies/schemes?level=Central") + assert len(response.json()) == 2 + + def test_get_scheme_detail(self, client, seed_data): + response = client.get("/api/v1/policies/schemes/NRHM") + assert response.status_code == 200 + scheme = response.json() + assert scheme["scheme_name"] == "National Rural Health Mission" + assert len(scheme["budgets"]) == 2 + assert len(scheme["coverage"]) == 2 + + def test_scheme_not_found(self, client): + response = client.get("/api/v1/policies/schemes/NONEXISTENT") + assert response.status_code == 404 + + def test_list_budgets(self, client, seed_data): + response = client.get("/api/v1/policies/budgets") + assert response.status_code == 200 + assert len(response.json()) == 3 + + def test_filter_budgets_by_scheme(self, client, seed_data): + response = client.get("/api/v1/policies/budgets?scheme_id=NRHM") + budgets = response.json() + assert len(budgets) == 2 + + def test_filter_budgets_by_fiscal_year(self, client, seed_data): + response = client.get("/api/v1/policies/budgets?fiscal_year=2022-23") + assert len(response.json()) == 2 + + def test_list_coverage(self, client, seed_data): + response = client.get("/api/v1/policies/coverage") + assert response.status_code == 200 + assert len(response.json()) == 2 + + def test_filter_coverage_by_state(self, client, seed_data): + response = client.get("/api/v1/policies/coverage?state_id=KA") + coverage = response.json() + assert len(coverage) == 1 + assert coverage[0]["achievement_pct"] == 90.0 + + +class TestTools: + def test_list_tools_empty(self, client): + response = client.get("/api/v1/tools/") + assert response.status_code == 200 + assert response.json() == [] + + def test_list_tools(self, client, seed_data): + response = client.get("/api/v1/tools/") + assert len(response.json()) == 2 + + def test_filter_tools_by_stack(self, client, seed_data): + response = client.get("/api/v1/tools/?stack=ViewStack") + tools = response.json() + assert len(tools) == 1 + assert tools[0]["tool_name"] == "District Health Dashboard" + + def test_filter_tools_by_language(self, client, seed_data): + response = client.get("/api/v1/tools/?language=Python") + assert len(response.json()) == 1 + + def test_filter_tools_by_difficulty(self, client, seed_data): + response = client.get("/api/v1/tools/?difficulty=intermediate") + assert len(response.json()) == 1 + + def test_tool_not_found(self, client): + response = client.get("/api/v1/tools/999") + assert response.status_code == 404