From cce1e352aba4a3b543f622fef4d70df9c3851c0f Mon Sep 17 00:00:00 2001 From: koladev Date: Tue, 23 Sep 2025 08:11:36 +0100 Subject: [PATCH] add taskmaster internal api project --- taskmaster-internal-api/.gitignore | 155 ++ taskmaster-internal-api/README.md | 37 + taskmaster-internal-api/app.py | 297 ++++ taskmaster-internal-api/app/__init__.py | 0 taskmaster-internal-api/app/api/__init__.py | 0 .../app/api/dependencies.py | 69 + taskmaster-internal-api/app/core/__init__.py | 0 taskmaster-internal-api/app/core/config.py | 26 + taskmaster-internal-api/app/core/database.py | 98 ++ taskmaster-internal-api/app/core/security.py | 39 + taskmaster-internal-api/app/main.py | 491 ++++++ .../app/models/__init__.py | 0 .../app/models/campaign.py | 45 + taskmaster-internal-api/app/models/license.py | 71 + .../app/models/support_ticket.py | 50 + taskmaster-internal-api/app/models/user.py | 53 + taskmaster-internal-api/gen.sh | 34 + taskmaster-internal-api/generate_openapi.py | 103 ++ taskmaster-internal-api/openapi.json | 1471 +++++++++++++++++ taskmaster-internal-api/openapi.yaml | 981 +++++++++++ taskmaster-internal-api/pyproject.toml | 20 + taskmaster-internal-api/requirements.txt | 11 + taskmaster-internal-api/scripts/init_db.py | 64 + taskmaster-internal-api/setup.sh | 39 + taskmaster-internal-api/test_app.py | 161 ++ taskmaster-internal-api/tests/test_main.py | 101 ++ taskmaster-internal-api/uv.lock | 774 +++++++++ 27 files changed, 5190 insertions(+) create mode 100644 taskmaster-internal-api/.gitignore create mode 100644 taskmaster-internal-api/README.md create mode 100644 taskmaster-internal-api/app.py create mode 100644 taskmaster-internal-api/app/__init__.py create mode 100644 taskmaster-internal-api/app/api/__init__.py create mode 100644 taskmaster-internal-api/app/api/dependencies.py create mode 100644 taskmaster-internal-api/app/core/__init__.py create mode 100644 taskmaster-internal-api/app/core/config.py create mode 100644 taskmaster-internal-api/app/core/database.py create mode 100644 taskmaster-internal-api/app/core/security.py create mode 100644 taskmaster-internal-api/app/main.py create mode 100644 taskmaster-internal-api/app/models/__init__.py create mode 100644 taskmaster-internal-api/app/models/campaign.py create mode 100644 taskmaster-internal-api/app/models/license.py create mode 100644 taskmaster-internal-api/app/models/support_ticket.py create mode 100644 taskmaster-internal-api/app/models/user.py create mode 100755 taskmaster-internal-api/gen.sh create mode 100755 taskmaster-internal-api/generate_openapi.py create mode 100644 taskmaster-internal-api/openapi.json create mode 100644 taskmaster-internal-api/openapi.yaml create mode 100644 taskmaster-internal-api/pyproject.toml create mode 100644 taskmaster-internal-api/requirements.txt create mode 100755 taskmaster-internal-api/scripts/init_db.py create mode 100755 taskmaster-internal-api/setup.sh create mode 100644 taskmaster-internal-api/test_app.py create mode 100644 taskmaster-internal-api/tests/test_main.py create mode 100644 taskmaster-internal-api/uv.lock diff --git a/taskmaster-internal-api/.gitignore b/taskmaster-internal-api/.gitignore new file mode 100644 index 0000000..dc38caa --- /dev/null +++ b/taskmaster-internal-api/.gitignore @@ -0,0 +1,155 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# Database +*.db +*.sqlite +*.sqlite3 + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Project specific +test.db + +licenses.db diff --git a/taskmaster-internal-api/README.md b/taskmaster-internal-api/README.md new file mode 100644 index 0000000..0de7335 --- /dev/null +++ b/taskmaster-internal-api/README.md @@ -0,0 +1,37 @@ +# Taskmaster Internal API + +The Taskmaster Internal API with customer service, marketing, license and user endpoints. + + +## Usage + +1. Clone the project: + +```bash +git clone +cd license-distribution-api +``` + +2. Install Dependencies + +```bash +uv sync +``` + +3. Initialize Database + +```bash +python scripts/init_db.py +``` + +4. Run the Server + +```bash +uv run uvicorn app.main:app --reload +``` + +5. Expose the server via Ngrok (optional) + +```bash +ngrok http 127.0.0.1:8000 +``` \ No newline at end of file diff --git a/taskmaster-internal-api/app.py b/taskmaster-internal-api/app.py new file mode 100644 index 0000000..b416bc9 --- /dev/null +++ b/taskmaster-internal-api/app.py @@ -0,0 +1,297 @@ +from fastapi import FastAPI, HTTPException, Depends +from fastapi.openapi.utils import get_openapi +from pydantic import BaseModel, Field +from typing import Optional, List +from datetime import datetime +import sqlite3 +import os + +app = FastAPI( + title="TaskMaster License Management API", + description="A simple API for managing TaskMaster software licenses with SQLite database", + version="1.0.0", + openapi_tags=[ + { + "name": "licenses", + "description": "Operations for managing TaskMaster software licenses", + }, + { + "name": "system", + "description": "System health and status operations", + }, + ], +) + +# Database setup +DATABASE_URL = "licenses.db" + +def get_db_connection(): + """Get database connection""" + conn = sqlite3.connect(DATABASE_URL, check_same_thread=False) + conn.row_factory = sqlite3.Row + return conn + +# Dependency for database connection +def get_db(): + """Dependency to get database connection""" + conn = get_db_connection() + try: + yield conn + finally: + conn.close() + +def init_database(): + """Initialize the database with required tables""" + conn = get_db_connection() + cursor = conn.cursor() + + cursor.execute(''' + CREATE TABLE IF NOT EXISTS licenses ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + description TEXT, + license_key TEXT UNIQUE NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + is_active BOOLEAN DEFAULT 1 + ) + ''') + + conn.commit() + conn.close() + +# Initialize database on startup +init_database() + +# Pydantic models +class LicenseBase(BaseModel): + """Base TaskMaster license model for common fields""" + name: str = Field(..., description="Name of the TaskMaster license", json_schema_extra={"example": "TaskMaster Premium License"}) + description: Optional[str] = Field(None, description="Description of the TaskMaster license", json_schema_extra={"example": "Full access to all TaskMaster features"}) + license_key: str = Field(..., description="Unique TaskMaster license key", json_schema_extra={"example": "TASKMASTER-PREMIUM-12345-ABCDE"}) + is_active: bool = Field(True, description="Whether the TaskMaster license is active", json_schema_extra={"example": True}) + +class LicenseCreate(LicenseBase): + """Model for creating a new TaskMaster license""" + pass + +class License(LicenseBase): + """Model representing a TaskMaster license with all fields""" + id: int = Field(..., description="Unique identifier for the TaskMaster license", json_schema_extra={"example": 1}) + created_at: datetime = Field(..., description="When the TaskMaster license was created", json_schema_extra={"example": "2023-12-01T10:00:00Z"}) + + model_config = {"from_attributes": True} + +class LicenseList(BaseModel): + """Response model for listing TaskMaster licenses""" + licenses: List[License] = Field(..., description="List of TaskMaster licenses") + total: int = Field(..., description="Total number of TaskMaster licenses", json_schema_extra={"example": 5}) + +class ErrorResponse(BaseModel): + """Error response model""" + error: str = Field(..., description="Error message", json_schema_extra={"example": "TaskMaster license key already exists"}) + +class HealthResponse(BaseModel): + """Health check response model""" + status: str = Field(..., description="Health status", json_schema_extra={"example": "healthy"}) + timestamp: datetime = Field(..., description="Current timestamp", json_schema_extra={"example": "2023-12-01T10:00:00Z"}) + +# Helper functions +def license_from_row(row) -> License: + """Convert database row to License model""" + return License( + id=row['id'], + name=row['name'], + description=row['description'] or "", + license_key=row['license_key'], + created_at=datetime.fromisoformat(row['created_at']), + is_active=bool(row['is_active']) + ) + +# API Endpoints +@app.get( + "/licenses", + response_model=List[License], + tags=["licenses"], + summary="List all TaskMaster licenses", + operation_id="list_licenses", + description=""" + Retrieve a list of all TaskMaster licenses in the system. + Returns all TaskMaster licenses with their details including creation timestamp and status. + """, + responses={ + 200: {"description": "Successful response with list of licenses"}, + }, +) +async def list_licenses(db: sqlite3.Connection = Depends(get_db)): + """List all licenses in the system""" + cursor = db.cursor() + cursor.execute("SELECT * FROM licenses ORDER BY created_at DESC") + rows = cursor.fetchall() + + return [license_from_row(row) for row in rows] + +@app.post( + "/licenses", + response_model=License, + status_code=201, + tags=["licenses"], + summary="Create a new TaskMaster license", + operation_id="create_license", + description=""" + Create a new TaskMaster license in the system. + Requires a unique TaskMaster license key and a name. Description and active status are optional. + """, + responses={ + 201: {"description": "License created successfully"}, + 400: {"description": "Bad request - missing required fields"}, + 409: {"description": "Conflict - license key already exists"}, + 500: {"description": "Internal server error"}, + }, +) +async def create_license(license_data: LicenseCreate, db: sqlite3.Connection = Depends(get_db)): + """Create a new license""" + cursor = db.cursor() + + try: + # Check if license_key already exists + cursor.execute("SELECT id FROM licenses WHERE license_key = ?", (license_data.license_key,)) + if cursor.fetchone(): + raise HTTPException(status_code=409, detail="TaskMaster license key already exists") + + # Insert new license + cursor.execute(""" + INSERT INTO licenses (name, description, license_key, is_active) + VALUES (?, ?, ?, ?) + """, ( + license_data.name, + license_data.description or "", + license_data.license_key, + license_data.is_active + )) + + # Get the created license + license_id = cursor.lastrowid + cursor.execute("SELECT * FROM licenses WHERE id = ?", (license_id,)) + row = cursor.fetchone() + + db.commit() + + return license_from_row(row) + + except HTTPException: + db.rollback() + raise + except Exception as e: + db.rollback() + raise HTTPException(status_code=500, detail="Failed to create TaskMaster license") + +@app.get( + "/health", + response_model=HealthResponse, + tags=["system"], + summary="Health check", + operation_id="health_check", + description=""" + Check if the API is running and healthy. + Returns current status and timestamp. + """, + responses={ + 200: {"description": "API is healthy"}, + }, +) +async def health_check(): + """Health check endpoint""" + return HealthResponse( + status="healthy", + timestamp=datetime.now() + ) + +def custom_openapi(): + """Customize OpenAPI Output with x-gram extensions for getgram MCP servers""" + + if app.openapi_schema: + return app.openapi_schema + + openapi_schema = get_openapi( + title=app.title, + version=app.version, + description=app.description, + routes=app.routes, + tags=app.openapi_tags, + ) + + # Add x-gram extensions to specific operations + x_gram_extensions = { + "list_licenses": { + "x-gram": { + "name": "list_taskmaster_licenses", + "summary": "List all TaskMaster software licenses", + "description": """ + This tool retrieves all TaskMaster software licenses in the system, including their details + such as name, description, license key, creation date, and active status. + Perfect for getting an overview of all available TaskMaster licenses. + + + + - No authentication required + - Returns all TaskMaster licenses regardless of status + - Results are ordered by creation date (newest first) + """, + "responseFilterType": "jq", + } + }, + "create_license": { + "x-gram": { + "name": "create_taskmaster_license", + "summary": "Create a new TaskMaster software license", + "description": """ + This tool creates a new TaskMaster software license in the system. Requires a unique + TaskMaster license key and name. Useful for adding new TaskMaster licenses to the system. + + + + - TaskMaster license key must be unique across all licenses + - Name is required and should be descriptive + - Description is optional but recommended + - TaskMaster license will be active by default unless specified otherwise + """, + "responseFilterType": "jq", + } + }, + "health_check": { + "x-gram": { + "name": "health_check", + "summary": "Check TaskMaster License API health status", + "description": """ + This endpoint provides a simple health check to verify that the TaskMaster License + Management API is running and responsive. Returns current timestamp and status. + + + + - No authentication required + - Always available for monitoring purposes + - Returns current system timestamp + """, + "responseFilterType": "jq", + } + }, + } + + # Apply x-gram extensions to paths + if "paths" in openapi_schema: + for path, path_item in openapi_schema["paths"].items(): + for method, operation in path_item.items(): + if method.lower() in ["get", "post", "put", "delete", "patch"]: + operation_id = operation.get("operationId") + if operation_id in x_gram_extensions: + operation.update(x_gram_extensions[operation_id]) + + app.openapi_schema = openapi_schema + return app.openapi_schema + +# Override the default OpenAPI function +app.openapi = custom_openapi + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=5000) \ No newline at end of file diff --git a/taskmaster-internal-api/app/__init__.py b/taskmaster-internal-api/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/taskmaster-internal-api/app/api/__init__.py b/taskmaster-internal-api/app/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/taskmaster-internal-api/app/api/dependencies.py b/taskmaster-internal-api/app/api/dependencies.py new file mode 100644 index 0000000..3264c3e --- /dev/null +++ b/taskmaster-internal-api/app/api/dependencies.py @@ -0,0 +1,69 @@ +from fastapi import Depends, HTTPException, status +from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials +import sqlite3 +from app.core.database import get_db +from app.core.security import verify_token +from app.models.user import User, UserRole + +security = HTTPBearer() + + +def get_current_user( + credentials: HTTPAuthorizationCredentials = Depends(security), + db: sqlite3.Connection = Depends(get_db) +) -> User: + """Get current authenticated user""" + token = credentials.credentials + payload = verify_token(token) + username = payload.get("sub") + + if username is None: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + + cursor = db.cursor() + cursor.execute("SELECT * FROM users WHERE username = ?", (username,)) + row = cursor.fetchone() + + if row is None: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="User not found", + headers={"WWW-Authenticate": "Bearer"}, + ) + + return User( + id=row['id'], + email=row['email'], + username=row['username'], + full_name=row['full_name'], + role=row['role'], + is_active=bool(row['is_active']), + created_at=row['created_at'], + updated_at=row['updated_at'] + ) + + +def get_current_active_user(current_user: User = Depends(get_current_user)) -> User: + """Get current active user""" + if not current_user.is_active: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Inactive user" + ) + return current_user + + +def require_role(required_role: UserRole): + """Require specific role for endpoint access""" + def role_checker(current_user: User = Depends(get_current_active_user)) -> User: + if current_user.role != required_role and current_user.role != UserRole.ADMIN: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Not enough permissions" + ) + return current_user + return role_checker \ No newline at end of file diff --git a/taskmaster-internal-api/app/core/__init__.py b/taskmaster-internal-api/app/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/taskmaster-internal-api/app/core/config.py b/taskmaster-internal-api/app/core/config.py new file mode 100644 index 0000000..97e928d --- /dev/null +++ b/taskmaster-internal-api/app/core/config.py @@ -0,0 +1,26 @@ +from pydantic_settings import BaseSettings +from typing import Optional + + +class Settings(BaseSettings): + # Database + database_url: str = "licenses.db" + + # JWT Settings + secret_key: str = "your-secret-key-here-change-in-production" + algorithm: str = "HS256" + access_token_expire_minutes: int = 30 + + # API Settings + api_v1_str: str = "/api/v1" + project_name: str = "License Distribution API" + + # Environment + environment: str = "development" + + class Config: + env_file = ".env" + case_sensitive = False + + +settings = Settings() diff --git a/taskmaster-internal-api/app/core/database.py b/taskmaster-internal-api/app/core/database.py new file mode 100644 index 0000000..975da25 --- /dev/null +++ b/taskmaster-internal-api/app/core/database.py @@ -0,0 +1,98 @@ +import sqlite3 +import os +from app.core.config import settings + + +def get_db_connection(): + """Get database connection""" + conn = sqlite3.connect(settings.database_url, check_same_thread=False) + conn.row_factory = sqlite3.Row + return conn + + +def get_db(): + """Dependency to get database connection""" + conn = get_db_connection() + try: + yield conn + finally: + conn.close() + + +def init_database(): + """Initialize the database with required tables""" + conn = get_db_connection() + cursor = conn.cursor() + + # Licenses table (existing) + cursor.execute(''' + CREATE TABLE IF NOT EXISTS licenses ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + description TEXT, + license_key TEXT UNIQUE NOT NULL, + user_id INTEGER, + license_type TEXT DEFAULT 'basic', + status TEXT DEFAULT 'active', + expires_at TIMESTAMP, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + is_active BOOLEAN DEFAULT 1, + FOREIGN KEY (user_id) REFERENCES users (id) + ) + ''') + + # Users table + cursor.execute(''' + CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + email TEXT UNIQUE NOT NULL, + username TEXT UNIQUE NOT NULL, + hashed_password TEXT NOT NULL, + full_name TEXT, + role TEXT DEFAULT 'user', + is_active BOOLEAN DEFAULT 1, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + ''') + + # Campaigns table + cursor.execute(''' + CREATE TABLE IF NOT EXISTS campaigns ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + description TEXT, + status TEXT DEFAULT 'draft', + target_audience TEXT, + start_date TIMESTAMP, + end_date TIMESTAMP, + created_by INTEGER NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + is_active BOOLEAN DEFAULT 1, + FOREIGN KEY (created_by) REFERENCES users (id) + ) + ''') + + # Support tickets table + cursor.execute(''' + CREATE TABLE IF NOT EXISTS support_tickets ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT NOT NULL, + description TEXT NOT NULL, + status TEXT DEFAULT 'open', + priority TEXT DEFAULT 'medium', + user_id INTEGER NOT NULL, + assigned_to INTEGER, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + resolved_at TIMESTAMP, + is_active BOOLEAN DEFAULT 1, + FOREIGN KEY (user_id) REFERENCES users (id), + FOREIGN KEY (assigned_to) REFERENCES users (id) + ) + ''') + + conn.commit() + conn.close() diff --git a/taskmaster-internal-api/app/core/security.py b/taskmaster-internal-api/app/core/security.py new file mode 100644 index 0000000..12cc00b --- /dev/null +++ b/taskmaster-internal-api/app/core/security.py @@ -0,0 +1,39 @@ +from datetime import datetime, timedelta +from typing import Optional +from jose import JWTError, jwt +from passlib.context import CryptContext +from fastapi import HTTPException, status +from app.core.config import settings + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + + +def verify_password(plain_password: str, hashed_password: str) -> bool: + return pwd_context.verify(plain_password, hashed_password) + + +def get_password_hash(password: str) -> str: + return pwd_context.hash(password) + + +def create_access_token(data: dict, expires_delta: Optional[timedelta] = None): + to_encode = data.copy() + if expires_delta: + expire = datetime.utcnow() + expires_delta + else: + expire = datetime.utcnow() + timedelta(minutes=settings.access_token_expire_minutes) + to_encode.update({"exp": expire}) + encoded_jwt = jwt.encode(to_encode, settings.secret_key, algorithm=settings.algorithm) + return encoded_jwt + + +def verify_token(token: str) -> dict: + try: + payload = jwt.decode(token, settings.secret_key, algorithms=[settings.algorithm]) + return payload + except JWTError: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) diff --git a/taskmaster-internal-api/app/main.py b/taskmaster-internal-api/app/main.py new file mode 100644 index 0000000..43bb91a --- /dev/null +++ b/taskmaster-internal-api/app/main.py @@ -0,0 +1,491 @@ +from fastapi import FastAPI, HTTPException, Depends +from fastapi.openapi.utils import get_openapi +from fastapi.middleware.cors import CORSMiddleware +from pydantic import BaseModel, Field +from typing import Optional, List +from datetime import datetime +import sqlite3 +import os +from app.core.database import get_db, init_database +from app.core.config import settings +from app.models.license import License, LicenseCreate, LicenseList, LicenseValidation, LicenseValidationResponse, ErrorResponse +from app.models.user import User, UserCreate, UserLogin, Token, UserUpdate +from app.models.campaign import Campaign, CampaignCreate, CampaignUpdate +from app.models.support_ticket import SupportTicket, SupportTicketCreate, SupportTicketUpdate +from app.core.security import get_password_hash, verify_password, create_access_token +from app.api.dependencies import get_current_active_user, require_role +from app.models.user import UserRole +from datetime import timedelta + +app = FastAPI( + title="License Distribution API", + version="1.0.0", + openapi_tags=[ + {"name": "licenses"}, + {"name": "users"}, + {"name": "marketing"}, + {"name": "customer-service"}, + {"name": "system"}, + ], +) + +# CORS middleware +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], # Configure this properly for production + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# Initialize database on startup +init_database() + +# Helper functions +def license_from_row(row) -> License: + """Convert database row to License model""" + return License( + id=row['id'], + name=row['name'], + description=row['description'] or "", + license_key=row['license_key'], + user_id=row['user_id'] if 'user_id' in row.keys() else None, + license_type=row['license_type'] if 'license_type' in row.keys() else 'basic', + status=row['status'] if 'status' in row.keys() else 'active', + expires_at=row['expires_at'] if 'expires_at' in row.keys() else None, + created_at=datetime.fromisoformat(row['created_at']), + updated_at=datetime.fromisoformat(row['updated_at'] if 'updated_at' in row.keys() else row['created_at']), + is_active=bool(row['is_active']) + ) + +def user_from_row(row) -> User: + """Convert database row to User model""" + return User( + id=row['id'], + email=row['email'], + username=row['username'], + full_name=row['full_name'], + role=row['role'], + is_active=bool(row['is_active']), + created_at=datetime.fromisoformat(row['created_at']), + updated_at=datetime.fromisoformat(row['updated_at']) + ) + +def campaign_from_row(row) -> Campaign: + """Convert database row to Campaign model""" + return Campaign( + id=row['id'], + name=row['name'], + description=row['description'], + status=row['status'], + target_audience=row['target_audience'], + start_date=row['start_date'], + end_date=row['end_date'], + created_by=row['created_by'], + created_at=datetime.fromisoformat(row['created_at']), + updated_at=datetime.fromisoformat(row['updated_at']), + is_active=bool(row['is_active']) + ) + +def ticket_from_row(row) -> SupportTicket: + """Convert database row to SupportTicket model""" + return SupportTicket( + id=row['id'], + title=row['title'], + description=row['description'], + status=row['status'], + priority=row['priority'], + user_id=row['user_id'], + assigned_to=row['assigned_to'], + created_at=datetime.fromisoformat(row['created_at']), + updated_at=datetime.fromisoformat(row['updated_at']), + resolved_at=row['resolved_at'], + is_active=bool(row['is_active']) + ) + +# Health check endpoint +class HealthResponse(BaseModel): + status: str + timestamp: datetime + +@app.get("/health", response_model=HealthResponse, tags=["system"]) +async def health_check(): + return HealthResponse( + status="healthy", + timestamp=datetime.now() + ) + +# User Management Endpoints +@app.post("/users/register", response_model=User, status_code=201, tags=["users"]) +async def register_user(user: UserCreate, db: sqlite3.Connection = Depends(get_db)): + cursor = db.cursor() + + try: + # Check if user already exists + cursor.execute("SELECT id FROM users WHERE email = ? OR username = ?", (user.email, user.username)) + if cursor.fetchone(): + raise HTTPException(status_code=400, detail="Email or username already registered") + + # Create new user + hashed_password = get_password_hash(user.password) + cursor.execute(""" + INSERT INTO users (email, username, hashed_password, full_name, role, is_active) + VALUES (?, ?, ?, ?, ?, ?) + """, ( + user.email, + user.username, + hashed_password, + user.full_name, + user.role.value, + True + )) + + # Get the created user + user_id = cursor.lastrowid + cursor.execute("SELECT * FROM users WHERE id = ?", (user_id,)) + row = cursor.fetchone() + + db.commit() + return user_from_row(row) + + except HTTPException: + db.rollback() + raise + except Exception as e: + db.rollback() + raise HTTPException(status_code=500, detail="Failed to create user") + +@app.post("/users/login", response_model=Token, tags=["users"]) +async def login_user(user_credentials: UserLogin, db: sqlite3.Connection = Depends(get_db)): + cursor = db.cursor() + cursor.execute("SELECT * FROM users WHERE username = ?", (user_credentials.username,)) + row = cursor.fetchone() + + if not row or not row['is_active']: + raise HTTPException( + status_code=401, + detail="Incorrect username or password" + ) + + # Verify password + if not verify_password(user_credentials.password, row['hashed_password']): + raise HTTPException( + status_code=401, + detail="Incorrect username or password" + ) + + # Create access token + access_token_expires = timedelta(minutes=settings.access_token_expire_minutes) + access_token = create_access_token( + data={"sub": row['username']}, expires_delta=access_token_expires + ) + + return {"access_token": access_token, "token_type": "bearer"} + +@app.get("/users", response_model=List[User], tags=["users"]) +async def list_users(name: Optional[str] = None, db: sqlite3.Connection = Depends(get_db)): + cursor = db.cursor() + if name: + cursor.execute("SELECT * FROM users WHERE full_name LIKE ? OR username LIKE ?", (f"%{name}%", f"%{name}%")) + else: + cursor.execute("SELECT * FROM users ORDER BY created_at DESC") + rows = cursor.fetchall() + return [user_from_row(row) for row in rows] + +@app.get("/users/{user_id}", response_model=User, tags=["users"]) +async def get_user(user_id: int, db: sqlite3.Connection = Depends(get_db)): + cursor = db.cursor() + cursor.execute("SELECT * FROM users WHERE id = ?", (user_id,)) + row = cursor.fetchone() + if not row: + raise HTTPException(status_code=404, detail="User not found") + return user_from_row(row) + +# License Management Endpoints (existing functionality enhanced) +@app.get("/licenses", response_model=List[License], tags=["licenses"]) +async def list_licenses(db: sqlite3.Connection = Depends(get_db)): + """List all licenses in the system""" + cursor = db.cursor() + cursor.execute("SELECT * FROM licenses ORDER BY created_at DESC") + rows = cursor.fetchall() + + return [license_from_row(row) for row in rows] + +@app.post("/licenses/users", response_model=User, status_code=201, tags=["licenses"]) +async def create_license_user(user: UserCreate, db: sqlite3.Connection = Depends(get_db)): + cursor = db.cursor() + try: + cursor.execute("SELECT id FROM users WHERE email = ? OR username = ?", (user.email, user.username)) + if cursor.fetchone(): + raise HTTPException(status_code=400, detail="Email or username already registered") + + hashed_password = get_password_hash(user.password) + cursor.execute(""" + INSERT INTO users (email, username, hashed_password, full_name, role, is_active) + VALUES (?, ?, ?, ?, ?, ?) + """, (user.email, user.username, hashed_password, user.full_name, user.role.value, True)) + + user_id = cursor.lastrowid + cursor.execute("SELECT * FROM users WHERE id = ?", (user_id,)) + row = cursor.fetchone() + + db.commit() + return user_from_row(row) + except HTTPException: + db.rollback() + raise + except Exception as e: + db.rollback() + raise HTTPException(status_code=500, detail="Failed to create user") + +@app.post("/licenses", response_model=License, status_code=201, tags=["licenses"]) +async def create_license(license_data: LicenseCreate, db: sqlite3.Connection = Depends(get_db)): + cursor = db.cursor() + + try: + # Check if license_key already exists + cursor.execute("SELECT id FROM licenses WHERE license_key = ?", (license_data.license_key,)) + if cursor.fetchone(): + raise HTTPException(status_code=409, detail="License key already exists") + + # Insert new license + license_type_value = license_data.license_type.value if hasattr(license_data.license_type, 'value') else str(license_data.license_type) + cursor.execute(""" + INSERT INTO licenses (name, description, license_key, user_id, license_type, status, expires_at, is_active) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + """, ( + license_data.name, + license_data.description or "", + license_data.license_key, + license_data.user_id, + license_type_value, + 'active', + license_data.expires_at, + license_data.is_active + )) + + # Get the created license + license_id = cursor.lastrowid + cursor.execute("SELECT * FROM licenses WHERE id = ?", (license_id,)) + row = cursor.fetchone() + + db.commit() + + return license_from_row(row) + + except HTTPException: + db.rollback() + raise + except Exception as e: + db.rollback() + raise HTTPException(status_code=500, detail="Failed to create license") + +@app.post("/licenses/validate", response_model=LicenseValidationResponse, tags=["licenses"]) +async def validate_license(license_validation: LicenseValidation, db: sqlite3.Connection = Depends(get_db)): + """Validate a license key""" + cursor = db.cursor() + cursor.execute("SELECT * FROM licenses WHERE license_key = ?", (license_validation.license_key,)) + row = cursor.fetchone() + + if not row: + return LicenseValidationResponse( + is_valid=False, + license_type=None, + status=None, + expires_at=None, + user_id=None + ) + + # Check if license is active and not expired + is_valid = ( + bool(row['is_active']) and + row['status'] == 'active' and + (row['expires_at'] is None or datetime.fromisoformat(row['expires_at']).replace(tzinfo=None) > datetime.now()) + ) + + return LicenseValidationResponse( + is_valid=is_valid, + license_type=row['license_type'], + status=row['status'], + expires_at=row['expires_at'], + user_id=row['user_id'] + ) + +# Marketing Campaign Endpoints +@app.post("/marketing/campaigns", response_model=Campaign, status_code=201, tags=["marketing"]) +async def create_campaign( + campaign_data: CampaignCreate, + db: sqlite3.Connection = Depends(get_db) +): + """Create a new marketing campaign""" + cursor = db.cursor() + + try: + cursor.execute(""" + INSERT INTO campaigns (name, description, target_audience, start_date, end_date, created_by, status, is_active) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + """, ( + campaign_data.name, + campaign_data.description, + campaign_data.target_audience, + campaign_data.start_date, + campaign_data.end_date, + 1, # Demo user ID + 'draft', + True + )) + + campaign_id = cursor.lastrowid + cursor.execute("SELECT * FROM campaigns WHERE id = ?", (campaign_id,)) + row = cursor.fetchone() + + db.commit() + return campaign_from_row(row) + + except Exception as e: + db.rollback() + raise HTTPException(status_code=500, detail="Failed to create campaign") + +@app.get("/marketing/campaigns", response_model=List[Campaign], tags=["marketing"]) +async def list_campaigns( + db: sqlite3.Connection = Depends(get_db) +): + """List all marketing campaigns""" + cursor = db.cursor() + cursor.execute("SELECT * FROM campaigns ORDER BY created_at DESC") + rows = cursor.fetchall() + + return [campaign_from_row(row) for row in rows] + +@app.post("/marketing/users", response_model=User, status_code=201, tags=["marketing"]) +async def create_marketing_user(user: UserCreate, db: sqlite3.Connection = Depends(get_db)): + cursor = db.cursor() + try: + cursor.execute("SELECT id FROM users WHERE email = ? OR username = ?", (user.email, user.username)) + if cursor.fetchone(): + raise HTTPException(status_code=400, detail="Email or username already registered") + + hashed_password = get_password_hash(user.password) + cursor.execute(""" + INSERT INTO users (email, username, hashed_password, full_name, role, is_active) + VALUES (?, ?, ?, ?, ?, ?) + """, (user.email, user.username, hashed_password, user.full_name, user.role.value, True)) + + user_id = cursor.lastrowid + cursor.execute("SELECT * FROM users WHERE id = ?", (user_id,)) + row = cursor.fetchone() + + db.commit() + return user_from_row(row) + except HTTPException: + db.rollback() + raise + except Exception as e: + db.rollback() + raise HTTPException(status_code=500, detail="Failed to create user") + +# Customer Service Endpoints +@app.post("/customer-service/tickets", response_model=SupportTicket, status_code=201, tags=["customer-service"]) +async def create_support_ticket( + ticket_data: SupportTicketCreate, + db: sqlite3.Connection = Depends(get_db) +): + """Create a new support ticket""" + cursor = db.cursor() + + try: + cursor.execute(""" + INSERT INTO support_tickets (title, description, priority, user_id, status, is_active) + VALUES (?, ?, ?, ?, ?, ?) + """, ( + ticket_data.title, + ticket_data.description, + ticket_data.priority.value, + 1, # Demo user ID + 'open', + True + )) + + ticket_id = cursor.lastrowid + cursor.execute("SELECT * FROM support_tickets WHERE id = ?", (ticket_id,)) + row = cursor.fetchone() + + db.commit() + return ticket_from_row(row) + + except Exception as e: + db.rollback() + raise HTTPException(status_code=500, detail="Failed to create support ticket") + +@app.get("/customer-service/tickets", response_model=List[SupportTicket], tags=["customer-service"]) +async def list_support_tickets( + db: sqlite3.Connection = Depends(get_db) +): + """List all support tickets""" + cursor = db.cursor() + cursor.execute("SELECT * FROM support_tickets ORDER BY created_at DESC") + rows = cursor.fetchall() + + return [ticket_from_row(row) for row in rows] + +@app.get("/customer-service/my-tickets", response_model=List[SupportTicket], tags=["customer-service"]) +async def get_my_tickets( + db: sqlite3.Connection = Depends(get_db) +): + """Get current user's support tickets""" + cursor = db.cursor() + cursor.execute("SELECT * FROM support_tickets WHERE user_id = ? ORDER BY created_at DESC", (1,)) # Demo user ID + rows = cursor.fetchall() + + return [ticket_from_row(row) for row in rows] + +@app.post("/customer-service/users", response_model=User, status_code=201, tags=["customer-service"]) +async def create_customer_service_user(user: UserCreate, db: sqlite3.Connection = Depends(get_db)): + cursor = db.cursor() + try: + cursor.execute("SELECT id FROM users WHERE email = ? OR username = ?", (user.email, user.username)) + if cursor.fetchone(): + raise HTTPException(status_code=400, detail="Email or username already registered") + + hashed_password = get_password_hash(user.password) + cursor.execute(""" + INSERT INTO users (email, username, hashed_password, full_name, role, is_active) + VALUES (?, ?, ?, ?, ?, ?) + """, (user.email, user.username, hashed_password, user.full_name, user.role.value, True)) + + user_id = cursor.lastrowid + cursor.execute("SELECT * FROM users WHERE id = ?", (user_id,)) + row = cursor.fetchone() + + db.commit() + return user_from_row(row) + except HTTPException: + db.rollback() + raise + except Exception as e: + db.rollback() + raise HTTPException(status_code=500, detail="Failed to create user") + +# Custom OpenAPI with x-gram extensions +def custom_openapi(): + """Generate clean OpenAPI schema without any descriptions or summaries""" + + if app.openapi_schema: + return app.openapi_schema + + openapi_schema = get_openapi( + title=app.title, + version=app.version, + routes=app.routes, + tags=app.openapi_tags, + ) + + app.openapi_schema = openapi_schema + return app.openapi_schema + +# Override the default OpenAPI function +app.openapi = custom_openapi + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8000) \ No newline at end of file diff --git a/taskmaster-internal-api/app/models/__init__.py b/taskmaster-internal-api/app/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/taskmaster-internal-api/app/models/campaign.py b/taskmaster-internal-api/app/models/campaign.py new file mode 100644 index 0000000..a5dd8fd --- /dev/null +++ b/taskmaster-internal-api/app/models/campaign.py @@ -0,0 +1,45 @@ +from pydantic import BaseModel, Field +from typing import Optional +from datetime import datetime +from enum import Enum + + +class CampaignStatus(str, Enum): + DRAFT = "draft" + ACTIVE = "active" + PAUSED = "paused" + COMPLETED = "completed" + CANCELLED = "cancelled" + + +class CampaignBase(BaseModel): + name: str = Field(..., description="Campaign name", json_schema_extra={"example": "Q1 2024 Product Launch"}) + description: Optional[str] = Field(None, description="Campaign description", json_schema_extra={"example": "Marketing campaign for new product launch"}) + target_audience: Optional[str] = Field(None, description="Target audience", json_schema_extra={"example": "Enterprise customers"}) + start_date: Optional[datetime] = Field(None, description="Campaign start date", json_schema_extra={"example": "2024-01-01T00:00:00Z"}) + end_date: Optional[datetime] = Field(None, description="Campaign end date", json_schema_extra={"example": "2024-03-31T23:59:59Z"}) + + +class CampaignCreate(CampaignBase): + pass + + +class CampaignUpdate(BaseModel): + name: Optional[str] = None + description: Optional[str] = None + status: Optional[CampaignStatus] = None + target_audience: Optional[str] = None + start_date: Optional[datetime] = None + end_date: Optional[datetime] = None + is_active: Optional[bool] = None + + +class Campaign(CampaignBase): + id: int = Field(..., description="Campaign ID", json_schema_extra={"example": 1}) + status: CampaignStatus = Field(..., description="Campaign status", json_schema_extra={"example": "active"}) + created_by: int = Field(..., description="User ID who created the campaign", json_schema_extra={"example": 1}) + created_at: datetime = Field(..., description="Creation timestamp", json_schema_extra={"example": "2023-12-01T10:00:00Z"}) + updated_at: datetime = Field(..., description="Last update timestamp", json_schema_extra={"example": "2023-12-01T10:00:00Z"}) + is_active: bool = Field(..., description="Whether campaign is active", json_schema_extra={"example": True}) + + model_config = {"from_attributes": True} diff --git a/taskmaster-internal-api/app/models/license.py b/taskmaster-internal-api/app/models/license.py new file mode 100644 index 0000000..f409174 --- /dev/null +++ b/taskmaster-internal-api/app/models/license.py @@ -0,0 +1,71 @@ +from pydantic import BaseModel, Field +from typing import Optional, List +from datetime import datetime +from enum import Enum + + +class LicenseStatus(str, Enum): + ACTIVE = "active" + EXPIRED = "expired" + REVOKED = "revoked" + PENDING = "pending" + + +class LicenseType(str, Enum): + TRIAL = "trial" + BASIC = "basic" + PREMIUM = "premium" + ENTERPRISE = "enterprise" + + +class LicenseBase(BaseModel): + name: str = Field(..., description="Name of the license", json_schema_extra={"example": "TaskMaster Premium License"}) + description: Optional[str] = Field(None, description="Description of the license", json_schema_extra={"example": "Full access to all TaskMaster features"}) + license_key: str = Field(..., description="Unique license key", json_schema_extra={"example": "TASKMASTER-PREMIUM-12345-ABCDE"}) + license_type: LicenseType = Field(LicenseType.BASIC, description="Type of license", json_schema_extra={"example": "premium"}) + expires_at: Optional[datetime] = Field(None, description="License expiration date", json_schema_extra={"example": "2024-12-01T10:00:00Z"}) + is_active: bool = Field(True, description="Whether the license is active", json_schema_extra={"example": True}) + + +class LicenseCreate(LicenseBase): + user_id: Optional[int] = Field(None, description="User ID to assign license to", json_schema_extra={"example": 1}) + + +class LicenseUpdate(BaseModel): + name: Optional[str] = None + description: Optional[str] = None + license_type: Optional[LicenseType] = None + status: Optional[LicenseStatus] = None + expires_at: Optional[datetime] = None + is_active: Optional[bool] = None + + +class License(LicenseBase): + id: int = Field(..., description="License ID", json_schema_extra={"example": 1}) + user_id: Optional[int] = Field(None, description="User ID", json_schema_extra={"example": 1}) + status: LicenseStatus = Field(..., description="License status", json_schema_extra={"example": "active"}) + created_at: datetime = Field(..., description="Creation timestamp", json_schema_extra={"example": "2023-12-01T10:00:00Z"}) + updated_at: datetime = Field(..., description="Last update timestamp", json_schema_extra={"example": "2023-12-01T10:00:00Z"}) + + model_config = {"from_attributes": True} + + +class LicenseList(BaseModel): + licenses: List[License] = Field(..., description="List of licenses") + total: int = Field(..., description="Total number of licenses", json_schema_extra={"example": 5}) + + +class LicenseValidation(BaseModel): + license_key: str = Field(..., description="License key to validate", json_schema_extra={"example": "TASKMASTER-PREMIUM-12345-ABCDE"}) + + +class LicenseValidationResponse(BaseModel): + is_valid: bool = Field(..., description="Whether license is valid", json_schema_extra={"example": True}) + license_type: Optional[LicenseType] = Field(None, description="License type if valid") + status: Optional[LicenseStatus] = Field(None, description="License status if valid") + expires_at: Optional[datetime] = Field(None, description="Expiration date if valid") + user_id: Optional[int] = Field(None, description="User ID if valid") + + +class ErrorResponse(BaseModel): + error: str = Field(..., description="Error message", json_schema_extra={"example": "License key already exists"}) diff --git a/taskmaster-internal-api/app/models/support_ticket.py b/taskmaster-internal-api/app/models/support_ticket.py new file mode 100644 index 0000000..1120e3c --- /dev/null +++ b/taskmaster-internal-api/app/models/support_ticket.py @@ -0,0 +1,50 @@ +from pydantic import BaseModel, Field +from typing import Optional +from datetime import datetime +from enum import Enum + + +class TicketStatus(str, Enum): + OPEN = "open" + IN_PROGRESS = "in_progress" + RESOLVED = "resolved" + CLOSED = "closed" + + +class TicketPriority(str, Enum): + LOW = "low" + MEDIUM = "medium" + HIGH = "high" + URGENT = "urgent" + + +class SupportTicketBase(BaseModel): + title: str = Field(..., description="Ticket title", json_schema_extra={"example": "Login issues with TaskMaster"}) + description: str = Field(..., description="Ticket description", json_schema_extra={"example": "Unable to login to TaskMaster application"}) + priority: TicketPriority = Field(TicketPriority.MEDIUM, description="Ticket priority", json_schema_extra={"example": "medium"}) + + +class SupportTicketCreate(SupportTicketBase): + pass + + +class SupportTicketUpdate(BaseModel): + title: Optional[str] = None + description: Optional[str] = None + status: Optional[TicketStatus] = None + priority: Optional[TicketPriority] = None + assigned_to: Optional[int] = None + is_active: Optional[bool] = None + + +class SupportTicket(SupportTicketBase): + id: int = Field(..., description="Ticket ID", json_schema_extra={"example": 1}) + status: TicketStatus = Field(..., description="Ticket status", json_schema_extra={"example": "open"}) + user_id: int = Field(..., description="User ID who created the ticket", json_schema_extra={"example": 1}) + assigned_to: Optional[int] = Field(None, description="User ID assigned to handle the ticket", json_schema_extra={"example": 2}) + created_at: datetime = Field(..., description="Creation timestamp", json_schema_extra={"example": "2023-12-01T10:00:00Z"}) + updated_at: datetime = Field(..., description="Last update timestamp", json_schema_extra={"example": "2023-12-01T10:00:00Z"}) + resolved_at: Optional[datetime] = Field(None, description="Resolution timestamp", json_schema_extra={"example": "2023-12-02T15:30:00Z"}) + is_active: bool = Field(..., description="Whether ticket is active", json_schema_extra={"example": True}) + + model_config = {"from_attributes": True} diff --git a/taskmaster-internal-api/app/models/user.py b/taskmaster-internal-api/app/models/user.py new file mode 100644 index 0000000..24ad68f --- /dev/null +++ b/taskmaster-internal-api/app/models/user.py @@ -0,0 +1,53 @@ +from pydantic import BaseModel, EmailStr, Field +from typing import Optional +from datetime import datetime +from enum import Enum + + +class UserRole(str, Enum): + ADMIN = "admin" + MARKETING = "marketing" + CUSTOMER_SERVICE = "customer_service" + USER = "user" + + +class UserBase(BaseModel): + email: EmailStr = Field(..., description="User email address", json_schema_extra={"example": "user@example.com"}) + username: str = Field(..., description="Username", json_schema_extra={"example": "johndoe"}) + full_name: Optional[str] = Field(None, description="Full name", json_schema_extra={"example": "John Doe"}) + role: UserRole = Field(UserRole.USER, description="User role", json_schema_extra={"example": "user"}) + + +class UserCreate(UserBase): + password: str = Field(..., description="Password", json_schema_extra={"example": "securepassword123"}) + + +class UserUpdate(BaseModel): + email: Optional[EmailStr] = None + username: Optional[str] = None + full_name: Optional[str] = None + role: Optional[UserRole] = None + is_active: Optional[bool] = None + + +class User(UserBase): + id: int = Field(..., description="User ID", json_schema_extra={"example": 1}) + is_active: bool = Field(..., description="Whether user is active", json_schema_extra={"example": True}) + created_at: datetime = Field(..., description="Creation timestamp", json_schema_extra={"example": "2023-12-01T10:00:00Z"}) + updated_at: datetime = Field(..., description="Last update timestamp", json_schema_extra={"example": "2023-12-01T10:00:00Z"}) + + model_config = {"from_attributes": True} + + +class UserLogin(BaseModel): + username: str = Field(..., description="Username", json_schema_extra={"example": "johndoe"}) + password: str = Field(..., description="Password", json_schema_extra={"example": "securepassword123"}) + + +class Token(BaseModel): + access_token: str = Field(..., description="JWT access token", json_schema_extra={"example": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..."}) + token_type: str = Field(..., description="Token type", json_schema_extra={"example": "bearer"}) + + +class TokenData(BaseModel): + username: Optional[str] = None \ No newline at end of file diff --git a/taskmaster-internal-api/gen.sh b/taskmaster-internal-api/gen.sh new file mode 100755 index 0000000..2c21dca --- /dev/null +++ b/taskmaster-internal-api/gen.sh @@ -0,0 +1,34 @@ +#!/bin/bash + +# Script to generate OpenAPI JSON and YAML files from FastAPI app + +# Kill any existing process using port 5000 +lsof -ti:5000 | xargs kill -9 2>/dev/null || true + +echo "šŸš€ Starting FastAPI server..." + +# Start the FastAPI server using uv in the background +uv run uvicorn app:app --host 127.0.0.1 --port 5000 & +SERVER_PID=$! + +# Wait for server to start up +echo "ā³ Waiting for server to start..." +sleep 3 + +# Health check to ensure server is running +if curl -s http://127.0.0.1:5000/health > /dev/null; then + echo "āœ… Server is running" +else + echo "āŒ Server failed to start" + kill $SERVER_PID 2>/dev/null + exit 1 +fi + +echo "šŸ“„ Generating OpenAPI files..." + +# Generate OpenAPI files using the Python script +uv run python generate_openapi.py + +# Stop the server +echo "šŸ›‘ Stopping server..." +kill $SERVER_PID diff --git a/taskmaster-internal-api/generate_openapi.py b/taskmaster-internal-api/generate_openapi.py new file mode 100755 index 0000000..372eea2 --- /dev/null +++ b/taskmaster-internal-api/generate_openapi.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 +""" +Generate OpenAPI specification files for the License Distribution API +""" + +import json +import yaml +import requests +import sys +from pathlib import Path + + +def generate_openapi_files(base_url="http://localhost:8000", output_dir="."): + """Generate OpenAPI JSON and YAML files""" + + try: + # Fetch OpenAPI JSON from the running server + print(f"Fetching OpenAPI specification from {base_url}/openapi.json...") + response = requests.get(f"{base_url}/openapi.json") + response.raise_for_status() + + openapi_data = response.json() + + # Ensure output directory exists + output_path = Path(output_dir) + output_path.mkdir(exist_ok=True) + + # Write JSON file (formatted) + json_file = output_path / "openapi.json" + with open(json_file, 'w') as f: + json.dump(openapi_data, f, indent=2, sort_keys=False) + print(f"āœ… OpenAPI JSON file generated: {json_file}") + + # Write YAML file + yaml_file = output_path / "openapi.yaml" + with open(yaml_file, 'w') as f: + yaml.dump(openapi_data, f, default_flow_style=False, sort_keys=False) + print(f"āœ… OpenAPI YAML file generated: {yaml_file}") + + # Print summary + print(f"\nšŸ“Š OpenAPI Specification Summary:") + print(f" Title: {openapi_data.get('info', {}).get('title', 'N/A')}") + print(f" Version: {openapi_data.get('info', {}).get('version', 'N/A')}") + print(f" Description: {openapi_data.get('info', {}).get('description', 'N/A')[:100]}...") + + paths = openapi_data.get('paths', {}) + print(f" Endpoints: {len(paths)}") + + # List all endpoints + print(f"\nšŸ”— Available Endpoints:") + for path, methods in paths.items(): + for method, details in methods.items(): + if method.lower() in ['get', 'post', 'put', 'delete', 'patch']: + summary = details.get('summary', 'No summary') + print(f" {method.upper()} {path} - {summary}") + + return True + + except requests.exceptions.ConnectionError: + print(f"āŒ Error: Could not connect to {base_url}") + print(" Make sure the server is running with: uv run uvicorn app.main:app --reload") + return False + + except requests.exceptions.RequestException as e: + print(f"āŒ Error fetching OpenAPI specification: {e}") + return False + + except Exception as e: + print(f"āŒ Error generating OpenAPI files: {e}") + return False + + +def main(): + """Main function""" + import argparse + + parser = argparse.ArgumentParser(description="Generate OpenAPI specification files") + parser.add_argument("--url", default="http://localhost:8000", + help="Base URL of the running API server") + parser.add_argument("--output", default=".", + help="Output directory for generated files") + + args = parser.parse_args() + + print("šŸš€ License Distribution API - OpenAPI Generator") + print("=" * 50) + + success = generate_openapi_files(args.url, args.output) + + if success: + print("\nšŸŽ‰ OpenAPI files generated successfully!") + print(" You can now use these files for:") + print(" - API documentation") + print(" - Client code generation") + print(" - API testing tools") + print(" - Integration with other services") + else: + print("\nšŸ’„ Failed to generate OpenAPI files") + sys.exit(1) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/taskmaster-internal-api/openapi.json b/taskmaster-internal-api/openapi.json new file mode 100644 index 0000000..3bf23e7 --- /dev/null +++ b/taskmaster-internal-api/openapi.json @@ -0,0 +1,1471 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "License Distribution API", + "version": "1.0.0" + }, + "paths": { + "/health": { + "get": { + "tags": [ + "system" + ], + "summary": "Health Check", + "operationId": "health_check_health_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HealthResponse" + } + } + } + } + } + } + }, + "/users/register": { + "post": { + "tags": [ + "users" + ], + "summary": "Register User", + "operationId": "register_user_users_register_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/users/login": { + "post": { + "tags": [ + "users" + ], + "summary": "Login User", + "operationId": "login_user_users_login_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserLogin" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Token" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/users": { + "get": { + "tags": [ + "users" + ], + "summary": "List Users", + "operationId": "list_users_users_get", + "parameters": [ + { + "name": "name", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Name" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/User" + }, + "title": "Response List Users Users Get" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/users/{user_id}": { + "get": { + "tags": [ + "users" + ], + "summary": "Get User", + "operationId": "get_user_users__user_id__get", + "parameters": [ + { + "name": "user_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "title": "User Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/licenses": { + "get": { + "tags": [ + "licenses" + ], + "summary": "List Licenses", + "description": "List all licenses in the system", + "operationId": "list_licenses_licenses_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/License" + }, + "type": "array", + "title": "Response List Licenses Licenses Get" + } + } + } + } + } + }, + "post": { + "tags": [ + "licenses" + ], + "summary": "Create License", + "description": "Create a new license", + "operationId": "create_license_licenses_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LicenseCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/License" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/licenses/users": { + "post": { + "tags": [ + "licenses" + ], + "summary": "Create License User", + "operationId": "create_license_user_licenses_users_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/licenses/validate": { + "post": { + "tags": [ + "licenses" + ], + "summary": "Validate License", + "description": "Validate a license key", + "operationId": "validate_license_licenses_validate_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LicenseValidation" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LicenseValidationResponse" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/marketing/campaigns": { + "get": { + "tags": [ + "marketing" + ], + "summary": "List Campaigns", + "description": "List all marketing campaigns", + "operationId": "list_campaigns_marketing_campaigns_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/Campaign" + }, + "type": "array", + "title": "Response List Campaigns Marketing Campaigns Get" + } + } + } + } + } + }, + "post": { + "tags": [ + "marketing" + ], + "summary": "Create Campaign", + "description": "Create a new marketing campaign", + "operationId": "create_campaign_marketing_campaigns_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CampaignCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Campaign" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/marketing/users": { + "post": { + "tags": [ + "marketing" + ], + "summary": "Create Marketing User", + "operationId": "create_marketing_user_marketing_users_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/customer-service/tickets": { + "get": { + "tags": [ + "customer-service" + ], + "summary": "List Support Tickets", + "description": "List all support tickets", + "operationId": "list_support_tickets_customer_service_tickets_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/SupportTicket" + }, + "type": "array", + "title": "Response List Support Tickets Customer Service Tickets Get" + } + } + } + } + } + }, + "post": { + "tags": [ + "customer-service" + ], + "summary": "Create Support Ticket", + "description": "Create a new support ticket", + "operationId": "create_support_ticket_customer_service_tickets_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SupportTicketCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SupportTicket" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/customer-service/my-tickets": { + "get": { + "tags": [ + "customer-service" + ], + "summary": "Get My Tickets", + "description": "Get current user's support tickets", + "operationId": "get_my_tickets_customer_service_my_tickets_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/SupportTicket" + }, + "type": "array", + "title": "Response Get My Tickets Customer Service My Tickets Get" + } + } + } + } + } + } + }, + "/customer-service/users": { + "post": { + "tags": [ + "customer-service" + ], + "summary": "Create Customer Service User", + "operationId": "create_customer_service_user_customer_service_users_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "Campaign": { + "properties": { + "name": { + "type": "string", + "title": "Name", + "description": "Campaign name", + "example": "Q1 2024 Product Launch" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Campaign description", + "example": "Marketing campaign for new product launch" + }, + "target_audience": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Target Audience", + "description": "Target audience", + "example": "Enterprise customers" + }, + "start_date": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Start Date", + "description": "Campaign start date", + "example": "2024-01-01T00:00:00Z" + }, + "end_date": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "End Date", + "description": "Campaign end date", + "example": "2024-03-31T23:59:59Z" + }, + "id": { + "type": "integer", + "title": "Id", + "description": "Campaign ID", + "example": 1 + }, + "status": { + "$ref": "#/components/schemas/CampaignStatus", + "description": "Campaign status", + "example": "active" + }, + "created_by": { + "type": "integer", + "title": "Created By", + "description": "User ID who created the campaign", + "example": 1 + }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Created At", + "description": "Creation timestamp", + "example": "2023-12-01T10:00:00Z" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "title": "Updated At", + "description": "Last update timestamp", + "example": "2023-12-01T10:00:00Z" + }, + "is_active": { + "type": "boolean", + "title": "Is Active", + "description": "Whether campaign is active", + "example": true + } + }, + "type": "object", + "required": [ + "name", + "id", + "status", + "created_by", + "created_at", + "updated_at", + "is_active" + ], + "title": "Campaign" + }, + "CampaignCreate": { + "properties": { + "name": { + "type": "string", + "title": "Name", + "description": "Campaign name", + "example": "Q1 2024 Product Launch" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Campaign description", + "example": "Marketing campaign for new product launch" + }, + "target_audience": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Target Audience", + "description": "Target audience", + "example": "Enterprise customers" + }, + "start_date": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Start Date", + "description": "Campaign start date", + "example": "2024-01-01T00:00:00Z" + }, + "end_date": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "End Date", + "description": "Campaign end date", + "example": "2024-03-31T23:59:59Z" + } + }, + "type": "object", + "required": [ + "name" + ], + "title": "CampaignCreate" + }, + "CampaignStatus": { + "type": "string", + "enum": [ + "draft", + "active", + "paused", + "completed", + "cancelled" + ], + "title": "CampaignStatus" + }, + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "type": "array", + "title": "Detail" + } + }, + "type": "object", + "title": "HTTPValidationError" + }, + "HealthResponse": { + "properties": { + "status": { + "type": "string", + "title": "Status" + }, + "timestamp": { + "type": "string", + "format": "date-time", + "title": "Timestamp" + } + }, + "type": "object", + "required": [ + "status", + "timestamp" + ], + "title": "HealthResponse" + }, + "License": { + "properties": { + "name": { + "type": "string", + "title": "Name", + "description": "Name of the license", + "example": "TaskMaster Premium License" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Description of the license", + "example": "Full access to all TaskMaster features" + }, + "license_key": { + "type": "string", + "title": "License Key", + "description": "Unique license key", + "example": "TASKMASTER-PREMIUM-12345-ABCDE" + }, + "license_type": { + "$ref": "#/components/schemas/LicenseType", + "description": "Type of license", + "default": "basic", + "example": "premium" + }, + "expires_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Expires At", + "description": "License expiration date", + "example": "2024-12-01T10:00:00Z" + }, + "is_active": { + "type": "boolean", + "title": "Is Active", + "description": "Whether the license is active", + "default": true, + "example": true + }, + "id": { + "type": "integer", + "title": "Id", + "description": "License ID", + "example": 1 + }, + "user_id": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "User Id", + "description": "User ID", + "example": 1 + }, + "status": { + "$ref": "#/components/schemas/LicenseStatus", + "description": "License status", + "example": "active" + }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Created At", + "description": "Creation timestamp", + "example": "2023-12-01T10:00:00Z" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "title": "Updated At", + "description": "Last update timestamp", + "example": "2023-12-01T10:00:00Z" + } + }, + "type": "object", + "required": [ + "name", + "license_key", + "id", + "status", + "created_at", + "updated_at" + ], + "title": "License" + }, + "LicenseCreate": { + "properties": { + "name": { + "type": "string", + "title": "Name", + "description": "Name of the license", + "example": "TaskMaster Premium License" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Description of the license", + "example": "Full access to all TaskMaster features" + }, + "license_key": { + "type": "string", + "title": "License Key", + "description": "Unique license key", + "example": "TASKMASTER-PREMIUM-12345-ABCDE" + }, + "license_type": { + "$ref": "#/components/schemas/LicenseType", + "description": "Type of license", + "default": "basic", + "example": "premium" + }, + "expires_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Expires At", + "description": "License expiration date", + "example": "2024-12-01T10:00:00Z" + }, + "is_active": { + "type": "boolean", + "title": "Is Active", + "description": "Whether the license is active", + "default": true, + "example": true + }, + "user_id": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "User Id", + "description": "User ID to assign license to", + "example": 1 + } + }, + "type": "object", + "required": [ + "name", + "license_key" + ], + "title": "LicenseCreate" + }, + "LicenseStatus": { + "type": "string", + "enum": [ + "active", + "expired", + "revoked", + "pending" + ], + "title": "LicenseStatus" + }, + "LicenseType": { + "type": "string", + "enum": [ + "trial", + "basic", + "premium", + "enterprise" + ], + "title": "LicenseType" + }, + "LicenseValidation": { + "properties": { + "license_key": { + "type": "string", + "title": "License Key", + "description": "License key to validate", + "example": "TASKMASTER-PREMIUM-12345-ABCDE" + } + }, + "type": "object", + "required": [ + "license_key" + ], + "title": "LicenseValidation" + }, + "LicenseValidationResponse": { + "properties": { + "is_valid": { + "type": "boolean", + "title": "Is Valid", + "description": "Whether license is valid", + "example": true + }, + "license_type": { + "anyOf": [ + { + "$ref": "#/components/schemas/LicenseType" + }, + { + "type": "null" + } + ], + "description": "License type if valid" + }, + "status": { + "anyOf": [ + { + "$ref": "#/components/schemas/LicenseStatus" + }, + { + "type": "null" + } + ], + "description": "License status if valid" + }, + "expires_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Expires At", + "description": "Expiration date if valid" + }, + "user_id": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "User Id", + "description": "User ID if valid" + } + }, + "type": "object", + "required": [ + "is_valid" + ], + "title": "LicenseValidationResponse" + }, + "SupportTicket": { + "properties": { + "title": { + "type": "string", + "title": "Title", + "description": "Ticket title", + "example": "Login issues with TaskMaster" + }, + "description": { + "type": "string", + "title": "Description", + "description": "Ticket description", + "example": "Unable to login to TaskMaster application" + }, + "priority": { + "$ref": "#/components/schemas/TicketPriority", + "description": "Ticket priority", + "default": "medium", + "example": "medium" + }, + "id": { + "type": "integer", + "title": "Id", + "description": "Ticket ID", + "example": 1 + }, + "status": { + "$ref": "#/components/schemas/TicketStatus", + "description": "Ticket status", + "example": "open" + }, + "user_id": { + "type": "integer", + "title": "User Id", + "description": "User ID who created the ticket", + "example": 1 + }, + "assigned_to": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Assigned To", + "description": "User ID assigned to handle the ticket", + "example": 2 + }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Created At", + "description": "Creation timestamp", + "example": "2023-12-01T10:00:00Z" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "title": "Updated At", + "description": "Last update timestamp", + "example": "2023-12-01T10:00:00Z" + }, + "resolved_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Resolved At", + "description": "Resolution timestamp", + "example": "2023-12-02T15:30:00Z" + }, + "is_active": { + "type": "boolean", + "title": "Is Active", + "description": "Whether ticket is active", + "example": true + } + }, + "type": "object", + "required": [ + "title", + "description", + "id", + "status", + "user_id", + "created_at", + "updated_at", + "is_active" + ], + "title": "SupportTicket" + }, + "SupportTicketCreate": { + "properties": { + "title": { + "type": "string", + "title": "Title", + "description": "Ticket title", + "example": "Login issues with TaskMaster" + }, + "description": { + "type": "string", + "title": "Description", + "description": "Ticket description", + "example": "Unable to login to TaskMaster application" + }, + "priority": { + "$ref": "#/components/schemas/TicketPriority", + "description": "Ticket priority", + "default": "medium", + "example": "medium" + } + }, + "type": "object", + "required": [ + "title", + "description" + ], + "title": "SupportTicketCreate" + }, + "TicketPriority": { + "type": "string", + "enum": [ + "low", + "medium", + "high", + "urgent" + ], + "title": "TicketPriority" + }, + "TicketStatus": { + "type": "string", + "enum": [ + "open", + "in_progress", + "resolved", + "closed" + ], + "title": "TicketStatus" + }, + "Token": { + "properties": { + "access_token": { + "type": "string", + "title": "Access Token", + "description": "JWT access token", + "example": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..." + }, + "token_type": { + "type": "string", + "title": "Token Type", + "description": "Token type", + "example": "bearer" + } + }, + "type": "object", + "required": [ + "access_token", + "token_type" + ], + "title": "Token" + }, + "User": { + "properties": { + "email": { + "type": "string", + "format": "email", + "title": "Email", + "description": "User email address", + "example": "user@example.com" + }, + "username": { + "type": "string", + "title": "Username", + "description": "Username", + "example": "johndoe" + }, + "full_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Full Name", + "description": "Full name", + "example": "John Doe" + }, + "role": { + "$ref": "#/components/schemas/UserRole", + "description": "User role", + "default": "user", + "example": "user" + }, + "id": { + "type": "integer", + "title": "Id", + "description": "User ID", + "example": 1 + }, + "is_active": { + "type": "boolean", + "title": "Is Active", + "description": "Whether user is active", + "example": true + }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Created At", + "description": "Creation timestamp", + "example": "2023-12-01T10:00:00Z" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "title": "Updated At", + "description": "Last update timestamp", + "example": "2023-12-01T10:00:00Z" + } + }, + "type": "object", + "required": [ + "email", + "username", + "id", + "is_active", + "created_at", + "updated_at" + ], + "title": "User" + }, + "UserCreate": { + "properties": { + "email": { + "type": "string", + "format": "email", + "title": "Email", + "description": "User email address", + "example": "user@example.com" + }, + "username": { + "type": "string", + "title": "Username", + "description": "Username", + "example": "johndoe" + }, + "full_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Full Name", + "description": "Full name", + "example": "John Doe" + }, + "role": { + "$ref": "#/components/schemas/UserRole", + "description": "User role", + "default": "user", + "example": "user" + }, + "password": { + "type": "string", + "title": "Password", + "description": "Password", + "example": "securepassword123" + } + }, + "type": "object", + "required": [ + "email", + "username", + "password" + ], + "title": "UserCreate" + }, + "UserLogin": { + "properties": { + "username": { + "type": "string", + "title": "Username", + "description": "Username", + "example": "johndoe" + }, + "password": { + "type": "string", + "title": "Password", + "description": "Password", + "example": "securepassword123" + } + }, + "type": "object", + "required": [ + "username", + "password" + ], + "title": "UserLogin" + }, + "UserRole": { + "type": "string", + "enum": [ + "admin", + "marketing", + "customer_service", + "user" + ], + "title": "UserRole" + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer" + } + ] + }, + "type": "array", + "title": "Location" + }, + "msg": { + "type": "string", + "title": "Message" + }, + "type": { + "type": "string", + "title": "Error Type" + } + }, + "type": "object", + "required": [ + "loc", + "msg", + "type" + ], + "title": "ValidationError" + } + } + }, + "tags": [ + { + "name": "licenses" + }, + { + "name": "users" + }, + { + "name": "marketing" + }, + { + "name": "customer-service" + }, + { + "name": "system" + } + ] +} \ No newline at end of file diff --git a/taskmaster-internal-api/openapi.yaml b/taskmaster-internal-api/openapi.yaml new file mode 100644 index 0000000..1c1ba52 --- /dev/null +++ b/taskmaster-internal-api/openapi.yaml @@ -0,0 +1,981 @@ +openapi: 3.1.0 +info: + title: License Distribution API + version: 1.0.0 +paths: + /health: + get: + tags: + - system + summary: Health Check + operationId: health_check_health_get + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/HealthResponse' + /users/register: + post: + tags: + - users + summary: Register User + operationId: register_user_users_register_post + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UserCreate' + required: true + responses: + '201': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/User' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /users/login: + post: + tags: + - users + summary: Login User + operationId: login_user_users_login_post + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UserLogin' + required: true + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/Token' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /users: + get: + tags: + - users + summary: List Users + operationId: list_users_users_get + parameters: + - name: name + in: query + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: Name + responses: + '200': + description: Successful Response + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/User' + title: Response List Users Users Get + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /users/{user_id}: + get: + tags: + - users + summary: Get User + operationId: get_user_users__user_id__get + parameters: + - name: user_id + in: path + required: true + schema: + type: integer + title: User Id + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/User' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /licenses: + get: + tags: + - licenses + summary: List Licenses + description: List all licenses in the system + operationId: list_licenses_licenses_get + responses: + '200': + description: Successful Response + content: + application/json: + schema: + items: + $ref: '#/components/schemas/License' + type: array + title: Response List Licenses Licenses Get + post: + tags: + - licenses + summary: Create License + description: Create a new license + operationId: create_license_licenses_post + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/LicenseCreate' + required: true + responses: + '201': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/License' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /licenses/users: + post: + tags: + - licenses + summary: Create License User + operationId: create_license_user_licenses_users_post + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UserCreate' + required: true + responses: + '201': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/User' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /licenses/validate: + post: + tags: + - licenses + summary: Validate License + description: Validate a license key + operationId: validate_license_licenses_validate_post + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/LicenseValidation' + required: true + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/LicenseValidationResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /marketing/campaigns: + get: + tags: + - marketing + summary: List Campaigns + description: List all marketing campaigns + operationId: list_campaigns_marketing_campaigns_get + responses: + '200': + description: Successful Response + content: + application/json: + schema: + items: + $ref: '#/components/schemas/Campaign' + type: array + title: Response List Campaigns Marketing Campaigns Get + post: + tags: + - marketing + summary: Create Campaign + description: Create a new marketing campaign + operationId: create_campaign_marketing_campaigns_post + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/CampaignCreate' + required: true + responses: + '201': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/Campaign' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /marketing/users: + post: + tags: + - marketing + summary: Create Marketing User + operationId: create_marketing_user_marketing_users_post + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UserCreate' + required: true + responses: + '201': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/User' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /customer-service/tickets: + get: + tags: + - customer-service + summary: List Support Tickets + description: List all support tickets + operationId: list_support_tickets_customer_service_tickets_get + responses: + '200': + description: Successful Response + content: + application/json: + schema: + items: + $ref: '#/components/schemas/SupportTicket' + type: array + title: Response List Support Tickets Customer Service Tickets Get + post: + tags: + - customer-service + summary: Create Support Ticket + description: Create a new support ticket + operationId: create_support_ticket_customer_service_tickets_post + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/SupportTicketCreate' + required: true + responses: + '201': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/SupportTicket' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /customer-service/my-tickets: + get: + tags: + - customer-service + summary: Get My Tickets + description: Get current user's support tickets + operationId: get_my_tickets_customer_service_my_tickets_get + responses: + '200': + description: Successful Response + content: + application/json: + schema: + items: + $ref: '#/components/schemas/SupportTicket' + type: array + title: Response Get My Tickets Customer Service My Tickets Get + /customer-service/users: + post: + tags: + - customer-service + summary: Create Customer Service User + operationId: create_customer_service_user_customer_service_users_post + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UserCreate' + required: true + responses: + '201': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/User' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' +components: + schemas: + Campaign: + properties: + name: + type: string + title: Name + description: Campaign name + example: Q1 2024 Product Launch + description: + anyOf: + - type: string + - type: 'null' + title: Description + description: Campaign description + example: Marketing campaign for new product launch + target_audience: + anyOf: + - type: string + - type: 'null' + title: Target Audience + description: Target audience + example: Enterprise customers + start_date: + anyOf: + - type: string + format: date-time + - type: 'null' + title: Start Date + description: Campaign start date + example: '2024-01-01T00:00:00Z' + end_date: + anyOf: + - type: string + format: date-time + - type: 'null' + title: End Date + description: Campaign end date + example: '2024-03-31T23:59:59Z' + id: + type: integer + title: Id + description: Campaign ID + example: 1 + status: + $ref: '#/components/schemas/CampaignStatus' + description: Campaign status + example: active + created_by: + type: integer + title: Created By + description: User ID who created the campaign + example: 1 + created_at: + type: string + format: date-time + title: Created At + description: Creation timestamp + example: '2023-12-01T10:00:00Z' + updated_at: + type: string + format: date-time + title: Updated At + description: Last update timestamp + example: '2023-12-01T10:00:00Z' + is_active: + type: boolean + title: Is Active + description: Whether campaign is active + example: true + type: object + required: + - name + - id + - status + - created_by + - created_at + - updated_at + - is_active + title: Campaign + CampaignCreate: + properties: + name: + type: string + title: Name + description: Campaign name + example: Q1 2024 Product Launch + description: + anyOf: + - type: string + - type: 'null' + title: Description + description: Campaign description + example: Marketing campaign for new product launch + target_audience: + anyOf: + - type: string + - type: 'null' + title: Target Audience + description: Target audience + example: Enterprise customers + start_date: + anyOf: + - type: string + format: date-time + - type: 'null' + title: Start Date + description: Campaign start date + example: '2024-01-01T00:00:00Z' + end_date: + anyOf: + - type: string + format: date-time + - type: 'null' + title: End Date + description: Campaign end date + example: '2024-03-31T23:59:59Z' + type: object + required: + - name + title: CampaignCreate + CampaignStatus: + type: string + enum: + - draft + - active + - paused + - completed + - cancelled + title: CampaignStatus + HTTPValidationError: + properties: + detail: + items: + $ref: '#/components/schemas/ValidationError' + type: array + title: Detail + type: object + title: HTTPValidationError + HealthResponse: + properties: + status: + type: string + title: Status + timestamp: + type: string + format: date-time + title: Timestamp + type: object + required: + - status + - timestamp + title: HealthResponse + License: + properties: + name: + type: string + title: Name + description: Name of the license + example: TaskMaster Premium License + description: + anyOf: + - type: string + - type: 'null' + title: Description + description: Description of the license + example: Full access to all TaskMaster features + license_key: + type: string + title: License Key + description: Unique license key + example: TASKMASTER-PREMIUM-12345-ABCDE + license_type: + $ref: '#/components/schemas/LicenseType' + description: Type of license + default: basic + example: premium + expires_at: + anyOf: + - type: string + format: date-time + - type: 'null' + title: Expires At + description: License expiration date + example: '2024-12-01T10:00:00Z' + is_active: + type: boolean + title: Is Active + description: Whether the license is active + default: true + example: true + id: + type: integer + title: Id + description: License ID + example: 1 + user_id: + anyOf: + - type: integer + - type: 'null' + title: User Id + description: User ID + example: 1 + status: + $ref: '#/components/schemas/LicenseStatus' + description: License status + example: active + created_at: + type: string + format: date-time + title: Created At + description: Creation timestamp + example: '2023-12-01T10:00:00Z' + updated_at: + type: string + format: date-time + title: Updated At + description: Last update timestamp + example: '2023-12-01T10:00:00Z' + type: object + required: + - name + - license_key + - id + - status + - created_at + - updated_at + title: License + LicenseCreate: + properties: + name: + type: string + title: Name + description: Name of the license + example: TaskMaster Premium License + description: + anyOf: + - type: string + - type: 'null' + title: Description + description: Description of the license + example: Full access to all TaskMaster features + license_key: + type: string + title: License Key + description: Unique license key + example: TASKMASTER-PREMIUM-12345-ABCDE + license_type: + $ref: '#/components/schemas/LicenseType' + description: Type of license + default: basic + example: premium + expires_at: + anyOf: + - type: string + format: date-time + - type: 'null' + title: Expires At + description: License expiration date + example: '2024-12-01T10:00:00Z' + is_active: + type: boolean + title: Is Active + description: Whether the license is active + default: true + example: true + user_id: + anyOf: + - type: integer + - type: 'null' + title: User Id + description: User ID to assign license to + example: 1 + type: object + required: + - name + - license_key + title: LicenseCreate + LicenseStatus: + type: string + enum: + - active + - expired + - revoked + - pending + title: LicenseStatus + LicenseType: + type: string + enum: + - trial + - basic + - premium + - enterprise + title: LicenseType + LicenseValidation: + properties: + license_key: + type: string + title: License Key + description: License key to validate + example: TASKMASTER-PREMIUM-12345-ABCDE + type: object + required: + - license_key + title: LicenseValidation + LicenseValidationResponse: + properties: + is_valid: + type: boolean + title: Is Valid + description: Whether license is valid + example: true + license_type: + anyOf: + - $ref: '#/components/schemas/LicenseType' + - type: 'null' + description: License type if valid + status: + anyOf: + - $ref: '#/components/schemas/LicenseStatus' + - type: 'null' + description: License status if valid + expires_at: + anyOf: + - type: string + format: date-time + - type: 'null' + title: Expires At + description: Expiration date if valid + user_id: + anyOf: + - type: integer + - type: 'null' + title: User Id + description: User ID if valid + type: object + required: + - is_valid + title: LicenseValidationResponse + SupportTicket: + properties: + title: + type: string + title: Title + description: Ticket title + example: Login issues with TaskMaster + description: + type: string + title: Description + description: Ticket description + example: Unable to login to TaskMaster application + priority: + $ref: '#/components/schemas/TicketPriority' + description: Ticket priority + default: medium + example: medium + id: + type: integer + title: Id + description: Ticket ID + example: 1 + status: + $ref: '#/components/schemas/TicketStatus' + description: Ticket status + example: open + user_id: + type: integer + title: User Id + description: User ID who created the ticket + example: 1 + assigned_to: + anyOf: + - type: integer + - type: 'null' + title: Assigned To + description: User ID assigned to handle the ticket + example: 2 + created_at: + type: string + format: date-time + title: Created At + description: Creation timestamp + example: '2023-12-01T10:00:00Z' + updated_at: + type: string + format: date-time + title: Updated At + description: Last update timestamp + example: '2023-12-01T10:00:00Z' + resolved_at: + anyOf: + - type: string + format: date-time + - type: 'null' + title: Resolved At + description: Resolution timestamp + example: '2023-12-02T15:30:00Z' + is_active: + type: boolean + title: Is Active + description: Whether ticket is active + example: true + type: object + required: + - title + - description + - id + - status + - user_id + - created_at + - updated_at + - is_active + title: SupportTicket + SupportTicketCreate: + properties: + title: + type: string + title: Title + description: Ticket title + example: Login issues with TaskMaster + description: + type: string + title: Description + description: Ticket description + example: Unable to login to TaskMaster application + priority: + $ref: '#/components/schemas/TicketPriority' + description: Ticket priority + default: medium + example: medium + type: object + required: + - title + - description + title: SupportTicketCreate + TicketPriority: + type: string + enum: + - low + - medium + - high + - urgent + title: TicketPriority + TicketStatus: + type: string + enum: + - open + - in_progress + - resolved + - closed + title: TicketStatus + Token: + properties: + access_token: + type: string + title: Access Token + description: JWT access token + example: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9... + token_type: + type: string + title: Token Type + description: Token type + example: bearer + type: object + required: + - access_token + - token_type + title: Token + User: + properties: + email: + type: string + format: email + title: Email + description: User email address + example: user@example.com + username: + type: string + title: Username + description: Username + example: johndoe + full_name: + anyOf: + - type: string + - type: 'null' + title: Full Name + description: Full name + example: John Doe + role: + $ref: '#/components/schemas/UserRole' + description: User role + default: user + example: user + id: + type: integer + title: Id + description: User ID + example: 1 + is_active: + type: boolean + title: Is Active + description: Whether user is active + example: true + created_at: + type: string + format: date-time + title: Created At + description: Creation timestamp + example: '2023-12-01T10:00:00Z' + updated_at: + type: string + format: date-time + title: Updated At + description: Last update timestamp + example: '2023-12-01T10:00:00Z' + type: object + required: + - email + - username + - id + - is_active + - created_at + - updated_at + title: User + UserCreate: + properties: + email: + type: string + format: email + title: Email + description: User email address + example: user@example.com + username: + type: string + title: Username + description: Username + example: johndoe + full_name: + anyOf: + - type: string + - type: 'null' + title: Full Name + description: Full name + example: John Doe + role: + $ref: '#/components/schemas/UserRole' + description: User role + default: user + example: user + password: + type: string + title: Password + description: Password + example: securepassword123 + type: object + required: + - email + - username + - password + title: UserCreate + UserLogin: + properties: + username: + type: string + title: Username + description: Username + example: johndoe + password: + type: string + title: Password + description: Password + example: securepassword123 + type: object + required: + - username + - password + title: UserLogin + UserRole: + type: string + enum: + - admin + - marketing + - customer_service + - user + title: UserRole + ValidationError: + properties: + loc: + items: + anyOf: + - type: string + - type: integer + type: array + title: Location + msg: + type: string + title: Message + type: + type: string + title: Error Type + type: object + required: + - loc + - msg + - type + title: ValidationError +tags: +- name: licenses +- name: users +- name: marketing +- name: customer-service +- name: system diff --git a/taskmaster-internal-api/pyproject.toml b/taskmaster-internal-api/pyproject.toml new file mode 100644 index 0000000..ae63609 --- /dev/null +++ b/taskmaster-internal-api/pyproject.toml @@ -0,0 +1,20 @@ +[project] +name = "taskmaster-license" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.13" +dependencies = [ + "email-validator>=2.3.0", + "fastapi>=0.116.1", + "httpx>=0.28.1", + "passlib[bcrypt]>=1.7.4", + "pydantic>=2.11.7", + "pydantic-settings>=2.10.1", + "pytest>=8.4.2", + "python-jose[cryptography]>=3.5.0", + "python-multipart>=0.0.20", + "pyyaml>=6.0.2", + "requests>=2.32.5", + "uvicorn[standard]>=0.35.0", +] diff --git a/taskmaster-internal-api/requirements.txt b/taskmaster-internal-api/requirements.txt new file mode 100644 index 0000000..e513a28 --- /dev/null +++ b/taskmaster-internal-api/requirements.txt @@ -0,0 +1,11 @@ +fastapi==0.104.1 +uvicorn[standard]==0.24.0 +pydantic==2.6.0 +pydantic-settings==2.1.0 +python-multipart==0.0.6 +python-jose[cryptography]==3.3.0 +passlib[bcrypt]==1.7.4 +email-validator==2.1.0 +pytest==7.4.3 +httpx==0.25.2 +PyYAML==6.0.1 diff --git a/taskmaster-internal-api/scripts/init_db.py b/taskmaster-internal-api/scripts/init_db.py new file mode 100755 index 0000000..905e3f9 --- /dev/null +++ b/taskmaster-internal-api/scripts/init_db.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 +""" +Database initialization script +Creates tables and initial admin user +""" + +import sys +import os +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +import sqlite3 +from app.core.database import init_database, get_db_connection +from app.core.security import get_password_hash + + +def create_admin_user(): + """Create initial admin user""" + conn = get_db_connection() + cursor = conn.cursor() + + try: + # Check if admin user already exists + cursor.execute("SELECT id FROM users WHERE username = ?", ("admin",)) + if cursor.fetchone(): + print("Admin user already exists!") + return + + # Create admin user + hashed_password = get_password_hash("admin123") + cursor.execute(""" + INSERT INTO users (email, username, hashed_password, full_name, role, is_active) + VALUES (?, ?, ?, ?, ?, ?) + """, ( + "admin@example.com", + "admin", + hashed_password, + "System Administrator", + "admin", + True + )) + + conn.commit() + print("Admin user created successfully!") + print("Username: admin") + print("Password: admin123") + print("Please change the password after first login!") + + except Exception as e: + print(f"Error creating admin user: {e}") + conn.rollback() + finally: + conn.close() + + +def main(): + """Main initialization function""" + print("Initializing database...") + init_database() + create_admin_user() + print("Database initialization complete!") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/taskmaster-internal-api/setup.sh b/taskmaster-internal-api/setup.sh new file mode 100755 index 0000000..729edfa --- /dev/null +++ b/taskmaster-internal-api/setup.sh @@ -0,0 +1,39 @@ +#!/bin/bash + +set -e # Exit on any error + +echo "šŸš€ Setting up License Management API..." + +# Check if uv is installed +if ! command -v uv &> /dev/null; then + echo "āŒ Error: uv is not installed. Please install uv first:" + echo " curl -LsSf https://astral.sh/uv/install.sh | sh" + exit 1 +fi + +echo "āœ… uv is installed" + +# Install dependencies +echo "šŸ“¦ Installing dependencies..." +uv sync + +if [ $? -eq 0 ]; then + echo "āœ… Dependencies installed successfully" +else + echo "āŒ Failed to install dependencies" + exit 1 +fi + +# Check if the app.py file exists +if [ ! -f "app.py" ]; then + echo "āŒ Error: app.py not found in current directory" + exit 1 +fi + +echo "āœ… app.py found" + +# Run the server +echo "🌐 Starting FastAPI server..." +echo " Server will be available at: http://127.0.0.1:8000" + +uv run uvicorn app:app --reload diff --git a/taskmaster-internal-api/test_app.py b/taskmaster-internal-api/test_app.py new file mode 100644 index 0000000..b17b10d --- /dev/null +++ b/taskmaster-internal-api/test_app.py @@ -0,0 +1,161 @@ +import pytest +import json +import os +import tempfile +import sqlite3 +from fastapi.testclient import TestClient +from app import app, get_db + +@pytest.fixture +def client(): + """Create a test client with a temporary database""" + # Create a temporary database file + db_fd, temp_db_path = tempfile.mkstemp(suffix='.db') + + # Override the database URL for testing + def get_test_db(): + conn = sqlite3.connect(temp_db_path, check_same_thread=False) + conn.row_factory = sqlite3.Row + return conn + + # Initialize test database + conn = get_test_db() + cursor = conn.cursor() + cursor.execute(''' + CREATE TABLE IF NOT EXISTS licenses ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + description TEXT, + license_key TEXT UNIQUE NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + is_active BOOLEAN DEFAULT 1 + ) + ''') + conn.commit() + conn.close() + + # Override the database connection function + app.dependency_overrides[get_db] = lambda: get_test_db() + + with TestClient(app) as client: + yield client + + # Clean up + app.dependency_overrides.clear() + os.close(db_fd) + os.unlink(temp_db_path) + +def test_health_check(client): + """Test the health check endpoint""" + response = client.get('/health') + assert response.status_code == 200 + data = response.json() + assert data['status'] == 'healthy' + assert 'timestamp' in data + +def test_list_licenses_empty(client): + """Test listing licenses when none exist""" + response = client.get('/licenses') + assert response.status_code == 200 + data = response.json() + assert data == [] + +def test_create_license_success(client): + """Test creating a TaskMaster license successfully""" + license_data = { + 'name': 'TaskMaster Test License', + 'description': 'A test TaskMaster license', + 'license_key': 'TASKMASTER-TEST-12345-ABCDE', + 'is_active': True + } + + response = client.post('/licenses', json=license_data) + + assert response.status_code == 201 + data = response.json() + assert data['name'] == 'TaskMaster Test License' + assert data['description'] == 'A test TaskMaster license' + assert data['license_key'] == 'TASKMASTER-TEST-12345-ABCDE' + assert data['is_active'] == True + assert 'id' in data + assert 'created_at' in data + +def test_create_license_missing_fields(client): + """Test creating a TaskMaster license with missing required fields""" + license_data = { + 'name': 'TaskMaster Test License' + # Missing license_key + } + + response = client.post('/licenses', json=license_data) + + assert response.status_code == 422 # FastAPI returns 422 for validation errors + data = response.json() + assert 'detail' in data + +def test_create_license_duplicate_key(client): + """Test creating a TaskMaster license with duplicate license key""" + license_data = { + 'name': 'TaskMaster Test License', + 'license_key': 'TASKMASTER-TEST-12345-ABCDE' + } + + # Create first license + response1 = client.post('/licenses', json=license_data) + assert response1.status_code == 201 + + # Try to create second license with same key + response2 = client.post('/licenses', json=license_data) + assert response2.status_code == 409 + data = response2.json() + assert 'already exists' in data['detail'] + +def test_list_licenses_with_data(client): + """Test listing TaskMaster licenses when data exists""" + # Create a test license + license_data = { + 'name': 'TaskMaster Test License 1', + 'description': 'First test TaskMaster license', + 'license_key': 'TASKMASTER-TEST-11111-AAAAA' + } + + client.post('/licenses', json=license_data) + + # Create another test license + license_data2 = { + 'name': 'TaskMaster Test License 2', + 'description': 'Second test TaskMaster license', + 'license_key': 'TASKMASTER-TEST-22222-BBBBB' + } + + client.post('/licenses', json=license_data2) + + # List all licenses + response = client.get('/licenses') + assert response.status_code == 200 + data = response.json() + assert len(data) == 2 + + # Check that both licenses are in the response + license_names = [license['name'] for license in data] + assert 'TaskMaster Test License 1' in license_names + assert 'TaskMaster Test License 2' in license_names + +def test_create_license_minimal_data(client): + """Test creating a TaskMaster license with only required fields""" + license_data = { + 'name': 'TaskMaster Minimal License', + 'license_key': 'TASKMASTER-MINIMAL-12345' + } + + response = client.post('/licenses', json=license_data) + + assert response.status_code == 201 + data = response.json() + assert data['name'] == 'TaskMaster Minimal License' + assert data['license_key'] == 'TASKMASTER-MINIMAL-12345' + assert data['description'] == '' # Should default to empty string + assert data['is_active'] == True # Should default to True + +if __name__ == '__main__': + pytest.main([__file__]) diff --git a/taskmaster-internal-api/tests/test_main.py b/taskmaster-internal-api/tests/test_main.py new file mode 100644 index 0000000..ab15f8d --- /dev/null +++ b/taskmaster-internal-api/tests/test_main.py @@ -0,0 +1,101 @@ +import pytest +from fastapi.testclient import TestClient +from app.main import app + +client = TestClient(app) + + +def test_health_check(): + """Test health check endpoint""" + response = client.get("/health") + assert response.status_code == 200 + data = response.json() + assert data["status"] == "healthy" + assert "timestamp" in data + + +def test_list_licenses(): + """Test listing licenses""" + response = client.get("/licenses") + assert response.status_code == 200 + data = response.json() + assert isinstance(data, list) + + +def test_create_license(): + """Test creating a license""" + license_data = { + "name": "Test License", + "description": "Test license description", + "license_key": "TEST-LICENSE-12345", + "license_type": "basic", + "is_active": True + } + response = client.post("/licenses", json=license_data) + assert response.status_code == 201 + data = response.json() + assert data["name"] == license_data["name"] + assert data["license_key"] == license_data["license_key"] + + +def test_validate_license(): + """Test license validation""" + # First create a license + license_data = { + "name": "Test License for Validation", + "description": "Test license for validation", + "license_key": "VALIDATE-TEST-12345", + "license_type": "basic", + "is_active": True + } + create_response = client.post("/licenses", json=license_data) + assert create_response.status_code == 201 + + # Then validate it + validation_data = {"license_key": "VALIDATE-TEST-12345"} + response = client.post("/licenses/validate", json=validation_data) + assert response.status_code == 200 + data = response.json() + assert data["is_valid"] is True + + +def test_register_user(): + """Test user registration""" + user_data = { + "email": "test@example.com", + "username": "testuser", + "password": "testpassword123", + "full_name": "Test User", + "role": "user" + } + response = client.post("/users/register", json=user_data) + assert response.status_code == 201 + data = response.json() + assert data["email"] == user_data["email"] + assert data["username"] == user_data["username"] + assert data["role"] == user_data["role"] + + +def test_login_user(): + """Test user login""" + # First register a user + user_data = { + "email": "logintest@example.com", + "username": "logintest", + "password": "logintest123", + "full_name": "Login Test User", + "role": "user" + } + register_response = client.post("/users/register", json=user_data) + assert register_response.status_code == 201 + + # Then login + login_data = { + "username": "logintest", + "password": "logintest123" + } + response = client.post("/users/login", json=login_data) + assert response.status_code == 200 + data = response.json() + assert "access_token" in data + assert data["token_type"] == "bearer" diff --git a/taskmaster-internal-api/uv.lock b/taskmaster-internal-api/uv.lock new file mode 100644 index 0000000..a482e95 --- /dev/null +++ b/taskmaster-internal-api/uv.lock @@ -0,0 +1,774 @@ +version = 1 +requires-python = ">=3.13" + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, +] + +[[package]] +name = "anyio" +version = "4.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "sniffio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f1/b4/636b3b65173d3ce9a38ef5f0522789614e590dab6a8d505340a4efe4c567/anyio-4.10.0.tar.gz", hash = "sha256:3f3fae35c96039744587aa5b8371e7e8e603c0702999535961dd336026973ba6", size = 213252 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/12/e5e0282d673bb9746bacfb6e2dba8719989d3660cdb2ea79aee9a9651afb/anyio-4.10.0-py3-none-any.whl", hash = "sha256:60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1", size = 107213 }, +] + +[[package]] +name = "bcrypt" +version = "4.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bb/5d/6d7433e0f3cd46ce0b43cd65e1db465ea024dbb8216fb2404e919c2ad77b/bcrypt-4.3.0.tar.gz", hash = "sha256:3a3fd2204178b6d2adcf09cb4f6426ffef54762577a7c9b54c159008cb288c18", size = 25697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/2c/3d44e853d1fe969d229bd58d39ae6902b3d924af0e2b5a60d17d4b809ded/bcrypt-4.3.0-cp313-cp313t-macosx_10_12_universal2.whl", hash = "sha256:f01e060f14b6b57bbb72fc5b4a83ac21c443c9a2ee708e04a10e9192f90a6281", size = 483719 }, + { url = "https://files.pythonhosted.org/packages/a1/e2/58ff6e2a22eca2e2cff5370ae56dba29d70b1ea6fc08ee9115c3ae367795/bcrypt-4.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5eeac541cefd0bb887a371ef73c62c3cd78535e4887b310626036a7c0a817bb", size = 272001 }, + { url = "https://files.pythonhosted.org/packages/37/1f/c55ed8dbe994b1d088309e366749633c9eb90d139af3c0a50c102ba68a1a/bcrypt-4.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59e1aa0e2cd871b08ca146ed08445038f42ff75968c7ae50d2fdd7860ade2180", size = 277451 }, + { url = "https://files.pythonhosted.org/packages/d7/1c/794feb2ecf22fe73dcfb697ea7057f632061faceb7dcf0f155f3443b4d79/bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:0042b2e342e9ae3d2ed22727c1262f76cc4f345683b5c1715f0250cf4277294f", size = 272792 }, + { url = "https://files.pythonhosted.org/packages/13/b7/0b289506a3f3598c2ae2bdfa0ea66969812ed200264e3f61df77753eee6d/bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74a8d21a09f5e025a9a23e7c0fd2c7fe8e7503e4d356c0a2c1486ba010619f09", size = 289752 }, + { url = "https://files.pythonhosted.org/packages/dc/24/d0fb023788afe9e83cc118895a9f6c57e1044e7e1672f045e46733421fe6/bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:0142b2cb84a009f8452c8c5a33ace5e3dfec4159e7735f5afe9a4d50a8ea722d", size = 277762 }, + { url = "https://files.pythonhosted.org/packages/e4/38/cde58089492e55ac4ef6c49fea7027600c84fd23f7520c62118c03b4625e/bcrypt-4.3.0-cp313-cp313t-manylinux_2_34_aarch64.whl", hash = "sha256:12fa6ce40cde3f0b899729dbd7d5e8811cb892d31b6f7d0334a1f37748b789fd", size = 272384 }, + { url = "https://files.pythonhosted.org/packages/de/6a/d5026520843490cfc8135d03012a413e4532a400e471e6188b01b2de853f/bcrypt-4.3.0-cp313-cp313t-manylinux_2_34_x86_64.whl", hash = "sha256:5bd3cca1f2aa5dbcf39e2aa13dd094ea181f48959e1071265de49cc2b82525af", size = 277329 }, + { url = "https://files.pythonhosted.org/packages/b3/a3/4fc5255e60486466c389e28c12579d2829b28a527360e9430b4041df4cf9/bcrypt-4.3.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:335a420cfd63fc5bc27308e929bee231c15c85cc4c496610ffb17923abf7f231", size = 305241 }, + { url = "https://files.pythonhosted.org/packages/c7/15/2b37bc07d6ce27cc94e5b10fd5058900eb8fb11642300e932c8c82e25c4a/bcrypt-4.3.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:0e30e5e67aed0187a1764911af023043b4542e70a7461ad20e837e94d23e1d6c", size = 309617 }, + { url = "https://files.pythonhosted.org/packages/5f/1f/99f65edb09e6c935232ba0430c8c13bb98cb3194b6d636e61d93fe60ac59/bcrypt-4.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b8d62290ebefd49ee0b3ce7500f5dbdcf13b81402c05f6dafab9a1e1b27212f", size = 335751 }, + { url = "https://files.pythonhosted.org/packages/00/1b/b324030c706711c99769988fcb694b3cb23f247ad39a7823a78e361bdbb8/bcrypt-4.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2ef6630e0ec01376f59a006dc72918b1bf436c3b571b80fa1968d775fa02fe7d", size = 355965 }, + { url = "https://files.pythonhosted.org/packages/aa/dd/20372a0579dd915dfc3b1cd4943b3bca431866fcb1dfdfd7518c3caddea6/bcrypt-4.3.0-cp313-cp313t-win32.whl", hash = "sha256:7a4be4cbf241afee43f1c3969b9103a41b40bcb3a3f467ab19f891d9bc4642e4", size = 155316 }, + { url = "https://files.pythonhosted.org/packages/6d/52/45d969fcff6b5577c2bf17098dc36269b4c02197d551371c023130c0f890/bcrypt-4.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5c1949bf259a388863ced887c7861da1df681cb2388645766c89fdfd9004c669", size = 147752 }, + { url = "https://files.pythonhosted.org/packages/11/22/5ada0b9af72b60cbc4c9a399fdde4af0feaa609d27eb0adc61607997a3fa/bcrypt-4.3.0-cp38-abi3-macosx_10_12_universal2.whl", hash = "sha256:f81b0ed2639568bf14749112298f9e4e2b28853dab50a8b357e31798686a036d", size = 498019 }, + { url = "https://files.pythonhosted.org/packages/b8/8c/252a1edc598dc1ce57905be173328eda073083826955ee3c97c7ff5ba584/bcrypt-4.3.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:864f8f19adbe13b7de11ba15d85d4a428c7e2f344bac110f667676a0ff84924b", size = 279174 }, + { url = "https://files.pythonhosted.org/packages/29/5b/4547d5c49b85f0337c13929f2ccbe08b7283069eea3550a457914fc078aa/bcrypt-4.3.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e36506d001e93bffe59754397572f21bb5dc7c83f54454c990c74a468cd589e", size = 283870 }, + { url = "https://files.pythonhosted.org/packages/be/21/7dbaf3fa1745cb63f776bb046e481fbababd7d344c5324eab47f5ca92dd2/bcrypt-4.3.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:842d08d75d9fe9fb94b18b071090220697f9f184d4547179b60734846461ed59", size = 279601 }, + { url = "https://files.pythonhosted.org/packages/6d/64/e042fc8262e971347d9230d9abbe70d68b0a549acd8611c83cebd3eaec67/bcrypt-4.3.0-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7c03296b85cb87db865d91da79bf63d5609284fc0cab9472fdd8367bbd830753", size = 297660 }, + { url = "https://files.pythonhosted.org/packages/50/b8/6294eb84a3fef3b67c69b4470fcdd5326676806bf2519cda79331ab3c3a9/bcrypt-4.3.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:62f26585e8b219cdc909b6a0069efc5e4267e25d4a3770a364ac58024f62a761", size = 284083 }, + { url = "https://files.pythonhosted.org/packages/62/e6/baff635a4f2c42e8788fe1b1633911c38551ecca9a749d1052d296329da6/bcrypt-4.3.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:beeefe437218a65322fbd0069eb437e7c98137e08f22c4660ac2dc795c31f8bb", size = 279237 }, + { url = "https://files.pythonhosted.org/packages/39/48/46f623f1b0c7dc2e5de0b8af5e6f5ac4cc26408ac33f3d424e5ad8da4a90/bcrypt-4.3.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:97eea7408db3a5bcce4a55d13245ab3fa566e23b4c67cd227062bb49e26c585d", size = 283737 }, + { url = "https://files.pythonhosted.org/packages/49/8b/70671c3ce9c0fca4a6cc3cc6ccbaa7e948875a2e62cbd146e04a4011899c/bcrypt-4.3.0-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:191354ebfe305e84f344c5964c7cd5f924a3bfc5d405c75ad07f232b6dffb49f", size = 312741 }, + { url = "https://files.pythonhosted.org/packages/27/fb/910d3a1caa2d249b6040a5caf9f9866c52114d51523ac2fb47578a27faee/bcrypt-4.3.0-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:41261d64150858eeb5ff43c753c4b216991e0ae16614a308a15d909503617732", size = 316472 }, + { url = "https://files.pythonhosted.org/packages/dc/cf/7cf3a05b66ce466cfb575dbbda39718d45a609daa78500f57fa9f36fa3c0/bcrypt-4.3.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:33752b1ba962ee793fa2b6321404bf20011fe45b9afd2a842139de3011898fef", size = 343606 }, + { url = "https://files.pythonhosted.org/packages/e3/b8/e970ecc6d7e355c0d892b7f733480f4aa8509f99b33e71550242cf0b7e63/bcrypt-4.3.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:50e6e80a4bfd23a25f5c05b90167c19030cf9f87930f7cb2eacb99f45d1c3304", size = 362867 }, + { url = "https://files.pythonhosted.org/packages/a9/97/8d3118efd8354c555a3422d544163f40d9f236be5b96c714086463f11699/bcrypt-4.3.0-cp38-abi3-win32.whl", hash = "sha256:67a561c4d9fb9465ec866177e7aebcad08fe23aaf6fbd692a6fab69088abfc51", size = 160589 }, + { url = "https://files.pythonhosted.org/packages/29/07/416f0b99f7f3997c69815365babbc2e8754181a4b1899d921b3c7d5b6f12/bcrypt-4.3.0-cp38-abi3-win_amd64.whl", hash = "sha256:584027857bc2843772114717a7490a37f68da563b3620f78a849bcb54dc11e62", size = 152794 }, + { url = "https://files.pythonhosted.org/packages/6e/c1/3fa0e9e4e0bfd3fd77eb8b52ec198fd6e1fd7e9402052e43f23483f956dd/bcrypt-4.3.0-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:0d3efb1157edebfd9128e4e46e2ac1a64e0c1fe46fb023158a407c7892b0f8c3", size = 498969 }, + { url = "https://files.pythonhosted.org/packages/ce/d4/755ce19b6743394787fbd7dff6bf271b27ee9b5912a97242e3caf125885b/bcrypt-4.3.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08bacc884fd302b611226c01014eca277d48f0a05187666bca23aac0dad6fe24", size = 279158 }, + { url = "https://files.pythonhosted.org/packages/9b/5d/805ef1a749c965c46b28285dfb5cd272a7ed9fa971f970435a5133250182/bcrypt-4.3.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6746e6fec103fcd509b96bacdfdaa2fbde9a553245dbada284435173a6f1aef", size = 284285 }, + { url = "https://files.pythonhosted.org/packages/ab/2b/698580547a4a4988e415721b71eb45e80c879f0fb04a62da131f45987b96/bcrypt-4.3.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:afe327968aaf13fc143a56a3360cb27d4ad0345e34da12c7290f1b00b8fe9a8b", size = 279583 }, + { url = "https://files.pythonhosted.org/packages/f2/87/62e1e426418204db520f955ffd06f1efd389feca893dad7095bf35612eec/bcrypt-4.3.0-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d9af79d322e735b1fc33404b5765108ae0ff232d4b54666d46730f8ac1a43676", size = 297896 }, + { url = "https://files.pythonhosted.org/packages/cb/c6/8fedca4c2ada1b6e889c52d2943b2f968d3427e5d65f595620ec4c06fa2f/bcrypt-4.3.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f1e3ffa1365e8702dc48c8b360fef8d7afeca482809c5e45e653af82ccd088c1", size = 284492 }, + { url = "https://files.pythonhosted.org/packages/4d/4d/c43332dcaaddb7710a8ff5269fcccba97ed3c85987ddaa808db084267b9a/bcrypt-4.3.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:3004df1b323d10021fda07a813fd33e0fd57bef0e9a480bb143877f6cba996fe", size = 279213 }, + { url = "https://files.pythonhosted.org/packages/dc/7f/1e36379e169a7df3a14a1c160a49b7b918600a6008de43ff20d479e6f4b5/bcrypt-4.3.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:531457e5c839d8caea9b589a1bcfe3756b0547d7814e9ce3d437f17da75c32b0", size = 284162 }, + { url = "https://files.pythonhosted.org/packages/1c/0a/644b2731194b0d7646f3210dc4d80c7fee3ecb3a1f791a6e0ae6bb8684e3/bcrypt-4.3.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:17a854d9a7a476a89dcef6c8bd119ad23e0f82557afbd2c442777a16408e614f", size = 312856 }, + { url = "https://files.pythonhosted.org/packages/dc/62/2a871837c0bb6ab0c9a88bf54de0fc021a6a08832d4ea313ed92a669d437/bcrypt-4.3.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:6fb1fd3ab08c0cbc6826a2e0447610c6f09e983a281b919ed721ad32236b8b23", size = 316726 }, + { url = "https://files.pythonhosted.org/packages/0c/a1/9898ea3faac0b156d457fd73a3cb9c2855c6fd063e44b8522925cdd8ce46/bcrypt-4.3.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e965a9c1e9a393b8005031ff52583cedc15b7884fce7deb8b0346388837d6cfe", size = 343664 }, + { url = "https://files.pythonhosted.org/packages/40/f2/71b4ed65ce38982ecdda0ff20c3ad1b15e71949c78b2c053df53629ce940/bcrypt-4.3.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:79e70b8342a33b52b55d93b3a59223a844962bef479f6a0ea318ebbcadf71505", size = 363128 }, + { url = "https://files.pythonhosted.org/packages/11/99/12f6a58eca6dea4be992d6c681b7ec9410a1d9f5cf368c61437e31daa879/bcrypt-4.3.0-cp39-abi3-win32.whl", hash = "sha256:b4d4e57f0a63fd0b358eb765063ff661328f69a04494427265950c71b992a39a", size = 160598 }, + { url = "https://files.pythonhosted.org/packages/a9/cf/45fb5261ece3e6b9817d3d82b2f343a505fd58674a92577923bc500bd1aa/bcrypt-4.3.0-cp39-abi3-win_amd64.whl", hash = "sha256:e53e074b120f2877a35cc6c736b8eb161377caae8925c17688bd46ba56daaa5b", size = 152799 }, +] + +[[package]] +name = "certifi" +version = "2025.8.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216 }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230 }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043 }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446 }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101 }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948 }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422 }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499 }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928 }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302 }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909 }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402 }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780 }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320 }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487 }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049 }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793 }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300 }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244 }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828 }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926 }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328 }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650 }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687 }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773 }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013 }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593 }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354 }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480 }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584 }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443 }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437 }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487 }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726 }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195 }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", size = 122371 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/65/ca/2135ac97709b400c7654b4b764daf5c5567c2da45a30cdd20f9eefe2d658/charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe", size = 205326 }, + { url = "https://files.pythonhosted.org/packages/71/11/98a04c3c97dd34e49c7d247083af03645ca3730809a5509443f3c37f7c99/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8", size = 146008 }, + { url = "https://files.pythonhosted.org/packages/60/f5/4659a4cb3c4ec146bec80c32d8bb16033752574c20b1252ee842a95d1a1e/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9", size = 159196 }, + { url = "https://files.pythonhosted.org/packages/86/9e/f552f7a00611f168b9a5865a1414179b2c6de8235a4fa40189f6f79a1753/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31", size = 156819 }, + { url = "https://files.pythonhosted.org/packages/7e/95/42aa2156235cbc8fa61208aded06ef46111c4d3f0de233107b3f38631803/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f", size = 151350 }, + { url = "https://files.pythonhosted.org/packages/c2/a9/3865b02c56f300a6f94fc631ef54f0a8a29da74fb45a773dfd3dcd380af7/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927", size = 148644 }, + { url = "https://files.pythonhosted.org/packages/77/d9/cbcf1a2a5c7d7856f11e7ac2d782aec12bdfea60d104e60e0aa1c97849dc/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9", size = 160468 }, + { url = "https://files.pythonhosted.org/packages/f6/42/6f45efee8697b89fda4d50580f292b8f7f9306cb2971d4b53f8914e4d890/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5", size = 158187 }, + { url = "https://files.pythonhosted.org/packages/70/99/f1c3bdcfaa9c45b3ce96f70b14f070411366fa19549c1d4832c935d8e2c3/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc", size = 152699 }, + { url = "https://files.pythonhosted.org/packages/a3/ad/b0081f2f99a4b194bcbb1934ef3b12aa4d9702ced80a37026b7607c72e58/charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce", size = 99580 }, + { url = "https://files.pythonhosted.org/packages/9a/8f/ae790790c7b64f925e5c953b924aaa42a243fb778fed9e41f147b2a5715a/charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef", size = 107366 }, + { url = "https://files.pythonhosted.org/packages/8e/91/b5a06ad970ddc7a0e513112d40113e834638f4ca1120eb727a249fb2715e/charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15", size = 204342 }, + { url = "https://files.pythonhosted.org/packages/ce/ec/1edc30a377f0a02689342f214455c3f6c2fbedd896a1d2f856c002fc3062/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db", size = 145995 }, + { url = "https://files.pythonhosted.org/packages/17/e5/5e67ab85e6d22b04641acb5399c8684f4d37caf7558a53859f0283a650e9/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d", size = 158640 }, + { url = "https://files.pythonhosted.org/packages/f1/e5/38421987f6c697ee3722981289d554957c4be652f963d71c5e46a262e135/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096", size = 156636 }, + { url = "https://files.pythonhosted.org/packages/a0/e4/5a075de8daa3ec0745a9a3b54467e0c2967daaaf2cec04c845f73493e9a1/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa", size = 150939 }, + { url = "https://files.pythonhosted.org/packages/02/f7/3611b32318b30974131db62b4043f335861d4d9b49adc6d57c1149cc49d4/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049", size = 148580 }, + { url = "https://files.pythonhosted.org/packages/7e/61/19b36f4bd67f2793ab6a99b979b4e4f3d8fc754cbdffb805335df4337126/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0", size = 159870 }, + { url = "https://files.pythonhosted.org/packages/06/57/84722eefdd338c04cf3030ada66889298eaedf3e7a30a624201e0cbe424a/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92", size = 157797 }, + { url = "https://files.pythonhosted.org/packages/72/2a/aff5dd112b2f14bcc3462c312dce5445806bfc8ab3a7328555da95330e4b/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16", size = 152224 }, + { url = "https://files.pythonhosted.org/packages/b7/8c/9839225320046ed279c6e839d51f028342eb77c91c89b8ef2549f951f3ec/charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce", size = 100086 }, + { url = "https://files.pythonhosted.org/packages/ee/7a/36fbcf646e41f710ce0a563c1c9a343c6edf9be80786edeb15b6f62e17db/charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c", size = 107400 }, + { url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175 }, +] + +[[package]] +name = "click" +version = "8.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215 }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "cryptography" +version = "46.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/62/e3664e6ffd7743e1694b244dde70b43a394f6f7fbcacf7014a8ff5197c73/cryptography-46.0.1.tar.gz", hash = "sha256:ed570874e88f213437f5cf758f9ef26cbfc3f336d889b1e592ee11283bb8d1c7", size = 749198 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/8c/44ee01267ec01e26e43ebfdae3f120ec2312aa72fa4c0507ebe41a26739f/cryptography-46.0.1-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:1cd6d50c1a8b79af1a6f703709d8973845f677c8e97b1268f5ff323d38ce8475", size = 7285044 }, + { url = "https://files.pythonhosted.org/packages/22/59/9ae689a25047e0601adfcb159ec4f83c0b4149fdb5c3030cc94cd218141d/cryptography-46.0.1-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0ff483716be32690c14636e54a1f6e2e1b7bf8e22ca50b989f88fa1b2d287080", size = 4308182 }, + { url = "https://files.pythonhosted.org/packages/c4/ee/ca6cc9df7118f2fcd142c76b1da0f14340d77518c05b1ebfbbabca6b9e7d/cryptography-46.0.1-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9873bf7c1f2a6330bdfe8621e7ce64b725784f9f0c3a6a55c3047af5849f920e", size = 4572393 }, + { url = "https://files.pythonhosted.org/packages/7f/a3/0f5296f63815d8e985922b05c31f77ce44787b3127a67c0b7f70f115c45f/cryptography-46.0.1-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0dfb7c88d4462a0cfdd0d87a3c245a7bc3feb59de101f6ff88194f740f72eda6", size = 4308400 }, + { url = "https://files.pythonhosted.org/packages/5d/8c/74fcda3e4e01be1d32775d5b4dd841acaac3c1b8fa4d0774c7ac8d52463d/cryptography-46.0.1-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e22801b61613ebdebf7deb18b507919e107547a1d39a3b57f5f855032dd7cfb8", size = 4015786 }, + { url = "https://files.pythonhosted.org/packages/dc/b8/85d23287baeef273b0834481a3dd55bbed3a53587e3b8d9f0898235b8f91/cryptography-46.0.1-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:757af4f6341ce7a1e47c326ca2a81f41d236070217e5fbbad61bbfe299d55d28", size = 4982606 }, + { url = "https://files.pythonhosted.org/packages/e5/d3/de61ad5b52433b389afca0bc70f02a7a1f074651221f599ce368da0fe437/cryptography-46.0.1-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f7a24ea78de345cfa7f6a8d3bde8b242c7fac27f2bd78fa23474ca38dfaeeab9", size = 4604234 }, + { url = "https://files.pythonhosted.org/packages/dc/1f/dbd4d6570d84748439237a7478d124ee0134bf166ad129267b7ed8ea6d22/cryptography-46.0.1-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:9e8776dac9e660c22241b6587fae51a67b4b0147daa4d176b172c3ff768ad736", size = 4307669 }, + { url = "https://files.pythonhosted.org/packages/ec/fd/ca0a14ce7f0bfe92fa727aacaf2217eb25eb7e4ed513b14d8e03b26e63ed/cryptography-46.0.1-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9f40642a140c0c8649987027867242b801486865277cbabc8c6059ddef16dc8b", size = 4947579 }, + { url = "https://files.pythonhosted.org/packages/89/6b/09c30543bb93401f6f88fce556b3bdbb21e55ae14912c04b7bf355f5f96c/cryptography-46.0.1-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:449ef2b321bec7d97ef2c944173275ebdab78f3abdd005400cc409e27cd159ab", size = 4603669 }, + { url = "https://files.pythonhosted.org/packages/23/9a/38cb01cb09ce0adceda9fc627c9cf98eb890fc8d50cacbe79b011df20f8a/cryptography-46.0.1-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2dd339ba3345b908fa3141ddba4025568fa6fd398eabce3ef72a29ac2d73ad75", size = 4435828 }, + { url = "https://files.pythonhosted.org/packages/0f/53/435b5c36a78d06ae0bef96d666209b0ecd8f8181bfe4dda46536705df59e/cryptography-46.0.1-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7411c910fb2a412053cf33cfad0153ee20d27e256c6c3f14d7d7d1d9fec59fd5", size = 4709553 }, + { url = "https://files.pythonhosted.org/packages/f5/c4/0da6e55595d9b9cd3b6eb5dc22f3a07ded7f116a3ea72629cab595abb804/cryptography-46.0.1-cp311-abi3-win32.whl", hash = "sha256:cbb8e769d4cac884bb28e3ff620ef1001b75588a5c83c9c9f1fdc9afbe7f29b0", size = 3058327 }, + { url = "https://files.pythonhosted.org/packages/95/0f/cd29a35e0d6e78a0ee61793564c8cff0929c38391cb0de27627bdc7525aa/cryptography-46.0.1-cp311-abi3-win_amd64.whl", hash = "sha256:92e8cfe8bd7dd86eac0a677499894862cd5cc2fd74de917daa881d00871ac8e7", size = 3523893 }, + { url = "https://files.pythonhosted.org/packages/f2/dd/eea390f3e78432bc3d2f53952375f8b37cb4d37783e626faa6a51e751719/cryptography-46.0.1-cp311-abi3-win_arm64.whl", hash = "sha256:db5597a4c7353b2e5fb05a8e6cb74b56a4658a2b7bf3cb6b1821ae7e7fd6eaa0", size = 2932145 }, + { url = "https://files.pythonhosted.org/packages/0a/fb/c73588561afcd5e24b089952bd210b14676c0c5bf1213376350ae111945c/cryptography-46.0.1-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:4c49eda9a23019e11d32a0eb51a27b3e7ddedde91e099c0ac6373e3aacc0d2ee", size = 7193928 }, + { url = "https://files.pythonhosted.org/packages/26/34/0ff0bb2d2c79f25a2a63109f3b76b9108a906dd2a2eb5c1d460b9938adbb/cryptography-46.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9babb7818fdd71394e576cf26c5452df77a355eac1a27ddfa24096665a27f8fd", size = 4293515 }, + { url = "https://files.pythonhosted.org/packages/df/b7/d4f848aee24ecd1be01db6c42c4a270069a4f02a105d9c57e143daf6cf0f/cryptography-46.0.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9f2c4cc63be3ef43c0221861177cee5d14b505cd4d4599a89e2cd273c4d3542a", size = 4545619 }, + { url = "https://files.pythonhosted.org/packages/44/a5/42fedefc754fd1901e2d95a69815ea4ec8a9eed31f4c4361fcab80288661/cryptography-46.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:41c281a74df173876da1dc9a9b6953d387f06e3d3ed9284e3baae3ab3f40883a", size = 4299160 }, + { url = "https://files.pythonhosted.org/packages/86/a1/cd21174f56e769c831fbbd6399a1b7519b0ff6280acec1b826d7b072640c/cryptography-46.0.1-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0a17377fa52563d730248ba1f68185461fff36e8bc75d8787a7dd2e20a802b7a", size = 3994491 }, + { url = "https://files.pythonhosted.org/packages/8d/2f/a8cbfa1c029987ddc746fd966711d4fa71efc891d37fbe9f030fe5ab4eec/cryptography-46.0.1-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:0d1922d9280e08cde90b518a10cd66831f632960a8d08cb3418922d83fce6f12", size = 4960157 }, + { url = "https://files.pythonhosted.org/packages/67/ae/63a84e6789e0d5a2502edf06b552bcb0fa9ff16147265d5c44a211942abe/cryptography-46.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:af84e8e99f1a82cea149e253014ea9dc89f75b82c87bb6c7242203186f465129", size = 4577263 }, + { url = "https://files.pythonhosted.org/packages/ef/8f/1b9fa8e92bd9cbcb3b7e1e593a5232f2c1e6f9bd72b919c1a6b37d315f92/cryptography-46.0.1-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ef648d2c690703501714588b2ba640facd50fd16548133b11b2859e8655a69da", size = 4298703 }, + { url = "https://files.pythonhosted.org/packages/c3/af/bb95db070e73fea3fae31d8a69ac1463d89d1c084220f549b00dd01094a8/cryptography-46.0.1-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:e94eb5fa32a8a9f9bf991f424f002913e3dd7c699ef552db9b14ba6a76a6313b", size = 4926363 }, + { url = "https://files.pythonhosted.org/packages/f5/3b/d8fb17ffeb3a83157a1cc0aa5c60691d062aceecba09c2e5e77ebfc1870c/cryptography-46.0.1-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:534b96c0831855e29fc3b069b085fd185aa5353033631a585d5cd4dd5d40d657", size = 4576958 }, + { url = "https://files.pythonhosted.org/packages/d9/46/86bc3a05c10c8aa88c8ae7e953a8b4e407c57823ed201dbcba55c4d655f4/cryptography-46.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f9b55038b5c6c47559aa33626d8ecd092f354e23de3c6975e4bb205df128a2a0", size = 4422507 }, + { url = "https://files.pythonhosted.org/packages/a8/4e/387e5a21dfd2b4198e74968a541cfd6128f66f8ec94ed971776e15091ac3/cryptography-46.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ec13b7105117dbc9afd023300fb9954d72ca855c274fe563e72428ece10191c0", size = 4683964 }, + { url = "https://files.pythonhosted.org/packages/25/a3/f9f5907b166adb8f26762071474b38bbfcf89858a5282f032899075a38a1/cryptography-46.0.1-cp314-cp314t-win32.whl", hash = "sha256:504e464944f2c003a0785b81668fe23c06f3b037e9cb9f68a7c672246319f277", size = 3029705 }, + { url = "https://files.pythonhosted.org/packages/12/66/4d3a4f1850db2e71c2b1628d14b70b5e4c1684a1bd462f7fffb93c041c38/cryptography-46.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c52fded6383f7e20eaf70a60aeddd796b3677c3ad2922c801be330db62778e05", size = 3502175 }, + { url = "https://files.pythonhosted.org/packages/52/c7/9f10ad91435ef7d0d99a0b93c4360bea3df18050ff5b9038c489c31ac2f5/cryptography-46.0.1-cp314-cp314t-win_arm64.whl", hash = "sha256:9495d78f52c804b5ec8878b5b8c7873aa8e63db9cd9ee387ff2db3fffe4df784", size = 2912354 }, + { url = "https://files.pythonhosted.org/packages/98/e5/fbd632385542a3311915976f88e0dfcf09e62a3fc0aff86fb6762162a24d/cryptography-46.0.1-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:d84c40bdb8674c29fa192373498b6cb1e84f882889d21a471b45d1f868d8d44b", size = 7255677 }, + { url = "https://files.pythonhosted.org/packages/56/3e/13ce6eab9ad6eba1b15a7bd476f005a4c1b3f299f4c2f32b22408b0edccf/cryptography-46.0.1-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9ed64e5083fa806709e74fc5ea067dfef9090e5b7a2320a49be3c9df3583a2d8", size = 4301110 }, + { url = "https://files.pythonhosted.org/packages/a2/67/65dc233c1ddd688073cf7b136b06ff4b84bf517ba5529607c9d79720fc67/cryptography-46.0.1-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:341fb7a26bc9d6093c1b124b9f13acc283d2d51da440b98b55ab3f79f2522ead", size = 4562369 }, + { url = "https://files.pythonhosted.org/packages/17/db/d64ae4c6f4e98c3dac5bf35dd4d103f4c7c345703e43560113e5e8e31b2b/cryptography-46.0.1-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6ef1488967e729948d424d09c94753d0167ce59afba8d0f6c07a22b629c557b2", size = 4302126 }, + { url = "https://files.pythonhosted.org/packages/3d/19/5f1eea17d4805ebdc2e685b7b02800c4f63f3dd46cfa8d4c18373fea46c8/cryptography-46.0.1-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7823bc7cdf0b747ecfb096d004cc41573c2f5c7e3a29861603a2871b43d3ef32", size = 4009431 }, + { url = "https://files.pythonhosted.org/packages/81/b5/229ba6088fe7abccbfe4c5edb96c7a5ad547fac5fdd0d40aa6ea540b2985/cryptography-46.0.1-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:f736ab8036796f5a119ff8211deda416f8c15ce03776db704a7a4e17381cb2ef", size = 4980739 }, + { url = "https://files.pythonhosted.org/packages/3a/9c/50aa38907b201e74bc43c572f9603fa82b58e831bd13c245613a23cff736/cryptography-46.0.1-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:e46710a240a41d594953012213ea8ca398cd2448fbc5d0f1be8160b5511104a0", size = 4592289 }, + { url = "https://files.pythonhosted.org/packages/5a/33/229858f8a5bb22f82468bb285e9f4c44a31978d5f5830bb4ea1cf8a4e454/cryptography-46.0.1-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:84ef1f145de5aee82ea2447224dc23f065ff4cc5791bb3b506615957a6ba8128", size = 4301815 }, + { url = "https://files.pythonhosted.org/packages/52/cb/b76b2c87fbd6ed4a231884bea3ce073406ba8e2dae9defad910d33cbf408/cryptography-46.0.1-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9394c7d5a7565ac5f7d9ba38b2617448eba384d7b107b262d63890079fad77ca", size = 4943251 }, + { url = "https://files.pythonhosted.org/packages/94/0f/f66125ecf88e4cb5b8017ff43f3a87ede2d064cb54a1c5893f9da9d65093/cryptography-46.0.1-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:ed957044e368ed295257ae3d212b95456bd9756df490e1ac4538857f67531fcc", size = 4591247 }, + { url = "https://files.pythonhosted.org/packages/f6/22/9f3134ae436b63b463cfdf0ff506a0570da6873adb4bf8c19b8a5b4bac64/cryptography-46.0.1-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f7de12fa0eee6234de9a9ce0ffcfa6ce97361db7a50b09b65c63ac58e5f22fc7", size = 4428534 }, + { url = "https://files.pythonhosted.org/packages/89/39/e6042bcb2638650b0005c752c38ea830cbfbcbb1830e4d64d530000aa8dc/cryptography-46.0.1-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7fab1187b6c6b2f11a326f33b036f7168f5b996aedd0c059f9738915e4e8f53a", size = 4699541 }, + { url = "https://files.pythonhosted.org/packages/68/46/753d457492d15458c7b5a653fc9a84a1c9c7a83af6ebdc94c3fc373ca6e8/cryptography-46.0.1-cp38-abi3-win32.whl", hash = "sha256:45f790934ac1018adeba46a0f7289b2b8fe76ba774a88c7f1922213a56c98bc1", size = 3043779 }, + { url = "https://files.pythonhosted.org/packages/2f/50/b6f3b540c2f6ee712feeb5fa780bb11fad76634e71334718568e7695cb55/cryptography-46.0.1-cp38-abi3-win_amd64.whl", hash = "sha256:7176a5ab56fac98d706921f6416a05e5aff7df0e4b91516f450f8627cda22af3", size = 3517226 }, + { url = "https://files.pythonhosted.org/packages/ff/e8/77d17d00981cdd27cc493e81e1749a0b8bbfb843780dbd841e30d7f50743/cryptography-46.0.1-cp38-abi3-win_arm64.whl", hash = "sha256:efc9e51c3e595267ff84adf56e9b357db89ab2279d7e375ffcaf8f678606f3d9", size = 2923149 }, +] + +[[package]] +name = "dnspython" +version = "2.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094 }, +] + +[[package]] +name = "ecdsa" +version = "0.19.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/1f/924e3caae75f471eae4b26bd13b698f6af2c44279f67af317439c2f4c46a/ecdsa-0.19.1.tar.gz", hash = "sha256:478cba7b62555866fcb3bb3fe985e06decbdb68ef55713c4e5ab98c57d508e61", size = 201793 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/a3/460c57f094a4a165c84a1341c373b0a4f5ec6ac244b998d5021aade89b77/ecdsa-0.19.1-py2.py3-none-any.whl", hash = "sha256:30638e27cf77b7e15c4c4cc1973720149e1033827cfd00661ca5c8cc0cdb24c3", size = 150607 }, +] + +[[package]] +name = "email-validator" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dnspython" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/22/900cb125c76b7aaa450ce02fd727f452243f2e91a61af068b40adba60ea9/email_validator-2.3.0.tar.gz", hash = "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426", size = 51238 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604 }, +] + +[[package]] +name = "fastapi" +version = "0.116.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/d7/6c8b3bfe33eeffa208183ec037fee0cce9f7f024089ab1c5d12ef04bd27c/fastapi-0.116.1.tar.gz", hash = "sha256:ed52cbf946abfd70c5a0dccb24673f0670deeb517a88b3544d03c2a6bf283143", size = 296485 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/47/d63c60f59a59467fda0f93f46335c9d18526d7071f025cb5b89d5353ea42/fastapi-0.116.1-py3-none-any.whl", hash = "sha256:c46ac7c312df840f0c9e220f7964bada936781bc4e2e6eb71f1c4d7553786565", size = 95631 }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515 }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784 }, +] + +[[package]] +name = "httptools" +version = "0.6.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/9a/ce5e1f7e131522e6d3426e8e7a490b3a01f39a6696602e1c4f33f9e94277/httptools-0.6.4.tar.gz", hash = "sha256:4e93eee4add6493b59a5c514da98c939b244fce4a0d8879cd3f466562f4b7d5c", size = 240639 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/a3/9fe9ad23fd35f7de6b91eeb60848986058bd8b5a5c1e256f5860a160cc3e/httptools-0.6.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ade273d7e767d5fae13fa637f4d53b6e961fb7fd93c7797562663f0171c26660", size = 197214 }, + { url = "https://files.pythonhosted.org/packages/ea/d9/82d5e68bab783b632023f2fa31db20bebb4e89dfc4d2293945fd68484ee4/httptools-0.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:856f4bc0478ae143bad54a4242fccb1f3f86a6e1be5548fecfd4102061b3a083", size = 102431 }, + { url = "https://files.pythonhosted.org/packages/96/c1/cb499655cbdbfb57b577734fde02f6fa0bbc3fe9fb4d87b742b512908dff/httptools-0.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:322d20ea9cdd1fa98bd6a74b77e2ec5b818abdc3d36695ab402a0de8ef2865a3", size = 473121 }, + { url = "https://files.pythonhosted.org/packages/af/71/ee32fd358f8a3bb199b03261f10921716990808a675d8160b5383487a317/httptools-0.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d87b29bd4486c0093fc64dea80231f7c7f7eb4dc70ae394d70a495ab8436071", size = 473805 }, + { url = "https://files.pythonhosted.org/packages/8a/0a/0d4df132bfca1507114198b766f1737d57580c9ad1cf93c1ff673e3387be/httptools-0.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:342dd6946aa6bda4b8f18c734576106b8a31f2fe31492881a9a160ec84ff4bd5", size = 448858 }, + { url = "https://files.pythonhosted.org/packages/1e/6a/787004fdef2cabea27bad1073bf6a33f2437b4dbd3b6fb4a9d71172b1c7c/httptools-0.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b36913ba52008249223042dca46e69967985fb4051951f94357ea681e1f5dc0", size = 452042 }, + { url = "https://files.pythonhosted.org/packages/4d/dc/7decab5c404d1d2cdc1bb330b1bf70e83d6af0396fd4fc76fc60c0d522bf/httptools-0.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:28908df1b9bb8187393d5b5db91435ccc9c8e891657f9cbb42a2541b44c82fc8", size = 87682 }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050 }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469 }, +] + +[[package]] +name = "passlib" +version = "1.7.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/06/9da9ee59a67fae7761aab3ccc84fa4f3f33f125b370f1ccdb915bf967c11/passlib-1.7.4.tar.gz", hash = "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04", size = 689844 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/a4/ab6b7589382ca3df236e03faa71deac88cae040af60c071a78d254a62172/passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1", size = 525554 }, +] + +[package.optional-dependencies] +bcrypt = [ + { name = "bcrypt" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538 }, +] + +[[package]] +name = "pyasn1" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135 }, +] + +[[package]] +name = "pycparser" +version = "2.23" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140 }, +] + +[[package]] +name = "pydantic" +version = "2.11.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/dd/4325abf92c39ba8623b5af936ddb36ffcfe0beae70405d456ab1fb2f5b8c/pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db", size = 788350 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b", size = 444782 }, +] + +[[package]] +name = "pydantic-core" +version = "2.33.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688 }, + { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808 }, + { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580 }, + { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859 }, + { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810 }, + { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498 }, + { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611 }, + { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924 }, + { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196 }, + { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389 }, + { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223 }, + { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473 }, + { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269 }, + { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921 }, + { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162 }, + { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560 }, + { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777 }, +] + +[[package]] +name = "pydantic-settings" +version = "2.10.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/68/85/1ea668bbab3c50071ca613c6ab30047fb36ab0da1b92fa8f17bbc38fd36c/pydantic_settings-2.10.1.tar.gz", hash = "sha256:06f0062169818d0f5524420a360d632d5857b83cffd4d42fe29597807a1614ee", size = 172583 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/f0/427018098906416f580e3cf1366d3b1abfb408a0652e9f31600c24a1903c/pydantic_settings-2.10.1-py3-none-any.whl", hash = "sha256:a60952460b99cf661dc25c29c0ef171721f98bfcb52ef8d9ea4c943d7c8cc796", size = 45235 }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217 }, +] + +[[package]] +name = "pytest" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750 }, +] + +[[package]] +name = "python-dotenv" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556 }, +] + +[[package]] +name = "python-jose" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ecdsa" }, + { name = "pyasn1" }, + { name = "rsa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c6/77/3a1c9039db7124eb039772b935f2244fbb73fc8ee65b9acf2375da1c07bf/python_jose-3.5.0.tar.gz", hash = "sha256:fb4eaa44dbeb1c26dcc69e4bd7ec54a1cb8dd64d3b4d81ef08d90ff453f2b01b", size = 92726 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/c3/0bd11992072e6a1c513b16500a5d07f91a24017c5909b02c72c62d7ad024/python_jose-3.5.0-py2.py3-none-any.whl", hash = "sha256:abd1202f23d34dfad2c3d28cb8617b90acf34132c7afd60abd0b0b7d3cb55771", size = 34624 }, +] + +[package.optional-dependencies] +cryptography = [ + { name = "cryptography" }, +] + +[[package]] +name = "python-multipart" +version = "0.0.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546 }, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738 }, +] + +[[package]] +name = "rsa" +version = "4.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696 }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, +] + +[[package]] +name = "starlette" +version = "0.47.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/15/b9/cc3017f9a9c9b6e27c5106cc10cc7904653c3eec0729793aec10479dd669/starlette-0.47.3.tar.gz", hash = "sha256:6bc94f839cc176c4858894f1f8908f0ab79dfec1a6b8402f6da9be26ebea52e9", size = 2584144 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/fd/901cfa59aaa5b30a99e16876f11abe38b59a1a2c51ffb3d7142bb6089069/starlette-0.47.3-py3-none-any.whl", hash = "sha256:89c0778ca62a76b826101e7c709e70680a1699ca7da6b44d38eb0a7e61fe4b51", size = 72991 }, +] + +[[package]] +name = "taskmaster-license" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "email-validator" }, + { name = "fastapi" }, + { name = "httpx" }, + { name = "passlib", extra = ["bcrypt"] }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "pytest" }, + { name = "python-jose", extra = ["cryptography"] }, + { name = "python-multipart" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "uvicorn", extra = ["standard"] }, +] + +[package.metadata] +requires-dist = [ + { name = "email-validator", specifier = ">=2.3.0" }, + { name = "fastapi", specifier = ">=0.116.1" }, + { name = "httpx", specifier = ">=0.28.1" }, + { name = "passlib", extras = ["bcrypt"], specifier = ">=1.7.4" }, + { name = "pydantic", specifier = ">=2.11.7" }, + { name = "pydantic-settings", specifier = ">=2.10.1" }, + { name = "pytest", specifier = ">=8.4.2" }, + { name = "python-jose", extras = ["cryptography"], specifier = ">=3.5.0" }, + { name = "python-multipart", specifier = ">=0.0.20" }, + { name = "pyyaml", specifier = ">=6.0.2" }, + { name = "requests", specifier = ">=2.32.5" }, + { name = "uvicorn", extras = ["standard"], specifier = ">=0.35.0" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614 }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552 }, +] + +[[package]] +name = "urllib3" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795 }, +] + +[[package]] +name = "uvicorn" +version = "0.35.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/42/e0e305207bb88c6b8d3061399c6a961ffe5fbb7e2aa63c9234df7259e9cd/uvicorn-0.35.0.tar.gz", hash = "sha256:bc662f087f7cf2ce11a1d7fd70b90c9f98ef2e2831556dd078d131b96cc94a01", size = 78473 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/e2/dc81b1bd1dcfe91735810265e9d26bc8ec5da45b4c0f6237e286819194c3/uvicorn-0.35.0-py3-none-any.whl", hash = "sha256:197535216b25ff9b785e29a0b79199f55222193d47f820816e7da751e9bc8d4a", size = 66406 }, +] + +[package.optional-dependencies] +standard = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "httptools" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, + { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, + { name = "watchfiles" }, + { name = "websockets" }, +] + +[[package]] +name = "uvloop" +version = "0.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/c0/854216d09d33c543f12a44b393c402e89a920b1a0a7dc634c42de91b9cf6/uvloop-0.21.0.tar.gz", hash = "sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3", size = 2492741 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/8d/2cbef610ca21539f0f36e2b34da49302029e7c9f09acef0b1c3b5839412b/uvloop-0.21.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bfd55dfcc2a512316e65f16e503e9e450cab148ef11df4e4e679b5e8253a5281", size = 1468123 }, + { url = "https://files.pythonhosted.org/packages/93/0d/b0038d5a469f94ed8f2b2fce2434a18396d8fbfb5da85a0a9781ebbdec14/uvloop-0.21.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787ae31ad8a2856fc4e7c095341cccc7209bd657d0e71ad0dc2ea83c4a6fa8af", size = 819325 }, + { url = "https://files.pythonhosted.org/packages/50/94/0a687f39e78c4c1e02e3272c6b2ccdb4e0085fda3b8352fecd0410ccf915/uvloop-0.21.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ee4d4ef48036ff6e5cfffb09dd192c7a5027153948d85b8da7ff705065bacc6", size = 4582806 }, + { url = "https://files.pythonhosted.org/packages/d2/19/f5b78616566ea68edd42aacaf645adbf71fbd83fc52281fba555dc27e3f1/uvloop-0.21.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3df876acd7ec037a3d005b3ab85a7e4110422e4d9c1571d4fc89b0fc41b6816", size = 4701068 }, + { url = "https://files.pythonhosted.org/packages/47/57/66f061ee118f413cd22a656de622925097170b9380b30091b78ea0c6ea75/uvloop-0.21.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd53ecc9a0f3d87ab847503c2e1552b690362e005ab54e8a48ba97da3924c0dc", size = 4454428 }, + { url = "https://files.pythonhosted.org/packages/63/9a/0962b05b308494e3202d3f794a6e85abe471fe3cafdbcf95c2e8c713aabd/uvloop-0.21.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5c39f217ab3c663dc699c04cbd50c13813e31d917642d459fdcec07555cc553", size = 4660018 }, +] + +[[package]] +name = "watchfiles" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2a/9a/d451fcc97d029f5812e898fd30a53fd8c15c7bbd058fd75cfc6beb9bd761/watchfiles-1.1.0.tar.gz", hash = "sha256:693ed7ec72cbfcee399e92c895362b6e66d63dac6b91e2c11ae03d10d503e575", size = 94406 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/42/fae874df96595556a9089ade83be34a2e04f0f11eb53a8dbf8a8a5e562b4/watchfiles-1.1.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5007f860c7f1f8df471e4e04aaa8c43673429047d63205d1630880f7637bca30", size = 402004 }, + { url = "https://files.pythonhosted.org/packages/fa/55/a77e533e59c3003d9803c09c44c3651224067cbe7fb5d574ddbaa31e11ca/watchfiles-1.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:20ecc8abbd957046f1fe9562757903f5eaf57c3bce70929fda6c7711bb58074a", size = 393671 }, + { url = "https://files.pythonhosted.org/packages/05/68/b0afb3f79c8e832e6571022611adbdc36e35a44e14f129ba09709aa4bb7a/watchfiles-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2f0498b7d2a3c072766dba3274fe22a183dbea1f99d188f1c6c72209a1063dc", size = 449772 }, + { url = "https://files.pythonhosted.org/packages/ff/05/46dd1f6879bc40e1e74c6c39a1b9ab9e790bf1f5a2fe6c08b463d9a807f4/watchfiles-1.1.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:239736577e848678e13b201bba14e89718f5c2133dfd6b1f7846fa1b58a8532b", size = 456789 }, + { url = "https://files.pythonhosted.org/packages/8b/ca/0eeb2c06227ca7f12e50a47a3679df0cd1ba487ea19cf844a905920f8e95/watchfiles-1.1.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eff4b8d89f444f7e49136dc695599a591ff769300734446c0a86cba2eb2f9895", size = 482551 }, + { url = "https://files.pythonhosted.org/packages/31/47/2cecbd8694095647406645f822781008cc524320466ea393f55fe70eed3b/watchfiles-1.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12b0a02a91762c08f7264e2e79542f76870c3040bbc847fb67410ab81474932a", size = 597420 }, + { url = "https://files.pythonhosted.org/packages/d9/7e/82abc4240e0806846548559d70f0b1a6dfdca75c1b4f9fa62b504ae9b083/watchfiles-1.1.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:29e7bc2eee15cbb339c68445959108803dc14ee0c7b4eea556400131a8de462b", size = 477950 }, + { url = "https://files.pythonhosted.org/packages/25/0d/4d564798a49bf5482a4fa9416dea6b6c0733a3b5700cb8a5a503c4b15853/watchfiles-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d9481174d3ed982e269c090f780122fb59cee6c3796f74efe74e70f7780ed94c", size = 451706 }, + { url = "https://files.pythonhosted.org/packages/81/b5/5516cf46b033192d544102ea07c65b6f770f10ed1d0a6d388f5d3874f6e4/watchfiles-1.1.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:80f811146831c8c86ab17b640801c25dc0a88c630e855e2bef3568f30434d52b", size = 625814 }, + { url = "https://files.pythonhosted.org/packages/0c/dd/7c1331f902f30669ac3e754680b6edb9a0dd06dea5438e61128111fadd2c/watchfiles-1.1.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:60022527e71d1d1fda67a33150ee42869042bce3d0fcc9cc49be009a9cded3fb", size = 622820 }, + { url = "https://files.pythonhosted.org/packages/1b/14/36d7a8e27cd128d7b1009e7715a7c02f6c131be9d4ce1e5c3b73d0e342d8/watchfiles-1.1.0-cp313-cp313-win32.whl", hash = "sha256:32d6d4e583593cb8576e129879ea0991660b935177c0f93c6681359b3654bfa9", size = 279194 }, + { url = "https://files.pythonhosted.org/packages/25/41/2dd88054b849aa546dbeef5696019c58f8e0774f4d1c42123273304cdb2e/watchfiles-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:f21af781a4a6fbad54f03c598ab620e3a77032c5878f3d780448421a6e1818c7", size = 292349 }, + { url = "https://files.pythonhosted.org/packages/c8/cf/421d659de88285eb13941cf11a81f875c176f76a6d99342599be88e08d03/watchfiles-1.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:5366164391873ed76bfdf618818c82084c9db7fac82b64a20c44d335eec9ced5", size = 283836 }, + { url = "https://files.pythonhosted.org/packages/45/10/6faf6858d527e3599cc50ec9fcae73590fbddc1420bd4fdccfebffeedbc6/watchfiles-1.1.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:17ab167cca6339c2b830b744eaf10803d2a5b6683be4d79d8475d88b4a8a4be1", size = 400343 }, + { url = "https://files.pythonhosted.org/packages/03/20/5cb7d3966f5e8c718006d0e97dfe379a82f16fecd3caa7810f634412047a/watchfiles-1.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:328dbc9bff7205c215a7807da7c18dce37da7da718e798356212d22696404339", size = 392916 }, + { url = "https://files.pythonhosted.org/packages/8c/07/d8f1176328fa9e9581b6f120b017e286d2a2d22ae3f554efd9515c8e1b49/watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7208ab6e009c627b7557ce55c465c98967e8caa8b11833531fdf95799372633", size = 449582 }, + { url = "https://files.pythonhosted.org/packages/66/e8/80a14a453cf6038e81d072a86c05276692a1826471fef91df7537dba8b46/watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a8f6f72974a19efead54195bc9bed4d850fc047bb7aa971268fd9a8387c89011", size = 456752 }, + { url = "https://files.pythonhosted.org/packages/5a/25/0853b3fe0e3c2f5af9ea60eb2e781eade939760239a72c2d38fc4cc335f6/watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d181ef50923c29cf0450c3cd47e2f0557b62218c50b2ab8ce2ecaa02bd97e670", size = 481436 }, + { url = "https://files.pythonhosted.org/packages/fe/9e/4af0056c258b861fbb29dcb36258de1e2b857be4a9509e6298abcf31e5c9/watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:adb4167043d3a78280d5d05ce0ba22055c266cf8655ce942f2fb881262ff3cdf", size = 596016 }, + { url = "https://files.pythonhosted.org/packages/c5/fa/95d604b58aa375e781daf350897aaaa089cff59d84147e9ccff2447c8294/watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5701dc474b041e2934a26d31d39f90fac8a3dee2322b39f7729867f932b1d4", size = 476727 }, + { url = "https://files.pythonhosted.org/packages/65/95/fe479b2664f19be4cf5ceeb21be05afd491d95f142e72d26a42f41b7c4f8/watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b067915e3c3936966a8607f6fe5487df0c9c4afb85226613b520890049deea20", size = 451864 }, + { url = "https://files.pythonhosted.org/packages/d3/8a/3c4af14b93a15ce55901cd7a92e1a4701910f1768c78fb30f61d2b79785b/watchfiles-1.1.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:9c733cda03b6d636b4219625a4acb5c6ffb10803338e437fb614fef9516825ef", size = 625626 }, + { url = "https://files.pythonhosted.org/packages/da/f5/cf6aa047d4d9e128f4b7cde615236a915673775ef171ff85971d698f3c2c/watchfiles-1.1.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:cc08ef8b90d78bfac66f0def80240b0197008e4852c9f285907377b2947ffdcb", size = 622744 }, + { url = "https://files.pythonhosted.org/packages/2c/00/70f75c47f05dea6fd30df90f047765f6fc2d6eb8b5a3921379b0b04defa2/watchfiles-1.1.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:9974d2f7dc561cce3bb88dfa8eb309dab64c729de85fba32e98d75cf24b66297", size = 402114 }, + { url = "https://files.pythonhosted.org/packages/53/03/acd69c48db4a1ed1de26b349d94077cca2238ff98fd64393f3e97484cae6/watchfiles-1.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c68e9f1fcb4d43798ad8814c4c1b61547b014b667216cb754e606bfade587018", size = 393879 }, + { url = "https://files.pythonhosted.org/packages/2f/c8/a9a2a6f9c8baa4eceae5887fecd421e1b7ce86802bcfc8b6a942e2add834/watchfiles-1.1.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95ab1594377effac17110e1352989bdd7bdfca9ff0e5eeccd8c69c5389b826d0", size = 450026 }, + { url = "https://files.pythonhosted.org/packages/fe/51/d572260d98388e6e2b967425c985e07d47ee6f62e6455cefb46a6e06eda5/watchfiles-1.1.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fba9b62da882c1be1280a7584ec4515d0a6006a94d6e5819730ec2eab60ffe12", size = 457917 }, + { url = "https://files.pythonhosted.org/packages/c6/2d/4258e52917bf9f12909b6ec314ff9636276f3542f9d3807d143f27309104/watchfiles-1.1.0-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3434e401f3ce0ed6b42569128b3d1e3af773d7ec18751b918b89cd49c14eaafb", size = 483602 }, + { url = "https://files.pythonhosted.org/packages/84/99/bee17a5f341a4345fe7b7972a475809af9e528deba056f8963d61ea49f75/watchfiles-1.1.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fa257a4d0d21fcbca5b5fcba9dca5a78011cb93c0323fb8855c6d2dfbc76eb77", size = 596758 }, + { url = "https://files.pythonhosted.org/packages/40/76/e4bec1d59b25b89d2b0716b41b461ed655a9a53c60dc78ad5771fda5b3e6/watchfiles-1.1.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7fd1b3879a578a8ec2076c7961076df540b9af317123f84569f5a9ddee64ce92", size = 477601 }, + { url = "https://files.pythonhosted.org/packages/1f/fa/a514292956f4a9ce3c567ec0c13cce427c158e9f272062685a8a727d08fc/watchfiles-1.1.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:62cc7a30eeb0e20ecc5f4bd113cd69dcdb745a07c68c0370cea919f373f65d9e", size = 451936 }, + { url = "https://files.pythonhosted.org/packages/32/5d/c3bf927ec3bbeb4566984eba8dd7a8eb69569400f5509904545576741f88/watchfiles-1.1.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:891c69e027748b4a73847335d208e374ce54ca3c335907d381fde4e41661b13b", size = 626243 }, + { url = "https://files.pythonhosted.org/packages/e6/65/6e12c042f1a68c556802a84d54bb06d35577c81e29fba14019562479159c/watchfiles-1.1.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:12fe8eaffaf0faa7906895b4f8bb88264035b3f0243275e0bf24af0436b27259", size = 623073 }, + { url = "https://files.pythonhosted.org/packages/89/ab/7f79d9bf57329e7cbb0a6fd4c7bd7d0cee1e4a8ef0041459f5409da3506c/watchfiles-1.1.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:bfe3c517c283e484843cb2e357dd57ba009cff351edf45fb455b5fbd1f45b15f", size = 400872 }, + { url = "https://files.pythonhosted.org/packages/df/d5/3f7bf9912798e9e6c516094db6b8932df53b223660c781ee37607030b6d3/watchfiles-1.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a9ccbf1f129480ed3044f540c0fdbc4ee556f7175e5ab40fe077ff6baf286d4e", size = 392877 }, + { url = "https://files.pythonhosted.org/packages/0d/c5/54ec7601a2798604e01c75294770dbee8150e81c6e471445d7601610b495/watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba0e3255b0396cac3cc7bbace76404dd72b5438bf0d8e7cefa2f79a7f3649caa", size = 449645 }, + { url = "https://files.pythonhosted.org/packages/0a/04/c2f44afc3b2fce21ca0b7802cbd37ed90a29874f96069ed30a36dfe57c2b/watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4281cd9fce9fc0a9dbf0fc1217f39bf9cf2b4d315d9626ef1d4e87b84699e7e8", size = 457424 }, + { url = "https://files.pythonhosted.org/packages/9f/b0/eec32cb6c14d248095261a04f290636da3df3119d4040ef91a4a50b29fa5/watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6d2404af8db1329f9a3c9b79ff63e0ae7131986446901582067d9304ae8aaf7f", size = 481584 }, + { url = "https://files.pythonhosted.org/packages/d1/e2/ca4bb71c68a937d7145aa25709e4f5d68eb7698a25ce266e84b55d591bbd/watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e78b6ed8165996013165eeabd875c5dfc19d41b54f94b40e9fff0eb3193e5e8e", size = 596675 }, + { url = "https://files.pythonhosted.org/packages/a1/dd/b0e4b7fb5acf783816bc950180a6cd7c6c1d2cf7e9372c0ea634e722712b/watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:249590eb75ccc117f488e2fabd1bfa33c580e24b96f00658ad88e38844a040bb", size = 477363 }, + { url = "https://files.pythonhosted.org/packages/69/c4/088825b75489cb5b6a761a4542645718893d395d8c530b38734f19da44d2/watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d05686b5487cfa2e2c28ff1aa370ea3e6c5accfe6435944ddea1e10d93872147", size = 452240 }, + { url = "https://files.pythonhosted.org/packages/10/8c/22b074814970eeef43b7c44df98c3e9667c1f7bf5b83e0ff0201b0bd43f9/watchfiles-1.1.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:d0e10e6f8f6dc5762adee7dece33b722282e1f59aa6a55da5d493a97282fedd8", size = 625607 }, + { url = "https://files.pythonhosted.org/packages/32/fa/a4f5c2046385492b2273213ef815bf71a0d4c1943b784fb904e184e30201/watchfiles-1.1.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:af06c863f152005c7592df1d6a7009c836a247c9d8adb78fef8575a5a98699db", size = 623315 }, +] + +[[package]] +name = "websockets" +version = "15.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440 }, + { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098 }, + { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329 }, + { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111 }, + { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054 }, + { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496 }, + { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829 }, + { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217 }, + { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195 }, + { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393 }, + { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837 }, + { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743 }, +]