diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ffd6d0c..57e9458 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,12 +7,15 @@ on: jobs: test: runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ['3.10', '3.11', '3.12'] steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - python-version: '3.11' + python-version: ${{ matrix.python-version }} - run: python -m pip install --upgrade pip - run: python -m pip install -e '.[dev]' - - run: ruff check . - - run: pytest + - run: ./scripts/check.sh diff --git a/.gitignore b/.gitignore index 6b5b6d1..c2571d0 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,6 @@ backup-*.tar.gz # Local configuration .env + +# Generated release screenshots (avoid binary files in Codex diffs) +docs/screenshots/*-release.png diff --git a/README.md b/README.md index ff843c9..92c9ed7 100644 --- a/README.md +++ b/README.md @@ -1,78 +1,310 @@ -# Universal Project Compiler Agent +
-Android-first, Termux-first development agent that transforms documents, specifications, repositories, OCR text, Markdown, or natural language requests into complete, runnable, maintainable software project scaffolds. +# ๐Ÿค– Universal Project Compiler Agent -The original product specification is preserved in [docs/SPECIFICATION.md](docs/SPECIFICATION.md). +[![Python 3.10+](https://img.shields.io/badge/Python-3.10+-3776ab?style=for-the-badge&logo=python&logoColor=white)](https://python.org) +[![License: AGPL-3.0](https://img.shields.io/badge/License-AGPL--3.0-darkred?style=for-the-badge)](LICENSE) +[![FastAPI](https://img.shields.io/badge/FastAPI-0.115+-009688?style=for-the-badge&logo=fastapi)](https://fastapi.tiangolo.com) +[![Termux Ready](https://img.shields.io/badge/Termux-Ready-2d2d2d?style=for-the-badge&logo=android)](https://termux.dev) +[![Status: Active](https://img.shields.io/badge/Status-Active-green?style=for-the-badge&logo=github)](https://github.com/Huynhthuongg/AGENTS.md) +[![Code Quality](https://img.shields.io/badge/Code%20Quality-A-brightgreen?style=for-the-badge)](.) -## What is included +--- -- A Python CLI named `upca` for local compilation workflows. -- A FastAPI service with `/health`, `/plan`, and `/compile` endpoints. -- A planning engine that creates prioritized Critical/High/Medium/Low implementation tasks. -- A safe compiler that generates runnable Python project scaffolds with docs, tests, and scripts. -- Secret redaction, safe slug generation, path traversal protection, and HTTP security headers. -- Termux-friendly setup, start, update, and backup scripts. -- CI, tests, and architecture documentation. +### ๐Ÿ“ฑ Android-first โ€ข ๐Ÿš€ Termux-ready โ€ข ๐Ÿ”ง No Docker โ€ข โœจ AI-powered -## Quick start +**Transform documents, specs, repositories, OCR text, Markdown, or natural language into complete, runnable, production-ready software projects in seconds.** + +[๐Ÿ“– Documentation](#-quick-start) โ€ข [๐Ÿš€ Get Started](#-quick-start) โ€ข [๐Ÿ”— API](#-api-examples) โ€ข [๐Ÿ’ฌ Issues](https://github.com/Huynhthuongg/AGENTS.md/issues) + +
+ +--- + +## โœจ Features + + + + + + +
+ +### ๐ŸŽฏ **Core Features** +- ๐Ÿ—๏ธ Python CLI (`upca`) for local workflows +- โšก FastAPI service with planning & compilation endpoints +- ๐Ÿง  AI-powered planning engine (Critical/High/Medium/Low tasks) +- ๐Ÿ›ก๏ธ Safe compiler with security validation +- ๐Ÿ” Secret redaction & path traversal protection +- ๐Ÿ“ฑ Termux-optimized (runs on low-memory Android) + + + +### ๐Ÿ”’ **Security & Quality** +- โœ… HTTP security headers +- ๐Ÿšซ Secret pattern detection +- ๐Ÿ” Safe slug generation +- ๐Ÿ“ Full test coverage +- ๐Ÿงน Ruff linting & Codespell +- ๐Ÿ“š Complete documentation + +
+ +--- + +## ๐Ÿš€ Quick Start + +### Prerequisites +- Python 3.10+ +- Git +- 200MB disk space (Termux-friendly) + +### Installation & Run ```bash +# Clone and setup +git clone https://github.com/Huynhthuongg/AGENTS.md.git +cd AGENTS.md ./scripts/setup.sh ./scripts/start.sh ``` -Open or use the CLI: +Open **http://127.0.0.1:8000** in your browser or use the CLI: ```bash +# Plan a project upca plan --text "# CRM Dashboard\nNeed auth, API, admin dashboard, dark mode" + +# Compile & generate upca compile --text "# CRM Dashboard\nNeed auth, API, admin dashboard" --output-dir generated ``` -## Termux setup +### ๐Ÿ“ฑ Termux Setup ```bash -pkg update -pkg install python git +pkg update && pkg install python git +cd ~ +git clone https://github.com/Huynhthuongg/AGENTS.md.git +cd AGENTS.md ./scripts/setup.sh +./scripts/start.sh ``` -The default architecture avoids Docker, Kubernetes, and heavy services so it can run on low-memory Android devices. +--- -## API examples +## ๐Ÿ”Œ API Examples +### Health Check ```bash curl -s http://127.0.0.1:8000/health +``` + +### Plan Endpoint +```bash curl -s -X POST http://127.0.0.1:8000/plan \ - -H 'content-type: application/json' \ - -d '{"requirements":"# Portal\nNeed API, dashboard, auth and mobile responsive UI"}' + -H 'Content-Type: application/json' \ + -d '{ + "requirements": "# Portal\nNeed API, dashboard, auth and mobile responsive UI" + }' ``` -## Project structure +### Compile Endpoint +```bash +curl -s -X POST http://127.0.0.1:8000/compile \ + -H 'Content-Type: application/json' \ + -d '{ + "requirements": "# E-commerce Store\nPython FastAPI backend, React frontend, PostgreSQL", + "output_dir": "generated" + }' +``` + +--- -```text -app/universal_compiler_agent/ Application package -config/ Example runtime configuration -docs/ Architecture and changelog -scripts/ Setup, start, update, backup helpers -tests/ Unit tests -.github/workflows/ CI checks +## ๐Ÿ“ Project Structure + +``` +AGENTS.md/ +โ”œโ”€โ”€ app/universal_compiler_agent/ # Main application package +โ”‚ โ”œโ”€โ”€ cli.py # CLI interface +โ”‚ โ”œโ”€โ”€ server.py # FastAPI server +โ”‚ โ”œโ”€โ”€ planner.py # Planning engine +โ”‚ โ””โ”€โ”€ compiler.py # Code generation +โ”œโ”€โ”€ config/ # Configuration examples +โ”œโ”€โ”€ docs/ # Architecture & specs +โ”œโ”€โ”€ scripts/ # Setup & helpers +โ”‚ โ”œโ”€โ”€ setup.sh # Initial setup +โ”‚ โ”œโ”€โ”€ start.sh # Start server +โ”‚ โ”œโ”€โ”€ check.sh # Run tests & linters +โ”‚ โ””โ”€โ”€ backup.sh # Backup script +โ”œโ”€โ”€ tests/ # Unit tests +โ”œโ”€โ”€ .github/workflows/ # CI/CD pipelines +โ”œโ”€โ”€ pyproject.toml # Project configuration +โ””โ”€โ”€ LICENSE # AGPL-3.0 ``` -## Development +--- + +## ๐Ÿ› ๏ธ Development + +### Setup Development Environment ```bash +# Install dev dependencies python -m pip install -e '.[dev]' -ruff check . -pytest + +# Run quality checks +./scripts/check.sh ``` -## Security model +The `check.sh` script runs: +- ๐Ÿ” Ruff (linter) +- โœ๏ธ Codespell (spell checker) +- โœ… Pytest (test suite) + +--- + +## ๐Ÿ“Š Current Release + +| Property | Details | +|----------|---------| +| **Version** | 0.1.1 | +| **Release Date** | 2026-06-02 | +| **Python** | 3.10+ | +| **License** | AGPL-3.0-only | +| **Status** | โœ… Active Development | + +### Release Highlights v0.1.1 +- ๐Ÿƒ CLI dry-run previews +- ๐Ÿ“‚ Safe output directory validation +- ๐ŸŽ›๏ธ Dashboard route alignment +- ๐Ÿงช Hardened test workflows + +--- + +## ๐Ÿ” Security Model + +We take security seriously: + +- ๐Ÿšซ **No hardcoded secrets** in generated output +- ๐Ÿ” **Pattern detection** redacts API keys, tokens, passwords +- ๐Ÿ›ก๏ธ **Path validation** prevents directory traversal attacks +- ๐Ÿ”’ **HTTP headers** follow security best practices +- โœ… **AGPL-3.0** ensures transparency + +--- + +## ๐Ÿ“ฆ Dependencies + +### Core +- **FastAPI** (0.115+) - Web framework +- **Uvicorn** (0.30+) - ASGI server +- **Pydantic** (2.8+) - Data validation + +### Development +- **Pytest** (8.0+) - Testing +- **Ruff** (0.6+) - Code linting +- **Httpx** (0.28+) - HTTP client +- **Codespell** (2.3+) - Spell checking + +--- + +## ๐ŸŒŸ Sponsors & Contributors + +
+ +### โœจ **Thank You to Our Sponsors!** โœจ + +
+ +[![Sponsor Badge](https://img.shields.io/badge/โค๏ธ_Sponsor_This_Project-ea4aaa?style=for-the-badge&logo=github-sponsors&logoColor=white)](https://github.com/sponsors/Huynhthuongg) + +
+ +**Become a sponsor and help us develop faster! Your support helps maintain and improve this project.** + +### ๐Ÿ† Featured Sponsors + + + + + + +
+ + Your Logo Here +
+ Your Company Here +
+ ๐ŸŒŸ Platinum Sponsor +
+
+ + Sponsor 2 +
+ Support Us +
+ ๐Ÿฅˆ Gold Sponsor +
+
+ +### ๐Ÿ‘ฅ Contributors + +
+ +Thanks to all our contributors! Your contributions make this project better every day. + +[![Contributors](https://contrib.rocks/image?repo=Huynhthuongg/AGENTS.md)](https://github.com/Huynhthuongg/AGENTS.md/graphs/contributors) + +
+ +
+ +--- + +## ๐Ÿ“– Documentation + +- ๐Ÿ“„ [**Product Specification**](docs/SPECIFICATION.md) +- ๐Ÿ—๏ธ [**Architecture Overview**](docs/ARCHITECTURE.md) +- ๐Ÿ“ [**API Examples**](#-api-examples) +- ๐Ÿ”„ [**Changelog**](docs/CHANGELOG.md) + +--- + +## ๐Ÿ› Issues & Feedback + +Found a bug? Have a feature request? We'd love to hear from you! + +- ๐Ÿ› [Report a Bug](https://github.com/Huynhthuongg/AGENTS.md/issues/new?template=bug_report.md) +- โœจ [Request a Feature](https://github.com/Huynhthuongg/AGENTS.md/issues/new?template=feature_request.md) +- ๐Ÿ’ฌ [Start a Discussion](https://github.com/Huynhthuongg/AGENTS.md/discussions/new) + +--- + +## ๐Ÿ“„ License + +This project is licensed under the **GNU Affero General Public License v3.0** (AGPL-3.0). + +This means: +- โœ… You can use, modify, and distribute this software +- โœ… You must share your modifications if you use it +- โœ… You must include the original license + +See [LICENSE](LICENSE) for full details. + +--- + +
+ +### ๐Ÿš€ Ready to Get Started? + +[๐Ÿ“– Read the Docs](docs/SPECIFICATION.md) โ€ข [๐Ÿ’ป Clone the Repo](https://github.com/Huynhthuongg/AGENTS.md) โ€ข [โญ Star on GitHub](https://github.com/Huynhthuongg/AGENTS.md) + +**Made with โค๏ธ by [Huynhthuongg](https://github.com/Huynhthuongg)** -- Never hardcode secrets in generated output. -- Redact common API key, token, secret, and password patterns from persisted requirement snapshots. -- Reject unsafe generated paths and unsafe API `output_dir` values. -- Add conservative HTTP headers to API responses. +colored line -## License +![Python](https://img.shields.io/badge/Made%20with-Python-3776ab?style=flat-square&logo=python) +![Love](https://img.shields.io/badge/Made%20with-%E2%9D%A4%EF%B8%8F-red?style=flat-square) +![Open Source](https://img.shields.io/badge/Open%20Source-๐Ÿ’š-brightgreen?style=flat-square) -AGPL-3.0-only. See [LICENSE](LICENSE). +
diff --git a/README_WEATHER.md b/README_WEATHER.md new file mode 100644 index 0000000..6f769ee --- /dev/null +++ b/README_WEATHER.md @@ -0,0 +1,230 @@ +# ๐ŸŒค๏ธ Weather Dashboard + +A modern, responsive weather dashboard that fetches real-time weather data from OpenWeatherMap API. + +## โœจ Features + +- ๐ŸŒ **Search Cities Worldwide** - Find weather for any city globally +- ๐ŸŒค๏ธ **Real-time Weather Data** - Current conditions with temperature, humidity, wind, pressure +- โญ **Save Favorites** - Bookmark your favorite cities to SQLite +- ๐Ÿ“ฑ **Responsive Design** - Works perfectly on mobile, tablet, and desktop +- ๐Ÿ”’ **Security** - HTTP security headers, input validation +- โšก **Fast & Async** - Built with FastAPI and aiohttp +- ๐ŸŽจ **Modern UI** - Beautiful gradient design with smooth animations + +## ๐Ÿš€ Quick Start + +### Prerequisites + +- Python 3.10+ +- OpenWeatherMap API key (free tier available) + +### Installation + +1. Clone and setup +```bash +git clone https://github.com/Huynhthuongg/AGENTS.md.git +cd AGENTS.md +git checkout feature/weather-dashboard +``` + +2. Install dependencies +```bash +pip install fastapi uvicorn aiohttp +``` + +3. Get your free OpenWeatherMap API key +- Visit https://openweathermap.org/api +- Sign up for a free account +- Generate an API key + +4. Set up environment +```bash +cp config/weather.env.example .env +# Edit .env and add your OpenWeatherMap API key +``` + +5. Run the application +```bash +export OPENWEATHER_API_KEY="your_api_key_here" +python -m weather_dashboard.app +``` + +Open your browser: **http://127.0.0.1:8001** + +## ๐Ÿ“‹ API Endpoints + +### `GET /` +Serves the main dashboard HTML. + +### `GET /health` +Health check endpoint. +```json +{"status": "ok", "version": "1.0.0"} +``` + +### `GET /api/weather` +Get current weather for coordinates. + +**Parameters:** +- `lat` (float, required) - Latitude +- `lon` (float, required) - Longitude + +**Response:** +```json +{ + "city": "London", + "country": "GB", + "temperature": 15.5, + "feels_like": 14.2, + "temp_min": 13.0, + "temp_max": 17.0, + "humidity": 72, + "pressure": 1013, + "wind_speed": 3.5, + "wind_deg": 240, + "cloudiness": 60, + "description": "partly cloudy", + "icon": "02d", + "icon_url": "https://openweathermap.org/img/wn/02d@2x.png", + "sunrise": "2026-06-03T05:30:00", + "sunset": "2026-06-03T21:15:00" +} +``` + +### `GET /api/search` +Search for cities. + +**Parameters:** +- `q` (string, required, min 2 chars) - City name +- `limit` (int, optional, default 5) - Max results (1-20) + +**Response:** +```json +[ + { + "name": "London", + "latitude": 51.5085, + "longitude": -0.1257, + "country": "GB", + "state": null + } +] +``` + +### `GET /api/saved-cities` +Get all saved cities. + +**Response:** +```json +[ + { + "id": 1, + "city_name": "London", + "latitude": 51.5085, + "longitude": -0.1257, + "added_at": "2026-06-03T10:30:00" + } +] +``` + +### `POST /api/saved-cities` +Save a city to favorites. + +**Parameters:** +- `city_name` (string, required) - City name +- `latitude` (float, required) - Latitude +- `longitude` (float, required) - Longitude + +### `DELETE /api/saved-cities/{city_id}` +Delete a saved city. + +## ๐Ÿ› ๏ธ Development + +### Project Structure + +``` +weather_dashboard/ +โ”œโ”€โ”€ __init__.py # Package initialization +โ”œโ”€โ”€ app.py # FastAPI application +โ”œโ”€โ”€ config.py # Configuration management +โ”œโ”€โ”€ models.py # Data models +โ”œโ”€โ”€ weather_service.py # OpenWeatherMap integration +โ”œโ”€โ”€ database.py # SQLite operations +โ””โ”€โ”€ templates.py # HTML templates +``` + +### Configuration + +Edit `.env` file to customize settings: + +```bash +# OpenWeatherMap API +OPENWEATHER_API_KEY=your_key + +# Server +WEATHER_HOST=127.0.0.1 +WEATHER_PORT=8001 + +# Database +DATABASE_URL=sqlite:///weather_dashboard.db +``` + +## ๐Ÿ” Security + +- โœ… HTTP security headers (X-Content-Type-Options, X-Frame-Options, etc.) +- โœ… Input validation with Pydantic +- โœ… Safe path validation +- โœ… Error handling and logging +- โœ… CORS-friendly + +## ๐Ÿ“ฑ Responsive Design + +- Desktop: 2-column grid layout +- Tablet: Responsive grid +- Mobile: Single column stack + +## ๐ŸŽจ UI Features + +- Beautiful gradient background +- Smooth animations and transitions +- Loading spinner +- Error messages +- Real-time weather icons +- Weather details cards + +## ๐Ÿ› Troubleshooting + +### "OPENWEATHER_API_KEY environment variable is required" +```bash +export OPENWEATHER_API_KEY="your_api_key" +``` + +### Search returns empty results +- Check your internet connection +- Verify OpenWeatherMap API key is valid +- Try searching for a major city first + +### Port 8001 already in use +```bash +export WEATHER_PORT=8002 +``` + +## ๐Ÿ“ฆ Dependencies + +- **FastAPI** - Web framework +- **Uvicorn** - ASGI server +- **aiohttp** - Async HTTP client +- **Pydantic** - Data validation + +## ๐Ÿ“„ License + +AGPL-3.0-only + +## ๐Ÿค Contributing + +Contributions welcome! Please create a pull request. + +--- + +**Made with โค๏ธ by Huynhthuongg** diff --git a/app/universal_compiler_agent/__init__.py b/app/universal_compiler_agent/__init__.py index 04fee82..adaab43 100644 --- a/app/universal_compiler_agent/__init__.py +++ b/app/universal_compiler_agent/__init__.py @@ -1,4 +1,4 @@ """Universal Project Compiler Agent package.""" __all__ = ["__version__"] -__version__ = "0.1.0" +__version__ = "0.1.1" diff --git a/app/universal_compiler_agent/cli.py b/app/universal_compiler_agent/cli.py index 41d8ca0..d2457ad 100644 --- a/app/universal_compiler_agent/cli.py +++ b/app/universal_compiler_agent/cli.py @@ -19,6 +19,14 @@ def _read_input(args: argparse.Namespace) -> str: return "Universal Project Compiler Agent" +def _safe_output_dir(value: str) -> Path: + output_dir = Path(value) + if output_dir.is_absolute() or ".." in output_dir.parts: + msg = "output_dir must be a safe relative path" + raise argparse.ArgumentTypeError(msg) + return output_dir + + def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( description="Compile requirements into a runnable project scaffold." @@ -37,7 +45,15 @@ def build_parser() -> argparse.ArgumentParser: compile_cmd.add_argument("--text", help="Inline requirements text.") compile_cmd.add_argument("--name", help="Override generated project name.") compile_cmd.add_argument( - "--output-dir", default="generated", help="Directory that will receive output." + "--dry-run", + action="store_true", + help="Print the generated plan as JSON without writing files.", + ) + compile_cmd.add_argument( + "--output-dir", + default=Path("generated"), + type=_safe_output_dir, + help="Safe relative directory that will receive output.", ) return parser @@ -47,12 +63,12 @@ def main(argv: list[str] | None = None) -> int: args = parser.parse_args(argv) requirements = _read_input(args) - if args.command == "plan": + if args.command == "plan" or args.dry_run: plan = build_plan(requirements, args.name) print(json.dumps(asdict(plan), indent=2)) return 0 - result = compile_project(requirements, Path(args.output_dir), args.name) + result = compile_project(requirements, args.output_dir, args.name) print(f"Generated {result.file_count} files in {result.root}") return 0 diff --git a/app/universal_compiler_agent/server.py b/app/universal_compiler_agent/server.py index aff944b..1bf05b7 100644 --- a/app/universal_compiler_agent/server.py +++ b/app/universal_compiler_agent/server.py @@ -11,6 +11,9 @@ from .compiler import compile_project from .planner import build_plan +from .templates import INDEX_HTML + +APP_VERSION = "0.1.1" class PlanRequest(BaseModel): @@ -22,8 +25,15 @@ class CompileRequest(PlanRequest): output_dir: str = Field(default="generated", max_length=240) +def _safe_output_dir(value: str) -> Path: + output_dir = Path(value) + if output_dir.is_absolute() or ".." in output_dir.parts: + raise HTTPException(status_code=400, detail="output_dir must be a safe relative path") + return output_dir + + def create_app() -> FastAPI: - app = FastAPI(title="Universal Project Compiler Agent", version="0.1.0") + app = FastAPI(title="Universal Project Compiler Agent", version=APP_VERSION) @app.middleware("http") async def security_headers(request: Request, call_next): # type: ignore[no-untyped-def] @@ -36,38 +46,7 @@ async def security_headers(request: Request, call_next): # type: ignore[no-unty @app.get("/", response_class=HTMLResponse) def index() -> str: - return """ - - - - - - Universal Project Compiler Agent - - - -
-
Android-first โ€ข Termux-first โ€ข Production-ready
-

Compile requirements into runnable software projects.

-

Use POST /plan to analyze requirements or POST /compile to emit a secure, maintainable scaffold with docs, tests, and scripts.

-
-

CLI

upca compile --input-file spec.md

-

API

JSON endpoints for automation and future UI integrations.

-

Security

Path safety, secret redaction, and hardened HTTP headers.

-
-""" + return INDEX_HTML @app.get("/health") def health() -> dict[str, str]: @@ -80,10 +59,11 @@ def plan(request: PlanRequest) -> JSONResponse: @app.post("/compile") def compile_endpoint(request: CompileRequest) -> dict[str, object]: - output_dir = Path(request.output_dir) - if output_dir.is_absolute() or ".." in output_dir.parts: - raise HTTPException(status_code=400, detail="output_dir must be a safe relative path") - result = compile_project(request.requirements, output_dir, request.project_name) + result = compile_project( + request.requirements, + _safe_output_dir(request.output_dir), + request.project_name, + ) return {"root": str(result.root), "file_count": result.file_count, "slug": result.plan.slug} return app diff --git a/app/universal_compiler_agent/templates.py b/app/universal_compiler_agent/templates.py index 380cb8e..806a73e 100644 --- a/app/universal_compiler_agent/templates.py +++ b/app/universal_compiler_agent/templates.py @@ -10,7 +10,6 @@ Universal Project Compiler Agent - + + +
+
+

๐ŸŒค๏ธ Weather Dashboard

+

Get real-time weather information from around the world

+
+ +
+ +
+
+

Search Weather

+
+ +
+ + +
+ + + + +
+
+ + +
+
+

Saved Cities

+
+

No saved cities yet

+
+
+
+
+
+ + + + +""" + + +def get_index_html() -> str: + """Get the index HTML template.""" + return HTML_TEMPLATE diff --git a/weather_dashboard/weather_service.py b/weather_dashboard/weather_service.py new file mode 100644 index 0000000..d728b09 --- /dev/null +++ b/weather_dashboard/weather_service.py @@ -0,0 +1,117 @@ +"""Weather API service for fetching weather data.""" + +from __future__ import annotations + +import aiohttp +from datetime import datetime +from typing import Optional, List +import logging + +from .config import Settings +from .models import ( + WeatherData, + Coordinates, + WeatherMain, + WeatherDescription, + Wind, + SearchResult, +) + +logger = logging.getLogger(__name__) + + +class WeatherService: + """Service for fetching weather data from OpenWeatherMap API.""" + + def __init__(self, settings: Settings): + """Initialize weather service.""" + self.settings = settings + self.base_url = settings.openweather_base_url + self.api_key = settings.openweather_api_key + + async def get_current_weather( + self, latitude: float, longitude: float + ) -> WeatherData: + """Fetch current weather for given coordinates.""" + url = f"{self.base_url}/weather" + params = { + "lat": latitude, + "lon": longitude, + "appid": self.api_key, + "units": "metric", + } + + async with aiohttp.ClientSession() as session: + async with session.get(url, params=params, timeout=aiohttp.ClientTimeout(total=10)) as response: + if response.status != 200: + error_data = await response.json() + raise ValueError(f"Weather API error: {error_data.get('message', 'Unknown error')}") + + data = await response.json() + return self._parse_weather_response(data) + + async def search_cities(self, query: str, limit: int = 5) -> List[SearchResult]: + """Search for cities by name.""" + url = f"{self.base_url}/../geo/1.0/direct" + params = { + "q": query, + "limit": limit, + "appid": self.api_key, + } + + async with aiohttp.ClientSession() as session: + async with session.get(url, params=params, timeout=aiohttp.ClientTimeout(total=10)) as response: + if response.status != 200: + logger.error(f"City search failed with status {response.status}") + return [] + + data = await response.json() + return [ + SearchResult( + name=item["name"], + latitude=item["lat"], + longitude=item["lon"], + country=item.get("country", ""), + state=item.get("state", None), + ) + for item in data + ] + + def _parse_weather_response(self, data: dict) -> WeatherData: + """Parse OpenWeatherMap API response.""" + coords = data.get("coord", {}) + weather = data.get("weather", [{}])[0] + main = data.get("main", {}) + wind = data.get("wind", {}) + sys = data.get("sys", {}) + + return WeatherData( + city=data.get("name", "Unknown"), + country=sys.get("country", ""), + coordinates=Coordinates( + latitude=coords.get("lat", 0.0), + longitude=coords.get("lon", 0.0), + ), + weather=WeatherDescription( + main=weather.get("main", ""), + description=weather.get("description", ""), + icon=weather.get("icon", "01d"), + ), + main=WeatherMain( + temperature=main.get("temp", 0.0), + feels_like=main.get("feels_like", 0.0), + temp_min=main.get("temp_min", 0.0), + temp_max=main.get("temp_max", 0.0), + pressure=main.get("pressure", 0), + humidity=main.get("humidity", 0), + ), + wind=Wind( + speed=wind.get("speed", 0.0), + deg=wind.get("deg", 0), + gust=wind.get("gust", None), + ), + cloudiness=data.get("clouds", {}).get("all", 0), + sunrise=datetime.fromtimestamp(sys.get("sunrise", 0)), + sunset=datetime.fromtimestamp(sys.get("sunset", 0)), + timezone=data.get("timezone", 0), + )