From 99d4551f6a810a4a9e0d9aa630e4ad92ee999b41 Mon Sep 17 00:00:00 2001 From: Ashutosh Dash Date: Sat, 20 Jun 2026 15:37:41 +0530 Subject: [PATCH] feat: setup agents.md file with relevant skills for the repo --- .agents/skills/conventional-commit/SKILL.md | 72 +++ .agents/skills/flask-api-development/SKILL.md | 102 ++++ .../application-factory-and-configuration.md | 53 ++ .../references/authentication-and-jwt.md | 80 +++ .../blueprints-for-modular-api-design.md | 76 +++ .../database-models-with-sqlalchemy.md | 71 +++ .../references/flask-application-setup.md | 55 ++ .../references/request-validation.md | 43 ++ .../scripts/validate-api.sh | 18 + .../templates/api-scaffold.yaml | 22 + .../modern-javascript-patterns/SKILL.md | 43 ++ .../references/advanced-patterns.md | 487 ++++++++++++++++++ .../references/details.md | 457 ++++++++++++++++ .../skills/python-design-patterns/SKILL.md | 85 +++ .../references/details.md | 353 +++++++++++++ .agents/skills/python-observability/SKILL.md | 229 ++++++++ .../references/details.md | 176 +++++++ AGENTS.md | 90 ++++ skills-lock.json | 35 ++ 19 files changed, 2547 insertions(+) create mode 100644 .agents/skills/conventional-commit/SKILL.md create mode 100644 .agents/skills/flask-api-development/SKILL.md create mode 100644 .agents/skills/flask-api-development/references/application-factory-and-configuration.md create mode 100644 .agents/skills/flask-api-development/references/authentication-and-jwt.md create mode 100644 .agents/skills/flask-api-development/references/blueprints-for-modular-api-design.md create mode 100644 .agents/skills/flask-api-development/references/database-models-with-sqlalchemy.md create mode 100644 .agents/skills/flask-api-development/references/flask-application-setup.md create mode 100644 .agents/skills/flask-api-development/references/request-validation.md create mode 100644 .agents/skills/flask-api-development/scripts/validate-api.sh create mode 100644 .agents/skills/flask-api-development/templates/api-scaffold.yaml create mode 100644 .agents/skills/modern-javascript-patterns/SKILL.md create mode 100644 .agents/skills/modern-javascript-patterns/references/advanced-patterns.md create mode 100644 .agents/skills/modern-javascript-patterns/references/details.md create mode 100644 .agents/skills/python-design-patterns/SKILL.md create mode 100644 .agents/skills/python-design-patterns/references/details.md create mode 100644 .agents/skills/python-observability/SKILL.md create mode 100644 .agents/skills/python-observability/references/details.md create mode 100644 AGENTS.md create mode 100644 skills-lock.json diff --git a/.agents/skills/conventional-commit/SKILL.md b/.agents/skills/conventional-commit/SKILL.md new file mode 100644 index 0000000..3884217 --- /dev/null +++ b/.agents/skills/conventional-commit/SKILL.md @@ -0,0 +1,72 @@ +--- +name: conventional-commit +description: 'Prompt and workflow for generating conventional commit messages using a structured XML format. Guides users to create standardized, descriptive commit messages in line with the Conventional Commits specification, including instructions, examples, and validation.' +--- + +### Instructions + +```xml + This file contains a prompt template for generating conventional commit messages. It provides instructions, examples, and formatting guidelines to help users write standardized, descriptive commit messages in accordance with the Conventional Commits specification. +``` + +### Workflow + +**Follow these steps:** + +1. Run `git status` to review changed files. +2. Run `git diff` or `git diff --cached` to inspect changes. +3. Stage your changes with `git add `. +4. Construct your commit message using the following XML structure. +5. After generating your commit message, Copilot will automatically run the following command in your integrated terminal (no confirmation needed): + +```bash +git commit -m "type(scope): description" +``` + +6. Just execute this prompt and Copilot will handle the commit for you in the terminal. + +### Commit Message Structure + +```xml + + feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert + () + A short, imperative summary of the change + (optional: more detailed explanation) +
(optional: e.g. BREAKING CHANGE: details, or issue references)
+
+``` + +### Examples + +```xml + + feat(parser): add ability to parse arrays + fix(ui): correct button alignment + docs: update README with usage instructions + refactor: improve performance of data processing + chore: update dependencies + feat!: send email on registration (BREAKING CHANGE: email service required) + +``` + +### Validation + +```xml + + Must be one of the allowed types. See https://www.conventionalcommits.org/en/v1.0.0/#specification + Optional, but recommended for clarity. + Required. Use the imperative mood (e.g., "add", not "added"). + Optional. Use for additional context. +
Use for breaking changes or issue references.
+
+``` + +### Final Step + +```xml + + git commit -m "type(scope): description" + Replace with your constructed message. Include body and footer if needed. + +``` diff --git a/.agents/skills/flask-api-development/SKILL.md b/.agents/skills/flask-api-development/SKILL.md new file mode 100644 index 0000000..e943a77 --- /dev/null +++ b/.agents/skills/flask-api-development/SKILL.md @@ -0,0 +1,102 @@ +--- +name: flask-api-development +description: > + Develop lightweight Flask APIs with routing, blueprints, database integration, + authentication, and request/response handling. Use when building RESTful APIs, + microservices, or lightweight web services with Flask. +--- + +# Flask API Development + +## Table of Contents + +- [Overview](#overview) +- [When to Use](#when-to-use) +- [Quick Start](#quick-start) +- [Reference Guides](#reference-guides) +- [Best Practices](#best-practices) + +## Overview + +Create efficient Flask APIs with blueprints for modular organization, SQLAlchemy for ORM, JWT authentication, comprehensive error handling, and proper request validation following REST principles. + +## When to Use + +- Building RESTful APIs with Flask +- Creating microservices with minimal overhead +- Implementing lightweight authentication systems +- Designing API endpoints with proper validation +- Integrating with relational databases +- Building request/response handling systems + +## Quick Start + +Minimal working example: + +```python +# app.py +from flask import Flask, request, jsonify +from flask_cors import CORS +from flask_sqlalchemy import SQLAlchemy +from flask_jwt_extended import JWTManager +import os + +app = Flask(__name__) +app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv('DATABASE_URL', 'sqlite:///app.db') +app.config['JWT_SECRET_KEY'] = os.getenv('JWT_SECRET_KEY', 'dev-secret') +app.config['JSON_SORT_KEYS'] = False + +db = SQLAlchemy(app) +jwt = JWTManager(app) +CORS(app) + +# Request ID middleware +@app.before_request +def assign_request_id(): + import uuid + request.request_id = str(uuid.uuid4()) + +# Error handlers +@app.errorhandler(400) +def bad_request(error): +// ... (see reference guides for full implementation) +``` + +## Reference Guides + +Detailed implementations in the `references/` directory: + +| Guide | Contents | +|---|---| +| [Flask Application Setup](references/flask-application-setup.md) | Flask Application Setup | +| [Database Models with SQLAlchemy](references/database-models-with-sqlalchemy.md) | Database Models with SQLAlchemy | +| [Authentication and JWT](references/authentication-and-jwt.md) | Authentication and JWT | +| [Blueprints for Modular API Design](references/blueprints-for-modular-api-design.md) | Blueprints for Modular API Design | +| [Request Validation](references/request-validation.md) | Request Validation | +| [Application Factory and Configuration](references/application-factory-and-configuration.md) | Application Factory and Configuration | + +## Best Practices + +### ✅ DO + +- Use blueprints for modular organization +- Implement proper authentication with JWT +- Validate all user input +- Use SQLAlchemy ORM for database operations +- Implement comprehensive error handling +- Use pagination for collection endpoints +- Log errors and important events +- Return appropriate HTTP status codes +- Implement CORS properly +- Use environment variables for configuration + +### ❌ DON'T + +- Store secrets in code +- Use global variables for shared state +- Ignore database transactions +- Trust user input without validation +- Return stack traces in production +- Use mutable default arguments +- Forget to handle database connection errors +- Implement authentication in route handlers diff --git a/.agents/skills/flask-api-development/references/application-factory-and-configuration.md b/.agents/skills/flask-api-development/references/application-factory-and-configuration.md new file mode 100644 index 0000000..c5ab405 --- /dev/null +++ b/.agents/skills/flask-api-development/references/application-factory-and-configuration.md @@ -0,0 +1,53 @@ +# Application Factory and Configuration + +## Application Factory and Configuration + +```python +# config.py +import os + +class Config: + SQLALCHEMY_TRACK_MODIFICATIONS = False + JSON_SORT_KEYS = False + +class DevelopmentConfig(Config): + DEBUG = True + TESTING = False + SQLALCHEMY_DATABASE_URI = 'sqlite:///app.db' + +class ProductionConfig(Config): + DEBUG = False + TESTING = False + SQLALCHEMY_DATABASE_URI = os.getenv('DATABASE_URL') + JWT_SECRET_KEY = os.getenv('JWT_SECRET_KEY') + +class TestingConfig(Config): + TESTING = True + SQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:' + +# factory.py +from flask import Flask +from flask_sqlalchemy import SQLAlchemy +from flask_jwt_extended import JWTManager + +def create_app(config_name='development'): + app = Flask(__name__) + + if config_name == 'production': + from config import ProductionConfig + app.config.from_object(ProductionConfig) + else: + from config import DevelopmentConfig + app.config.from_object(DevelopmentConfig) + + db = SQLAlchemy(app) + jwt = JWTManager(app) + + # Register blueprints + from routes.auth import auth_bp + from routes.users import users_bp + app.register_blueprint(auth_bp) + app.register_blueprint(users_bp) + + return app +``` diff --git a/.agents/skills/flask-api-development/references/authentication-and-jwt.md b/.agents/skills/flask-api-development/references/authentication-and-jwt.md new file mode 100644 index 0000000..9538601 --- /dev/null +++ b/.agents/skills/flask-api-development/references/authentication-and-jwt.md @@ -0,0 +1,80 @@ +# Authentication and JWT + +## Authentication and JWT + +```python +# auth.py +from flask import request, jsonify +from flask_jwt_extended import create_access_token, jwt_required, get_jwt_identity +from functools import wraps +from models import User, db + +def authenticate_user(email, password): + user = User.query.filter_by(email=email).first() + if user and user.verify_password(password): + return user + return None + +def login_required(f): + @wraps(f) + @jwt_required() + def decorated_function(*args, **kwargs): + identity = get_jwt_identity() + user = User.query.get(identity) + if not user or not user.is_active: + return jsonify({'error': 'User not found or inactive'}), 401 + request.current_user = user + return f(*args, **kwargs) + return decorated_function + +def admin_required(f): + @wraps(f) + @login_required + def decorated_function(*args, **kwargs): + if request.current_user.role != 'admin': + return jsonify({'error': 'Admin access required'}), 403 + return f(*args, **kwargs) + return decorated_function + +# routes/auth.py +from flask import Blueprint, request, jsonify +from auth import authenticate_user, login_required +from models import User, db +from flask_jwt_extended import create_access_token + +auth_bp = Blueprint('auth', __name__, url_prefix='/api/auth') + +@auth_bp.route('/login', methods=['POST']) +def login(): + data = request.get_json() + if not data or not data.get('email') or not data.get('password'): + return jsonify({'error': 'Missing credentials'}), 400 + + user = authenticate_user(data['email'], data['password']) + if not user: + return jsonify({'error': 'Invalid credentials'}), 401 + + access_token = create_access_token(identity=str(user.id)) + return jsonify({ + 'access_token': access_token, + 'user': user.to_dict() + }), 200 + +@auth_bp.route('/register', methods=['POST']) +def register(): + data = request.get_json() + if User.query.filter_by(email=data['email']).first(): + return jsonify({'error': 'Email already exists'}), 409 + + user = User(email=data['email'], first_name=data.get('first_name')) + user.set_password(data['password']) + db.session.add(user) + db.session.commit() + + return jsonify({'user': user.to_dict()}), 201 + +@auth_bp.route('/profile', methods=['GET']) +@login_required +def get_profile(): + return jsonify({'user': request.current_user.to_dict()}), 200 +``` diff --git a/.agents/skills/flask-api-development/references/blueprints-for-modular-api-design.md b/.agents/skills/flask-api-development/references/blueprints-for-modular-api-design.md new file mode 100644 index 0000000..e7977ed --- /dev/null +++ b/.agents/skills/flask-api-development/references/blueprints-for-modular-api-design.md @@ -0,0 +1,76 @@ +# Blueprints for Modular API Design + +## Blueprints for Modular API Design + +```python +# routes/users.py +from flask import Blueprint, request, jsonify +from auth import login_required, admin_required +from models import User, db +from sqlalchemy import or_ + +users_bp = Blueprint('users', __name__, url_prefix='/api/users') + +@users_bp.route('', methods=['GET']) +@login_required +def list_users(): + page = request.args.get('page', 1, type=int) + limit = request.args.get('limit', 20, type=int) + search = request.args.get('q', '', type=str) + + query = User.query + if search: + query = query.filter(or_( + User.email.ilike(f'%{search}%'), + User.first_name.ilike(f'%{search}%') + )) + + paginated = query.paginate(page=page, per_page=limit) + return jsonify({ + 'data': [user.to_dict() for user in paginated.items], + 'pagination': { + 'page': page, + 'limit': limit, + 'total': paginated.total, + 'pages': paginated.pages + } + }), 200 + +@users_bp.route('/', methods=['GET']) +@login_required +def get_user(user_id): + user = User.query.get(user_id) + if not user: + return jsonify({'error': 'User not found'}), 404 + return jsonify({'user': user.to_dict()}), 200 + +@users_bp.route('/', methods=['PATCH']) +@login_required +def update_user(user_id): + if str(request.current_user.id) != user_id: + return jsonify({'error': 'Unauthorized'}), 403 + + user = User.query.get(user_id) + if not user: + return jsonify({'error': 'User not found'}), 404 + + data = request.get_json() + if 'first_name' in data: + user.first_name = data['first_name'] + if 'last_name' in data: + user.last_name = data['last_name'] + + db.session.commit() + return jsonify({'user': user.to_dict()}), 200 + +@users_bp.route('/', methods=['DELETE']) +@admin_required +def delete_user(user_id): + user = User.query.get(user_id) + if not user: + return jsonify({'error': 'User not found'}), 404 + + db.session.delete(user) + db.session.commit() + return '', 204 +``` diff --git a/.agents/skills/flask-api-development/references/database-models-with-sqlalchemy.md b/.agents/skills/flask-api-development/references/database-models-with-sqlalchemy.md new file mode 100644 index 0000000..6f7cab5 --- /dev/null +++ b/.agents/skills/flask-api-development/references/database-models-with-sqlalchemy.md @@ -0,0 +1,71 @@ +# Database Models with SQLAlchemy + +## Database Models with SQLAlchemy + +```python +# models.py +from datetime import datetime +from flask_sqlalchemy import SQLAlchemy +from sqlalchemy.dialects.postgresql import UUID +import uuid + +db = SQLAlchemy() + +class User(db.Model): + __tablename__ = 'users' + + id = db.Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + email = db.Column(db.String(255), unique=True, nullable=False, index=True) + password_hash = db.Column(db.String(255), nullable=False) + first_name = db.Column(db.String(100)) + last_name = db.Column(db.String(100)) + role = db.Column(db.String(20), default='user', index=True) + is_active = db.Column(db.Boolean, default=True) + created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False) + updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + # Relationships + posts = db.relationship('Post', backref='author', lazy='dynamic', cascade='all, delete-orphan') + + def __repr__(self): + return f'' + + def set_password(self, password): + from werkzeug.security import generate_password_hash + self.password_hash = generate_password_hash(password) + + def verify_password(self, password): + from werkzeug.security import check_password_hash + return check_password_hash(self.password_hash, password) + + def to_dict(self): + return { + 'id': str(self.id), + 'email': self.email, + 'first_name': self.first_name, + 'last_name': self.last_name, + 'role': self.role, + 'created_at': self.created_at.isoformat() + } + +class Post(db.Model): + __tablename__ = 'posts' + + id = db.Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + title = db.Column(db.String(255), nullable=False, index=True) + content = db.Column(db.Text, nullable=False) + published = db.Column(db.Boolean, default=False) + user_id = db.Column(UUID(as_uuid=True), db.ForeignKey('users.id'), nullable=False) + created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False) + updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + def to_dict(self): + return { + 'id': str(self.id), + 'title': self.title, + 'content': self.content, + 'published': self.published, + 'author_id': str(self.user_id), + 'created_at': self.created_at.isoformat() + } +``` diff --git a/.agents/skills/flask-api-development/references/flask-application-setup.md b/.agents/skills/flask-api-development/references/flask-application-setup.md new file mode 100644 index 0000000..8999591 --- /dev/null +++ b/.agents/skills/flask-api-development/references/flask-application-setup.md @@ -0,0 +1,55 @@ +# Flask Application Setup + +## Flask Application Setup + +```python +# app.py +from flask import Flask, request, jsonify +from flask_cors import CORS +from flask_sqlalchemy import SQLAlchemy +from flask_jwt_extended import JWTManager +import os + +app = Flask(__name__) +app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv('DATABASE_URL', 'sqlite:///app.db') +app.config['JWT_SECRET_KEY'] = os.getenv('JWT_SECRET_KEY', 'dev-secret') +app.config['JSON_SORT_KEYS'] = False + +db = SQLAlchemy(app) +jwt = JWTManager(app) +CORS(app) + +# Request ID middleware +@app.before_request +def assign_request_id(): + import uuid + request.request_id = str(uuid.uuid4()) + +# Error handlers +@app.errorhandler(400) +def bad_request(error): + return jsonify({ + 'error': 'Bad Request', + 'message': str(error), + 'request_id': request.request_id + }), 400 + +@app.errorhandler(404) +def not_found(error): + return jsonify({ + 'error': 'Not Found', + 'message': 'Resource does not exist', + 'request_id': request.request_id + }), 404 + +@app.errorhandler(500) +def internal_error(error): + db.session.rollback() + return jsonify({ + 'error': 'Internal Server Error', + 'request_id': request.request_id + }), 500 + +if __name__ == '__main__': + app.run(debug=os.getenv('ENV') != 'production') +``` diff --git a/.agents/skills/flask-api-development/references/request-validation.md b/.agents/skills/flask-api-development/references/request-validation.md new file mode 100644 index 0000000..0b1b321 --- /dev/null +++ b/.agents/skills/flask-api-development/references/request-validation.md @@ -0,0 +1,43 @@ +# Request Validation + +## Request Validation + +```python +# validators.py +from flask import request, jsonify +from functools import wraps + +def validate_json(*required_fields): + def decorator(f): + @wraps(f) + def decorated_function(*args, **kwargs): + if not request.is_json: + return jsonify({'error': 'Request body must be JSON'}), 400 + + data = request.get_json() + missing = [field for field in required_fields if field not in data] + + if missing: + return jsonify({ + 'error': 'Missing required fields', + 'missing_fields': missing + }), 400 + + return f(*args, **kwargs) + return decorated_function + return decorator + +def validate_email(email): + import re + pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$' + return re.match(pattern, email) is not None + +# Usage +@users_bp.route('', methods=['POST']) +@validate_json('email', 'password', 'first_name') +def create_user(): + data = request.get_json() + if not validate_email(data['email']): + return jsonify({'error': 'Invalid email format'}), 400 + # ... rest of logic +``` diff --git a/.agents/skills/flask-api-development/scripts/validate-api.sh b/.agents/skills/flask-api-development/scripts/validate-api.sh new file mode 100644 index 0000000..e4478b6 --- /dev/null +++ b/.agents/skills/flask-api-development/scripts/validate-api.sh @@ -0,0 +1,18 @@ +#!/bin/bash +# validate-api.sh - Validate API specification +# Usage: ./validate-api.sh + +set -euo pipefail + +SPEC_FILE="${{1:?Usage: $0 }}" + +echo "Validating API spec: $SPEC_FILE" + +# TODO: Add API validation +# - Validate OpenAPI/Swagger syntax +# - Check endpoint naming conventions +# - Verify response schemas +# - Check for required headers +# - Validate authentication definitions + +echo "API validation complete." diff --git a/.agents/skills/flask-api-development/templates/api-scaffold.yaml b/.agents/skills/flask-api-development/templates/api-scaffold.yaml new file mode 100644 index 0000000..3196297 --- /dev/null +++ b/.agents/skills/flask-api-development/templates/api-scaffold.yaml @@ -0,0 +1,22 @@ +# API Endpoint Scaffold +# TODO: Customize for your API framework + +openapi: "3.0.3" +info: + title: "API Service" + version: "1.0.0" + +paths: + /api/v1/resource: + get: + summary: "List resources" + # TODO: Define parameters and responses + responses: + "200": + description: "Success" + post: + summary: "Create resource" + # TODO: Define request body and responses + responses: + "201": + description: "Created" diff --git a/.agents/skills/modern-javascript-patterns/SKILL.md b/.agents/skills/modern-javascript-patterns/SKILL.md new file mode 100644 index 0000000..a7739c7 --- /dev/null +++ b/.agents/skills/modern-javascript-patterns/SKILL.md @@ -0,0 +1,43 @@ +--- +name: modern-javascript-patterns +description: Master ES6+ features including async/await, destructuring, spread operators, arrow functions, promises, modules, iterators, generators, and functional programming patterns for writing clean, efficient JavaScript code. Use when refactoring legacy code, implementing modern patterns, or optimizing JavaScript applications. +--- + +# Modern JavaScript Patterns + +Comprehensive guide for mastering modern JavaScript (ES6+) features, functional programming patterns, and best practices for writing clean, maintainable, and performant code. + +## When to Use This Skill + +- Refactoring legacy JavaScript to modern syntax +- Implementing functional programming patterns +- Optimizing JavaScript performance +- Writing maintainable and readable code +- Working with asynchronous operations +- Building modern web applications +- Migrating from callbacks to Promises/async-await +- Implementing data transformation pipelines + +## Detailed patterns and worked examples + +Detailed pattern documentation lives in `references/details.md`. Read that file when the navigation tier above is insufficient. + +## Best Practices + +1. **Use const by default**: Only use let when reassignment is needed +2. **Prefer arrow functions**: Especially for callbacks +3. **Use template literals**: Instead of string concatenation +4. **Destructure objects and arrays**: For cleaner code +5. **Use async/await**: Instead of Promise chains +6. **Avoid mutating data**: Use spread operator and array methods +7. **Use optional chaining**: Prevent "Cannot read property of undefined" +8. **Use nullish coalescing**: For default values +9. **Prefer array methods**: Over traditional loops +10. **Use modules**: For better code organization +11. **Write pure functions**: Easier to test and reason about +12. **Use meaningful variable names**: Self-documenting code +13. **Keep functions small**: Single responsibility principle +14. **Handle errors properly**: Use try/catch with async/await +15. **Use strict mode**: `'use strict'` for better error catching + +For common pitfalls (this binding, promise anti-patterns, memory leaks), see [references/advanced-patterns.md](references/advanced-patterns.md). diff --git a/.agents/skills/modern-javascript-patterns/references/advanced-patterns.md b/.agents/skills/modern-javascript-patterns/references/advanced-patterns.md new file mode 100644 index 0000000..a0237b7 --- /dev/null +++ b/.agents/skills/modern-javascript-patterns/references/advanced-patterns.md @@ -0,0 +1,487 @@ +# Advanced Modern JavaScript Patterns + +Advanced patterns covering functional programming, modern class features, ES6 modules, iterators, generators, modern operators, and performance optimization. + +## Functional Programming Patterns + +### 1. Array Methods + +**Map, Filter, Reduce:** + +```javascript +const users = [ + { id: 1, name: "John", age: 30, active: true }, + { id: 2, name: "Jane", age: 25, active: false }, + { id: 3, name: "Bob", age: 35, active: true }, +]; + +// Map - Transform array +const names = users.map((user) => user.name); +const upperNames = users.map((user) => user.name.toUpperCase()); + +// Filter - Select elements +const activeUsers = users.filter((user) => user.active); +const adults = users.filter((user) => user.age >= 18); + +// Reduce - Aggregate data +const totalAge = users.reduce((sum, user) => sum + user.age, 0); +const avgAge = totalAge / users.length; + +// Group by property +const byActive = users.reduce((groups, user) => { + const key = user.active ? "active" : "inactive"; + return { + ...groups, + [key]: [...(groups[key] || []), user], + }; +}, {}); + +// Chaining methods +const result = users + .filter((user) => user.active) + .map((user) => user.name) + .sort() + .join(", "); +``` + +**Advanced Array Methods:** + +```javascript +// Find - First matching element +const user = users.find((u) => u.id === 2); + +// FindIndex - Index of first match +const index = users.findIndex((u) => u.name === "Jane"); + +// Some - At least one matches +const hasActive = users.some((u) => u.active); + +// Every - All match +const allAdults = users.every((u) => u.age >= 18); + +// FlatMap - Map and flatten +const userTags = [ + { name: "John", tags: ["admin", "user"] }, + { name: "Jane", tags: ["user"] }, +]; +const allTags = userTags.flatMap((u) => u.tags); + +// From - Create array from iterable +const str = "hello"; +const chars = Array.from(str); +const numbers = Array.from({ length: 5 }, (_, i) => i + 1); + +// Of - Create array from arguments +const arr = Array.of(1, 2, 3); +``` + +### 2. Higher-Order Functions + +**Functions as Arguments:** + +```javascript +// Custom forEach +function forEach(array, callback) { + for (let i = 0; i < array.length; i++) { + callback(array[i], i, array); + } +} + +// Custom map +function map(array, transform) { + const result = []; + for (const item of array) { + result.push(transform(item)); + } + return result; +} + +// Custom filter +function filter(array, predicate) { + const result = []; + for (const item of array) { + if (predicate(item)) { + result.push(item); + } + } + return result; +} +``` + +**Functions Returning Functions:** + +```javascript +// Currying +const multiply = (a) => (b) => a * b; +const double = multiply(2); +const triple = multiply(3); + +console.log(double(5)); // 10 +console.log(triple(5)); // 15 + +// Partial application +function partial(fn, ...args) { + return (...moreArgs) => fn(...args, ...moreArgs); +} + +const add = (a, b, c) => a + b + c; +const add5 = partial(add, 5); +console.log(add5(3, 2)); // 10 + +// Memoization +function memoize(fn) { + const cache = new Map(); + return (...args) => { + const key = JSON.stringify(args); + if (cache.has(key)) { + return cache.get(key); + } + const result = fn(...args); + cache.set(key, result); + return result; + }; +} + +const fibonacci = memoize((n) => { + if (n <= 1) return n; + return fibonacci(n - 1) + fibonacci(n - 2); +}); +``` + +### 3. Composition and Piping + +```javascript +// Function composition +const compose = + (...fns) => + (x) => + fns.reduceRight((acc, fn) => fn(acc), x); + +const pipe = + (...fns) => + (x) => + fns.reduce((acc, fn) => fn(acc), x); + +// Example usage +const addOne = (x) => x + 1; +const double = (x) => x * 2; +const square = (x) => x * x; + +const composed = compose(square, double, addOne); +console.log(composed(3)); // ((3 + 1) * 2)^2 = 64 + +const piped = pipe(addOne, double, square); +console.log(piped(3)); // ((3 + 1) * 2)^2 = 64 + +// Practical example +const processUser = pipe( + (user) => ({ ...user, name: user.name.trim() }), + (user) => ({ ...user, email: user.email.toLowerCase() }), + (user) => ({ ...user, age: parseInt(user.age) }), +); + +const user = processUser({ + name: " John ", + email: "JOHN@EXAMPLE.COM", + age: "30", +}); +``` + +### 4. Pure Functions and Immutability + +```javascript +// Impure function (modifies input) +function addItemImpure(cart, item) { + cart.items.push(item); + cart.total += item.price; + return cart; +} + +// Pure function (no side effects) +function addItemPure(cart, item) { + return { + ...cart, + items: [...cart.items, item], + total: cart.total + item.price, + }; +} + +// Immutable array operations +const numbers = [1, 2, 3, 4, 5]; + +// Add to array +const withSix = [...numbers, 6]; + +// Remove from array +const withoutThree = numbers.filter((n) => n !== 3); + +// Update array element +const doubled = numbers.map((n) => (n === 3 ? n * 2 : n)); + +// Immutable object operations +const user = { name: "John", age: 30 }; + +// Update property +const olderUser = { ...user, age: 31 }; + +// Add property +const withEmail = { ...user, email: "john@example.com" }; + +// Remove property +const { age, ...withoutAge } = user; + +// Deep cloning (simple approach) +const deepClone = (obj) => JSON.parse(JSON.stringify(obj)); + +// Better deep cloning +const structuredClone = (obj) => globalThis.structuredClone(obj); +``` + +## Modern Class Features + +```javascript +// Class syntax +class User { + // Private fields + #password; + + // Public fields + id; + name; + + // Static field + static count = 0; + + constructor(id, name, password) { + this.id = id; + this.name = name; + this.#password = password; + User.count++; + } + + // Public method + greet() { + return `Hello, ${this.name}`; + } + + // Private method + #hashPassword(password) { + return `hashed_${password}`; + } + + // Getter + get displayName() { + return this.name.toUpperCase(); + } + + // Setter + set password(newPassword) { + this.#password = this.#hashPassword(newPassword); + } + + // Static method + static create(id, name, password) { + return new User(id, name, password); + } +} + +// Inheritance +class Admin extends User { + constructor(id, name, password, role) { + super(id, name, password); + this.role = role; + } + + greet() { + return `${super.greet()}, I'm an admin`; + } +} +``` + +## Modules (ES6) + +```javascript +// Exporting +// math.js +export const PI = 3.14159; +export function add(a, b) { + return a + b; +} +export class Calculator { + // ... +} + +// Default export +export default function multiply(a, b) { + return a * b; +} + +// Importing +// app.js +import multiply, { PI, add, Calculator } from "./math.js"; + +// Rename imports +import { add as sum } from "./math.js"; + +// Import all +import * as Math from "./math.js"; + +// Dynamic imports +const module = await import("./math.js"); +const { add } = await import("./math.js"); + +// Conditional loading +if (condition) { + const module = await import("./feature.js"); + module.init(); +} +``` + +## Iterators and Generators + +```javascript +// Custom iterator +const range = { + from: 1, + to: 5, + + [Symbol.iterator]() { + return { + current: this.from, + last: this.to, + + next() { + if (this.current <= this.last) { + return { done: false, value: this.current++ }; + } else { + return { done: true }; + } + }, + }; + }, +}; + +for (const num of range) { + console.log(num); // 1, 2, 3, 4, 5 +} + +// Generator function +function* rangeGenerator(from, to) { + for (let i = from; i <= to; i++) { + yield i; + } +} + +for (const num of rangeGenerator(1, 5)) { + console.log(num); +} + +// Infinite generator +function* fibonacci() { + let [prev, curr] = [0, 1]; + while (true) { + yield curr; + [prev, curr] = [curr, prev + curr]; + } +} + +// Async generator +async function* fetchPages(url) { + let page = 1; + while (true) { + const response = await fetch(`${url}?page=${page}`); + const data = await response.json(); + if (data.length === 0) break; + yield data; + page++; + } +} + +for await (const page of fetchPages("/api/users")) { + console.log(page); +} +``` + +## Modern Operators + +```javascript +// Optional chaining +const user = { name: "John", address: { city: "NYC" } }; +const city = user?.address?.city; +const zipCode = user?.address?.zipCode; // undefined + +// Function call +const result = obj.method?.(); + +// Array access +const first = arr?.[0]; + +// Nullish coalescing +const value = null ?? "default"; // 'default' +const value = undefined ?? "default"; // 'default' +const value = 0 ?? "default"; // 0 (not 'default') +const value = "" ?? "default"; // '' (not 'default') + +// Logical assignment +let a = null; +a ??= "default"; // a = 'default' + +let b = 5; +b ??= 10; // b = 5 (unchanged) + +let obj = { count: 0 }; +obj.count ||= 1; // obj.count = 1 +obj.count &&= 2; // obj.count = 2 +``` + +## Performance Optimization + +```javascript +// Debounce +function debounce(fn, delay) { + let timeoutId; + return (...args) => { + clearTimeout(timeoutId); + timeoutId = setTimeout(() => fn(...args), delay); + }; +} + +const searchDebounced = debounce(search, 300); + +// Throttle +function throttle(fn, limit) { + let inThrottle; + return (...args) => { + if (!inThrottle) { + fn(...args); + inThrottle = true; + setTimeout(() => (inThrottle = false), limit); + } + }; +} + +const scrollThrottled = throttle(handleScroll, 100); + +// Lazy evaluation +function* lazyMap(iterable, transform) { + for (const item of iterable) { + yield transform(item); + } +} + +// Use only what you need +const numbers = [1, 2, 3, 4, 5]; +const doubled = lazyMap(numbers, (x) => x * 2); +const first = doubled.next().value; // Only computes first value +``` + +## Common Pitfalls + +1. **this binding confusion**: Use arrow functions or bind() +2. **Async/await without error handling**: Always use try/catch +3. **Promise creation unnecessary**: Don't wrap already async functions +4. **Mutation of objects**: Use spread operator or Object.assign() +5. **Forgetting await**: Async functions return promises +6. **Blocking event loop**: Avoid synchronous operations +7. **Memory leaks**: Clean up event listeners and timers +8. **Not handling promise rejections**: Use catch() or try/catch diff --git a/.agents/skills/modern-javascript-patterns/references/details.md b/.agents/skills/modern-javascript-patterns/references/details.md new file mode 100644 index 0000000..eeed3fc --- /dev/null +++ b/.agents/skills/modern-javascript-patterns/references/details.md @@ -0,0 +1,457 @@ +# modern-javascript-patterns — detailed patterns and worked examples + +## ES6+ Core Features + +### 1. Arrow Functions + +**Syntax and Use Cases:** + +```javascript +// Traditional function +function add(a, b) { + return a + b; +} + +// Arrow function +const add = (a, b) => a + b; + +// Single parameter (parentheses optional) +const double = (x) => x * 2; + +// No parameters +const getRandom = () => Math.random(); + +// Multiple statements (need curly braces) +const processUser = (user) => { + const normalized = user.name.toLowerCase(); + return { ...user, name: normalized }; +}; + +// Returning objects (wrap in parentheses) +const createUser = (name, age) => ({ name, age }); +``` + +**Lexical 'this' Binding:** + +```javascript +class Counter { + constructor() { + this.count = 0; + } + + // Arrow function preserves 'this' context + increment = () => { + this.count++; + }; + + // Traditional function loses 'this' in callbacks + incrementTraditional() { + setTimeout(function () { + this.count++; // 'this' is undefined + }, 1000); + } + + // Arrow function maintains 'this' + incrementArrow() { + setTimeout(() => { + this.count++; // 'this' refers to Counter instance + }, 1000); + } +} +``` + +### 2. Destructuring + +**Object Destructuring:** + +```javascript +const user = { + id: 1, + name: "John Doe", + email: "john@example.com", + address: { + city: "New York", + country: "USA", + }, +}; + +// Basic destructuring +const { name, email } = user; + +// Rename variables +const { name: userName, email: userEmail } = user; + +// Default values +const { age = 25 } = user; + +// Nested destructuring +const { + address: { city, country }, +} = user; + +// Rest operator +const { id, ...userWithoutId } = user; + +// Function parameters +function greet({ name, age = 18 }) { + console.log(`Hello ${name}, you are ${age}`); +} +greet(user); +``` + +**Array Destructuring:** + +```javascript +const numbers = [1, 2, 3, 4, 5]; + +// Basic destructuring +const [first, second] = numbers; + +// Skip elements +const [, , third] = numbers; + +// Rest operator +const [head, ...tail] = numbers; + +// Swapping variables +let a = 1, + b = 2; +[a, b] = [b, a]; + +// Function return values +function getCoordinates() { + return [10, 20]; +} +const [x, y] = getCoordinates(); + +// Default values +const [one, two, three = 0] = [1, 2]; +``` + +### 3. Spread and Rest Operators + +**Spread Operator:** + +```javascript +// Array spreading +const arr1 = [1, 2, 3]; +const arr2 = [4, 5, 6]; +const combined = [...arr1, ...arr2]; + +// Object spreading +const defaults = { theme: "dark", lang: "en" }; +const userPrefs = { theme: "light" }; +const settings = { ...defaults, ...userPrefs }; + +// Function arguments +const numbers = [1, 2, 3]; +Math.max(...numbers); + +// Copying arrays/objects (shallow copy) +const copy = [...arr1]; +const objCopy = { ...user }; + +// Adding items immutably +const newArr = [...arr1, 4, 5]; +const newObj = { ...user, age: 30 }; +``` + +**Rest Parameters:** + +```javascript +// Collect function arguments +function sum(...numbers) { + return numbers.reduce((total, num) => total + num, 0); +} +sum(1, 2, 3, 4, 5); + +// With regular parameters +function greet(greeting, ...names) { + return `${greeting} ${names.join(", ")}`; +} +greet("Hello", "John", "Jane", "Bob"); + +// Object rest +const { id, ...userData } = user; + +// Array rest +const [first, ...rest] = [1, 2, 3, 4, 5]; +``` + +### 4. Template Literals + +```javascript +// Basic usage +const name = "John"; +const greeting = `Hello, ${name}!`; + +// Multi-line strings +const html = ` +
+

${title}

+

${content}

+
+`; + +// Expression evaluation +const price = 19.99; +const total = `Total: $${(price * 1.2).toFixed(2)}`; + +// Tagged template literals +function highlight(strings, ...values) { + return strings.reduce((result, str, i) => { + const value = values[i] || ""; + return result + str + `${value}`; + }, ""); +} + +const name = "John"; +const age = 30; +const html = highlight`Name: ${name}, Age: ${age}`; +// Output: "Name: John, Age: 30" +``` + +### 5. Enhanced Object Literals + +```javascript +const name = "John"; +const age = 30; + +// Shorthand property names +const user = { name, age }; + +// Shorthand method names +const calculator = { + add(a, b) { + return a + b; + }, + subtract(a, b) { + return a - b; + }, +}; + +// Computed property names +const field = "email"; +const user = { + name: "John", + [field]: "john@example.com", + [`get${field.charAt(0).toUpperCase()}${field.slice(1)}`]() { + return this[field]; + }, +}; + +// Dynamic property creation +const createUser = (name, ...props) => { + return props.reduce( + (user, [key, value]) => ({ + ...user, + [key]: value, + }), + { name }, + ); +}; + +const user = createUser("John", ["age", 30], ["email", "john@example.com"]); +``` + +## Asynchronous Patterns + +### 1. Promises + +**Creating and Using Promises:** + +```javascript +// Creating a promise +const fetchUser = (id) => { + return new Promise((resolve, reject) => { + setTimeout(() => { + if (id > 0) { + resolve({ id, name: "John" }); + } else { + reject(new Error("Invalid ID")); + } + }, 1000); + }); +}; + +// Using promises +fetchUser(1) + .then((user) => console.log(user)) + .catch((error) => console.error(error)) + .finally(() => console.log("Done")); + +// Chaining promises +fetchUser(1) + .then((user) => fetchUserPosts(user.id)) + .then((posts) => processPosts(posts)) + .then((result) => console.log(result)) + .catch((error) => console.error(error)); +``` + +**Promise Combinators:** + +```javascript +// Promise.all - Wait for all promises +const promises = [fetchUser(1), fetchUser(2), fetchUser(3)]; + +Promise.all(promises) + .then((users) => console.log(users)) + .catch((error) => console.error("At least one failed:", error)); + +// Promise.allSettled - Wait for all, regardless of outcome +Promise.allSettled(promises).then((results) => { + results.forEach((result) => { + if (result.status === "fulfilled") { + console.log("Success:", result.value); + } else { + console.log("Error:", result.reason); + } + }); +}); + +// Promise.race - First to complete +Promise.race(promises) + .then((winner) => console.log("First:", winner)) + .catch((error) => console.error(error)); + +// Promise.any - First to succeed +Promise.any(promises) + .then((first) => console.log("First success:", first)) + .catch((error) => console.error("All failed:", error)); +``` + +### 2. Async/Await + +**Basic Usage:** + +```javascript +// Async function always returns a Promise +async function fetchUser(id) { + const response = await fetch(`/api/users/${id}`); + const user = await response.json(); + return user; +} + +// Error handling with try/catch +async function getUserData(id) { + try { + const user = await fetchUser(id); + const posts = await fetchUserPosts(user.id); + return { user, posts }; + } catch (error) { + console.error("Error fetching data:", error); + throw error; + } +} + +// Sequential vs Parallel execution +async function sequential() { + const user1 = await fetchUser(1); // Wait + const user2 = await fetchUser(2); // Then wait + return [user1, user2]; +} + +async function parallel() { + const [user1, user2] = await Promise.all([fetchUser(1), fetchUser(2)]); + return [user1, user2]; +} +``` + +**Advanced Patterns:** + +```javascript +// Async IIFE +(async () => { + const result = await someAsyncOperation(); + console.log(result); +})(); + +// Async iteration +async function processUsers(userIds) { + for (const id of userIds) { + const user = await fetchUser(id); + await processUser(user); + } +} + +// Top-level await (ES2022) +const config = await fetch("/config.json").then((r) => r.json()); + +// Retry logic +async function fetchWithRetry(url, retries = 3) { + for (let i = 0; i < retries; i++) { + try { + return await fetch(url); + } catch (error) { + if (i === retries - 1) throw error; + await new Promise((resolve) => setTimeout(resolve, 1000 * (i + 1))); + } + } +} + +// Timeout wrapper +async function withTimeout(promise, ms) { + const timeout = new Promise((_, reject) => + setTimeout(() => reject(new Error("Timeout")), ms), + ); + return Promise.race([promise, timeout]); +} +``` + +## Functional Programming Patterns + +Functional programming in JavaScript centers on pure functions, immutability, and composable transformations. + +Key topics covered in [references/advanced-patterns.md](references/advanced-patterns.md): +- **Array methods** — `map`, `filter`, `reduce`, `find`, `findIndex`, `some`, `every`, `flatMap`, `Array.from` +- **Higher-order functions** — custom `forEach`/`map`/`filter`, currying, partial application, memoization +- **Composition and piping** — `compose`/`pipe` utilities with practical data transformation examples +- **Pure functions and immutability** — immutable array/object operations, deep cloning with `structuredClone` + +## Modern Class Features + +ES2022 classes support private fields (`#field`), static fields, getters/setters, and private methods. See [references/advanced-patterns.md](references/advanced-patterns.md) for a full example with inheritance. + +## Modules (ES6) + +```javascript +// Named exports +export const PI = 3.14159; +export function add(a, b) { return a + b; } + +// Default export +export default function multiply(a, b) { return a * b; } + +// Import +import multiply, { PI, add } from "./math.js"; + +// Dynamic import (code splitting) +const { add } = await import("./math.js"); +``` + +For re-exports, namespace imports, and conditional dynamic loading see [references/advanced-patterns.md](references/advanced-patterns.md). + +## Iterators and Generators + +Generators (`function*`) and async generators (`async function*`) enable lazy sequences and async pagination. See [references/advanced-patterns.md](references/advanced-patterns.md) for custom iterator, range generator, fibonacci, and `for await...of` examples. + +## Modern Operators + +```javascript +// Optional chaining — safe property access +const city = user?.address?.city; +const result = obj.method?.(); + +// Nullish coalescing — default only for null/undefined (not 0 or "") +const value = null ?? "default"; // 'default' +const zero = 0 ?? "default"; // 0 + +// Logical assignment +a ??= "default"; // assign if null/undefined +obj.count ||= 1; // assign if falsy +obj.count &&= 2; // assign if truthy +``` + +## Performance Optimization + +See [references/advanced-patterns.md](references/advanced-patterns.md) for debounce, throttle, and lazy evaluation with generators. diff --git a/.agents/skills/python-design-patterns/SKILL.md b/.agents/skills/python-design-patterns/SKILL.md new file mode 100644 index 0000000..6d34a15 --- /dev/null +++ b/.agents/skills/python-design-patterns/SKILL.md @@ -0,0 +1,85 @@ +--- +name: python-design-patterns +description: Python design patterns including KISS, Separation of Concerns, Single Responsibility, and composition over inheritance. Use this skill when designing a new service or component from scratch and choosing how to layer responsibilities, when refactoring a God class or monolithic function that has grown too large, when deciding whether to add a new abstraction or live with duplication, when evaluating a pull request for structural issues like tight coupling or leaking internal types, when choosing between inheritance and composition for a new class hierarchy, or when a codebase is becoming hard to test because of entangled I/O and business logic. +--- + +# Python Design Patterns + +Write maintainable Python code using fundamental design principles. These patterns help you build systems that are easy to understand, test, and modify. + +## When to Use This Skill + +- Designing new components or services +- Refactoring complex or tangled code +- Deciding whether to create an abstraction +- Choosing between inheritance and composition +- Evaluating code complexity and coupling +- Planning modular architectures + +## Core Concepts + +### 1. KISS (Keep It Simple) + +Choose the simplest solution that works. Complexity must be justified by concrete requirements. + +### 2. Single Responsibility (SRP) + +Each unit should have one reason to change. Separate concerns into focused components. + +### 3. Composition Over Inheritance + +Build behavior by combining objects, not extending classes. + +### 4. Rule of Three + +Wait until you have three instances before abstracting. Duplication is often better than premature abstraction. + +## Quick Start + +```python +# Simple beats clever +# Instead of a factory/registry pattern: +FORMATTERS = {"json": JsonFormatter, "csv": CsvFormatter} + +def get_formatter(name: str) -> Formatter: + return FORMATTERS[name]() +``` + +## Detailed patterns and worked examples + +Detailed pattern documentation lives in `references/details.md`. Read that file when the navigation tier above is insufficient. + +## Best Practices Summary + +1. **Keep it simple** - Choose the simplest solution that works +2. **Single responsibility** - Each unit has one reason to change +3. **Separate concerns** - Distinct layers with clear purposes +4. **Compose, don't inherit** - Combine objects for flexibility +5. **Rule of three** - Wait before abstracting +6. **Keep functions small** - 20-50 lines (varies by complexity), one purpose +7. **Inject dependencies** - Constructor injection for testability +8. **Delete before abstracting** - Remove dead code, then consider patterns +9. **Test each layer** - Isolated tests for each concern +10. **Explicit over clever** - Readable code beats elegant code + +## Troubleshooting + +**A class is growing and seems to have multiple responsibilities, but splitting it feels wrong.** +Apply the "reason to change" test: list every change that could require editing this class. If the list has items from different domains (e.g., HTTP parsing AND business rules AND formatting), split it. If all changes stem from the same domain concern, the class may be appropriately sized. + +**Injecting all dependencies through the constructor is producing constructors with 7+ parameters.** +This is a sign of too many responsibilities in one class, not a problem with dependency injection. Split the class into smaller units first, then each constructor naturally becomes smaller. + +**Composition is producing deeply nested wrapper objects that are hard to trace.** +Keep the composition shallow (2-3 levels). If wrapping is the only mechanism, consider whether a Protocol-based approach or simple function composition would be cleaner than a chain of decorator objects. + +**The rule of three says not to abstract yet, but the duplication is causing bugs when one copy is updated but not the other.** +Duplication that diverges in dangerous ways should be abstracted sooner. The rule of three is a heuristic, not a law. If the copies are already diverging incorrectly, extract immediately and add a test that exercises the shared behavior. + +**A service layer is importing from the API layer, breaking the dependency direction.** +This is a layering violation. The service layer must not import from handlers. Introduce a shared types/models layer that both can import from, keeping the dependency arrow pointing downward (API → Service → Repository). + +## Related Skills + +- [python-testing-patterns](../python-testing-patterns/SKILL.md) — Test each layer in isolation using the dependency injection structure established here +- [python-project-setup](../python-project-setup/SKILL.md) — Set up project structure and tooling that enforces layer boundaries from the start diff --git a/.agents/skills/python-design-patterns/references/details.md b/.agents/skills/python-design-patterns/references/details.md new file mode 100644 index 0000000..707d9c4 --- /dev/null +++ b/.agents/skills/python-design-patterns/references/details.md @@ -0,0 +1,353 @@ +# python-design-patterns — detailed patterns and worked examples + +## Fundamental Patterns + +### Pattern 1: KISS - Keep It Simple + +Before adding complexity, ask: does a simpler solution work? + +```python +# Over-engineered: Factory with registration +class OutputFormatterFactory: + _formatters: dict[str, type[Formatter]] = {} + + @classmethod + def register(cls, name: str): + def decorator(formatter_cls): + cls._formatters[name] = formatter_cls + return formatter_cls + return decorator + + @classmethod + def create(cls, name: str) -> Formatter: + return cls._formatters[name]() + +@OutputFormatterFactory.register("json") +class JsonFormatter(Formatter): + ... + +# Simple: Just use a dictionary +FORMATTERS = { + "json": JsonFormatter, + "csv": CsvFormatter, + "xml": XmlFormatter, +} + +def get_formatter(name: str) -> Formatter: + """Get formatter by name.""" + if name not in FORMATTERS: + raise ValueError(f"Unknown format: {name}") + return FORMATTERS[name]() +``` + +The factory pattern adds code without adding value here. Save patterns for when they solve real problems. + +### Pattern 2: Single Responsibility Principle + +Each class or function should have one reason to change. + +```python +# BAD: Handler does everything +class UserHandler: + async def create_user(self, request: Request) -> Response: + # HTTP parsing + data = await request.json() + + # Validation + if not data.get("email"): + return Response({"error": "email required"}, status=400) + + # Database access + user = await db.execute( + "INSERT INTO users (email, name) VALUES ($1, $2) RETURNING *", + data["email"], data["name"] + ) + + # Response formatting + return Response({"id": user.id, "email": user.email}, status=201) + +# GOOD: Separated concerns +class UserService: + """Business logic only.""" + + def __init__(self, repo: UserRepository) -> None: + self._repo = repo + + async def create_user(self, data: CreateUserInput) -> User: + # Only business rules here + user = User(email=data.email, name=data.name) + return await self._repo.save(user) + +class UserHandler: + """HTTP concerns only.""" + + def __init__(self, service: UserService) -> None: + self._service = service + + async def create_user(self, request: Request) -> Response: + data = CreateUserInput(**(await request.json())) + user = await self._service.create_user(data) + return Response(user.to_dict(), status=201) +``` + +Now HTTP changes don't affect business logic, and vice versa. + +### Pattern 3: Separation of Concerns + +Organize code into distinct layers with clear responsibilities. + +``` +┌─────────────────────────────────────────────────────┐ +│ API Layer (handlers) │ +│ - Parse requests │ +│ - Call services │ +│ - Format responses │ +└─────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────┐ +│ Service Layer (business logic) │ +│ - Domain rules and validation │ +│ - Orchestrate operations │ +│ - Pure functions where possible │ +└─────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────┐ +│ Repository Layer (data access) │ +│ - SQL queries │ +│ - External API calls │ +│ - Cache operations │ +└─────────────────────────────────────────────────────┘ +``` + +Each layer depends only on layers below it: + +```python +# Repository: Data access +class UserRepository: + async def get_by_id(self, user_id: str) -> User | None: + row = await self._db.fetchrow( + "SELECT * FROM users WHERE id = $1", user_id + ) + return User(**row) if row else None + +# Service: Business logic +class UserService: + def __init__(self, repo: UserRepository) -> None: + self._repo = repo + + async def get_user(self, user_id: str) -> User: + user = await self._repo.get_by_id(user_id) + if user is None: + raise UserNotFoundError(user_id) + return user + +# Handler: HTTP concerns +@app.get("/users/{user_id}") +async def get_user(user_id: str) -> UserResponse: + user = await user_service.get_user(user_id) + return UserResponse.from_user(user) +``` + +### Pattern 4: Composition Over Inheritance + +Build behavior by combining objects rather than inheriting. + +```python +# Inheritance: Rigid and hard to test +class EmailNotificationService(NotificationService): + def __init__(self): + super().__init__() + self._smtp = SmtpClient() # Hard to mock + + def notify(self, user: User, message: str) -> None: + self._smtp.send(user.email, message) + +# Composition: Flexible and testable +class NotificationService: + """Send notifications via multiple channels.""" + + def __init__( + self, + email_sender: EmailSender, + sms_sender: SmsSender | None = None, + push_sender: PushSender | None = None, + ) -> None: + self._email = email_sender + self._sms = sms_sender + self._push = push_sender + + async def notify( + self, + user: User, + message: str, + channels: set[str] | None = None, + ) -> None: + channels = channels or {"email"} + + if "email" in channels: + await self._email.send(user.email, message) + + if "sms" in channels and self._sms and user.phone: + await self._sms.send(user.phone, message) + + if "push" in channels and self._push and user.device_token: + await self._push.send(user.device_token, message) + +# Easy to test with fakes +service = NotificationService( + email_sender=FakeEmailSender(), + sms_sender=FakeSmsSender(), +) +``` + +## Advanced Patterns + +### Pattern 5: Rule of Three + +Wait until you have three instances before abstracting. + +```python +# Two similar functions? Don't abstract yet +def process_orders(orders: list[Order]) -> list[Result]: + results = [] + for order in orders: + validated = validate_order(order) + result = process_validated_order(validated) + results.append(result) + return results + +def process_returns(returns: list[Return]) -> list[Result]: + results = [] + for ret in returns: + validated = validate_return(ret) + result = process_validated_return(validated) + results.append(result) + return results + +# These look similar, but wait! Are they actually the same? +# Different validation, different processing, different errors... +# Duplication is often better than the wrong abstraction + +# Only after a third case, consider if there's a real pattern +# But even then, sometimes explicit is better than abstract +``` + +### Pattern 6: Function Size Guidelines + +Keep functions focused. Extract when a function: + +- Exceeds 20-50 lines (varies by complexity) +- Serves multiple distinct purposes +- Has deeply nested logic (3+ levels) + +```python +# Too long, multiple concerns mixed +def process_order(order: Order) -> Result: + # 50 lines of validation... + # 30 lines of inventory check... + # 40 lines of payment processing... + # 20 lines of notification... + pass + +# Better: Composed from focused functions +def process_order(order: Order) -> Result: + """Process a customer order through the complete workflow.""" + validate_order(order) + reserve_inventory(order) + payment_result = charge_payment(order) + send_confirmation(order, payment_result) + return Result(success=True, order_id=order.id) +``` + +### Pattern 7: Dependency Injection + +Pass dependencies through constructors for testability. + +```python +from typing import Protocol + +class Logger(Protocol): + def info(self, msg: str, **kwargs) -> None: ... + def error(self, msg: str, **kwargs) -> None: ... + +class Cache(Protocol): + async def get(self, key: str) -> str | None: ... + async def set(self, key: str, value: str, ttl: int) -> None: ... + +class UserService: + """Service with injected dependencies.""" + + def __init__( + self, + repository: UserRepository, + cache: Cache, + logger: Logger, + ) -> None: + self._repo = repository + self._cache = cache + self._logger = logger + + async def get_user(self, user_id: str) -> User: + # Check cache first + cached = await self._cache.get(f"user:{user_id}") + if cached: + self._logger.info("Cache hit", user_id=user_id) + return User.from_json(cached) + + # Fetch from database + user = await self._repo.get_by_id(user_id) + if user: + await self._cache.set(f"user:{user_id}", user.to_json(), ttl=300) + + return user + +# Production +service = UserService( + repository=PostgresUserRepository(db), + cache=RedisCache(redis), + logger=StructlogLogger(), +) + +# Testing +service = UserService( + repository=InMemoryUserRepository(), + cache=FakeCache(), + logger=NullLogger(), +) +``` + +### Pattern 8: Avoiding Common Anti-Patterns + +**Don't expose internal types:** + +```python +# BAD: Leaking ORM model to API +@app.get("/users/{id}") +def get_user(id: str) -> UserModel: # SQLAlchemy model + return db.query(UserModel).get(id) + +# GOOD: Use response schemas +@app.get("/users/{id}") +def get_user(id: str) -> UserResponse: + user = db.query(UserModel).get(id) + return UserResponse.from_orm(user) +``` + +**Don't mix I/O with business logic:** + +```python +# BAD: SQL embedded in business logic +def calculate_discount(user_id: str) -> float: + user = db.query("SELECT * FROM users WHERE id = ?", user_id) + orders = db.query("SELECT * FROM orders WHERE user_id = ?", user_id) + # Business logic mixed with data access + +# GOOD: Repository pattern +def calculate_discount(user: User, order_history: list[Order]) -> float: + # Pure business logic, easily testable + if len(order_history) > 10: + return 0.15 + return 0.0 +``` diff --git a/.agents/skills/python-observability/SKILL.md b/.agents/skills/python-observability/SKILL.md new file mode 100644 index 0000000..5d3b9c8 --- /dev/null +++ b/.agents/skills/python-observability/SKILL.md @@ -0,0 +1,229 @@ +--- +name: python-observability +description: Python observability patterns including structured logging, metrics, and distributed tracing. Use when adding logging, implementing metrics collection, setting up tracing, or debugging production systems. +--- + +# Python Observability + +Instrument Python applications with structured logs, metrics, and traces. When something breaks in production, you need to answer "what, where, and why" without deploying new code. + +## When to Use This Skill + +- Adding structured logging to applications +- Implementing metrics collection with Prometheus +- Setting up distributed tracing across services +- Propagating correlation IDs through request chains +- Debugging production issues +- Building observability dashboards + +## Core Concepts + +### 1. Structured Logging + +Emit logs as JSON with consistent fields for production environments. Machine-readable logs enable powerful queries and alerts. For local development, consider human-readable formats. + +### 2. The Four Golden Signals + +Track latency, traffic, errors, and saturation for every service boundary. + +### 3. Correlation IDs + +Thread a unique ID through all logs and spans for a single request, enabling end-to-end tracing. + +### 4. Bounded Cardinality + +Keep metric label values bounded. Unbounded labels (like user IDs) explode storage costs. + +## Quick Start + +```python +import structlog + +structlog.configure( + processors=[ + structlog.processors.TimeStamper(fmt="iso"), + structlog.processors.JSONRenderer(), + ], +) + +logger = structlog.get_logger() +logger.info("Request processed", user_id="123", duration_ms=45) +``` + +## Fundamental Patterns + +### Pattern 1: Structured Logging with Structlog + +Configure structlog for JSON output with consistent fields. + +```python +import logging +import structlog + +def configure_logging(log_level: str = "INFO") -> None: + """Configure structured logging for the application.""" + structlog.configure( + processors=[ + structlog.contextvars.merge_contextvars, + structlog.processors.add_log_level, + structlog.processors.TimeStamper(fmt="iso"), + structlog.processors.StackInfoRenderer(), + structlog.processors.format_exc_info, + structlog.processors.JSONRenderer(), + ], + wrapper_class=structlog.make_filtering_bound_logger( + getattr(logging, log_level.upper()) + ), + context_class=dict, + logger_factory=structlog.PrintLoggerFactory(), + cache_logger_on_first_use=True, + ) + +# Initialize at application startup +configure_logging("INFO") +logger = structlog.get_logger() +``` + +### Pattern 2: Consistent Log Fields + +Every log entry should include standard fields for filtering and correlation. + +```python +import structlog +from contextvars import ContextVar + +# Store correlation ID in context +correlation_id: ContextVar[str] = ContextVar("correlation_id", default="") + +logger = structlog.get_logger() + +def process_request(request: Request) -> Response: + """Process request with structured logging.""" + logger.info( + "Request received", + correlation_id=correlation_id.get(), + method=request.method, + path=request.path, + user_id=request.user_id, + ) + + try: + result = handle_request(request) + logger.info( + "Request completed", + correlation_id=correlation_id.get(), + status_code=200, + duration_ms=elapsed, + ) + return result + except Exception as e: + logger.error( + "Request failed", + correlation_id=correlation_id.get(), + error_type=type(e).__name__, + error_message=str(e), + ) + raise +``` + +### Pattern 3: Semantic Log Levels + +Use log levels consistently across the application. + +| Level | Purpose | Examples | +|-------|---------|----------| +| `DEBUG` | Development diagnostics | Variable values, internal state | +| `INFO` | Request lifecycle, operations | Request start/end, job completion | +| `WARNING` | Recoverable anomalies | Retry attempts, fallback used | +| `ERROR` | Failures needing attention | Exceptions, service unavailable | + +```python +# DEBUG: Detailed internal information +logger.debug("Cache lookup", key=cache_key, hit=cache_hit) + +# INFO: Normal operational events +logger.info("Order created", order_id=order.id, total=order.total) + +# WARNING: Abnormal but handled situations +logger.warning( + "Rate limit approaching", + current_rate=950, + limit=1000, + reset_seconds=30, +) + +# ERROR: Failures requiring investigation +logger.error( + "Payment processing failed", + order_id=order.id, + error=str(e), + payment_provider="stripe", +) +``` + +Never log expected behavior at `ERROR`. A user entering a wrong password is `INFO`, not `ERROR`. + +### Pattern 4: Correlation ID Propagation + +Generate a unique ID at ingress and thread it through all operations. + +```python +from contextvars import ContextVar +import uuid +import structlog + +correlation_id: ContextVar[str] = ContextVar("correlation_id", default="") + +def set_correlation_id(cid: str | None = None) -> str: + """Set correlation ID for current context.""" + cid = cid or str(uuid.uuid4()) + correlation_id.set(cid) + structlog.contextvars.bind_contextvars(correlation_id=cid) + return cid + +# FastAPI middleware example +from fastapi import Request + +async def correlation_middleware(request: Request, call_next): + """Middleware to set and propagate correlation ID.""" + # Use incoming header or generate new + cid = request.headers.get("X-Correlation-ID") or str(uuid.uuid4()) + set_correlation_id(cid) + + response = await call_next(request) + response.headers["X-Correlation-ID"] = cid + return response +``` + +Propagate to outbound requests: + +```python +import httpx + +async def call_downstream_service(endpoint: str, data: dict) -> dict: + """Call downstream service with correlation ID.""" + async with httpx.AsyncClient() as client: + response = await client.post( + endpoint, + json=data, + headers={"X-Correlation-ID": correlation_id.get()}, + ) + return response.json() +``` + +## Detailed worked examples and patterns + +Detailed sections (starting with `## Advanced Patterns`) live in `references/details.md`. Read that file when the navigation summary above is insufficient. + +## Best Practices Summary + +1. **Use structured logging** - JSON logs with consistent fields +2. **Propagate correlation IDs** - Thread through all requests and logs +3. **Track the four golden signals** - Latency, traffic, errors, saturation +4. **Bound label cardinality** - Never use unbounded values as metric labels +5. **Log at appropriate levels** - Don't cry wolf with ERROR +6. **Include context** - User ID, request ID, operation name in logs +7. **Use context managers** - Consistent timing and error handling +8. **Separate concerns** - Observability code shouldn't pollute business logic +9. **Test your observability** - Verify logs and metrics in integration tests +10. **Set up alerts** - Metrics are useless without alerting diff --git a/.agents/skills/python-observability/references/details.md b/.agents/skills/python-observability/references/details.md new file mode 100644 index 0000000..b5aeff0 --- /dev/null +++ b/.agents/skills/python-observability/references/details.md @@ -0,0 +1,176 @@ +# python-observability — detailed worked examples + +## Advanced Patterns + +### Pattern 5: The Four Golden Signals with Prometheus + +Track these metrics for every service boundary: + +```python +from prometheus_client import Counter, Histogram, Gauge + +# Latency: How long requests take +REQUEST_LATENCY = Histogram( + "http_request_duration_seconds", + "Request latency in seconds", + ["method", "endpoint", "status"], + buckets=[0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10], +) + +# Traffic: Request rate +REQUEST_COUNT = Counter( + "http_requests_total", + "Total HTTP requests", + ["method", "endpoint", "status"], +) + +# Errors: Error rate +ERROR_COUNT = Counter( + "http_errors_total", + "Total HTTP errors", + ["method", "endpoint", "error_type"], +) + +# Saturation: Resource utilization +DB_POOL_USAGE = Gauge( + "db_connection_pool_used", + "Number of database connections in use", +) +``` + +Instrument your endpoints: + +```python +import time +from functools import wraps + +def track_request(func): + """Decorator to track request metrics.""" + @wraps(func) + async def wrapper(request: Request, *args, **kwargs): + method = request.method + endpoint = request.url.path + start = time.perf_counter() + + try: + response = await func(request, *args, **kwargs) + status = str(response.status_code) + return response + except Exception as e: + status = "500" + ERROR_COUNT.labels( + method=method, + endpoint=endpoint, + error_type=type(e).__name__, + ).inc() + raise + finally: + duration = time.perf_counter() - start + REQUEST_COUNT.labels(method=method, endpoint=endpoint, status=status).inc() + REQUEST_LATENCY.labels(method=method, endpoint=endpoint, status=status).observe(duration) + + return wrapper +``` + +### Pattern 6: Bounded Cardinality + +Avoid labels with unbounded values to prevent metric explosion. + +```python +# BAD: User ID has potentially millions of values +REQUEST_COUNT.labels(method="GET", user_id=user.id) # Don't do this! + +# GOOD: Bounded values only +REQUEST_COUNT.labels(method="GET", endpoint="/users", status="200") + +# If you need per-user metrics, use a different approach: +# - Log the user_id and query logs +# - Use a separate analytics system +# - Bucket users by type/tier +REQUEST_COUNT.labels( + method="GET", + endpoint="/users", + user_tier="premium", # Bounded set of values +) +``` + +### Pattern 7: Timed Operations with Context Manager + +Create a reusable timing context manager for operations. + +```python +from contextlib import contextmanager +import time +import structlog + +logger = structlog.get_logger() + +@contextmanager +def timed_operation(name: str, **extra_fields): + """Context manager for timing and logging operations.""" + start = time.perf_counter() + logger.debug("Operation started", operation=name, **extra_fields) + + try: + yield + except Exception as e: + elapsed_ms = (time.perf_counter() - start) * 1000 + logger.error( + "Operation failed", + operation=name, + duration_ms=round(elapsed_ms, 2), + error=str(e), + **extra_fields, + ) + raise + else: + elapsed_ms = (time.perf_counter() - start) * 1000 + logger.info( + "Operation completed", + operation=name, + duration_ms=round(elapsed_ms, 2), + **extra_fields, + ) + +# Usage +with timed_operation("fetch_user_orders", user_id=user.id): + orders = await order_repository.get_by_user(user.id) +``` + +### Pattern 8: OpenTelemetry Tracing + +Set up distributed tracing with OpenTelemetry. + +**Note:** OpenTelemetry is actively evolving. Check the [official Python documentation](https://opentelemetry.io/docs/languages/python/) for the latest API patterns and best practices. + +```python +from opentelemetry import trace +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import BatchSpanProcessor +from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter + +def configure_tracing(service_name: str, otlp_endpoint: str) -> None: + """Configure OpenTelemetry tracing.""" + provider = TracerProvider() + processor = BatchSpanProcessor(OTLPSpanExporter(endpoint=otlp_endpoint)) + provider.add_span_processor(processor) + trace.set_tracer_provider(provider) + +tracer = trace.get_tracer(__name__) + +async def process_order(order_id: str) -> Order: + """Process order with tracing.""" + with tracer.start_as_current_span("process_order") as span: + span.set_attribute("order.id", order_id) + + with tracer.start_as_current_span("validate_order"): + validate_order(order_id) + + with tracer.start_as_current_span("charge_payment"): + charge_payment(order_id) + + with tracer.start_as_current_span("send_confirmation"): + send_confirmation(order_id) + + return order +``` diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..55b942a --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,90 @@ +# AGENTS.md - Climate Shield + +## Quick Start + +```bash +# Backend (Flask, serves frontend + API) +python backend/alertsystem.py +# Runs on http://127.0.0.1:5000 + +# Frontend dev server (static only, no API) +npm run dev +# Runs on http://127.0.0.1:8080 +``` + +## Setup Requirements + +1. Copy `.env.example` to `.env` and add your `OPENWEATHER_API_KEY` +2. Install Python deps: `pip install flask flask-cors requests python-dotenv gunicorn` +3. Frontend dev: `npm install` (optional, only for `npm run dev`) + +## Architecture + +``` +Frontend (HTML/CSS/JS) → Flask backend (alertsystem.py) → OpenWeatherMap API + ↓ + AI-chatbot/chatbot.py (rule-based, no LLM) +``` + +- `backend/alertsystem.py` is the **single entrypoint** — serves frontend files AND API routes +- Chatbot is imported into backend via `sys.path.insert` (see `backend/alertsystem.py:68-71`) +- Frontend resolves API URL dynamically based on hostname (`Frontend/script.js:68-84`) + +## Key Files + +| File | Role | +|------|------| +| `backend/alertsystem.py` | Flask app, all API routes, risk calculations, frontend serving | +| `AI-chatbot/chatbot.py` | Rule-based chatbot with topic detection | +| `Frontend/script.js` | Main frontend logic, API calls, Leaflet map, Chart.js | +| `Frontend/Analysis/analysis.js` | Analysis page with charts and maps | +| `backend/test_alertsystem.py` | Only test file (pytest, tests GIS alert error handling) | + +## API Routes + +- `POST /weather` — city/state/country → weather data + risk scores + forecast +- `POST /chatbot` — message + optional context → chatbot response +- `POST /reverse-geocode` — lat/lon → city/state/country +- `GET /city-suggestions?q=` — autocomplete (limited to India: `countrycodes: in`) + +## Risk Thresholds + +Defined in `backend/alertsystem.py:104-108`: +- Flood: 0.65, Heat: 0.75, Wildfire: 0.65, Cyclone: 0.60, Drought: 0.70 + +Alerts trigger when score ≥ 0.6 (hardcoded in `/weather` route, differs from constants). + +## Testing + +```bash +pytest backend/test_alertsystem.py +``` + +No test runner config, no CI workflows, no linting setup. Tests use `unittest.mock.patch`. + +## Gotchas + +- **No `requirements.txt`** — README references it but it doesn't exist. Install deps manually. +- **Duplicate `fetch_gis_alert_data`** — defined twice in `alertsystem.py` (lines 11 and 178). Second definition wins. +- **Frontend serves from Flask** — when running `python backend/alertsystem.py`, open `http://127.0.0.1:5000` (not the HTML file directly). +- **Chatbot port** — standalone chatbot runs on port 5001, but when imported into backend it uses the backend's port. +- **City suggestions** — hardcoded to India (`countrycodes: "in"` in `alertsystem.py:628`). + +## Installed Skills + +| Skill | Use Case | +|-------|----------| +| `conventional-commit` | Write standardized git commit messages following the Conventional Commits spec | +| `flask-api-development` | Build Flask routes, blueprints, auth, and request/response handling | +| `modern-javascript-patterns` | Use ES6+ features like async/await, destructuring, modules, and functional patterns | +| `python-design-patterns` | Structure Python code with KISS, single responsibility, and composition principles | +| `python-observability` | Add structured logging, metrics, and tracing to debug production issues | + +## Deployment (Render) + +```yaml +buildCommand: pip install -r requirements.txt +startCommand: gunicorn backend.alertsystem:app +``` + +Requires `gunicorn` in dependencies and `OPENWEATHER_API_KEY` env var. diff --git a/skills-lock.json b/skills-lock.json new file mode 100644 index 0000000..0d8ad6d --- /dev/null +++ b/skills-lock.json @@ -0,0 +1,35 @@ +{ + "version": 1, + "skills": { + "conventional-commit": { + "source": "github/awesome-copilot", + "sourceType": "github", + "skillPath": "skills/conventional-commit/SKILL.md", + "computedHash": "f29c9486cede6c7b2df0cfb0a2e4aa67446552b991bcf17d1b309e903171f03d" + }, + "flask-api-development": { + "source": "aj-geddes/useful-ai-prompts", + "sourceType": "github", + "skillPath": "skills/flask-api-development/SKILL.md", + "computedHash": "9e5213f9fa4bde4175b717ab679d5a46497bd9edfff6673724a05320c741c6a9" + }, + "modern-javascript-patterns": { + "source": "wshobson/agents", + "sourceType": "github", + "skillPath": "plugins/javascript-typescript/skills/modern-javascript-patterns/SKILL.md", + "computedHash": "0d820adbeaa73ee1f6b1e075fb3ef1698d5d68ad59cb30fc935c857025e9b24f" + }, + "python-design-patterns": { + "source": "wshobson/agents", + "sourceType": "github", + "skillPath": "plugins/python-development/skills/python-design-patterns/SKILL.md", + "computedHash": "44976b88bb4b7e4a6ee51850a756e430edc68d5f65f0a7a083ea2d9bb0281237" + }, + "python-observability": { + "source": "wshobson/agents", + "sourceType": "github", + "skillPath": "plugins/python-development/skills/python-observability/SKILL.md", + "computedHash": "08bae7ce0b5bfccb6c882345969d0e7cd87ff5b72f14f65a6b2ace019189076f" + } + } +}