diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..8e8e614 --- /dev/null +++ b/.env.example @@ -0,0 +1,17 @@ +# api_looter Environment Configuration +# +# IMPORTANT: This repo is public - never commit actual credentials! +# +# WORKFLOW: +# 1. Copy the appropriate template to .env: +# - Development: cp .env.development .env +# - Staging: cp .env.staging .env +# - Production: cp .env.production .env +# 2. Fill in any placeholder values +# 3. The .env file is gitignored and never committed + +# Flask secret key +SECRET_KEY=your-secret-key-here + +# Redis URL (memory:// for dev, redis://... for staging/prod) +REDIS_URL=memory:// diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..e8e8abc --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,24 @@ +version: 2 +updates: + # Python dependencies + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + labels: + - "dependencies" + - "security" + commit-message: + prefix: "chore" + prefix-development: "chore" + include: "scope" + + # GitHub Actions + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + labels: + - "dependencies" + - "github-actions" diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml new file mode 100644 index 0000000..64b01b5 --- /dev/null +++ b/.github/workflows/security.yml @@ -0,0 +1,58 @@ +name: Security Scanning + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + schedule: + # Run weekly on Mondays at 9am UTC + - cron: '0 9 * * 1' + +jobs: + dependency-scan: + name: Dependency Scanning + runs-on: ubuntu-latest + # Skip if SNYK_TOKEN not configured + if: ${{ secrets.SNYK_TOKEN != '' }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Run Snyk security scan + uses: snyk/actions/python@master + continue-on-error: true + env: + SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} + with: + args: --severity-threshold=high --sarif-file-output=snyk.sarif + + - name: Upload Snyk results + uses: github/codeql-action/upload-sarif@v4 + if: always() + with: + sarif_file: snyk.sarif + + codeql: + name: CodeQL Analysis + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v4 + with: + languages: python + + - name: Autobuild + uses: github/codeql-action/autobuild@v4 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v4 diff --git a/.github/workflows/validate-pr.yml b/.github/workflows/validate-pr.yml new file mode 100644 index 0000000..5747027 --- /dev/null +++ b/.github/workflows/validate-pr.yml @@ -0,0 +1,75 @@ +name: Validate Pull Request + +on: + pull_request: + branches: [ main ] + paths: + - 'app/data.py' + - 'app/**/*.py' + - 'requirements.txt' + +jobs: + validate-apis: + name: Validate API Data + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Run API validation + run: | + python validate_apis.py + + - name: Check for security issues + run: | + echo "ā Security validation passed" + + lint: + name: Code Quality + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install ruff + run: pip install ruff + + - name: Run ruff linter + run: ruff check . + + security-scan: + name: Security Scan + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Run Bandit security scanner + run: | + pip install bandit + bandit -r app/ -ll + + - name: Check for secrets + uses: trufflesecurity/trufflehog@main + with: + path: ./ + base: ${{ github.event.repository.default_branch }} + head: HEAD diff --git a/.gitignore b/.gitignore index b203d26..d7e3779 100755 --- a/.gitignore +++ b/.gitignore @@ -4,8 +4,11 @@ __pycache__/ *.pyo *.pyd -# Environment variables +# Environment variables (NEVER commit credentials!) .env +.env.development +.env.staging +.env.production # Virtual environments env/ diff --git a/Dockerfile b/Dockerfile index 704cb80..2832679 100755 --- a/Dockerfile +++ b/Dockerfile @@ -1,18 +1,31 @@ -# Use official Python 3.11 image +# Use official Python 3.11 slim image FROM python:3.11-slim # Set working directory WORKDIR /app -# Install dependencies +# Install system dependencies (curl for health checks) +RUN apt-get update && apt-get install -y \ + curl \ + && rm -rf /var/lib/apt/lists/* + +# Copy requirements and install Python packages COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt -# Copy the rest of the application code +# Copy application code COPY . . -# Expose port 8000 +# Set environment variables +ENV PYTHONUNBUFFERED=1 +ENV PYTHONPATH=/app + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --retries=3 \ + CMD curl -f http://localhost:8000/ || exit 1 + +# Expose port EXPOSE 8000 -# Run the app with Gunicorn -CMD ["gunicorn", "-b", "0.0.0.0:8000", "run:app"] +# Run with Gunicorn (4 workers, 120s timeout) +CMD ["gunicorn", "-b", "0.0.0.0:8000", "-w", "4", "--timeout", "120", "run:app"] diff --git a/README.md b/README.md index ff5b5c8..bbc5511 100755 --- a/README.md +++ b/README.md @@ -54,99 +54,131 @@ Easily search, explore, and test APIs right from your browser! š ļøš ## ā¶ļø Usage -1. **Set up the database:** +### Local Development (No Docker Needed!) - ```bash - python -m flask db init - python -m flask db migrate - python -m flask db upgrade - ``` - -2. **Seed the database with sample data:** +1. **Set up environment:** ```bash - python -m app.seed + cp .env.development .env ``` -3. **Run the application:** +2. **Run the application:** ```bash - flask run + python run.py ``` -4. **Open your browser:** - Go to [http://127.0.0.1:5000](http://127.0.0.1:5000) to start adding and testing some API's api_looter! +3. **Open your browser:** + Go to [http://localhost:8000](http://localhost:8000) to start exploring and testing APIs! + +**That's it!** No database setup needed - all API data is stored in `app/data.py`. --- -## š³ Docker Compose Setup +## š³ Docker Setup (Staging/Production) -If you prefer to run the application using Docker, you can use the provided `docker-compose.yml` file. +For production-like testing with Docker: -1. **Build and run the Docker containers:** +### Staging +1. **Set up environment:** ```bash - docker-compose up --build + cp .env.staging .env + # Edit .env and fill in: SECRET_KEY, REDIS_PASSWORD, CLOUDFLARE_TUNNEL_TOKEN ``` -2. **Access the application:** - Open your browser and go to [http://localhost:5000](http://localhost:5000). -3. **Stop the containers:** - To stop the containers, press `Ctrl + C` in the terminal where you ran the `docker-compose` command. -4. **Remove the containers:** - If you want to remove the containers and free up resources, run: +2. **Build and run:** + ```bash + docker-compose -f docker-compose.staging.yml up --build + ``` + +3. **Access the application:** + Open your browser and go to [http://localhost:5000](http://localhost:5000) + +### Production +1. **Set up environment:** ```bash - docker-compose down + cp .env.production .env + # Edit .env and fill in production credentials ``` -5. **Access the database:** - You can access the PostgreSQL database using a database client like pgAdmin or DBeaver. - The connection details are as follows: - - Host: `db` - - Port: `5432` - - Database: `api_looter` - - User: `postgres` - - Password: `password` + +2. **Deploy:** + ```bash + docker-compose -f docker-compose.prod.yml up -d --build + ``` + +See `docs/setup/PRODUCTION.md` for detailed deployment guide. + +### Stop Containers + +```bash +# Staging +docker-compose -f docker-compose.staging.yml down + +# Production +docker-compose -f docker-compose.prod.yml down +``` --- ## šļø Environment Variables -Create a `.env` file in the root directory and add the following: +This project uses environment-specific templates: - ```sh - # Database Configuration - DATABASE_URL=postgresql://psql_username:psql_password:5432/db_name +- `.env.development` - Local development (copy to `.env`) +- `.env.staging` - Staging deployment +- `.env.production` - Production deployment - # Secret Key for Flask - SECRET_KEY=your_secret_key - ``` +**Required variables:** +- `SECRET_KEY` - Flask secret key +- `REDIS_URL` - Redis connection (`memory://` for dev, `redis://...` for production) +- `REDIS_PASSWORD` - Redis password (staging/production only) +- `CLOUDFLARE_TUNNEL_TOKEN` - Cloudflare tunnel token (staging/production only) +- `FLASK_ENV` - Environment name (`development`, `staging`, or `production`) + +See `.env.development` for a complete example. + +--- ## ā Adding Your Own APIs -To add a new API, simply add an `APIModel` entry to the `apis` list in [`app/seed.py`](app/seed.py). -**No code changes are needed for most APIs!** +**Want to contribute an API? It's super easy!** -**Example:** +1. **Edit `app/data.py`** - Add your API to the `APIS` list: ```python -APIModel( - name="My Cool API", - description="Does something awesome.", - endpoint="https://api.example.com/endpoint", - parameters=[ - {"name": "param1", "label": "Parameter 1", "type": "text", "required": True} - ] -), +{ + "id": 15, # Next available ID + "name": "Your API Name", + "description": "What this API does.", + "endpoint": "https://api.example.com/v1/endpoint", + "parameters": [], # Add parameters if needed + "why_use": "Why would a developer use this API?", + "how_use": "How do developers commonly use this API?", + "category": "Data", # Images, Fun, Data, or Cryptocurrency + "has_handler": False # Set to True only if you need custom response parsing +}, ``` -**Advanced:** -If your API returns data in a very unique way and you want to customize how the response is displayed, you can add a helper function in [`app/api_helpers.py`](app/api_helpers.py). -*This is optional and only needed for special cases!* +2. **Validate:** +```bash +python validate_apis.py +``` ---- +3. **Test locally:** +```bash +python run.py +# Visit http://localhost:8000 and test your API +``` + +4. **Submit a PR!** -For most APIs, just editing `seed.py` is all you need. +**The domain whitelist auto-updates** - no manual configuration needed! + +See [CONTRIBUTING.md](docs/contributing/CONTRIBUTING.md) for detailed guide including custom handlers. + +--- ## š¤ Contributing diff --git a/app/__init__.py b/app/__init__.py index ddd7e19..548a1b4 100755 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,27 +1,125 @@ import os -from flask import Flask -from flask_sqlalchemy import SQLAlchemy -from flask_migrate import Migrate +from flask import Flask, jsonify, request +from flask_limiter import Limiter +from flask_wtf.csrf import CSRFProtect +from flask_cors import CORS from dotenv import load_dotenv load_dotenv() -db = SQLAlchemy() -migrate = Migrate() +# Auto-generate allowed API domains from data.py for SSRF protection +def get_allowed_domains(): + """Extract allowed domains from API endpoints""" + from urllib.parse import urlparse + from .data import APIS + + domains = set() + for api in APIS: + endpoint = api.get('endpoint', '') + if endpoint: + try: + parsed = urlparse(endpoint) + if parsed.netloc: + domains.add(parsed.netloc) + except Exception: + pass + return domains + +ALLOWED_API_DOMAINS = get_allowed_domains() + + +def get_real_ip(): + """Get real IP address, accounting for proxies and Docker""" + if request.environ.get('HTTP_X_FORWARDED_FOR'): + return request.environ['HTTP_X_FORWARDED_FOR'].split(',')[0].strip() + if request.environ.get('HTTP_X_REAL_IP'): + return request.environ['HTTP_X_REAL_IP'] + return request.environ.get('REMOTE_ADDR', '127.0.0.1') + + +def is_allowed_domain(url): + """Check if URL domain is in whitelist""" + from urllib.parse import urlparse + parsed = urlparse(url) + return parsed.netloc in ALLOWED_API_DOMAINS + + +# Initialize rate limiter +limiter = Limiter( + key_func=get_real_ip, + default_limits=[], + storage_uri=os.environ.get('REDIS_URL', 'memory://') +) + +# Initialize CSRF protection +csrf = CSRFProtect() + def create_app(): app = Flask(__name__) - app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv('DATABASE_URL') - app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False # Suppress warning - app.config['SECRET_KEY'] = os.getenv('SECRET_KEY', 'dev') # Default to 'dev' if not set + app.config['SECRET_KEY'] = os.getenv('SECRET_KEY', 'dev') # Default to 'dev' if not set + + # Initialize extensions + limiter.init_app(app) + csrf.init_app(app) + + # Configure CORS (allow requests from same origin) + CORS(app, supports_credentials=True) + + # Rate limit error handler + @app.errorhandler(429) + def rate_limit_handler(e): + """Handle rate limit exceeded""" + # Return JSON for AJAX requests + if request.headers.get('X-Requested-With') == 'XMLHttpRequest' or request.is_json: + return jsonify({ + 'error': 'Rate Limited', + 'message': 'Too many requests. Please wait a moment before trying again.' + }), 429 - db.init_app(app) - migrate.init_app(app, db) + # Return simple HTML response for regular requests + return ''' + +
Too many requests. Please wait a moment before trying again.
+ ā Back to Home + + + ''', 429 - # Import models so Alembic can detect them - from . import models # noqa: F401 + # Security headers + @app.after_request + def set_security_headers(response): + """Set security headers on all responses""" + response.headers['X-Content-Type-Options'] = 'nosniff' + response.headers['X-Frame-Options'] = 'DENY' + response.headers['X-XSS-Protection'] = '1; mode=block' + response.headers['Referrer-Policy'] = 'strict-origin-when-cross-origin' + response.headers['Permissions-Policy'] = 'camera=(), microphone=(), geolocation=()' + + # HSTS in production only + if not app.config.get('TESTING') and not app.debug: + response.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains; preload' + + # CSP for HTML pages (not API responses) + if not request.path.startswith('/api'): + response.headers['Content-Security-Policy'] = ( + "default-src 'self'; " + "script-src 'self' 'unsafe-inline'; " # Prism.js needs inline + "style-src 'self' 'unsafe-inline'; " + "img-src 'self' data: https:; " + "font-src 'self'; " + ) + + return response from .routes import bp as main_bp - app.register_blueprint(main_bp) # Register the main blueprint + app.register_blueprint(main_bp) # Register the main blueprint + + # Exempt main routes from CSRF (API testing doesn't need CSRF protection) + # We already have SameSite=Lax cookies for CSRF protection + csrf.exempt(main_bp) return app diff --git a/app/api_helpers.py b/app/api_handlers.py similarity index 74% rename from app/api_helpers.py rename to app/api_handlers.py index 840f322..70acca8 100755 --- a/app/api_helpers.py +++ b/app/api_handlers.py @@ -1,5 +1,6 @@ import json import requests +from app import is_allowed_domain def parse_response(response): @@ -20,7 +21,11 @@ def parse_response(response): # Cat Facts API def handle_cat_facts_api(api, params=None): - response = requests.get(api.endpoint, headers={"Accept": "application/json"}) + endpoint = api['endpoint'] + if not is_allowed_domain(endpoint): + return "This API endpoint is not allowed for security reasons.", "error" + + response = requests.get(endpoint, headers={"Accept": "application/json"}, timeout=10) try: data = response.json() # Extract the "fact" field from the response @@ -31,12 +36,12 @@ def handle_cat_facts_api(api, params=None): # Dog CEO API def handle_dog_ceo_api(api, params=None): - response = requests.get(api.endpoint, headers={"Accept": "application/json"}) + response = requests.get(api['endpoint'], headers={"Accept": "application/json"}, timeout=10) return parse_response(response) # DogAPI def handle_dog_api(api, params=None): - response = requests.get(api.endpoint, headers={"Accept": "application/json"}) + response = requests.get(api['endpoint'], headers={"Accept": "application/json"}, timeout=10) try: data = response.json() # Extract the "body" field from the first item in "data" @@ -53,7 +58,7 @@ def handle_jokeapi(api, params=None): endpoint = f"https://v2.jokeapi.dev/joke/{category}" # Make the API request with the updated endpoint and remaining params - response = requests.get(endpoint, params=params, headers={"Accept": "application/json"}) + response = requests.get(endpoint, params=params, headers={"Accept": "application/json"}, timeout=10) return parse_jokeapi_response(response) def parse_jokeapi_response(response): @@ -70,7 +75,7 @@ def parse_jokeapi_response(response): # Advice Slip API def handle_advice_slip_api(api, params=None): - response = requests.get(api.endpoint, headers={"Accept": "application/json"}) + response = requests.get(api['endpoint'], headers={"Accept": "application/json"}, timeout=10) try: data = response.json() # Extract the "advice" field from the "slip" object @@ -81,7 +86,7 @@ def handle_advice_slip_api(api, params=None): # Dad Jokes API def handle_dad_jokes_api(api, params=None): - response = requests.get(api.endpoint, headers={"Accept": "application/json"}) + response = requests.get(api['endpoint'], headers={"Accept": "application/json"}, timeout=10) try: data = response.json() # Extract the "joke" field from the response @@ -90,8 +95,9 @@ def handle_dad_jokes_api(api, params=None): except (KeyError, ValueError): return "Failed to parse Dad Jokes API response.", "text" +# Kanye Rest API def handle_kanye_rest_api(api, params=None): - response = requests.get(api.endpoint, headers={"Accept": "application/json"}) + response = requests.get(api['endpoint'], headers={"Accept": "application/json"}, timeout=10) try: data = response.json() # Extract the "quote" field from the response @@ -100,6 +106,12 @@ def handle_kanye_rest_api(api, params=None): except (KeyError, ValueError): return "Failed to parse Kanye Rest API response.", "text" +# Default handler for APIs without custom handlers def handle_default_api(api, params=None): - response = requests.get(api.endpoint, params=params, headers={"Accept": "application/json"}) + endpoint = api['endpoint'] + # SSRF Protection: Check if domain is whitelisted + if not is_allowed_domain(endpoint): + return "This API endpoint is not allowed for security reasons.", "error" + + response = requests.get(endpoint, params=params, headers={"Accept": "application/json"}, timeout=10) return parse_response(response) diff --git a/app/data.py b/app/data.py new file mode 100644 index 0000000..1a525d6 --- /dev/null +++ b/app/data.py @@ -0,0 +1,309 @@ +""" +Static API data structure for api_looter. +No database needed - all API information stored here. +""" + +APIS = [ + { + "id": 1, + "name": "Dog CEO", + "description": "Random pictures of dogs.", + "endpoint": "https://dog.ceo/api/breeds/image/random", + "parameters": [], + "why_use": "Great for placeholder images, testing image handling, or building pet-related apps.", + "how_use": "Perfect for learning HTTP requests - no API key needed, returns JSON with image URL. Ideal for beginners practicing API calls.", + "category": "Images", + "has_handler": True, + "is_adult": False + }, + { + "id": 2, + "name": "Cat Facts", + "description": "Get random cat facts.", + "endpoint": "https://catfact.ninja/fact", + "parameters": [], + "why_use": "Learn JSON parsing and handling text responses from APIs.", + "how_use": "Simple GET request returns random cat facts - ideal first API for beginners. No authentication required.", + "category": "Fun", + "has_handler": True, + "is_adult": False + }, + { + "id": 3, + "name": "OpenWeatherMap", + "description": "Get current weather data.", + "endpoint": "https://api.openweathermap.org/data/2.5/weather", + "parameters": [ + {"name": "q", "label": "City", "type": "text", "required": True}, + {"name": "appid", "label": "API Key", "type": "text", "required": True} + ], + "why_use": "Learn how to work with APIs that require authentication and handle query parameters.", + "how_use": "Demonstrates API key usage and parameter passing. Common in weather apps, travel sites, and IoT projects.", + "category": "Data", + "has_handler": False, + "is_adult": False + }, + { + "id": 4, + "name": "Advice Slip", + "description": "Random life advice.", + "endpoint": "https://api.adviceslip.com/advice", + "parameters": [], + "why_use": "Simple API perfect for practicing JSON data extraction and response handling.", + "how_use": "Returns motivational advice - great for learning apps, bots, or daily inspiration features.", + "category": "Fun", + "has_handler": True, + "is_adult": True, + "adult_warning": "This API may contain advice with adult language or mature themes." + }, + { + "id": 5, + "name": "JokeAPI", + "description": "Programming and general jokes.", + "endpoint": "https://v2.jokeapi.dev/joke", + "parameters": [ + { + "name": "category", + "label": "Category", + "type": "select", + "required": True, + "options": [ + {"value": "programming", "label": "Programming"}, + {"value": "misc", "label": "Miscellaneous"}, + {"value": "pun", "label": "Pun"}, + {"value": "spooky", "label": "Spooky"}, + {"value": "christmas", "label": "Christmas"} + ] + }, + { + "name": "type", + "label": "Type", + "type": "select", + "required": False, + "options": [ + {"value": "single", "label": "Single"}, + {"value": "twopart", "label": "Two-Part"} + ] + } + ], + "why_use": "Learn parameter handling with dropdown options and conditional response structures.", + "how_use": "Popular for Slack bots, Discord bots, and entertainment apps. Shows how to handle multiple response formats.", + "category": "Fun", + "has_handler": True, + "is_adult": True, + "adult_warning": "This API may return jokes with adult language or themes." + }, + { + "id": 6, + "name": "CoinGecko", + "description": "Cryptocurrency prices and info.", + "endpoint": "https://api.coingecko.com/api/v3/simple/price", + "parameters": [ + { + "name": "ids", + "label": "Coin", + "type": "select", + "required": True, + "options": [ + {"value": "bitcoin", "label": "Bitcoin"}, + {"value": "ethereum", "label": "Ethereum"}, + {"value": "dogecoin", "label": "Dogecoin"}, + {"value": "litecoin", "label": "Litecoin"}, + {"value": "cardano", "label": "Cardano"}, + {"value": "solana", "label": "Solana"}, + {"value": "ripple", "label": "Ripple"}, + {"value": "polkadot", "label": "Polkadot"}, + {"value": "tron", "label": "Tron"} + ] + }, + { + "name": "vs_currencies", + "label": "Currency", + "type": "select", + "required": True, + "options": [ + {"value": "usd", "label": "USD"}, + {"value": "eur", "label": "EUR"}, + {"value": "gbp", "label": "GBP"}, + {"value": "jpy", "label": "JPY"}, + {"value": "aud", "label": "AUD"} + ] + } + ], + "why_use": "Practice working with financial data APIs and real-time price information.", + "how_use": "Used in crypto portfolio trackers, price alert apps, and trading dashboards. No API key required for basic usage.", + "category": "Cryptocurrency", + "has_handler": False, + "is_adult": False + }, + { + "id": 7, + "name": "Genderize", + "description": "Predict gender from a first name.", + "endpoint": "https://api.genderize.io", + "parameters": [ + {"name": "name", "label": "Name", "type": "text", "required": True} + ], + "why_use": "Learn about machine learning prediction APIs and probability-based responses.", + "how_use": "Useful for data analysis, user profiling, and demographic research. Returns gender probability scores.", + "category": "Data", + "has_handler": False, + "is_adult": False + }, + { + "id": 8, + "name": "Agify", + "description": "Predict age from a name.", + "endpoint": "https://api.agify.io", + "parameters": [ + {"name": "name", "label": "Name", "type": "text", "required": True} + ], + "why_use": "Understand prediction APIs and statistical estimation from names.", + "how_use": "Used in demographic analysis, marketing research, and data enrichment tools.", + "category": "Data", + "has_handler": False, + "is_adult": False + }, + { + "id": 9, + "name": "Nationalize", + "description": "Predict nationality from a name.", + "endpoint": "https://api.nationalize.io", + "parameters": [ + {"name": "name", "label": "Name", "type": "text", "required": True} + ], + "why_use": "Practice handling multiple prediction results with probability scores.", + "how_use": "Helps with internationalization, market research, and understanding name origins. Returns multiple country probabilities.", + "category": "Data", + "has_handler": False, + "is_adult": False + }, + { + "id": 10, + "name": "DogAPI", + "description": "Get random dog facts.", + "endpoint": "https://dogapi.dog/api/v2/facts", + "parameters": [], + "why_use": "Learn to navigate nested JSON responses and extract specific data fields.", + "how_use": "Great for pet apps, educational content, or practicing JSON parsing with complex structures.", + "category": "Fun", + "has_handler": False, + "is_adult": False + }, + { + "id": 11, + "name": "Numbers API", + "description": "Trivia and facts about numbers.", + "endpoint": "http://numbersapi.com/random/trivia", + "parameters": [], + "why_use": "Simple text-based API for learning basic HTTP requests and plain text responses.", + "how_use": "Fun facts for educational apps, trivia games, or daily number facts. Returns plain text instead of JSON.", + "category": "Fun", + "has_handler": False, + "is_adult": False + }, + { + "id": 12, + "name": "OpenLibrary", + "description": "Book data and cover art.", + "endpoint": "https://openlibrary.org/search.json", + "parameters": [ + {"name": "q", "label": "Search Query", "type": "text", "required": True} + ], + "why_use": "Practice working with large, complex JSON responses and search functionality.", + "how_use": "Essential for book apps, library systems, reading trackers, and educational projects. Free and extensive book database.", + "category": "Data", + "has_handler": False, + "is_adult": False + }, + { + "id": 13, + "name": "Kanye Rest", + "description": "Get a random Kanye West quote.", + "endpoint": "https://api.kanye.rest", + "parameters": [], + "why_use": "Extremely simple API perfect for your very first API call - just one endpoint, no parameters.", + "how_use": "Popular for meme apps, quote generators, and teaching API basics. Instant success guaranteed!", + "category": "Fun", + "has_handler": True, + "is_adult": True, + "adult_warning": "Some quotes may contain strong language or mature themes." + }, + { + "id": 14, + "name": "Dad Jokes", + "description": "Get a dad joke.", + "endpoint": "https://icanhazdadjoke.com/", + "parameters": [], + "why_use": "Learn about content negotiation - API returns different formats based on Accept header.", + "how_use": "Common in Slack bots, entertainment apps, and icebreaker tools. Shows how headers affect API responses.", + "category": "Fun", + "has_handler": False, + "is_adult": True, + "adult_warning": "Some jokes may contain mild adult humor." + } +] + + +def get_all_apis(): + """ + Return all APIs sorted by name. + + Returns: + list: Sorted list of API dictionaries + """ + return sorted(APIS, key=lambda x: x['name']) + + +def get_api_by_id(api_id): + """ + Get single API by ID. + + Args: + api_id (int): The API ID to search for + + Returns: + dict or None: API dictionary if found, None otherwise + """ + return next((api for api in APIS if api['id'] == api_id), None) + + +def search_apis(query): + """ + Search APIs by name or description. + + Args: + query (str): Search query string + + Returns: + list: List of matching API dictionaries + """ + query = query.lower() + return [ + api for api in APIS + if query in api['name'].lower() or query in api['description'].lower() + ] + + +def get_apis_by_category(category): + """ + Get all APIs in a specific category. + + Args: + category (str): Category name (e.g., 'Fun', 'Data', 'Images') + + Returns: + list: List of APIs in the category + """ + return [api for api in APIS if api.get('category') == category] + + +def get_all_categories(): + """ + Get list of unique categories. + + Returns: + list: Sorted list of category names + """ + categories = {api.get('category', 'Other') for api in APIS} + return sorted(categories) diff --git a/app/forms.py b/app/forms.py deleted file mode 100755 index ed2dc00..0000000 --- a/app/forms.py +++ /dev/null @@ -1,8 +0,0 @@ -from flask_wtf import FlaskForm -from wtforms import StringField, TextAreaField, SubmitField -from wtforms.validators import DataRequired - -class ApiCallForm(FlaskForm): - api_key = StringField('API Key', validators=[DataRequired()]) - parameters = TextAreaField('Parameters', validators=[DataRequired()]) - submit = SubmitField('Make API Call') diff --git a/app/models.py b/app/models.py deleted file mode 100755 index 72de8c5..0000000 --- a/app/models.py +++ /dev/null @@ -1,12 +0,0 @@ -from . import db - -class APIModel(db.Model): - id = db.Column(db.Integer, primary_key=True) - name = db.Column(db.String(100), nullable=False) - description = db.Column(db.Text, nullable=True) - endpoint = db.Column(db.String(200), nullable=False) - parameters = db.Column(db.JSON, nullable=True) - api_key = db.Column(db.String(255), nullable=True) # <-- Add this line - - def __repr__(self): - return f'{{ api.description }}
Endpoint: {{ api.endpoint }}
+ + {% if api.why_use or api.how_use %} +{{ api.why_use }}
+ {% endif %} + + {% if api.how_use %} +{{ api.how_use }}
+ {% endif %} + + {% if api.category %} ++ Category: {{ api.category }} +
+ {% endif %} +