Compare commits
85 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9ecda8c6b0 | |||
| aa89a3fb00 | |||
| 64aeb5fc80 | |||
| 9540520dc4 | |||
| 300ed387f5 | |||
| dea81de5b5 | |||
| 01a00a12af | |||
| 504d0162bb | |||
| cda23d074f | |||
| 0357108077 | |||
| ab1df3dd0f | |||
| c50091f6bf | |||
| 8b406370f1 | |||
| c56bf2b92c | |||
| b1507db4d0 | |||
| 3074962314 | |||
| 84799879bb | |||
| 1052c1b619 | |||
| 9958b8e848 | |||
| b15161dad7 | |||
| 52f025ae32 | |||
| 2cfe7a035b | |||
| 2441c2dc29 | |||
| 261a1f3918 | |||
| 253903a1e5 | |||
| 20a113e335 | |||
| fed83e7d79 | |||
| 3880a4ec49 | |||
| 6195abbaa5 | |||
| b132554631 | |||
| 561796cec1 | |||
| 26d90acc16 | |||
| d8234b2958 | |||
| 156d1fe567 | |||
| f8eee120cf | |||
| c5e4a5e1a7 | |||
| d10c9160f3 | |||
| 1f88e99e8b | |||
| e097a13221 | |||
| 086fff803d | |||
| 45fbf975b3 | |||
| b8f2798e29 | |||
| c762d91eb1 | |||
| 35a68387ab | |||
| 9b13c69631 | |||
| 2ca1ea29b2 | |||
| 5e86615bde | |||
| 6701a4b392 | |||
| 68372405d6 | |||
| f1ea0de247 | |||
| 974d008825 | |||
| 8a87d94e6d | |||
| ec99a501fc | |||
| c256b26601 | |||
| 56a3c1257d | |||
| 79d23f936a | |||
| f02e916d33 | |||
| 4e64c83c4b | |||
| 07cae9abd1 | |||
| 21b2dffc37 | |||
| 2d1055cccf | |||
| fdb2447862 | |||
| 13746ee8cc | |||
| 49f31e492f | |||
| f1fd1b11a1 | |||
| 6f3b21ab17 | |||
| 566f0f6ea2 | |||
| 340c54b3d8 | |||
| 8d0bc59d28 | |||
| f969724ee4 | |||
| ffd2678c91 | |||
| 365f110f9c | |||
| 59d40241e2 | |||
| 51fb30646c | |||
| 4966072f64 | |||
| 39acf8e1f2 | |||
| 4dadb9c4cf | |||
| b350bb12d3 | |||
| f88f512cb0 | |||
| 9a726b52bc | |||
| 8b94507aeb | |||
| 52d568e924 | |||
| da3d6f123d | |||
| ca63865b07 | |||
| ec7d2d623f |
@@ -0,0 +1,18 @@
|
||||
[tool.bumpversion]
|
||||
current_version = "0.1.7"
|
||||
parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
|
||||
serialize = ["{major}.{minor}.{patch}"]
|
||||
search = "{current_version}"
|
||||
replace = "{new_version}"
|
||||
regex = false
|
||||
ignore_missing_version = false
|
||||
tag = true
|
||||
sign_tags = false
|
||||
tag_name = "v{new_version}"
|
||||
tag_message = "Bump version: {current_version} → {new_version}"
|
||||
allow_dirty = false
|
||||
commit = true
|
||||
message = "chore: bump version {current_version} → {new_version}"
|
||||
|
||||
[[tool.bumpversion.files]]
|
||||
filename = "pyproject.toml"
|
||||
@@ -0,0 +1,53 @@
|
||||
# Git
|
||||
.git
|
||||
.gitignore
|
||||
.gitea
|
||||
|
||||
# Python
|
||||
__pycache__
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
.Python
|
||||
*.so
|
||||
.pytest_cache
|
||||
.coverage
|
||||
htmlcov
|
||||
.tox
|
||||
.nox
|
||||
.hypothesis
|
||||
|
||||
# Virtual environments
|
||||
venv
|
||||
.venv
|
||||
env
|
||||
.env
|
||||
.env-
|
||||
# IDE
|
||||
.vscode
|
||||
.idea
|
||||
*.swp
|
||||
*.swo
|
||||
.qodo
|
||||
|
||||
# Build
|
||||
build
|
||||
dist
|
||||
*.egg-info
|
||||
|
||||
# Documentation
|
||||
docs/
|
||||
*.md
|
||||
!README.md
|
||||
|
||||
# Data
|
||||
data/
|
||||
memory_data/
|
||||
logs/
|
||||
*.log
|
||||
|
||||
# Misc
|
||||
*.bak
|
||||
*.tmp
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
+90
-31
@@ -1,34 +1,93 @@
|
||||
# LLM Provider Selection
|
||||
# Options: "deepseek" or "ollama"
|
||||
LLM_PROVIDER=ollama
|
||||
|
||||
# DeepSeek LLM Configuration (if using DeepSeek)
|
||||
DEEPSEEK_API_KEY=your_deepseek_api_key
|
||||
DEEPSEEK_BASE_URL=https://api.deepseek.com
|
||||
DEEPSEEK_MODEL=deepseek-chat
|
||||
|
||||
# Ollama LLM Configuration (if using Ollama)
|
||||
OLLAMA_BASE_URL=http://localhost:11434
|
||||
OLLAMA_MODEL=llama3.2
|
||||
|
||||
# LLM Settings
|
||||
TEMPERATURE=0.2
|
||||
|
||||
# TMDB API Configuration
|
||||
TMDB_API_KEY=your_tmdb_api_key
|
||||
TMDB_BASE_URL=https://api.themoviedb.org/3
|
||||
|
||||
# Storage Configuration
|
||||
MEMORY_FILE=memory.json
|
||||
|
||||
# qBittorrent Configuration
|
||||
QBIT_HOST=http://192.168.178.47:30024
|
||||
QBIT_USER=admin
|
||||
QBIT_PASS=adminadmin
|
||||
|
||||
# Security Configuration
|
||||
MAX_HISTORY_MESSAGES=10
|
||||
MAX_TOOL_ITERATIONS=10
|
||||
REQUEST_TIMEOUT=30
|
||||
|
||||
# Memory Configuration
|
||||
MAX_HISTORY_MESSAGES=10
|
||||
# LLM Settings
|
||||
LLM_TEMPERATURE=0.2
|
||||
|
||||
# Persistence
|
||||
DATA_STORAGE_DIR=data
|
||||
|
||||
# Network configuration
|
||||
HOST=0.0.0.0
|
||||
PORT=3080
|
||||
|
||||
# Build informations (Synced with pyproject.toml via bootstrap)
|
||||
ALFRED_VERSION=
|
||||
IMAGE_NAME=
|
||||
LIBRECHAT_VERSION=
|
||||
PYTHON_VERSION=
|
||||
PYTHON_VERSION_SHORT=
|
||||
RAG_VERSION=
|
||||
RUNNER=
|
||||
SERVICE_NAME=
|
||||
|
||||
# --- SECURITY KEYS (CRITICAL) ---
|
||||
# These are used for session tokens and encrypting sensitive data in MongoDB.
|
||||
# If you lose these, you lose access to encrypted stored credentials.
|
||||
JWT_SECRET=
|
||||
JWT_REFRESH_SECRET=
|
||||
CREDS_KEY=
|
||||
CREDS_IV=
|
||||
|
||||
# --- DATABASES (AUTO-SECURED) ---
|
||||
# Alfred uses MongoDB for application state and PostgreSQL for Vector RAG.
|
||||
# Passwords will be generated as 24-character secure tokens if left blank.
|
||||
|
||||
# MongoDB (Application Data)
|
||||
MONGO_URI=
|
||||
MONGO_HOST=mongodb
|
||||
MONGO_PORT=27017
|
||||
MONGO_USER=alfred
|
||||
MONGO_PASSWORD=
|
||||
MONGO_DB_NAME=alfred
|
||||
|
||||
# PostgreSQL (Vector Database / RAG)
|
||||
POSTGRES_URI=
|
||||
POSTGRES_HOST=vectordb
|
||||
POSTGRES_PORT=5432
|
||||
POSTGRES_USER=alfred
|
||||
POSTGRES_PASSWORD=
|
||||
POSTGRES_DB_NAME=alfred
|
||||
|
||||
# --- EXTERNAL SERVICES ---
|
||||
# Media Metadata (Required)
|
||||
# Get your key at https://www.themoviedb.org/
|
||||
TMDB_API_KEY=
|
||||
TMDB_BASE_URL=https://api.themoviedb.org/3
|
||||
|
||||
# qBittorrent integration
|
||||
QBITTORRENT_URL=http://qbittorrent:16140
|
||||
QBITTORRENT_USERNAME=admin
|
||||
QBITTORRENT_PASSWORD=
|
||||
QBITTORRENT_PORT=16140
|
||||
|
||||
# Meilisearch
|
||||
MEILI_ENABLED=FALSE
|
||||
MEILI_NO_ANALYTICS=TRUE
|
||||
MEILI_HOST=http://meilisearch:7700
|
||||
MEILI_MASTER_KEY=
|
||||
|
||||
# --- LLM CONFIGURATION ---
|
||||
# Providers: 'local', 'openai', 'anthropic', 'deepseek', 'google', 'kimi'
|
||||
DEFAULT_LLM_PROVIDER=local
|
||||
|
||||
# Local LLM (Ollama)
|
||||
OLLAMA_BASE_URL=http://ollama:11434
|
||||
OLLAMA_MODEL=llama3.3:latest
|
||||
|
||||
# --- API KEYS (OPTIONAL) ---
|
||||
# Fill only the ones you intend to use.
|
||||
ANTHROPIC_API_KEY=
|
||||
DEEPSEEK_API_KEY=
|
||||
GOOGLE_API_KEY=
|
||||
KIMI_API_KEY=
|
||||
OPENAI_API_KEY=
|
||||
|
||||
# --- RAG ENGINE ---
|
||||
# Enable/Disable the Retrieval Augmented Generation system
|
||||
RAG_ENABLED=TRUE
|
||||
RAG_API_URL=http://rag_api:8000
|
||||
RAG_API_PORT=8000
|
||||
EMBEDDINGS_PROVIDER=ollama
|
||||
EMBEDDINGS_MODEL=nomic-embed-text
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
name: CI/CD Awesome Pipeline
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
REGISTRY_URL: ${{ vars.REGISTRY_URL || 'gitea.iswearihadsomethingforthis.net' }}
|
||||
REGISTRY_USER: ${{ vars.REGISTRY_USER || 'francwa' }}
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Build and run tests
|
||||
env:
|
||||
DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }}
|
||||
TMDB_API_KEY: ${{ secrets.TMDB_API_KEY }}
|
||||
run: make _ci-run-tests
|
||||
|
||||
build-and-push:
|
||||
name: Build & Push to Registry
|
||||
runs-on: ubuntu-latest
|
||||
needs: test
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Generate build variables
|
||||
run: python scripts/generate_build_vars.py
|
||||
|
||||
- name: Load config from Makefile
|
||||
id: config
|
||||
run: make -s _ci-dump-config >> $GITHUB_OUTPUT
|
||||
|
||||
- name: 🏷️ Docker Metadata (Tags & Labels)
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: gitea.iswearihadsomethingforthis.net/francwa/${{ steps.config.outputs.image_name }}
|
||||
tags: |
|
||||
# Tagged (v1.2.3)
|
||||
type=semver,pattern={{ version }}
|
||||
# Latest (main)
|
||||
type=raw,value=latest,enable={{ is_default_branch }}
|
||||
# Feature branches
|
||||
type=ref,event=branch
|
||||
|
||||
- name: Login to Gitea Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: gitea.iswearihadsomethingforthis.net
|
||||
username: ${{ gitea.actor }}
|
||||
password: ${{ secrets.G1T34_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
id: docker_build
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
PYTHON_VERSION=${{ steps.config.outputs.python_version }}
|
||||
PYTHON_VERSION_SHORT=${{ steps.config.outputs.python_version_short }}
|
||||
RUNNER=${{ steps.config.outputs.runner }}
|
||||
|
||||
- name: 🛡️ Run Trivy Vulnerability Scanner
|
||||
uses: docker://aquasec/trivy:latest
|
||||
env:
|
||||
TRIVY_USERNAME: ${{ gitea.actor }}
|
||||
TRIVY_PASSWORD: ${{ secrets.G1T34_TOKEN }}
|
||||
# Unset the fake GITHUB_TOKEN injected by Gitea
|
||||
GITHUB_TOKEN: ""
|
||||
with:
|
||||
args: image --format table --output trivy-report.txt --exit-code 0 --ignore-unfixed --severity CRITICAL,HIGH gitea.iswearihadsomethingforthis.net/francwa/${{ steps.config.outputs.image_name }}:latest
|
||||
|
||||
- name: 📤 Upload Security Report
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: security-report
|
||||
path: trivy-report.txt
|
||||
retention-days: 7
|
||||
@@ -0,0 +1,22 @@
|
||||
name: Renovate Bot
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Every Monday 4AM
|
||||
- cron: '0 4 * * 1'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
renovate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Run Renovate
|
||||
uses: docker://renovate/renovate:latest
|
||||
env:
|
||||
RENOVATE_PLATFORM: "gitea"
|
||||
RENOVATE_ENDPOINT: "https://gitea.iswearihadsomethingforthis.net/api/v1"
|
||||
RENOVATE_TOKEN: "${{ secrets.RENOVATE_TOKEN }}"
|
||||
RENOVATE_REPOSITORIES: '["${{ gitea.repository }}"]'
|
||||
RENOVATE_GIT_AUTHOR: "Renovate Bot <renovate@bot.local>"
|
||||
# Might need a free github token if lots of depencies
|
||||
# RENOVATE_GITHUB_TOKEN: "${{ secrets.GITHUB_COM_TOKEN }}"
|
||||
@@ -59,3 +59,9 @@ Thumbs.db
|
||||
|
||||
# Backup files
|
||||
*.backup
|
||||
|
||||
# Application data dir
|
||||
data/*
|
||||
|
||||
# Application logs
|
||||
logs/*
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v6.0.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
- id: end-of-file-fixer
|
||||
- id: check-yaml
|
||||
- id: check-toml
|
||||
- id: check-added-large-files
|
||||
|
||||
- repo: https://github.com/gitleaks/gitleaks
|
||||
rev: v8.30.0
|
||||
hooks:
|
||||
- id: gitleaks
|
||||
name: Gitleaks
|
||||
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: ruff-check
|
||||
name: Ruff Linter
|
||||
entry: bash -c 'make lint'
|
||||
language: system
|
||||
types: [python]
|
||||
|
||||
- id: ruff-format
|
||||
name: Ruff Formatter
|
||||
entry: bash -c 'make format'
|
||||
language: system
|
||||
types: [python]
|
||||
|
||||
- id: system-pytest
|
||||
name: Pytest
|
||||
entry: bash -c 'make test'
|
||||
language: system
|
||||
always_run: true
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
# check=skip=InvalidDefaultArgInFrom
|
||||
|
||||
ARG PYTHON_VERSION
|
||||
ARG PYTHON_VERSION_SHORT
|
||||
ARG RUNNER
|
||||
# ===========================================
|
||||
# Stage 1: Builder
|
||||
# ===========================================
|
||||
FROM python:${PYTHON_VERSION}-slim-bookworm AS builder
|
||||
|
||||
# Re-declare ARGs after FROM to make them available in this stage
|
||||
ARG RUNNER
|
||||
|
||||
# STFU - No need - Write logs asap
|
||||
ENV DEBIAN_FRONTEND=noninteractive \
|
||||
PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1
|
||||
|
||||
# Install build dependencies (needs root)
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
build-essential \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install runner globally (needs root) - Save cache for future
|
||||
RUN --mount=type=cache,target=/root/.cache/pip \
|
||||
pip install $RUNNER
|
||||
|
||||
# Set working directory for dependency installation
|
||||
WORKDIR /tmp
|
||||
|
||||
# Copy dependency files
|
||||
COPY pyproject.toml poetry.lock* uv.lock* Makefile ./
|
||||
|
||||
# Install dependencies as root (to avoid permission issues with system packages)
|
||||
RUN --mount=type=cache,target=/root/.cache/pip \
|
||||
--mount=type=cache,target=/root/.cache/pypoetry \
|
||||
--mount=type=cache,target=/root/.cache/uv \
|
||||
if [ "$RUNNER" = "poetry" ]; then \
|
||||
poetry config virtualenvs.create false && \
|
||||
poetry install --only main --no-root; \
|
||||
elif [ "$RUNNER" = "uv" ]; then \
|
||||
uv pip install --system -r pyproject.toml; \
|
||||
fi
|
||||
|
||||
COPY scripts/ ./scripts/
|
||||
COPY .env.example ./
|
||||
|
||||
# ===========================================
|
||||
# Stage 2: Testing
|
||||
# ===========================================
|
||||
FROM builder AS test
|
||||
|
||||
ARG RUNNER
|
||||
|
||||
RUN --mount=type=cache,target=/root/.cache/pip \
|
||||
--mount=type=cache,target=/root/.cache/pypoetry \
|
||||
--mount=type=cache,target=/root/.cache/uv \
|
||||
if [ "$RUNNER" = "poetry" ]; then \
|
||||
poetry install --no-root; \
|
||||
elif [ "$RUNNER" = "uv" ]; then \
|
||||
uv pip install --system -e .[dev]; \
|
||||
fi
|
||||
|
||||
COPY alfred/ ./alfred
|
||||
COPY scripts ./scripts
|
||||
COPY tests/ ./tests
|
||||
|
||||
# ===========================================
|
||||
# Stage 3: Runtime
|
||||
# ===========================================
|
||||
FROM python:${PYTHON_VERSION}-slim-bookworm AS runtime
|
||||
|
||||
ARG PYTHON_VERSION_SHORT
|
||||
|
||||
# TODO: A-t-on encore besoin de toutes les clés ?
|
||||
ENV LLM_PROVIDER=deepseek \
|
||||
MEMORY_STORAGE_DIR=/data/memory \
|
||||
PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONPATH=/home/appuser \
|
||||
PYTHONUNBUFFERED=1
|
||||
|
||||
# Install runtime dependencies (needs root)
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& apt-get clean
|
||||
|
||||
# Create non-root user
|
||||
RUN useradd -m -u 1000 -s /bin/bash appuser
|
||||
|
||||
# Create data directories (needs root for /data)
|
||||
RUN mkdir -p /data /logs \
|
||||
&& chown -R appuser:appuser /data /logs
|
||||
|
||||
# Switch to non-root user
|
||||
USER appuser
|
||||
|
||||
# Set working directory (owned by appuser)
|
||||
WORKDIR /home/appuser
|
||||
|
||||
# Copy Python packages from builder stage
|
||||
COPY --from=builder /usr/local/lib/python${PYTHON_VERSION_SHORT}/site-packages /usr/local/lib/python${PYTHON_VERSION_SHORT}/site-packages
|
||||
COPY --from=builder /usr/local/bin /usr/local/bin
|
||||
|
||||
# Copy application code (already owned by appuser)
|
||||
COPY --chown=appuser:appuser alfred/ ./alfred
|
||||
COPY --chown=appuser:appuser scripts/ ./scripts
|
||||
COPY --chown=appuser:appuser .env.example ./
|
||||
COPY --chown=appuser:appuser pyproject.toml ./
|
||||
|
||||
# Create volumes for persistent data
|
||||
VOLUME ["/data", "/logs"]
|
||||
|
||||
# Expose port
|
||||
EXPOSE 8000
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD python -c "import requests; requests.get('http://localhost:8000/health', timeout=5).raise_for_status()" || exit 1
|
||||
|
||||
CMD ["python", "-m", "uvicorn", "alfred.app:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
@@ -0,0 +1,182 @@
|
||||
.DEFAULT_GOAL := help
|
||||
|
||||
# --- Load Config from pyproject.toml ---
|
||||
-include .env.make
|
||||
|
||||
# --- Profiles management ---
|
||||
# Usage: make up p=rag,meili
|
||||
p ?= full
|
||||
PROFILES_PARAM := COMPOSE_PROFILES=$(p)
|
||||
|
||||
# --- Commands ---
|
||||
DOCKER_COMPOSE := docker compose
|
||||
DOCKER_BUILD := docker build --no-cache \
|
||||
--build-arg PYTHON_VERSION=$(PYTHON_VERSION) \
|
||||
--build-arg PYTHON_VERSION_SHORT=$(PYTHON_VERSION_SHORT) \
|
||||
--build-arg RUNNER=$(RUNNER)
|
||||
|
||||
# --- Phony ---
|
||||
.PHONY: .env bootstrap up down restart logs ps shell build build-test install \
|
||||
update install-hooks test coverage lint format clean major minor patch help
|
||||
|
||||
# --- Setup ---
|
||||
.env .env.make:
|
||||
@echo "Initializing environment..."
|
||||
@python scripts/bootstrap.py \
|
||||
&& echo "✓ Environment ready" \
|
||||
|| (echo "✗ Environment setup failed" && exit 1)
|
||||
|
||||
bootstrap: .env .env.make
|
||||
|
||||
# --- Docker ---
|
||||
up: .env
|
||||
@echo "Starting containers with profiles: [full]..."
|
||||
@$(PROFILES_PARAM) $(DOCKER_COMPOSE) up -d --remove-orphans \
|
||||
&& echo "✓ Containers started" \
|
||||
|| (echo "✗ Failed to start containers" && exit 1)
|
||||
|
||||
down:
|
||||
@echo "Stopping containers..."
|
||||
@$(PROFILES_PARAM) $(DOCKER_COMPOSE) down \
|
||||
&& echo "✓ Containers stopped" \
|
||||
|| (echo "✗ Failed to stop containers" && exit 1)
|
||||
|
||||
restart:
|
||||
@echo "Restarting containers..."
|
||||
@$(PROFILES_PARAM) $(DOCKER_COMPOSE) restart \
|
||||
&& echo "✓ Containers restarted" \
|
||||
|| (echo "✗ Failed to restart containers" && exit 1)
|
||||
|
||||
logs:
|
||||
@echo "Following logs (Ctrl+C to exit)..."
|
||||
@$(PROFILES_PARAM) $(DOCKER_COMPOSE) logs -f
|
||||
|
||||
ps:
|
||||
@echo "Container status:"
|
||||
@$(PROFILES_PARAM) $(DOCKER_COMPOSE) ps
|
||||
|
||||
shell:
|
||||
@echo "Opening shell in $(SERVICE_NAME)..."
|
||||
@$(DOCKER_COMPOSE) exec $(SERVICE_NAME) /bin/bash
|
||||
|
||||
# --- Build ---
|
||||
build: .env.make
|
||||
@echo "Building image $(IMAGE_NAME):latest ..."
|
||||
@$(DOCKER_BUILD) -t $(IMAGE_NAME):latest . \
|
||||
&& echo "✓ Build complete" \
|
||||
|| (echo "✗ Build failed" && exit 1)
|
||||
|
||||
build-test: .env.make
|
||||
@echo "Building test image $(IMAGE_NAME):test..."
|
||||
@$(DOCKER_BUILD) --target test -t $(IMAGE_NAME):test . \
|
||||
&& echo "✓ Test image built" \
|
||||
|| (echo "✗ Build failed" && exit 1)
|
||||
|
||||
# --- Dependencies ---
|
||||
install:
|
||||
@echo "Installing dependencies with $(RUNNER)..."
|
||||
@$(RUNNER) install \
|
||||
&& echo "✓ Dependencies installed" \
|
||||
|| (echo "✗ Installation failed" && exit 1)
|
||||
|
||||
install-hooks:
|
||||
@echo "Installing pre-commit hooks..."
|
||||
@$(RUNNER) run pre-commit install \
|
||||
&& echo "✓ Hooks installed" \
|
||||
|| (echo "✗ Hook installation failed" && exit 1)
|
||||
|
||||
update:
|
||||
@echo "Updating dependencies with $(RUNNER)..."
|
||||
@$(RUNNER) update \
|
||||
&& echo "✓ Dependencies updated" \
|
||||
|| (echo "✗ Update failed" && exit 1)
|
||||
|
||||
# --- Quality ---
|
||||
test:
|
||||
@echo "Running tests..."
|
||||
@$(RUNNER) run pytest \
|
||||
&& echo "✓ Tests passed" \
|
||||
|| (echo "✗ Tests failed" && exit 1)
|
||||
|
||||
coverage:
|
||||
@echo "Running tests with coverage..."
|
||||
@$(RUNNER) run pytest --cov=. --cov-report=html --cov-report=term \
|
||||
&& echo "✓ Coverage report generated" \
|
||||
|| (echo "✗ Coverage failed" && exit 1)
|
||||
|
||||
lint:
|
||||
@echo "Linting code..."
|
||||
@$(RUNNER) run ruff check --fix . \
|
||||
&& echo "✓ Linting complete" \
|
||||
|| (echo "✗ Linting failed" && exit 1)
|
||||
|
||||
format:
|
||||
@echo "Formatting code..."
|
||||
@$(RUNNER) run ruff format . && $(RUNNER) run ruff check --fix . \
|
||||
&& echo "✓ Code formatted" \
|
||||
|| (echo "✗ Formatting failed" && exit 1)
|
||||
|
||||
clean:
|
||||
@echo "Cleaning build artifacts..."
|
||||
@rm -rf .ruff_cache __pycache__ .pytest_cache htmlcov .coverage
|
||||
@find . -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true
|
||||
@echo "✓ Cleanup complete"
|
||||
|
||||
# --- Versioning ---
|
||||
major minor patch: _check-main
|
||||
@echo "Bumping $@ version..."
|
||||
@$(RUNNER) run bump-my-version bump $@ \
|
||||
&& echo "✓ Version bumped" \
|
||||
|| (echo "✗ Version bump failed" && exit 1)
|
||||
|
||||
@echo "Pushing tags..."
|
||||
@git push --tags \
|
||||
&& echo "✓ Tags pushed" \
|
||||
|| (echo "✗ Push failed" && exit 1)
|
||||
|
||||
# CI/CD helpers
|
||||
_ci-dump-config:
|
||||
@echo "image_name=$(IMAGE_NAME)"
|
||||
@echo "python_version=$(PYTHON_VERSION)"
|
||||
@echo "python_version_short=$(PYTHON_VERSION_SHORT)"
|
||||
@echo "runner=$(RUNNER)"
|
||||
@echo "service_name=$(SERVICE_NAME)"
|
||||
|
||||
_ci-run-tests:build-test
|
||||
@echo "Running tests in Docker..."
|
||||
docker run --rm \
|
||||
-e DEEPSEEK_API_KEY \
|
||||
-e TMDB_API_KEY \
|
||||
-e QBITTORRENT_URL \
|
||||
$(IMAGE_NAME):test pytest
|
||||
@echo "✓ Tests passed."
|
||||
|
||||
_check-main:
|
||||
@test "$$(git rev-parse --abbrev-ref HEAD)" = "main" \
|
||||
|| (echo "✗ ERROR: Not on main branch" && exit 1)
|
||||
|
||||
# --- Help ---
|
||||
help:
|
||||
@echo "Cleverly Crafted Unawareness - Management Commands"
|
||||
@echo ""
|
||||
@echo "Usage: make [target] [p=profile1,profile2]"
|
||||
@echo ""
|
||||
@echo "Docker:"
|
||||
@echo " up Start containers (default profile: core)"
|
||||
@echo " Example: make up p=rag,meili"
|
||||
@echo " down Stop all containers"
|
||||
@echo " restart Restart containers (supports p=...)"
|
||||
@echo " logs Follow logs (supports p=...)"
|
||||
@echo " ps Status of containers"
|
||||
@echo " shell Open bash in the core container"
|
||||
@echo " build Build the production Docker image"
|
||||
@echo ""
|
||||
@echo "Dev & Quality:"
|
||||
@echo " setup Bootstrap .env and security keys"
|
||||
@echo " install Install dependencies via $(RUNNER)"
|
||||
@echo " test Run pytest suite"
|
||||
@echo " coverage Run tests and generate HTML report"
|
||||
@echo " lint/format Quality and style checks"
|
||||
@echo ""
|
||||
@echo "Release:"
|
||||
@echo " major|minor|patch Bump version and push tags (main branch only)"
|
||||
@@ -1,89 +1,277 @@
|
||||
# Agent Media 🎬
|
||||
# Alfred Media Organizer 🎬
|
||||
|
||||
An AI-powered agent for managing your local media library with natural language. Search, download, and organize movies and TV shows effortlessly.
|
||||
An AI-powered agent for managing your local media library with natural language. Search, download, and organize movies and TV shows effortlessly through a conversational interface.
|
||||
|
||||
## Features
|
||||
[](https://www.python.org/downloads/)
|
||||
[](https://python-poetry.org/)
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
[](https://github.com/astral-sh/ruff)
|
||||
|
||||
- 🤖 **Natural Language Interface**: Talk to your media library in plain language
|
||||
- 🔍 **Smart Search**: Find movies and TV shows via TMDB
|
||||
- 📥 **Torrent Integration**: Search and download via qBittorrent
|
||||
- 🧠 **Contextual Memory**: Remembers your preferences and conversation history
|
||||
- 📁 **Auto-Organization**: Keeps your media library tidy
|
||||
- 🌐 **API Compatible**: OpenAI-compatible API for easy integration
|
||||
## ✨ Features
|
||||
|
||||
## Architecture
|
||||
- 🤖 **Natural Language Interface** — Talk to your media library in plain language
|
||||
- 🔍 **Smart Search** — Find movies and TV shows via TMDB with rich metadata
|
||||
- 📥 **Torrent Integration** — Search and download via qBittorrent
|
||||
- 🧠 **Contextual Memory** — Remembers your preferences and conversation history
|
||||
- 📁 **Auto-Organization** — Keeps your media library tidy and well-structured
|
||||
- 🌐 **OpenAI-Compatible API** — Works with any OpenAI-compatible client
|
||||
- 🖥️ **LibreChat Frontend** — Beautiful web UI included out of the box
|
||||
- 🔒 **Secure by Default** — Auto-generated secrets and encrypted credentials
|
||||
|
||||
Built with **Domain-Driven Design (DDD)** principles:
|
||||
## 🏗️ Architecture
|
||||
|
||||
Built with **Domain-Driven Design (DDD)** principles for clean separation of concerns:
|
||||
|
||||
```
|
||||
agent_media/
|
||||
├── agent/ # AI agent orchestration
|
||||
├── application/ # Use cases & DTOs
|
||||
├── domain/ # Business logic & entities
|
||||
└── infrastructure/ # External services & persistence
|
||||
alfred/
|
||||
├── agent/ # AI agent orchestration
|
||||
│ ├── llm/ # LLM clients (Ollama, DeepSeek)
|
||||
│ └── tools/ # Tool implementations
|
||||
├── application/ # Use cases & DTOs
|
||||
│ ├── movies/ # Movie search use cases
|
||||
│ ├── torrents/ # Torrent management
|
||||
│ └── filesystem/ # File operations
|
||||
├── domain/ # Business logic & entities
|
||||
│ ├── movies/ # Movie entities
|
||||
│ ├── tv_shows/ # TV show entities
|
||||
│ └── subtitles/ # Subtitle entities
|
||||
└── infrastructure/ # External services & persistence
|
||||
├── api/ # External API clients (TMDB, qBittorrent)
|
||||
├── filesystem/ # File system operations
|
||||
└── persistence/ # Memory & repositories
|
||||
```
|
||||
|
||||
See [architecture_diagram.md](docs/architecture_diagram.md) for architectural details.
|
||||
See [docs/architecture_diagram.md](docs/architecture_diagram.md) for detailed architectural diagrams.
|
||||
|
||||
## Quick Start
|
||||
## 🚀 Quick Start
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Python 3.12+
|
||||
- Poetry
|
||||
- qBittorrent (optional, for downloads)
|
||||
- API Keys:
|
||||
- DeepSeek API key (or Ollama for local LLM)
|
||||
- TMDB API key
|
||||
- **Python 3.14+** (required)
|
||||
- **Poetry** (dependency manager)
|
||||
- **Docker & Docker Compose** (recommended for full stack)
|
||||
- **API Keys:**
|
||||
- TMDB API key ([get one here](https://www.themoviedb.org/settings/api))
|
||||
- Optional: DeepSeek, OpenAI, Anthropic, or other LLM provider keys
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://github.com/your-username/agent-media.git
|
||||
cd agent-media
|
||||
git clone https://github.com/francwa/alfred_media_organizer.git
|
||||
cd alfred_media_organizer
|
||||
|
||||
# Install dependencies
|
||||
poetry install
|
||||
make install
|
||||
|
||||
# Copy environment template
|
||||
cp .env.example .env
|
||||
# Bootstrap environment (generates .env with secure secrets)
|
||||
make bootstrap
|
||||
|
||||
# Edit .env with your API keys
|
||||
nano .env
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
Edit `.env`:
|
||||
### Running with Docker (Recommended)
|
||||
|
||||
```bash
|
||||
# LLM Provider (deepseek or ollama)
|
||||
LLM_PROVIDER=deepseek
|
||||
DEEPSEEK_API_KEY=your-api-key-here
|
||||
# Start all services (LibreChat + Alfred + MongoDB + Ollama)
|
||||
make up
|
||||
|
||||
# TMDB (for movie/TV show metadata)
|
||||
TMDB_API_KEY=your-tmdb-key-here
|
||||
# Or start with specific profiles
|
||||
make up p=rag,meili # Include RAG and Meilisearch
|
||||
make up p=qbittorrent # Include qBittorrent
|
||||
make up p=full # Everything
|
||||
|
||||
# qBittorrent (optional)
|
||||
QBITTORRENT_HOST=http://localhost:8080
|
||||
QBITTORRENT_USERNAME=admin
|
||||
QBITTORRENT_PASSWORD=adminadmin
|
||||
# View logs
|
||||
make logs
|
||||
|
||||
# Stop all services
|
||||
make down
|
||||
```
|
||||
|
||||
### Run
|
||||
The web interface will be available at **http://localhost:3080**
|
||||
|
||||
### Running Locally (Development)
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
poetry install
|
||||
|
||||
# Start the API server
|
||||
poetry run uvicorn app:app --reload
|
||||
|
||||
# Or with Docker
|
||||
docker-compose up
|
||||
poetry run uvicorn alfred.app:app --reload --port 8000
|
||||
```
|
||||
|
||||
The API will be available at `http://localhost:8000`
|
||||
## ⚙️ Configuration
|
||||
|
||||
## Usage
|
||||
### Environment Bootstrap
|
||||
|
||||
Alfred uses a smart bootstrap system that:
|
||||
|
||||
1. **Generates secure secrets** automatically (JWT tokens, database passwords, encryption keys)
|
||||
2. **Syncs build variables** from `pyproject.toml` (versions, image names)
|
||||
3. **Preserves existing secrets** when re-running (never overwrites your API keys)
|
||||
4. **Computes database URIs** automatically from individual components
|
||||
|
||||
```bash
|
||||
# First time setup
|
||||
make bootstrap
|
||||
|
||||
# Re-run after updating pyproject.toml (secrets are preserved)
|
||||
make bootstrap
|
||||
```
|
||||
|
||||
### Configuration File (.env)
|
||||
|
||||
The `.env` file is generated from `.env.example` with secure defaults:
|
||||
|
||||
```bash
|
||||
# --- CORE SETTINGS ---
|
||||
HOST=0.0.0.0
|
||||
PORT=3080
|
||||
MAX_HISTORY_MESSAGES=10
|
||||
MAX_TOOL_ITERATIONS=10
|
||||
|
||||
# --- LLM CONFIGURATION ---
|
||||
# Providers: 'local' (Ollama), 'deepseek', 'openai', 'anthropic', 'google'
|
||||
DEFAULT_LLM_PROVIDER=local
|
||||
|
||||
# Local LLM (Ollama - included in Docker stack)
|
||||
OLLAMA_BASE_URL=http://ollama:11434
|
||||
OLLAMA_MODEL=llama3.3:latest
|
||||
LLM_TEMPERATURE=0.2
|
||||
|
||||
# --- API KEYS (fill only what you need) ---
|
||||
TMDB_API_KEY=your-tmdb-key-here # Required for movie search
|
||||
DEEPSEEK_API_KEY= # Optional
|
||||
OPENAI_API_KEY= # Optional
|
||||
ANTHROPIC_API_KEY= # Optional
|
||||
|
||||
# --- SECURITY (auto-generated, don't modify) ---
|
||||
JWT_SECRET=<auto-generated>
|
||||
JWT_REFRESH_SECRET=<auto-generated>
|
||||
CREDS_KEY=<auto-generated>
|
||||
CREDS_IV=<auto-generated>
|
||||
|
||||
# --- DATABASES (auto-generated passwords) ---
|
||||
MONGO_PASSWORD=<auto-generated>
|
||||
POSTGRES_PASSWORD=<auto-generated>
|
||||
```
|
||||
|
||||
### Security Keys
|
||||
|
||||
Security keys are defined in `pyproject.toml` and generated automatically:
|
||||
|
||||
```toml
|
||||
[tool.alfred.security]
|
||||
jwt_secret = "32:b64" # 32 bytes, base64 URL-safe
|
||||
jwt_refresh_secret = "32:b64"
|
||||
creds_key = "32:hex" # 32 bytes, hexadecimal (AES-256)
|
||||
creds_iv = "16:hex" # 16 bytes, hexadecimal (AES IV)
|
||||
mongo_password = "16:hex"
|
||||
postgres_password = "16:hex"
|
||||
```
|
||||
|
||||
**Formats:**
|
||||
- `b64` — Base64 URL-safe (for JWT tokens)
|
||||
- `hex` — Hexadecimal (for encryption keys, passwords)
|
||||
|
||||
## 🐳 Docker Services
|
||||
|
||||
### Service Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ alfred-net (bridge) │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
||||
│ │ LibreChat │───▶│ Alfred │───▶│ MongoDB │ │
|
||||
│ │ :3080 │ │ (core) │ │ :27017 │ │
|
||||
│ └──────────────┘ └──────────────┘ └──────────────┘ │
|
||||
│ │ │ │
|
||||
│ │ ▼ │
|
||||
│ │ ┌──────────────┐ │
|
||||
│ │ │ Ollama │ │
|
||||
│ │ │ (local) │ │
|
||||
│ │ └──────────────┘ │
|
||||
│ │ │
|
||||
│ ┌──────┴───────────────────────────────────────────────┐ │
|
||||
│ │ Optional Services (profiles) │ │
|
||||
│ ├──────────────┬──────────────┬──────────────┬─────────┤ │
|
||||
│ │ Meilisearch │ RAG API │ VectorDB │qBittor- │ │
|
||||
│ │ :7700 │ :8000 │ :5432 │ rent │ │
|
||||
│ │ [meili] │ [rag] │ [rag] │[qbit..] │ │
|
||||
│ └──────────────┴──────────────┴──────────────┴─────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Docker Profiles
|
||||
|
||||
| Profile | Services | Use Case |
|
||||
|---------|----------|----------|
|
||||
| (default) | LibreChat, Alfred, MongoDB, Ollama | Basic setup |
|
||||
| `meili` | + Meilisearch | Fast search |
|
||||
| `rag` | + RAG API, VectorDB | Document retrieval |
|
||||
| `qbittorrent` | + qBittorrent | Torrent downloads |
|
||||
| `full` | All services | Complete setup |
|
||||
|
||||
```bash
|
||||
# Start with specific profiles
|
||||
make up p=rag,meili
|
||||
make up p=full
|
||||
```
|
||||
|
||||
### Docker Commands
|
||||
|
||||
```bash
|
||||
make up # Start containers (default profile)
|
||||
make up p=full # Start with all services
|
||||
make down # Stop all containers
|
||||
make restart # Restart containers
|
||||
make logs # Follow logs
|
||||
make ps # Show container status
|
||||
make shell # Open bash in Alfred container
|
||||
make build # Build production image
|
||||
make build-test # Build test image
|
||||
```
|
||||
|
||||
## 🛠️ Available Tools
|
||||
|
||||
The agent has access to these tools for interacting with your media library:
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `find_media_imdb_id` | Search for movies/TV shows on TMDB by title |
|
||||
| `find_torrent` | Search for torrents across multiple indexers |
|
||||
| `get_torrent_by_index` | Get detailed info about a specific torrent result |
|
||||
| `add_torrent_by_index` | Download a torrent by its index in search results |
|
||||
| `add_torrent_to_qbittorrent` | Add a torrent via magnet link directly |
|
||||
| `set_path_for_folder` | Configure folder paths for media organization |
|
||||
| `list_folder` | List contents of a folder |
|
||||
| `set_language` | Set preferred language for searches |
|
||||
|
||||
## 💬 Usage Examples
|
||||
|
||||
### Via Web Interface (LibreChat)
|
||||
|
||||
Navigate to **http://localhost:3080** and start chatting:
|
||||
|
||||
```
|
||||
You: Find Inception in 1080p
|
||||
Alfred: I found 3 torrents for Inception (2010):
|
||||
1. Inception.2010.1080p.BluRay.x264 (150 seeders) - 2.1 GB
|
||||
2. Inception.2010.1080p.WEB-DL.x265 (80 seeders) - 1.8 GB
|
||||
3. Inception.2010.1080p.REMUX (45 seeders) - 25 GB
|
||||
|
||||
You: Download the first one
|
||||
Alfred: ✓ Added to qBittorrent! Download started.
|
||||
Saving to: /downloads/Movies/Inception (2010)/
|
||||
|
||||
You: What's downloading right now?
|
||||
Alfred: You have 1 active download:
|
||||
- Inception.2010.1080p.BluRay.x264 (45% complete, ETA: 12 min)
|
||||
```
|
||||
|
||||
### Via API
|
||||
|
||||
@@ -91,219 +279,177 @@ The API will be available at `http://localhost:8000`
|
||||
# Health check
|
||||
curl http://localhost:8000/health
|
||||
|
||||
# Chat with the agent
|
||||
# Chat with the agent (OpenAI-compatible)
|
||||
curl -X POST http://localhost:8000/v1/chat/completions \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"model": "agent-media",
|
||||
"model": "alfred",
|
||||
"messages": [
|
||||
{"role": "user", "content": "Find Inception 1080p"}
|
||||
{"role": "user", "content": "Find The Matrix 4K"}
|
||||
]
|
||||
}'
|
||||
|
||||
# List available models
|
||||
curl http://localhost:8000/v1/models
|
||||
|
||||
# View memory state (debug)
|
||||
curl http://localhost:8000/memory/state
|
||||
|
||||
# Clear session memory
|
||||
curl -X POST http://localhost:8000/memory/clear-session
|
||||
```
|
||||
|
||||
### Via OpenWebUI
|
||||
### Via OpenWebUI or Other Clients
|
||||
|
||||
Agent Media is compatible with [OpenWebUI](https://github.com/open-webui/open-webui):
|
||||
Alfred is compatible with any OpenAI-compatible client:
|
||||
|
||||
1. Add as OpenAI-compatible endpoint: `http://localhost:8000/v1`
|
||||
2. Model name: `agent-media`
|
||||
3. Start chatting!
|
||||
2. Model name: `alfred`
|
||||
3. No API key required (or use any placeholder)
|
||||
|
||||
### Example Conversations
|
||||
## 🧠 Memory System
|
||||
|
||||
```
|
||||
You: Find Inception in 1080p
|
||||
Agent: I found 3 torrents for Inception:
|
||||
1. Inception.2010.1080p.BluRay.x264 (150 seeders)
|
||||
2. Inception.2010.1080p.WEB-DL.x265 (80 seeders)
|
||||
3. Inception.2010.720p.BluRay (45 seeders)
|
||||
|
||||
You: Download the first one
|
||||
Agent: Added to qBittorrent! Download started.
|
||||
|
||||
You: List my downloads
|
||||
Agent: You have 1 active download:
|
||||
- Inception.2010.1080p.BluRay.x264 (45% complete)
|
||||
```
|
||||
|
||||
## Available Tools
|
||||
|
||||
The agent has access to these tools:
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `find_media_imdb_id` | Search for movies/TV shows on TMDB |
|
||||
| `find_torrents` | Search for torrents |
|
||||
| `get_torrent_by_index` | Get torrent details by index |
|
||||
| `add_torrent_by_index` | Download torrent by index |
|
||||
| `add_torrent_to_qbittorrent` | Add torrent via magnet link |
|
||||
| `set_path_for_folder` | Configure folder paths |
|
||||
| `list_folder` | List folder contents |
|
||||
|
||||
## Memory System
|
||||
|
||||
Agent Media uses a three-tier memory system:
|
||||
Alfred uses a three-tier memory system for context management:
|
||||
|
||||
### Long-Term Memory (LTM)
|
||||
- **Persistent** (saved to JSON)
|
||||
- Configuration, preferences, media library
|
||||
- Survives restarts
|
||||
- **Persistent** — Saved to JSON files
|
||||
- **Contents:** Configuration, user preferences, media library state
|
||||
- **Survives:** Application restarts
|
||||
|
||||
### Short-Term Memory (STM)
|
||||
- **Session-based** (RAM only)
|
||||
- Conversation history, current workflow
|
||||
- Cleared on restart
|
||||
- **Session-based** — Stored in RAM
|
||||
- **Contents:** Conversation history, current workflow state
|
||||
- **Cleared:** On session end or restart
|
||||
|
||||
### Episodic Memory
|
||||
- **Transient** (RAM only)
|
||||
- Search results, active downloads, recent errors
|
||||
- Cleared frequently
|
||||
- **Transient** — Stored in RAM
|
||||
- **Contents:** Search results, active downloads, recent errors
|
||||
- **Cleared:** Frequently, after task completion
|
||||
|
||||
## Development
|
||||
## 🧪 Development
|
||||
|
||||
### Project Structure
|
||||
### Project Setup
|
||||
|
||||
```
|
||||
agent_media/
|
||||
├── agent/
|
||||
│ ├── agent.py # Main agent orchestrator
|
||||
│ ├── prompts.py # System prompt builder
|
||||
│ ├── registry.py # Tool registration
|
||||
│ ├── tools/ # Tool implementations
|
||||
│ └── llm/ # LLM clients (DeepSeek, Ollama)
|
||||
├── application/
|
||||
│ ├── movies/ # Movie use cases
|
||||
│ ├── torrents/ # Torrent use cases
|
||||
│ └── filesystem/ # Filesystem use cases
|
||||
├── domain/
|
||||
│ ├── movies/ # Movie entities & value objects
|
||||
│ ├── tv_shows/ # TV show entities
|
||||
│ ├── subtitles/ # Subtitle entities
|
||||
│ └── shared/ # Shared value objects
|
||||
├── infrastructure/
|
||||
│ ├── api/ # External API clients
|
||||
│ │ ├── tmdb/ # TMDB client
|
||||
│ │ ├── knaben/ # Torrent search
|
||||
│ │ └── qbittorrent/ # qBittorrent client
|
||||
│ ├── filesystem/ # File operations
|
||||
│ └── persistence/ # Memory & repositories
|
||||
├── tests/ # Test suite (~500 tests)
|
||||
└── docs/ # Documentation
|
||||
```bash
|
||||
# Install all dependencies (including dev)
|
||||
poetry install
|
||||
|
||||
# Install pre-commit hooks
|
||||
make install-hooks
|
||||
|
||||
# Run the development server
|
||||
poetry run uvicorn alfred.app:app --reload
|
||||
```
|
||||
|
||||
### Running Tests
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
poetry run pytest
|
||||
# Run all tests (parallel execution)
|
||||
make test
|
||||
|
||||
# Run with coverage
|
||||
poetry run pytest --cov
|
||||
# Run with coverage report
|
||||
make coverage
|
||||
|
||||
# Run specific test file
|
||||
poetry run pytest tests/test_agent.py
|
||||
poetry run pytest tests/test_agent.py -v
|
||||
|
||||
# Run specific test
|
||||
poetry run pytest tests/test_agent.py::TestAgent::test_step
|
||||
poetry run pytest tests/test_config_loader.py::TestBootstrapEnv -v
|
||||
```
|
||||
|
||||
### Code Quality
|
||||
|
||||
```bash
|
||||
# Linting
|
||||
poetry run ruff check .
|
||||
# Lint and auto-fix
|
||||
make lint
|
||||
|
||||
# Formatting
|
||||
poetry run black .
|
||||
# Format code
|
||||
make format
|
||||
|
||||
# Type checking (if mypy is installed)
|
||||
poetry run mypy .
|
||||
# Clean build artifacts
|
||||
make clean
|
||||
```
|
||||
|
||||
### Adding a New Tool
|
||||
|
||||
Quick example:
|
||||
1. **Create the tool function** in `alfred/agent/tools/`:
|
||||
|
||||
```python
|
||||
# 1. Create the tool function in agent/tools/api.py
|
||||
def my_new_tool(param: str) -> Dict[str, Any]:
|
||||
"""Tool description."""
|
||||
# alfred/agent/tools/api.py
|
||||
def my_new_tool(param: str) -> dict[str, Any]:
|
||||
"""
|
||||
Short description of what this tool does.
|
||||
|
||||
This will be shown to the LLM to help it decide when to use this tool.
|
||||
"""
|
||||
memory = get_memory()
|
||||
# Implementation
|
||||
return {"status": "ok", "data": "result"}
|
||||
|
||||
# 2. Register in agent/registry.py
|
||||
Tool(
|
||||
name="my_new_tool",
|
||||
description="What this tool does",
|
||||
func=api_tools.my_new_tool,
|
||||
parameters={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"param": {"type": "string", "description": "Parameter description"},
|
||||
},
|
||||
"required": ["param"],
|
||||
},
|
||||
),
|
||||
|
||||
# Your implementation here
|
||||
result = do_something(param)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"data": result
|
||||
}
|
||||
```
|
||||
|
||||
## Docker
|
||||
2. **Register in the registry** (`alfred/agent/registry.py`):
|
||||
|
||||
### Build
|
||||
```python
|
||||
tool_functions = [
|
||||
# ... existing tools ...
|
||||
api_tools.my_new_tool, # Add your tool here
|
||||
]
|
||||
```
|
||||
|
||||
The tool will be automatically registered with its parameters extracted from the function signature.
|
||||
|
||||
### Version Management
|
||||
|
||||
```bash
|
||||
docker build -t agent-media .
|
||||
# Bump version (must be on main branch)
|
||||
make patch # 0.1.7 -> 0.1.8
|
||||
make minor # 0.1.7 -> 0.2.0
|
||||
make major # 0.1.7 -> 1.0.0
|
||||
```
|
||||
|
||||
### Run
|
||||
|
||||
```bash
|
||||
docker run -p 8000:8000 \
|
||||
-e DEEPSEEK_API_KEY=your-key \
|
||||
-e TMDB_API_KEY=your-key \
|
||||
-v $(pwd)/memory_data:/app/memory_data \
|
||||
agent-media
|
||||
```
|
||||
|
||||
### Docker Compose
|
||||
|
||||
```bash
|
||||
# Start all services (agent + qBittorrent)
|
||||
docker-compose up -d
|
||||
|
||||
# View logs
|
||||
docker-compose logs -f
|
||||
|
||||
# Stop
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
## API Documentation
|
||||
## 📚 API Reference
|
||||
|
||||
### Endpoints
|
||||
|
||||
#### `GET /health`
|
||||
Health check endpoint.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"status": "healthy",
|
||||
"version": "0.2.0"
|
||||
"version": "0.1.7"
|
||||
}
|
||||
```
|
||||
|
||||
#### `GET /v1/models`
|
||||
List available models (OpenAI-compatible).
|
||||
|
||||
```json
|
||||
{
|
||||
"object": "list",
|
||||
"data": [
|
||||
{
|
||||
"id": "alfred",
|
||||
"object": "model",
|
||||
"owned_by": "alfred"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### `POST /v1/chat/completions`
|
||||
Chat with the agent (OpenAI-compatible).
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"model": "agent-media",
|
||||
"model": "alfred",
|
||||
"messages": [
|
||||
{"role": "user", "content": "Find Inception"}
|
||||
],
|
||||
@@ -317,7 +463,7 @@ Chat with the agent (OpenAI-compatible).
|
||||
"id": "chatcmpl-xxx",
|
||||
"object": "chat.completion",
|
||||
"created": 1234567890,
|
||||
"model": "agent-media",
|
||||
"model": "alfred",
|
||||
"choices": [{
|
||||
"index": 0,
|
||||
"message": {
|
||||
@@ -330,71 +476,120 @@ Chat with the agent (OpenAI-compatible).
|
||||
```
|
||||
|
||||
#### `GET /memory/state`
|
||||
View full memory state (debug).
|
||||
View full memory state (debug endpoint).
|
||||
|
||||
#### `POST /memory/clear-session`
|
||||
Clear session memories (STM + Episodic).
|
||||
|
||||
## Troubleshooting
|
||||
## 🔧 Troubleshooting
|
||||
|
||||
### Agent doesn't respond
|
||||
- Check API keys in `.env`
|
||||
- Verify LLM provider is running (Ollama) or accessible (DeepSeek)
|
||||
- Check logs: `docker-compose logs agent-media`
|
||||
|
||||
1. Check API keys in `.env`
|
||||
2. Verify LLM provider is running:
|
||||
```bash
|
||||
# For Ollama
|
||||
docker logs alfred-ollama
|
||||
|
||||
# Check if model is pulled
|
||||
docker exec alfred-ollama ollama list
|
||||
```
|
||||
3. Check Alfred logs: `docker logs alfred-core`
|
||||
|
||||
### qBittorrent connection failed
|
||||
- Verify qBittorrent is running
|
||||
- Check `QBITTORRENT_HOST` in `.env`
|
||||
- Ensure Web UI is enabled in qBittorrent settings
|
||||
|
||||
1. Verify qBittorrent is running: `docker ps | grep qbittorrent`
|
||||
2. Check Web UI is enabled in qBittorrent settings
|
||||
3. Verify credentials in `.env`:
|
||||
```bash
|
||||
QBITTORRENT_URL=http://qbittorrent:16140
|
||||
QBITTORRENT_USERNAME=admin
|
||||
QBITTORRENT_PASSWORD=<check-your-env>
|
||||
```
|
||||
|
||||
### Database connection issues
|
||||
|
||||
1. Check MongoDB is healthy: `docker logs alfred-mongodb`
|
||||
2. Verify credentials match in `.env`
|
||||
3. Try restarting: `make restart`
|
||||
|
||||
### Memory not persisting
|
||||
- Check `memory_data/` directory exists and is writable
|
||||
- Verify volume mounts in Docker
|
||||
|
||||
1. Check `data/` directory exists and is writable
|
||||
2. Verify volume mounts in `docker-compose.yaml`
|
||||
3. Check file permissions: `ls -la data/`
|
||||
|
||||
### Bootstrap fails
|
||||
|
||||
1. Ensure `.env.example` exists
|
||||
2. Check `pyproject.toml` has required sections:
|
||||
```toml
|
||||
[tool.alfred.settings]
|
||||
[tool.alfred.security]
|
||||
```
|
||||
3. Run manually: `python scripts/bootstrap.py`
|
||||
|
||||
### Tests failing
|
||||
- Run `poetry install` to ensure dependencies are up to date
|
||||
- Check logs for specific error messages
|
||||
|
||||
## Contributing
|
||||
1. Update dependencies: `poetry install`
|
||||
2. Check Python version: `python --version` (needs 3.14+)
|
||||
3. Run specific failing test with verbose output:
|
||||
```bash
|
||||
poetry run pytest tests/test_failing.py -v --tb=long
|
||||
```
|
||||
|
||||
Contributions are welcome!
|
||||
## 🤝 Contributing
|
||||
|
||||
### Development Workflow
|
||||
Contributions are welcome! Please follow these steps:
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch: `git checkout -b feature/my-feature`
|
||||
3. Make your changes
|
||||
4. Run tests: `poetry run pytest`
|
||||
5. Run linting: `poetry run ruff check . && poetry run black .`
|
||||
6. Commit: `git commit -m "Add my feature"`
|
||||
7. Push: `git push origin feature/my-feature`
|
||||
8. Create a Pull Request
|
||||
1. **Fork** the repository
|
||||
2. **Create** a feature branch: `git checkout -b feature/my-feature`
|
||||
3. **Make** your changes
|
||||
4. **Run** tests: `make test`
|
||||
5. **Run** linting: `make lint && make format`
|
||||
6. **Commit**: `git commit -m "feat: add my feature"`
|
||||
7. **Push**: `git push origin feature/my-feature`
|
||||
8. **Create** a Pull Request
|
||||
|
||||
## Documentation
|
||||
### Commit Convention
|
||||
|
||||
- [Architecture Diagram](docs/architecture_diagram.md) - System architecture overview
|
||||
- [Class Diagram](docs/class_diagram.md) - Class structure and relationships
|
||||
- [Component Diagram](docs/component_diagram.md) - Component interactions
|
||||
- [Sequence Diagram](docs/sequence_diagram.md) - Sequence flows
|
||||
- [Flowchart](docs/flowchart.md) - System flowcharts
|
||||
We use [Conventional Commits](https://www.conventionalcommits.org/):
|
||||
|
||||
## License
|
||||
- `feat:` New feature
|
||||
- `fix:` Bug fix
|
||||
- `docs:` Documentation
|
||||
- `refactor:` Code refactoring
|
||||
- `test:` Adding tests
|
||||
- `chore:` Maintenance
|
||||
|
||||
MIT License - see [LICENSE](LICENSE) file for details.
|
||||
## 📖 Documentation
|
||||
|
||||
## Acknowledgments
|
||||
- [Architecture Diagram](docs/architecture_diagram.md) — System architecture overview
|
||||
- [Class Diagram](docs/class_diagram.md) — Class structure and relationships
|
||||
- [Component Diagram](docs/component_diagram.md) — Component interactions
|
||||
- [Sequence Diagram](docs/sequence_diagram.md) — Sequence flows
|
||||
- [Flowchart](docs/flowchart.md) — System flowcharts
|
||||
|
||||
- [DeepSeek](https://www.deepseek.com/) - LLM provider
|
||||
- [TMDB](https://www.themoviedb.org/) - Movie database
|
||||
- [qBittorrent](https://www.qbittorrent.org/) - Torrent client
|
||||
- [FastAPI](https://fastapi.tiangolo.com/) - Web framework
|
||||
## 📄 License
|
||||
|
||||
## Support
|
||||
MIT License — see [LICENSE](LICENSE) file for details.
|
||||
|
||||
## 🙏 Acknowledgments
|
||||
|
||||
- [LibreChat](https://github.com/danny-avila/LibreChat) — Beautiful chat interface
|
||||
- [Ollama](https://ollama.ai/) — Local LLM runtime
|
||||
- [DeepSeek](https://www.deepseek.com/) — LLM provider
|
||||
- [TMDB](https://www.themoviedb.org/) — Movie database
|
||||
- [qBittorrent](https://www.qbittorrent.org/) — Torrent client
|
||||
- [FastAPI](https://fastapi.tiangolo.com/) — Web framework
|
||||
- [Pydantic](https://docs.pydantic.dev/) — Data validation
|
||||
|
||||
## 📬 Support
|
||||
|
||||
- 📧 Email: francois.hodiaumont@gmail.com
|
||||
- 🐛 Issues: [GitHub Issues](https://github.com/your-username/agent-media/issues)
|
||||
- 💬 Discussions: [GitHub Discussions](https://github.com/your-username/agent-media/discussions)
|
||||
- 🐛 Issues: [GitHub Issues](https://github.com/francwa/alfred_media_organizer/issues)
|
||||
- 💬 Discussions: [GitHub Discussions](https://github.com/francwa/alfred_media_organizer/discussions)
|
||||
|
||||
---
|
||||
|
||||
Made with ❤️ by Francwa
|
||||
<p align="center">Made with ❤️ by <a href="https://github.com/francwa">Francwa</a></p>
|
||||
|
||||
-115
@@ -1,115 +0,0 @@
|
||||
"""Configuration management with validation."""
|
||||
|
||||
import os
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
|
||||
class ConfigurationError(Exception):
|
||||
"""Raised when configuration is invalid."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class Settings:
|
||||
"""Application settings loaded from environment variables."""
|
||||
|
||||
# LLM Configuration
|
||||
deepseek_api_key: str = field(
|
||||
default_factory=lambda: os.getenv("DEEPSEEK_API_KEY", "")
|
||||
)
|
||||
deepseek_base_url: str = field(
|
||||
default_factory=lambda: os.getenv(
|
||||
"DEEPSEEK_BASE_URL", "https://api.deepseek.com"
|
||||
)
|
||||
)
|
||||
model: str = field(
|
||||
default_factory=lambda: os.getenv("DEEPSEEK_MODEL", "deepseek-chat")
|
||||
)
|
||||
temperature: float = field(
|
||||
default_factory=lambda: float(os.getenv("TEMPERATURE", "0.2"))
|
||||
)
|
||||
|
||||
# TMDB Configuration
|
||||
tmdb_api_key: str = field(default_factory=lambda: os.getenv("TMDB_API_KEY", ""))
|
||||
tmdb_base_url: str = field(
|
||||
default_factory=lambda: os.getenv(
|
||||
"TMDB_BASE_URL", "https://api.themoviedb.org/3"
|
||||
)
|
||||
)
|
||||
|
||||
# Storage Configuration
|
||||
memory_file: str = field(
|
||||
default_factory=lambda: os.getenv("MEMORY_FILE", "memory.json")
|
||||
)
|
||||
|
||||
# Security Configuration
|
||||
max_tool_iterations: int = field(
|
||||
default_factory=lambda: int(os.getenv("MAX_TOOL_ITERATIONS", "5"))
|
||||
)
|
||||
request_timeout: int = field(
|
||||
default_factory=lambda: int(os.getenv("REQUEST_TIMEOUT", "30"))
|
||||
)
|
||||
|
||||
# Memory Configuration
|
||||
max_history_messages: int = field(
|
||||
default_factory=lambda: int(os.getenv("MAX_HISTORY_MESSAGES", "10"))
|
||||
)
|
||||
|
||||
def __post_init__(self):
|
||||
"""Validate settings after initialization."""
|
||||
self._validate()
|
||||
|
||||
def _validate(self) -> None:
|
||||
"""Validate configuration values."""
|
||||
# Validate temperature
|
||||
if not 0.0 <= self.temperature <= 2.0:
|
||||
raise ConfigurationError(
|
||||
f"Temperature must be between 0.0 and 2.0, got {self.temperature}"
|
||||
)
|
||||
|
||||
# Validate max_tool_iterations
|
||||
if self.max_tool_iterations < 1 or self.max_tool_iterations > 20:
|
||||
raise ConfigurationError(
|
||||
f"max_tool_iterations must be between 1 and 20, got {self.max_tool_iterations}"
|
||||
)
|
||||
|
||||
# Validate request_timeout
|
||||
if self.request_timeout < 1 or self.request_timeout > 300:
|
||||
raise ConfigurationError(
|
||||
f"request_timeout must be between 1 and 300 seconds, got {self.request_timeout}"
|
||||
)
|
||||
|
||||
# Validate URLs
|
||||
if not self.deepseek_base_url.startswith(("http://", "https://")):
|
||||
raise ConfigurationError(
|
||||
f"Invalid deepseek_base_url: {self.deepseek_base_url}"
|
||||
)
|
||||
|
||||
if not self.tmdb_base_url.startswith(("http://", "https://")):
|
||||
raise ConfigurationError(f"Invalid tmdb_base_url: {self.tmdb_base_url}")
|
||||
|
||||
# Validate memory file path
|
||||
memory_path = Path(self.memory_file)
|
||||
if memory_path.exists() and not memory_path.is_file():
|
||||
raise ConfigurationError(
|
||||
f"memory_file exists but is not a file: {self.memory_file}"
|
||||
)
|
||||
|
||||
def is_deepseek_configured(self) -> bool:
|
||||
"""Check if DeepSeek API is properly configured."""
|
||||
return bool(self.deepseek_api_key and self.deepseek_base_url)
|
||||
|
||||
def is_tmdb_configured(self) -> bool:
|
||||
"""Check if TMDB API is properly configured."""
|
||||
return bool(self.tmdb_api_key and self.tmdb_base_url)
|
||||
|
||||
|
||||
# Global settings instance
|
||||
settings = Settings()
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Agent module for media library management."""
|
||||
|
||||
from alfred.settings import settings
|
||||
|
||||
from .agent import Agent
|
||||
from .config import settings
|
||||
|
||||
__all__ = ["Agent", "settings"]
|
||||
@@ -5,9 +5,9 @@ import logging
|
||||
from collections.abc import AsyncGenerator
|
||||
from typing import Any
|
||||
|
||||
from infrastructure.persistence import get_memory
|
||||
from alfred.infrastructure.persistence import get_memory
|
||||
from alfred.settings import settings
|
||||
|
||||
from .config import settings
|
||||
from .prompts import PromptBuilder
|
||||
from .registry import Tool, make_tools
|
||||
|
||||
@@ -21,17 +21,20 @@ class Agent:
|
||||
Uses OpenAI-compatible tool calling API.
|
||||
"""
|
||||
|
||||
def __init__(self, llm, max_tool_iterations: int = 5):
|
||||
def __init__(self, settings, llm, max_tool_iterations: int = 5):
|
||||
"""
|
||||
Initialize the agent.
|
||||
|
||||
Args:
|
||||
settings: Application settings instance
|
||||
llm: LLM client with complete() method
|
||||
max_tool_iterations: Maximum number of tool execution iterations
|
||||
"""
|
||||
self.settings = settings
|
||||
self.llm = llm
|
||||
self.tools: dict[str, Tool] = make_tools()
|
||||
self.tools: dict[str, Tool] = make_tools(settings)
|
||||
self.prompt_builder = PromptBuilder(self.tools)
|
||||
self.settings = settings
|
||||
self.max_tool_iterations = max_tool_iterations
|
||||
|
||||
def step(self, user_input: str) -> str:
|
||||
@@ -78,7 +81,7 @@ class Agent:
|
||||
tools_spec = self.prompt_builder.build_tools_spec()
|
||||
|
||||
# Tool execution loop
|
||||
for _iteration in range(self.max_tool_iterations):
|
||||
for _iteration in range(self.settings.max_tool_iterations):
|
||||
# Call LLM with tools
|
||||
llm_result = self.llm.complete(messages, tools=tools_spec)
|
||||
|
||||
@@ -230,7 +233,7 @@ class Agent:
|
||||
tools_spec = self.prompt_builder.build_tools_spec()
|
||||
|
||||
# Tool execution loop
|
||||
for _iteration in range(self.max_tool_iterations):
|
||||
for _iteration in range(self.settings.max_tool_iterations):
|
||||
# Call LLM with tools
|
||||
llm_result = self.llm.complete(messages, tools=tools_spec)
|
||||
|
||||
@@ -6,7 +6,8 @@ from typing import Any
|
||||
import requests
|
||||
from requests.exceptions import HTTPError, RequestException, Timeout
|
||||
|
||||
from ..config import settings
|
||||
from alfred.settings import Settings, settings
|
||||
|
||||
from .exceptions import LLMAPIError, LLMConfigurationError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -21,6 +22,7 @@ class DeepSeekClient:
|
||||
base_url: str | None = None,
|
||||
model: str | None = None,
|
||||
timeout: int | None = None,
|
||||
settings: Settings | None = None,
|
||||
):
|
||||
"""
|
||||
Initialize DeepSeek client.
|
||||
@@ -34,10 +36,10 @@ class DeepSeekClient:
|
||||
Raises:
|
||||
LLMConfigurationError: If API key is missing
|
||||
"""
|
||||
self.api_key = api_key or settings.deepseek_api_key
|
||||
self.base_url = base_url or settings.deepseek_base_url
|
||||
self.model = model or settings.model
|
||||
self.timeout = timeout or settings.request_timeout
|
||||
self.api_key = api_key or self.settings.deepseek_api_key
|
||||
self.base_url = base_url or self.settings.deepseek_base_url
|
||||
self.model = model or self.settings.deepseek_model
|
||||
self.timeout = timeout or self.settings.request_timeout
|
||||
|
||||
if not self.api_key:
|
||||
raise LLMConfigurationError(
|
||||
@@ -94,7 +96,7 @@ class DeepSeekClient:
|
||||
payload = {
|
||||
"model": self.model,
|
||||
"messages": messages,
|
||||
"temperature": settings.temperature,
|
||||
"temperature": settings.llm_temperature,
|
||||
}
|
||||
|
||||
# Add tools if provided
|
||||
@@ -1,13 +1,13 @@
|
||||
"""Ollama LLM client with robust error handling."""
|
||||
|
||||
import logging
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
import requests
|
||||
from requests.exceptions import HTTPError, RequestException, Timeout
|
||||
|
||||
from ..config import settings
|
||||
from alfred.settings import Settings
|
||||
|
||||
from .exceptions import LLMAPIError, LLMConfigurationError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -32,6 +32,7 @@ class OllamaClient:
|
||||
model: str | None = None,
|
||||
timeout: int | None = None,
|
||||
temperature: float | None = None,
|
||||
settings: Settings | None = None,
|
||||
):
|
||||
"""
|
||||
Initialize Ollama client.
|
||||
@@ -45,13 +46,11 @@ class OllamaClient:
|
||||
Raises:
|
||||
LLMConfigurationError: If configuration is invalid
|
||||
"""
|
||||
self.base_url = base_url or os.getenv(
|
||||
"OLLAMA_BASE_URL", "http://localhost:11434"
|
||||
)
|
||||
self.model = model or os.getenv("OLLAMA_MODEL", "llama3.2")
|
||||
self.base_url = base_url or settings.ollama_base_url
|
||||
self.model = model or settings.ollama_model
|
||||
self.timeout = timeout or settings.request_timeout
|
||||
self.temperature = (
|
||||
temperature if temperature is not None else settings.temperature
|
||||
temperature if temperature is not None else settings.llm_temperature
|
||||
)
|
||||
|
||||
if not self.base_url:
|
||||
@@ -3,7 +3,7 @@
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
from infrastructure.persistence import get_memory
|
||||
from alfred.infrastructure.persistence import get_memory
|
||||
|
||||
from .registry import Tool
|
||||
|
||||
@@ -52,7 +52,7 @@ class PromptBuilder:
|
||||
# Show first 5 results
|
||||
for i, result in enumerate(result_list[:5]):
|
||||
name = result.get("name", "Unknown")
|
||||
lines.append(f" {i+1}. {name}")
|
||||
lines.append(f" {i + 1}. {name}")
|
||||
if len(result_list) > 5:
|
||||
lines.append(f" ... and {len(result_list) - 5} more")
|
||||
|
||||
@@ -78,10 +78,13 @@ def _create_tool_from_function(func: Callable) -> Tool:
|
||||
)
|
||||
|
||||
|
||||
def make_tools() -> dict[str, Tool]:
|
||||
def make_tools(settings) -> dict[str, Tool]:
|
||||
"""
|
||||
Create and register all available tools.
|
||||
|
||||
Args:
|
||||
settings: Application settings instance
|
||||
|
||||
Returns:
|
||||
Dictionary mapping tool names to Tool objects
|
||||
"""
|
||||
@@ -3,12 +3,12 @@
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from application.movies import SearchMovieUseCase
|
||||
from application.torrents import AddTorrentUseCase, SearchTorrentsUseCase
|
||||
from infrastructure.api.knaben import knaben_client
|
||||
from infrastructure.api.qbittorrent import qbittorrent_client
|
||||
from infrastructure.api.tmdb import tmdb_client
|
||||
from infrastructure.persistence import get_memory
|
||||
from alfred.application.movies import SearchMovieUseCase
|
||||
from alfred.application.torrents import AddTorrentUseCase, SearchTorrentsUseCase
|
||||
from alfred.infrastructure.api.knaben import knaben_client
|
||||
from alfred.infrastructure.api.qbittorrent import qbittorrent_client
|
||||
from alfred.infrastructure.api.tmdb import tmdb_client
|
||||
from alfred.infrastructure.persistence import get_memory
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
from typing import Any
|
||||
|
||||
from application.filesystem import ListFolderUseCase, SetFolderPathUseCase
|
||||
from infrastructure.filesystem import FileManager
|
||||
from alfred.application.filesystem import ListFolderUseCase, SetFolderPathUseCase
|
||||
from alfred.infrastructure.filesystem import FileManager
|
||||
|
||||
|
||||
def set_path_for_folder(folder_name: str, path_value: str) -> dict[str, Any]:
|
||||
@@ -3,7 +3,7 @@
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from infrastructure.persistence import get_memory
|
||||
from alfred.infrastructure.persistence import get_memory
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
+24
-17
@@ -2,21 +2,21 @@
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from fastapi.responses import JSONResponse, StreamingResponse
|
||||
from pydantic import BaseModel, Field, validator
|
||||
|
||||
from agent.agent import Agent
|
||||
from agent.config import settings
|
||||
from agent.llm.deepseek import DeepSeekClient
|
||||
from agent.llm.exceptions import LLMAPIError, LLMConfigurationError
|
||||
from agent.llm.ollama import OllamaClient
|
||||
from infrastructure.persistence import get_memory, init_memory
|
||||
from alfred.agent.agent import Agent
|
||||
from alfred.agent.llm.deepseek import DeepSeekClient
|
||||
from alfred.agent.llm.exceptions import LLMAPIError, LLMConfigurationError
|
||||
from alfred.agent.llm.ollama import OllamaClient
|
||||
from alfred.infrastructure.persistence import get_memory, init_memory
|
||||
from alfred.settings import settings
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||||
@@ -29,26 +29,33 @@ app = FastAPI(
|
||||
version="0.2.0",
|
||||
)
|
||||
|
||||
# Initialize memory context at startup
|
||||
init_memory(storage_dir="memory_data")
|
||||
logger.info("Memory context initialized")
|
||||
memory_path = Path(settings.data_storage) / "memory"
|
||||
init_memory(storage_dir=str(memory_path))
|
||||
logger.info(f"Memory context initialized (path: {memory_path})")
|
||||
|
||||
# Initialize LLM based on environment variable
|
||||
llm_provider = os.getenv("LLM_PROVIDER", "deepseek").lower()
|
||||
llm_provider = settings.default_llm_provider.lower()
|
||||
|
||||
|
||||
try:
|
||||
if llm_provider == "ollama":
|
||||
logger.info("Using Ollama LLM")
|
||||
llm = OllamaClient()
|
||||
else:
|
||||
if llm_provider == "local":
|
||||
logger.info("Using local Ollama LLM")
|
||||
llm = OllamaClient(settings=settings)
|
||||
elif llm_provider == "deepseek":
|
||||
logger.info("Using DeepSeek LLM")
|
||||
llm = DeepSeekClient()
|
||||
elif llm_provider == "claude":
|
||||
raise ValueError(f"LLM provider not fully implemented: {llm_provider}")
|
||||
else:
|
||||
raise ValueError(f"Unknown LLM provider: {llm_provider}")
|
||||
except LLMConfigurationError as e:
|
||||
logger.error(f"Failed to initialize LLM: {e}")
|
||||
raise
|
||||
|
||||
# Initialize agent
|
||||
agent = Agent(llm=llm, max_tool_iterations=settings.max_tool_iterations)
|
||||
agent = Agent(
|
||||
settings=settings, llm=llm, max_tool_iterations=settings.max_tool_iterations
|
||||
)
|
||||
logger.info("Agent Media API initialized")
|
||||
|
||||
|
||||
@@ -103,7 +110,7 @@ def extract_last_user_content(messages: list[dict[str, Any]]) -> str:
|
||||
@app.get("/health")
|
||||
async def health_check():
|
||||
"""Health check endpoint."""
|
||||
return {"status": "healthy", "version": "0.2.0"}
|
||||
return {"status": "healthy", "version": f"v{settings.alfred_version}"}
|
||||
|
||||
|
||||
@app.get("/v1/models")
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
import logging
|
||||
|
||||
from infrastructure.filesystem import FileManager
|
||||
from alfred.infrastructure.filesystem import FileManager
|
||||
|
||||
from .dto import ListFolderResponse
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
import logging
|
||||
|
||||
from infrastructure.filesystem import FileManager
|
||||
from alfred.infrastructure.filesystem import FileManager
|
||||
|
||||
from .dto import SetFolderPathResponse
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import logging
|
||||
|
||||
from infrastructure.api.tmdb import (
|
||||
from alfred.infrastructure.api.tmdb import (
|
||||
TMDBAPIError,
|
||||
TMDBClient,
|
||||
TMDBConfigurationError,
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import logging
|
||||
|
||||
from infrastructure.api.qbittorrent import (
|
||||
from alfred.infrastructure.api.qbittorrent import (
|
||||
QBittorrentAPIError,
|
||||
QBittorrentAuthError,
|
||||
QBittorrentClient,
|
||||
+5
-1
@@ -2,7 +2,11 @@
|
||||
|
||||
import logging
|
||||
|
||||
from infrastructure.api.knaben import KnabenAPIError, KnabenClient, KnabenNotFoundError
|
||||
from alfred.infrastructure.api.knaben import (
|
||||
KnabenAPIError,
|
||||
KnabenClient,
|
||||
KnabenNotFoundError,
|
||||
)
|
||||
|
||||
from .dto import SearchTorrentsResponse
|
||||
|
||||
@@ -6,7 +6,7 @@ from typing import Any
|
||||
import requests
|
||||
from requests.exceptions import HTTPError, RequestException, Timeout
|
||||
|
||||
from agent.config import Settings, settings
|
||||
from alfred.settings import Settings, settings
|
||||
|
||||
from .dto import TorrentResult
|
||||
from .exceptions import KnabenAPIError, KnabenNotFoundError
|
||||
+1
-1
@@ -6,7 +6,7 @@ from typing import Any
|
||||
import requests
|
||||
from requests.exceptions import HTTPError, RequestException, Timeout
|
||||
|
||||
from agent.config import Settings, settings
|
||||
from alfred.settings import Settings, settings
|
||||
|
||||
from .dto import TorrentInfo
|
||||
from .exceptions import QBittorrentAPIError, QBittorrentAuthError
|
||||
@@ -6,7 +6,7 @@ from typing import Any
|
||||
import requests
|
||||
from requests.exceptions import HTTPError, RequestException, Timeout
|
||||
|
||||
from agent.config import Settings, settings
|
||||
from alfred.settings import Settings, settings
|
||||
|
||||
from .dto import MediaResult
|
||||
from .exceptions import (
|
||||
+1
-1
@@ -7,7 +7,7 @@ from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from infrastructure.persistence import get_memory
|
||||
from alfred.infrastructure.persistence import get_memory
|
||||
|
||||
from .exceptions import PathTraversalError
|
||||
|
||||
+3
-3
@@ -3,9 +3,9 @@
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from domain.movies.entities import Movie
|
||||
from domain.tv_shows.entities import Episode, Season, TVShow
|
||||
from domain.tv_shows.value_objects import SeasonNumber
|
||||
from alfred.domain.movies.entities import Movie
|
||||
from alfred.domain.tv_shows.entities import Episode, Season, TVShow
|
||||
from alfred.domain.tv_shows.value_objects import SeasonNumber
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ without passing it explicitly through all function calls.
|
||||
|
||||
Usage:
|
||||
# At application startup
|
||||
from infrastructure.persistence import init_memory, get_memory
|
||||
from alfred.infrastructure.persistence import init_memory, get_memory
|
||||
|
||||
init_memory("memory_data")
|
||||
|
||||
+5
-5
@@ -4,11 +4,11 @@ import logging
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from domain.movies.entities import Movie
|
||||
from domain.movies.repositories import MovieRepository
|
||||
from domain.movies.value_objects import MovieTitle, Quality, ReleaseYear
|
||||
from domain.shared.value_objects import FilePath, FileSize, ImdbId
|
||||
from infrastructure.persistence import get_memory
|
||||
from alfred.domain.movies.entities import Movie
|
||||
from alfred.domain.movies.repositories import MovieRepository
|
||||
from alfred.domain.movies.value_objects import MovieTitle, Quality, ReleaseYear
|
||||
from alfred.domain.shared.value_objects import FilePath, FileSize, ImdbId
|
||||
from alfred.infrastructure.persistence import get_memory
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
+5
-5
@@ -3,11 +3,11 @@
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from domain.shared.value_objects import FilePath, ImdbId
|
||||
from domain.subtitles.entities import Subtitle
|
||||
from domain.subtitles.repositories import SubtitleRepository
|
||||
from domain.subtitles.value_objects import Language, SubtitleFormat, TimingOffset
|
||||
from infrastructure.persistence import get_memory
|
||||
from alfred.domain.shared.value_objects import FilePath, ImdbId
|
||||
from alfred.domain.subtitles.entities import Subtitle
|
||||
from alfred.domain.subtitles.repositories import SubtitleRepository
|
||||
from alfred.domain.subtitles.value_objects import Language, SubtitleFormat, TimingOffset
|
||||
from alfred.infrastructure.persistence import get_memory
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
+5
-5
@@ -4,11 +4,11 @@ import logging
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from domain.shared.value_objects import ImdbId
|
||||
from domain.tv_shows.entities import TVShow
|
||||
from domain.tv_shows.repositories import TVShowRepository
|
||||
from domain.tv_shows.value_objects import ShowStatus
|
||||
from infrastructure.persistence import get_memory
|
||||
from alfred.domain.shared.value_objects import ImdbId
|
||||
from alfred.domain.tv_shows.entities import TVShow
|
||||
from alfred.domain.tv_shows.repositories import TVShowRepository
|
||||
from alfred.domain.tv_shows.value_objects import ShowStatus
|
||||
from alfred.infrastructure.persistence import get_memory
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -496,7 +496,7 @@ class Memory:
|
||||
storage_dir: Directory for persistent storage
|
||||
"""
|
||||
self.storage_dir = Path(storage_dir)
|
||||
self.storage_dir.mkdir(exist_ok=True)
|
||||
self.storage_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
self.ltm_file = self.storage_dir / "ltm.json"
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
import secrets
|
||||
from pathlib import Path
|
||||
from typing import NamedTuple
|
||||
|
||||
import tomllib
|
||||
from pydantic import Field, computed_field, field_validator
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
ENV_FILE_PATH = BASE_DIR / ".env"
|
||||
toml_path = BASE_DIR / "pyproject.toml"
|
||||
|
||||
|
||||
class ConfigurationError(Exception):
|
||||
"""Raised when configuration is invalid."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class ProjectVersions(NamedTuple):
|
||||
"""
|
||||
Immutable structure for project versions.
|
||||
Forces explicit naming and prevents accidental swaps.
|
||||
"""
|
||||
|
||||
librechat: str
|
||||
rag: str
|
||||
alfred: str
|
||||
|
||||
|
||||
def get_versions_from_toml() -> ProjectVersions:
|
||||
"""
|
||||
Reads versioning information from pyproject.toml.
|
||||
Returns the default value if the file or key is missing.
|
||||
"""
|
||||
|
||||
if not toml_path.exists():
|
||||
raise FileNotFoundError(f"pyproject.toml not found: {toml_path}")
|
||||
|
||||
with open(toml_path, "rb") as f:
|
||||
data = tomllib.load(f)
|
||||
try:
|
||||
return ProjectVersions(
|
||||
librechat=data["tool"]["alfred"]["settings"]["librechat_version"],
|
||||
rag=data["tool"]["alfred"]["settings"]["rag_version"],
|
||||
alfred=data["tool"]["poetry"]["version"],
|
||||
)
|
||||
except KeyError as e:
|
||||
raise KeyError(f"Error: Missing key {e} in pyproject.toml") from e
|
||||
|
||||
|
||||
# Load versions once
|
||||
VERSIONS: ProjectVersions = get_versions_from_toml()
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
model_config = SettingsConfigDict(
|
||||
env_file=ENV_FILE_PATH,
|
||||
env_file_encoding="utf-8",
|
||||
extra="ignore",
|
||||
case_sensitive=False,
|
||||
)
|
||||
# --- GENERAL SETTINGS ---
|
||||
host: str = "0.0.0.0"
|
||||
port: int = 3080
|
||||
debug_logging: bool = False
|
||||
debug_console: bool = False
|
||||
data_storage: str = "data"
|
||||
librechat_version: str = Field(VERSIONS.librechat, description="Librechat version")
|
||||
rag_version: str = Field(VERSIONS.rag, description="RAG engine version")
|
||||
alfred_version: str = Field(VERSIONS.alfred, description="Alfred version")
|
||||
|
||||
# --- CONTEXT SETTINGS ---
|
||||
max_history_messages: int = 10
|
||||
max_tool_iterations: int = 10
|
||||
request_timeout: int = 30
|
||||
|
||||
# TODO: Finish
|
||||
deepseek_base_url: str = "https://api.deepseek.com"
|
||||
deepseek_model: str = "deepseek-chat"
|
||||
|
||||
# --- API KEYS ---
|
||||
anthropic_api_key: str | None = Field(None, description="Claude API key")
|
||||
deepseek_api_key: str | None = Field(None, description="Deepseek API key")
|
||||
google_api_key: str | None = Field(None, description="Gemini API key")
|
||||
kimi_api_key: str | None = Field(None, description="Kimi API key")
|
||||
openai_api_key: str | None = Field(None, description="ChatGPT API key")
|
||||
|
||||
# --- SECURITY KEYS ---
|
||||
# Generated automatically if not in .env to ensure "Secure by Default"
|
||||
jwt_secret: str = Field(default_factory=lambda: secrets.token_urlsafe(32))
|
||||
jwt_refresh_secret: str = Field(default_factory=lambda: secrets.token_urlsafe(32))
|
||||
|
||||
# We keep these for encryption of keys in MongoDB (AES-256 Hex format)
|
||||
creds_key: str = Field(default_factory=lambda: secrets.token_hex(32))
|
||||
creds_iv: str = Field(default_factory=lambda: secrets.token_hex(16))
|
||||
|
||||
# --- SERVICES ---
|
||||
qbittorrent_url: str = "http://qbittorrent:16140"
|
||||
qbittorrent_username: str = "admin"
|
||||
qbittorrent_password: str = Field(default_factory=lambda: secrets.token_urlsafe(16))
|
||||
|
||||
mongo_host: str = "mongodb"
|
||||
mongo_user: str = "alfred"
|
||||
mongo_password: str = Field(
|
||||
default_factory=lambda: secrets.token_urlsafe(24), repr=False, exclude=True
|
||||
)
|
||||
mongo_port: int = 27017
|
||||
mongo_db_name: str = "alfred"
|
||||
|
||||
@computed_field(repr=False)
|
||||
@property
|
||||
def mongo_uri(self) -> str:
|
||||
return (
|
||||
f"mongodb://{self.mongo_user}:{self.mongo_password}"
|
||||
f"@{self.mongo_host}:{self.mongo_port}/{self.mongo_db_name}"
|
||||
f"?authSource=admin"
|
||||
)
|
||||
|
||||
postgres_host: str = "vectordb"
|
||||
postgres_user: str = "alfred"
|
||||
postgres_password: str = Field(
|
||||
default_factory=lambda: secrets.token_urlsafe(24), repr=False, exclude=True
|
||||
)
|
||||
postgres_port: int = 5432
|
||||
postgres_db_name: str = "alfred"
|
||||
|
||||
@computed_field(repr=False)
|
||||
@property
|
||||
def postgres_uri(self) -> str:
|
||||
return (
|
||||
f"postgresql://{self.postgres_user}:{self.postgres_password}"
|
||||
f"@{self.postgres_host}:{self.postgres_port}/{self.postgres_db_name}"
|
||||
)
|
||||
|
||||
tmdb_api_key: str | None = Field(None, description="The Movie Database API key")
|
||||
tmdb_base_url: str = "https://api.themoviedb.org/3"
|
||||
|
||||
# --- LLM PICKER & CONFIG ---
|
||||
# Providers: 'local', 'deepseek', ...
|
||||
default_llm_provider: str = "local"
|
||||
ollama_base_url: str = "http://ollama:11434"
|
||||
# Models: ...
|
||||
ollama_model: str = "llama3.3:latest"
|
||||
llm_temperature: float = 0.2
|
||||
|
||||
# --- RAG ENGINE ---
|
||||
rag_enabled: bool = True # TODO: Handle False
|
||||
rag_api_url: str = "http://rag_api:8000"
|
||||
embeddings_provider: str = "ollama"
|
||||
# Models: ...
|
||||
embeddings_model: str = "nomic-embed-text"
|
||||
|
||||
# --- MEILISEARCH ---
|
||||
meili_enabled: bool = Field(True, description="Enable meili")
|
||||
meili_no_analytics: bool = True
|
||||
meili_host: str = "http://meilisearch:7700"
|
||||
meili_master_key: str = Field(
|
||||
default_factory=lambda: secrets.token_urlsafe(32),
|
||||
description="Master key for Meilisearch",
|
||||
repr=False,
|
||||
)
|
||||
|
||||
# --- VALIDATORS ---
|
||||
@field_validator("llm_temperature")
|
||||
@classmethod
|
||||
def validate_temperature(cls, v: float) -> float:
|
||||
if not 0.0 <= v <= 2.0:
|
||||
raise ConfigurationError(
|
||||
f"Temperature must be between 0.0 and 2.0, got {v}"
|
||||
)
|
||||
return v
|
||||
|
||||
@field_validator("max_tool_iterations")
|
||||
@classmethod
|
||||
def validate_max_iterations(cls, v: int) -> int:
|
||||
if not 1 <= v <= 20:
|
||||
raise ConfigurationError(
|
||||
f"max_tool_iterations must be between 1 and 50, got {v}"
|
||||
)
|
||||
return v
|
||||
|
||||
@field_validator("request_timeout")
|
||||
@classmethod
|
||||
def validate_timeout(cls, v: int) -> int:
|
||||
if not 1 <= v <= 300:
|
||||
raise ConfigurationError(
|
||||
f"request_timeout must be between 1 and 300 seconds, got {v}"
|
||||
)
|
||||
return v
|
||||
|
||||
@field_validator("deepseek_base_url", "tmdb_base_url")
|
||||
@classmethod
|
||||
def validate_url(cls, v: str, info) -> str:
|
||||
if not v.startswith(("http://", "https://")):
|
||||
raise ConfigurationError(f"Invalid {info.field_name}")
|
||||
return v
|
||||
|
||||
def is_tmdb_configured(self):
|
||||
return bool(self.tmdb_api_key)
|
||||
|
||||
def is_deepseek_configured(self):
|
||||
return bool(self.deepseek_api_key)
|
||||
|
||||
def dump_safe(self):
|
||||
return self.model_dump(exclude_none=False)
|
||||
|
||||
|
||||
settings = Settings()
|
||||
@@ -0,0 +1,206 @@
|
||||
services:
|
||||
# - CORE SERVICES -
|
||||
# --- .ENV INIT ---
|
||||
alfred-init:
|
||||
container_name: alfred-init
|
||||
build:
|
||||
context: .
|
||||
target: builder
|
||||
args:
|
||||
PYTHON_VERSION: ${PYTHON_VERSION}
|
||||
PYTHON_VERSION_SHORT: ${PYTHON_VERSION_SHORT}
|
||||
RUNNER: ${RUNNER}
|
||||
command: python scripts/bootstrap.py
|
||||
networks:
|
||||
- alfred-net
|
||||
|
||||
# --- MAIN APPLICATION ---
|
||||
alfred:
|
||||
container_name: alfred-core
|
||||
build:
|
||||
context: .
|
||||
args:
|
||||
PYTHON_VERSION: ${PYTHON_VERSION}
|
||||
PYTHON_VERSION_SHORT: ${PYTHON_VERSION_SHORT}
|
||||
RUNNER: ${RUNNER}
|
||||
depends_on:
|
||||
alfred-init:
|
||||
condition: service_completed_successfully
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- path: .env
|
||||
required: true
|
||||
volumes:
|
||||
- ./data:/data
|
||||
- ./logs:/logs
|
||||
# TODO: Hot reload (comment out in production)
|
||||
#- ./alfred:/home/appuser/alfred
|
||||
networks:
|
||||
- alfred-net
|
||||
|
||||
# --- FRONTEND LIBRECHAT ---
|
||||
librechat:
|
||||
container_name: alfred-librechat
|
||||
image: ghcr.io/danny-avila/librechat:${LIBRECHAT_VERSION}
|
||||
depends_on:
|
||||
alfred-init:
|
||||
condition: service_completed_successfully
|
||||
mongodb:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- path: .env
|
||||
required: true
|
||||
environment:
|
||||
# Remap value name
|
||||
- SEARCH=${MEILI_ENABLED}
|
||||
ports:
|
||||
- "${PORT}:${PORT}"
|
||||
volumes:
|
||||
- ./data/librechat/images:/app/client/public/images
|
||||
- ./data/librechat/uploads:/app/client/uploads
|
||||
- ./logs:/app/api/logs
|
||||
# Mount custom endpoint
|
||||
- ./librechat/manifests:/app/manifests:ro
|
||||
- ./librechat/librechat.yaml:/app/librechat.yaml:ro
|
||||
networks:
|
||||
- alfred-net
|
||||
|
||||
# --- DATABASE #1 - APP STATE ---
|
||||
mongodb:
|
||||
container_name: alfred-mongodb
|
||||
image: mongo:latest
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
alfred-init:
|
||||
condition: service_completed_successfully
|
||||
env_file:
|
||||
- path: .env
|
||||
required: true
|
||||
environment:
|
||||
# Remap value name
|
||||
- MONGO_INITDB_ROOT_USERNAME=${MONGO_USER}
|
||||
- MONGO_INITDB_ROOT_PASSWORD=${MONGO_PASSWORD}
|
||||
ports:
|
||||
- "${MONGO_PORT}:${MONGO_PORT}"
|
||||
volumes:
|
||||
- ./data/mongo:/data/db
|
||||
command: mongod --quiet --setParameter logComponentVerbosity='{"network":{"verbosity":0}}'
|
||||
healthcheck:
|
||||
test: |
|
||||
mongosh --quiet --eval "db.adminCommand('ping')" || \
|
||||
mongosh --quiet -u "${MONGO_USER}" -p "${MONGO_PASSWORD}" --authenticationDatabase admin --eval "db.adminCommand('ping')"
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
networks:
|
||||
- alfred-net
|
||||
|
||||
# --- OLLAMA - LOCAL LLM ENGINE ---
|
||||
ollama:
|
||||
image: ollama/ollama:latest
|
||||
container_name: alfred-ollama
|
||||
depends_on:
|
||||
alfred-init:
|
||||
condition: service_completed_successfully
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- path: .env
|
||||
required: true
|
||||
volumes:
|
||||
- ./data/ollama:/root/.ollama
|
||||
networks:
|
||||
- alfred-net
|
||||
|
||||
# - OPTIONAL SERVICES -
|
||||
# --- SEARCH ENGINE SUPER FAST (Optional) ---
|
||||
meilisearch:
|
||||
container_name: alfred-meilisearch
|
||||
image: getmeili/meilisearch:v1.12.3
|
||||
depends_on:
|
||||
alfred-init:
|
||||
condition: service_completed_successfully
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- path: .env
|
||||
required: true
|
||||
volumes:
|
||||
- ./data/meilisearch:/meili_data
|
||||
profiles: ["meili", "full"]
|
||||
networks:
|
||||
- alfred-net
|
||||
|
||||
# --- RETRIEVAL AUGMENTED GENERATION SYSTEM (Optional) ---
|
||||
rag_api:
|
||||
container_name: alfred-rag
|
||||
image: ghcr.io/danny-avila/librechat-rag-api-dev-lite:${RAG_VERSION}
|
||||
depends_on:
|
||||
alfred-init:
|
||||
condition: service_completed_successfully
|
||||
vectordb:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- path: .env
|
||||
required: true
|
||||
ports:
|
||||
- "${RAG_API_PORT}:${RAG_API_PORT}"
|
||||
volumes:
|
||||
- ./data/rag/uploads:/app/uploads
|
||||
profiles: ["rag", "full"]
|
||||
networks:
|
||||
- alfred-net
|
||||
|
||||
# --- DATABASE #2 - Vector RAG (Optional) ---
|
||||
vectordb:
|
||||
container_name: alfred-vectordb
|
||||
image: pgvector/pgvector:0.8.0-pg16-bookworm
|
||||
depends_on:
|
||||
alfred-init:
|
||||
condition: service_completed_successfully
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- path: .env
|
||||
required: true
|
||||
ports:
|
||||
- "${POSTGRES_PORT}:${POSTGRES_PORT}"
|
||||
volumes:
|
||||
- ./data/vectordb:/var/lib/postgresql/data
|
||||
profiles: ["rag", "full"]
|
||||
healthcheck:
|
||||
test: [ "CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-alfred} -d ${POSTGRES_DB_NAME:-alfred}" ]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
networks:
|
||||
- alfred-net
|
||||
|
||||
# --- QBITTORENT (Optional) ---
|
||||
qbittorrent:
|
||||
image: lscr.io/linuxserver/qbittorrent:latest
|
||||
container_name: alfred-qbittorrent
|
||||
depends_on:
|
||||
alfred-init:
|
||||
condition: service_completed_successfully
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- path: .env
|
||||
required: true
|
||||
environment:
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
- TZ=Europe/Paris
|
||||
- WEBUI_PORT=${QBITTORRENT_PORT}
|
||||
volumes:
|
||||
- ./data/qbittorrent/config:/config
|
||||
- ./data/qbittorrent/downloads:/downloads
|
||||
profiles: ["qbittorrent", "full"]
|
||||
ports:
|
||||
- "${QBITTORRENT_PORT}:${QBITTORRENT_PORT}"
|
||||
networks:
|
||||
- alfred-net
|
||||
|
||||
networks:
|
||||
alfred-net:
|
||||
name: alfred-internal
|
||||
driver: bridge
|
||||
@@ -0,0 +1,120 @@
|
||||
# For more information, see the Configuration Guide:
|
||||
# https://www.librechat.ai/docs/configuration/librechat_yaml
|
||||
|
||||
version: 1.2.1
|
||||
cache: true
|
||||
endpoints:
|
||||
anthropic:
|
||||
apiKey: "${ANTHROPIC_API_KEY}"
|
||||
models:
|
||||
default: ["claude-sonnet-4-5", "claude-haiku-4-5", "claude-opus-4-5"]
|
||||
fetch: false
|
||||
titleConvo: true
|
||||
titleModel: "claude-haiku-4-5"
|
||||
modelDisplayLabel: "Claude AI"
|
||||
streamRate: 1
|
||||
|
||||
custom:
|
||||
# Deepseek
|
||||
- name: "Deepseek"
|
||||
apiKey: "${DEEPSEEK_API_KEY}"
|
||||
baseURL: "https://api.deepseek.com/v1"
|
||||
models:
|
||||
default: ["deepseek-chat", "deepseek-coder", "deepseek-reasoner"]
|
||||
fetch: false
|
||||
titleConvo: true
|
||||
titleModel: "deepseek-chat"
|
||||
modelDisplayLabel: "Deepseek"
|
||||
streamRate: 1
|
||||
tools:
|
||||
- toolName: media_finder
|
||||
pluginKey: "media_finder_key"
|
||||
manifest:
|
||||
schema:
|
||||
type: openapi
|
||||
url: "http://alfred:8000/manifests/find_media_imdb_id.json"
|
||||
auth:
|
||||
type: none
|
||||
|
||||
# Outil 2: find_torrent
|
||||
- toolName: torrent_search
|
||||
pluginKey: "torrent_search_key"
|
||||
manifest:
|
||||
schema:
|
||||
type: openapi
|
||||
url: "http://alfred:8000/manifests/find_torrent.json"
|
||||
auth:
|
||||
type: none
|
||||
|
||||
# Outil 3: add_torrent_by_index
|
||||
- toolName: torrent_downloader
|
||||
pluginKey: "torrent_downloader_key"
|
||||
manifest:
|
||||
schema:
|
||||
type: openapi
|
||||
url: "http://alfred:8000/manifests/add_torrent_by_index.json"
|
||||
auth:
|
||||
type: none
|
||||
|
||||
# Outil 4: set_language
|
||||
- toolName: lang_setter
|
||||
pluginKey: "lang_setter_key"
|
||||
manifest:
|
||||
schema:
|
||||
type: openapi
|
||||
url: "http://alfred:8000/manifests/set_language.json"
|
||||
auth:
|
||||
type: none
|
||||
|
||||
|
||||
# Backend Local Agent
|
||||
- name: "Local Agent"
|
||||
apiKey: "dummy_key"
|
||||
baseURL: "http://alfred:8000/v1"
|
||||
models:
|
||||
default: ["local-deepseek-agent"]
|
||||
fetch: false
|
||||
titleConvo: false
|
||||
titleModel: "current_model"
|
||||
forcePrompt: true
|
||||
modelDisplayLabel: "Local Agent"
|
||||
streamRate: 1
|
||||
tools:
|
||||
- toolName: media_finder
|
||||
pluginKey: "media_finder_key"
|
||||
manifest:
|
||||
schema:
|
||||
type: openapi
|
||||
url: "http://alfred:8000/manifests/find_media_imdb_id.json"
|
||||
auth:
|
||||
type: none
|
||||
|
||||
# Outil 2: find_torrent
|
||||
- toolName: torrent_search
|
||||
pluginKey: "torrent_search_key"
|
||||
manifest:
|
||||
schema:
|
||||
type: openapi
|
||||
url: "http://alfred:8000/manifests/find_torrent.json"
|
||||
auth:
|
||||
type: none
|
||||
|
||||
# Outil 3: add_torrent_by_index
|
||||
- toolName: torrent_downloader
|
||||
pluginKey: "torrent_downloader_key"
|
||||
manifest:
|
||||
schema:
|
||||
type: openapi
|
||||
url: "http://alfred:8000/manifests/add_torrent_by_index.json"
|
||||
auth:
|
||||
type: none
|
||||
|
||||
# Outil 4: set_language
|
||||
- toolName: lang_setter
|
||||
pluginKey: "lang_setter_key"
|
||||
manifest:
|
||||
schema:
|
||||
type: openapi
|
||||
url: "http://alfred:8000/manifests/set_language.json"
|
||||
auth:
|
||||
type: none
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "add_torrent_by_index",
|
||||
"description": "Ajoute un torrent à la file d'attente de qBittorrent en utilisant l'index (1-basé) d'un résultat de recherche précédent (par exemple, 'download the 3rd one').",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"index": {
|
||||
"type": "integer",
|
||||
"description": "L'index (1-basé) du torrent dans les derniers résultats de recherche."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"index"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "find_media_imdb_id",
|
||||
"description": "Trouve l'ID IMDb et les informations d'un film ou d'une série télévisée à partir de son titre en utilisant l'API TMDB. À utiliser comme première étape avant de chercher des torrents.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"media_title": {
|
||||
"type": "string",
|
||||
"description": "Le titre exact du média à rechercher (par exemple, 'Inception', 'Breaking Bad')."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"media_title"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "find_torrent",
|
||||
"description": "Recherche des fichiers torrent pour un titre de média donné. Les résultats sont stockés dans la mémoire de l'agent pour une référence ultérieure par index (e.g., 'download the 3rd one').",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"media_title": {
|
||||
"type": "string",
|
||||
"description": "Le titre du média pour lequel rechercher des torrents (par exemple, 'Inception 2010')."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"media_title"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "set_language",
|
||||
"description": "Définit la langue de la conversation pour l'agent.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"language": {
|
||||
"type": "string",
|
||||
"description": "Le code de la langue (par exemple, 'en' pour Anglais, 'fr' pour Français, 'es' pour Espagnol)."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"language"
|
||||
]
|
||||
}
|
||||
}
|
||||
Generated
+487
-216
@@ -35,59 +35,42 @@ files = [
|
||||
|
||||
[package.dependencies]
|
||||
idna = ">=2.8"
|
||||
typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""}
|
||||
|
||||
[package.extras]
|
||||
trio = ["trio (>=0.31.0)", "trio (>=0.32.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "black"
|
||||
version = "25.11.0"
|
||||
description = "The uncompromising code formatter."
|
||||
name = "bracex"
|
||||
version = "2.6"
|
||||
description = "Bash style brace expander."
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
files = [
|
||||
{file = "black-25.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ec311e22458eec32a807f029b2646f661e6859c3f61bc6d9ffb67958779f392e"},
|
||||
{file = "black-25.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1032639c90208c15711334d681de2e24821af0575573db2810b0763bcd62e0f0"},
|
||||
{file = "black-25.11.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0c0f7c461df55cf32929b002335883946a4893d759f2df343389c4396f3b6b37"},
|
||||
{file = "black-25.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:f9786c24d8e9bd5f20dc7a7f0cdd742644656987f6ea6947629306f937726c03"},
|
||||
{file = "black-25.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:895571922a35434a9d8ca67ef926da6bc9ad464522a5fe0db99b394ef1c0675a"},
|
||||
{file = "black-25.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cb4f4b65d717062191bdec8e4a442539a8ea065e6af1c4f4d36f0cdb5f71e170"},
|
||||
{file = "black-25.11.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d81a44cbc7e4f73a9d6ae449ec2317ad81512d1e7dce7d57f6333fd6259737bc"},
|
||||
{file = "black-25.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:7eebd4744dfe92ef1ee349dc532defbf012a88b087bb7ddd688ff59a447b080e"},
|
||||
{file = "black-25.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:80e7486ad3535636657aa180ad32a7d67d7c273a80e12f1b4bfa0823d54e8fac"},
|
||||
{file = "black-25.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6cced12b747c4c76bc09b4db057c319d8545307266f41aaee665540bc0e04e96"},
|
||||
{file = "black-25.11.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6cb2d54a39e0ef021d6c5eef442e10fd71fcb491be6413d083a320ee768329dd"},
|
||||
{file = "black-25.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:ae263af2f496940438e5be1a0c1020e13b09154f3af4df0835ea7f9fe7bfa409"},
|
||||
{file = "black-25.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0a1d40348b6621cc20d3d7530a5b8d67e9714906dfd7346338249ad9c6cedf2b"},
|
||||
{file = "black-25.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:51c65d7d60bb25429ea2bf0731c32b2a2442eb4bd3b2afcb47830f0b13e58bfd"},
|
||||
{file = "black-25.11.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:936c4dd07669269f40b497440159a221ee435e3fddcf668e0c05244a9be71993"},
|
||||
{file = "black-25.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:f42c0ea7f59994490f4dccd64e6b2dd49ac57c7c84f38b8faab50f8759db245c"},
|
||||
{file = "black-25.11.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:35690a383f22dd3e468c85dc4b915217f87667ad9cce781d7b42678ce63c4170"},
|
||||
{file = "black-25.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:dae49ef7369c6caa1a1833fd5efb7c3024bb7e4499bf64833f65ad27791b1545"},
|
||||
{file = "black-25.11.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bd4a22a0b37401c8e492e994bce79e614f91b14d9ea911f44f36e262195fdda"},
|
||||
{file = "black-25.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:aa211411e94fdf86519996b7f5f05e71ba34835d8f0c0f03c00a26271da02664"},
|
||||
{file = "black-25.11.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a3bb5ce32daa9ff0605d73b6f19da0b0e6c1f8f2d75594db539fdfed722f2b06"},
|
||||
{file = "black-25.11.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9815ccee1e55717fe9a4b924cae1646ef7f54e0f990da39a34fc7b264fcf80a2"},
|
||||
{file = "black-25.11.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:92285c37b93a1698dcbc34581867b480f1ba3a7b92acf1fe0467b04d7a4da0dc"},
|
||||
{file = "black-25.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:43945853a31099c7c0ff8dface53b4de56c41294fa6783c0441a8b1d9bf668bc"},
|
||||
{file = "black-25.11.0-py3-none-any.whl", hash = "sha256:e3f562da087791e96cefcd9dda058380a442ab322a02e222add53736451f604b"},
|
||||
{file = "black-25.11.0.tar.gz", hash = "sha256:9a323ac32f5dc75ce7470501b887250be5005a01602e931a15e45593f70f6e08"},
|
||||
{file = "bracex-2.6-py3-none-any.whl", hash = "sha256:0b0049264e7340b3ec782b5cb99beb325f36c3782a32e36e876452fd49a09952"},
|
||||
{file = "bracex-2.6.tar.gz", hash = "sha256:98f1347cd77e22ee8d967a30ad4e310b233f7754dbf31ff3fceb76145ba47dc7"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bump-my-version"
|
||||
version = "1.2.6"
|
||||
description = "Version bump your Python project"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "bump_my_version-1.2.6-py3-none-any.whl", hash = "sha256:a2f567c10574a374b81a9bd6d2bd3cb2ca74befe5c24c3021123773635431659"},
|
||||
{file = "bump_my_version-1.2.6.tar.gz", hash = "sha256:1f2f0daa5d699904e9739be8efb51c4c945461bad83cd4da4c89d324d9a18343"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
click = ">=8.0.0"
|
||||
mypy-extensions = ">=0.4.3"
|
||||
packaging = ">=22.0"
|
||||
pathspec = ">=0.9.0"
|
||||
platformdirs = ">=2"
|
||||
pytokens = ">=0.3.0"
|
||||
|
||||
[package.extras]
|
||||
colorama = ["colorama (>=0.4.3)"]
|
||||
d = ["aiohttp (>=3.10)"]
|
||||
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
|
||||
uvloop = ["uvloop (>=0.15.2)"]
|
||||
click = "<8.4"
|
||||
httpx = ">=0.28.1"
|
||||
pydantic = ">=2.0.0"
|
||||
pydantic-settings = "*"
|
||||
questionary = "*"
|
||||
rich = "*"
|
||||
rich-click = "*"
|
||||
tomlkit = "*"
|
||||
wcmatch = ">=8.5.1"
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
@@ -100,6 +83,17 @@ files = [
|
||||
{file = "certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfgv"
|
||||
version = "3.5.0"
|
||||
description = "Validate configuration and produce human readable error messages."
|
||||
optional = false
|
||||
python-versions = ">=3.10"
|
||||
files = [
|
||||
{file = "cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0"},
|
||||
{file = "cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "charset-normalizer"
|
||||
version = "3.4.4"
|
||||
@@ -249,108 +243,119 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "coverage"
|
||||
version = "7.12.0"
|
||||
version = "7.13.1"
|
||||
description = "Code coverage measurement for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.10"
|
||||
files = [
|
||||
{file = "coverage-7.12.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:32b75c2ba3f324ee37af3ccee5b30458038c50b349ad9b88cee85096132a575b"},
|
||||
{file = "coverage-7.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cb2a1b6ab9fe833714a483a915de350abc624a37149649297624c8d57add089c"},
|
||||
{file = "coverage-7.12.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5734b5d913c3755e72f70bf6cc37a0518d4f4745cde760c5d8e12005e62f9832"},
|
||||
{file = "coverage-7.12.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b527a08cdf15753279b7afb2339a12073620b761d79b81cbe2cdebdb43d90daa"},
|
||||
{file = "coverage-7.12.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9bb44c889fb68004e94cab71f6a021ec83eac9aeabdbb5a5a88821ec46e1da73"},
|
||||
{file = "coverage-7.12.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4b59b501455535e2e5dde5881739897967b272ba25988c89145c12d772810ccb"},
|
||||
{file = "coverage-7.12.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d8842f17095b9868a05837b7b1b73495293091bed870e099521ada176aa3e00e"},
|
||||
{file = "coverage-7.12.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c5a6f20bf48b8866095c6820641e7ffbe23f2ac84a2efc218d91235e404c7777"},
|
||||
{file = "coverage-7.12.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:5f3738279524e988d9da2893f307c2093815c623f8d05a8f79e3eff3a7a9e553"},
|
||||
{file = "coverage-7.12.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e0d68c1f7eabbc8abe582d11fa393ea483caf4f44b0af86881174769f185c94d"},
|
||||
{file = "coverage-7.12.0-cp310-cp310-win32.whl", hash = "sha256:7670d860e18b1e3ee5930b17a7d55ae6287ec6e55d9799982aa103a2cc1fa2ef"},
|
||||
{file = "coverage-7.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:f999813dddeb2a56aab5841e687b68169da0d3f6fc78ccf50952fa2463746022"},
|
||||
{file = "coverage-7.12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aa124a3683d2af98bd9d9c2bfa7a5076ca7e5ab09fdb96b81fa7d89376ae928f"},
|
||||
{file = "coverage-7.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d93fbf446c31c0140208dcd07c5d882029832e8ed7891a39d6d44bd65f2316c3"},
|
||||
{file = "coverage-7.12.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:52ca620260bd8cd6027317bdd8b8ba929be1d741764ee765b42c4d79a408601e"},
|
||||
{file = "coverage-7.12.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f3433ffd541380f3a0e423cff0f4926d55b0cc8c1d160fdc3be24a4c03aa65f7"},
|
||||
{file = "coverage-7.12.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7bbb321d4adc9f65e402c677cd1c8e4c2d0105d3ce285b51b4d87f1d5db5245"},
|
||||
{file = "coverage-7.12.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:22a7aade354a72dff3b59c577bfd18d6945c61f97393bc5fb7bd293a4237024b"},
|
||||
{file = "coverage-7.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3ff651dcd36d2fea66877cd4a82de478004c59b849945446acb5baf9379a1b64"},
|
||||
{file = "coverage-7.12.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:31b8b2e38391a56e3cea39d22a23faaa7c3fc911751756ef6d2621d2a9daf742"},
|
||||
{file = "coverage-7.12.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:297bc2da28440f5ae51c845a47c8175a4db0553a53827886e4fb25c66633000c"},
|
||||
{file = "coverage-7.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6ff7651cc01a246908eac162a6a86fc0dbab6de1ad165dfb9a1e2ec660b44984"},
|
||||
{file = "coverage-7.12.0-cp311-cp311-win32.whl", hash = "sha256:313672140638b6ddb2c6455ddeda41c6a0b208298034544cfca138978c6baed6"},
|
||||
{file = "coverage-7.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:a1783ed5bd0d5938d4435014626568dc7f93e3cb99bc59188cc18857c47aa3c4"},
|
||||
{file = "coverage-7.12.0-cp311-cp311-win_arm64.whl", hash = "sha256:4648158fd8dd9381b5847622df1c90ff314efbfc1df4550092ab6013c238a5fc"},
|
||||
{file = "coverage-7.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:29644c928772c78512b48e14156b81255000dcfd4817574ff69def189bcb3647"},
|
||||
{file = "coverage-7.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8638cbb002eaa5d7c8d04da667813ce1067080b9a91099801a0053086e52b736"},
|
||||
{file = "coverage-7.12.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:083631eeff5eb9992c923e14b810a179798bb598e6a0dd60586819fc23be6e60"},
|
||||
{file = "coverage-7.12.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:99d5415c73ca12d558e07776bd957c4222c687b9f1d26fa0e1b57e3598bdcde8"},
|
||||
{file = "coverage-7.12.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e949ebf60c717c3df63adb4a1a366c096c8d7fd8472608cd09359e1bd48ef59f"},
|
||||
{file = "coverage-7.12.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6d907ddccbca819afa2cd014bc69983b146cca2735a0b1e6259b2a6c10be1e70"},
|
||||
{file = "coverage-7.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b1518ecbad4e6173f4c6e6c4a46e49555ea5679bf3feda5edb1b935c7c44e8a0"},
|
||||
{file = "coverage-7.12.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:51777647a749abdf6f6fd8c7cffab12de68ab93aab15efc72fbbb83036c2a068"},
|
||||
{file = "coverage-7.12.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:42435d46d6461a3b305cdfcad7cdd3248787771f53fe18305548cba474e6523b"},
|
||||
{file = "coverage-7.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5bcead88c8423e1855e64b8057d0544e33e4080b95b240c2a355334bb7ced937"},
|
||||
{file = "coverage-7.12.0-cp312-cp312-win32.whl", hash = "sha256:dcbb630ab034e86d2a0f79aefd2be07e583202f41e037602d438c80044957baa"},
|
||||
{file = "coverage-7.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:2fd8354ed5d69775ac42986a691fbf68b4084278710cee9d7c3eaa0c28fa982a"},
|
||||
{file = "coverage-7.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:737c3814903be30695b2de20d22bcc5428fdae305c61ba44cdc8b3252984c49c"},
|
||||
{file = "coverage-7.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:47324fffca8d8eae7e185b5bb20c14645f23350f870c1649003618ea91a78941"},
|
||||
{file = "coverage-7.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ccf3b2ede91decd2fb53ec73c1f949c3e034129d1e0b07798ff1d02ea0c8fa4a"},
|
||||
{file = "coverage-7.12.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b365adc70a6936c6b0582dc38746b33b2454148c02349345412c6e743efb646d"},
|
||||
{file = "coverage-7.12.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bc13baf85cd8a4cfcf4a35c7bc9d795837ad809775f782f697bf630b7e200211"},
|
||||
{file = "coverage-7.12.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:099d11698385d572ceafb3288a5b80fe1fc58bf665b3f9d362389de488361d3d"},
|
||||
{file = "coverage-7.12.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:473dc45d69694069adb7680c405fb1e81f60b2aff42c81e2f2c3feaf544d878c"},
|
||||
{file = "coverage-7.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:583f9adbefd278e9de33c33d6846aa8f5d164fa49b47144180a0e037f0688bb9"},
|
||||
{file = "coverage-7.12.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b2089cc445f2dc0af6f801f0d1355c025b76c24481935303cf1af28f636688f0"},
|
||||
{file = "coverage-7.12.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:950411f1eb5d579999c5f66c62a40961f126fc71e5e14419f004471957b51508"},
|
||||
{file = "coverage-7.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b1aab7302a87bafebfe76b12af681b56ff446dc6f32ed178ff9c092ca776e6bc"},
|
||||
{file = "coverage-7.12.0-cp313-cp313-win32.whl", hash = "sha256:d7e0d0303c13b54db495eb636bc2465b2fb8475d4c8bcec8fe4b5ca454dfbae8"},
|
||||
{file = "coverage-7.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:ce61969812d6a98a981d147d9ac583a36ac7db7766f2e64a9d4d059c2fe29d07"},
|
||||
{file = "coverage-7.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:bcec6f47e4cb8a4c2dc91ce507f6eefc6a1b10f58df32cdc61dff65455031dfc"},
|
||||
{file = "coverage-7.12.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:459443346509476170d553035e4a3eed7b860f4fe5242f02de1010501956ce87"},
|
||||
{file = "coverage-7.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:04a79245ab2b7a61688958f7a855275997134bc84f4a03bc240cf64ff132abf6"},
|
||||
{file = "coverage-7.12.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:09a86acaaa8455f13d6a99221d9654df249b33937b4e212b4e5a822065f12aa7"},
|
||||
{file = "coverage-7.12.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:907e0df1b71ba77463687a74149c6122c3f6aac56c2510a5d906b2f368208560"},
|
||||
{file = "coverage-7.12.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b57e2d0ddd5f0582bae5437c04ee71c46cd908e7bc5d4d0391f9a41e812dd12"},
|
||||
{file = "coverage-7.12.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:58c1c6aa677f3a1411fe6fb28ec3a942e4f665df036a3608816e0847fad23296"},
|
||||
{file = "coverage-7.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4c589361263ab2953e3c4cd2a94db94c4ad4a8e572776ecfbad2389c626e4507"},
|
||||
{file = "coverage-7.12.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:91b810a163ccad2e43b1faa11d70d3cf4b6f3d83f9fd5f2df82a32d47b648e0d"},
|
||||
{file = "coverage-7.12.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:40c867af715f22592e0d0fb533a33a71ec9e0f73a6945f722a0c85c8c1cbe3a2"},
|
||||
{file = "coverage-7.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:68b0d0a2d84f333de875666259dadf28cc67858bc8fd8b3f1eae84d3c2bec455"},
|
||||
{file = "coverage-7.12.0-cp313-cp313t-win32.whl", hash = "sha256:73f9e7fbd51a221818fd11b7090eaa835a353ddd59c236c57b2199486b116c6d"},
|
||||
{file = "coverage-7.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:24cff9d1f5743f67db7ba46ff284018a6e9aeb649b67aa1e70c396aa1b7cb23c"},
|
||||
{file = "coverage-7.12.0-cp313-cp313t-win_arm64.whl", hash = "sha256:c87395744f5c77c866d0f5a43d97cc39e17c7f1cb0115e54a2fe67ca75c5d14d"},
|
||||
{file = "coverage-7.12.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a1c59b7dc169809a88b21a936eccf71c3895a78f5592051b1af8f4d59c2b4f92"},
|
||||
{file = "coverage-7.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8787b0f982e020adb732b9f051f3e49dd5054cebbc3f3432061278512a2b1360"},
|
||||
{file = "coverage-7.12.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5ea5a9f7dc8877455b13dd1effd3202e0bca72f6f3ab09f9036b1bcf728f69ac"},
|
||||
{file = "coverage-7.12.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fdba9f15849534594f60b47c9a30bc70409b54947319a7c4fd0e8e3d8d2f355d"},
|
||||
{file = "coverage-7.12.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a00594770eb715854fb1c57e0dea08cce6720cfbc531accdb9850d7c7770396c"},
|
||||
{file = "coverage-7.12.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5560c7e0d82b42eb1951e4f68f071f8017c824ebfd5a6ebe42c60ac16c6c2434"},
|
||||
{file = "coverage-7.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d6c2e26b481c9159c2773a37947a9718cfdc58893029cdfb177531793e375cfc"},
|
||||
{file = "coverage-7.12.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:6e1a8c066dabcde56d5d9fed6a66bc19a2883a3fe051f0c397a41fc42aedd4cc"},
|
||||
{file = "coverage-7.12.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f7ba9da4726e446d8dd8aae5a6cd872511184a5d861de80a86ef970b5dacce3e"},
|
||||
{file = "coverage-7.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e0f483ab4f749039894abaf80c2f9e7ed77bbf3c737517fb88c8e8e305896a17"},
|
||||
{file = "coverage-7.12.0-cp314-cp314-win32.whl", hash = "sha256:76336c19a9ef4a94b2f8dc79f8ac2da3f193f625bb5d6f51a328cd19bfc19933"},
|
||||
{file = "coverage-7.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:7c1059b600aec6ef090721f8f633f60ed70afaffe8ecab85b59df748f24b31fe"},
|
||||
{file = "coverage-7.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:172cf3a34bfef42611963e2b661302a8931f44df31629e5b1050567d6b90287d"},
|
||||
{file = "coverage-7.12.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:aa7d48520a32cb21c7a9b31f81799e8eaec7239db36c3b670be0fa2403828d1d"},
|
||||
{file = "coverage-7.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:90d58ac63bc85e0fb919f14d09d6caa63f35a5512a2205284b7816cafd21bb03"},
|
||||
{file = "coverage-7.12.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ca8ecfa283764fdda3eae1bdb6afe58bf78c2c3ec2b2edcb05a671f0bba7b3f9"},
|
||||
{file = "coverage-7.12.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:874fe69a0785d96bd066059cd4368022cebbec1a8958f224f0016979183916e6"},
|
||||
{file = "coverage-7.12.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5b3c889c0b8b283a24d721a9eabc8ccafcfc3aebf167e4cd0d0e23bf8ec4e339"},
|
||||
{file = "coverage-7.12.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8bb5b894b3ec09dcd6d3743229dc7f2c42ef7787dc40596ae04c0edda487371e"},
|
||||
{file = "coverage-7.12.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:79a44421cd5fba96aa57b5e3b5a4d3274c449d4c622e8f76882d76635501fd13"},
|
||||
{file = "coverage-7.12.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:33baadc0efd5c7294f436a632566ccc1f72c867f82833eb59820ee37dc811c6f"},
|
||||
{file = "coverage-7.12.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c406a71f544800ef7e9e0000af706b88465f3573ae8b8de37e5f96c59f689ad1"},
|
||||
{file = "coverage-7.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e71bba6a40883b00c6d571599b4627f50c360b3d0d02bfc658168936be74027b"},
|
||||
{file = "coverage-7.12.0-cp314-cp314t-win32.whl", hash = "sha256:9157a5e233c40ce6613dead4c131a006adfda70e557b6856b97aceed01b0e27a"},
|
||||
{file = "coverage-7.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:e84da3a0fd233aeec797b981c51af1cabac74f9bd67be42458365b30d11b5291"},
|
||||
{file = "coverage-7.12.0-cp314-cp314t-win_arm64.whl", hash = "sha256:01d24af36fedda51c2b1aca56e4330a3710f83b02a5ff3743a6b015ffa7c9384"},
|
||||
{file = "coverage-7.12.0-py3-none-any.whl", hash = "sha256:159d50c0b12e060b15ed3d39f87ed43d4f7f7ad40b8a534f4dd331adbb51104a"},
|
||||
{file = "coverage-7.12.0.tar.gz", hash = "sha256:fc11e0a4e372cb5f282f16ef90d4a585034050ccda536451901abfb19a57f40c"},
|
||||
{file = "coverage-7.13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e1fa280b3ad78eea5be86f94f461c04943d942697e0dac889fa18fff8f5f9147"},
|
||||
{file = "coverage-7.13.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c3d8c679607220979434f494b139dfb00131ebf70bb406553d69c1ff01a5c33d"},
|
||||
{file = "coverage-7.13.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:339dc63b3eba969067b00f41f15ad161bf2946613156fb131266d8debc8e44d0"},
|
||||
{file = "coverage-7.13.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:db622b999ffe49cb891f2fff3b340cdc2f9797d01a0a202a0973ba2562501d90"},
|
||||
{file = "coverage-7.13.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1443ba9acbb593fa7c1c29e011d7c9761545fe35e7652e85ce7f51a16f7e08d"},
|
||||
{file = "coverage-7.13.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c832ec92c4499ac463186af72f9ed4d8daec15499b16f0a879b0d1c8e5cf4a3b"},
|
||||
{file = "coverage-7.13.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:562ec27dfa3f311e0db1ba243ec6e5f6ab96b1edfcfc6cf86f28038bc4961ce6"},
|
||||
{file = "coverage-7.13.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4de84e71173d4dada2897e5a0e1b7877e5eefbfe0d6a44edee6ce31d9b8ec09e"},
|
||||
{file = "coverage-7.13.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:a5a68357f686f8c4d527a2dc04f52e669c2fc1cbde38f6f7eb6a0e58cbd17cae"},
|
||||
{file = "coverage-7.13.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:77cc258aeb29a3417062758975521eae60af6f79e930d6993555eeac6a8eac29"},
|
||||
{file = "coverage-7.13.1-cp310-cp310-win32.whl", hash = "sha256:bb4f8c3c9a9f34423dba193f241f617b08ffc63e27f67159f60ae6baf2dcfe0f"},
|
||||
{file = "coverage-7.13.1-cp310-cp310-win_amd64.whl", hash = "sha256:c8e2706ceb622bc63bac98ebb10ef5da80ed70fbd8a7999a5076de3afaef0fb1"},
|
||||
{file = "coverage-7.13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a55d509a1dc5a5b708b5dad3b5334e07a16ad4c2185e27b40e4dba796ab7f88"},
|
||||
{file = "coverage-7.13.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4d010d080c4888371033baab27e47c9df7d6fb28d0b7b7adf85a4a49be9298b3"},
|
||||
{file = "coverage-7.13.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d938b4a840fb1523b9dfbbb454f652967f18e197569c32266d4d13f37244c3d9"},
|
||||
{file = "coverage-7.13.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bf100a3288f9bb7f919b87eb84f87101e197535b9bd0e2c2b5b3179633324fee"},
|
||||
{file = "coverage-7.13.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef6688db9bf91ba111ae734ba6ef1a063304a881749726e0d3575f5c10a9facf"},
|
||||
{file = "coverage-7.13.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0b609fc9cdbd1f02e51f67f51e5aee60a841ef58a68d00d5ee2c0faf357481a3"},
|
||||
{file = "coverage-7.13.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c43257717611ff5e9a1d79dce8e47566235ebda63328718d9b65dd640bc832ef"},
|
||||
{file = "coverage-7.13.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e09fbecc007f7b6afdfb3b07ce5bd9f8494b6856dd4f577d26c66c391b829851"},
|
||||
{file = "coverage-7.13.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:a03a4f3a19a189919c7055098790285cc5c5b0b3976f8d227aea39dbf9f8bfdb"},
|
||||
{file = "coverage-7.13.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3820778ea1387c2b6a818caec01c63adc5b3750211af6447e8dcfb9b6f08dbba"},
|
||||
{file = "coverage-7.13.1-cp311-cp311-win32.whl", hash = "sha256:ff10896fa55167371960c5908150b434b71c876dfab97b69478f22c8b445ea19"},
|
||||
{file = "coverage-7.13.1-cp311-cp311-win_amd64.whl", hash = "sha256:a998cc0aeeea4c6d5622a3754da5a493055d2d95186bad877b0a34ea6e6dbe0a"},
|
||||
{file = "coverage-7.13.1-cp311-cp311-win_arm64.whl", hash = "sha256:fea07c1a39a22614acb762e3fbbb4011f65eedafcb2948feeef641ac78b4ee5c"},
|
||||
{file = "coverage-7.13.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6f34591000f06e62085b1865c9bc5f7858df748834662a51edadfd2c3bfe0dd3"},
|
||||
{file = "coverage-7.13.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b67e47c5595b9224599016e333f5ec25392597a89d5744658f837d204e16c63e"},
|
||||
{file = "coverage-7.13.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3e7b8bd70c48ffb28461ebe092c2345536fb18bbbf19d287c8913699735f505c"},
|
||||
{file = "coverage-7.13.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c223d078112e90dc0e5c4e35b98b9584164bea9fbbd221c0b21c5241f6d51b62"},
|
||||
{file = "coverage-7.13.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:794f7c05af0763b1bbd1b9e6eff0e52ad068be3b12cd96c87de037b01390c968"},
|
||||
{file = "coverage-7.13.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0642eae483cc8c2902e4af7298bf886d605e80f26382124cddc3967c2a3df09e"},
|
||||
{file = "coverage-7.13.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9f5e772ed5fef25b3de9f2008fe67b92d46831bd2bc5bdc5dd6bfd06b83b316f"},
|
||||
{file = "coverage-7.13.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:45980ea19277dc0a579e432aef6a504fe098ef3a9032ead15e446eb0f1191aee"},
|
||||
{file = "coverage-7.13.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e4f18eca6028ffa62adbd185a8f1e1dd242f2e68164dba5c2b74a5204850b4cf"},
|
||||
{file = "coverage-7.13.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f8dca5590fec7a89ed6826fce625595279e586ead52e9e958d3237821fbc750c"},
|
||||
{file = "coverage-7.13.1-cp312-cp312-win32.whl", hash = "sha256:ff86d4e85188bba72cfb876df3e11fa243439882c55957184af44a35bd5880b7"},
|
||||
{file = "coverage-7.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:16cc1da46c04fb0fb128b4dc430b78fa2aba8a6c0c9f8eb391fd5103409a6ac6"},
|
||||
{file = "coverage-7.13.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d9bc218650022a768f3775dd7fdac1886437325d8d295d923ebcfef4892ad5c"},
|
||||
{file = "coverage-7.13.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cb237bfd0ef4d5eb6a19e29f9e528ac67ac3be932ea6b44fb6cc09b9f3ecff78"},
|
||||
{file = "coverage-7.13.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1dcb645d7e34dcbcc96cd7c132b1fc55c39263ca62eb961c064eb3928997363b"},
|
||||
{file = "coverage-7.13.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3d42df8201e00384736f0df9be2ced39324c3907607d17d50d50116c989d84cd"},
|
||||
{file = "coverage-7.13.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fa3edde1aa8807de1d05934982416cb3ec46d1d4d91e280bcce7cca01c507992"},
|
||||
{file = "coverage-7.13.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9edd0e01a343766add6817bc448408858ba6b489039eaaa2018474e4001651a4"},
|
||||
{file = "coverage-7.13.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:985b7836931d033570b94c94713c6dba5f9d3ff26045f72c3e5dbc5fe3361e5a"},
|
||||
{file = "coverage-7.13.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ffed1e4980889765c84a5d1a566159e363b71d6b6fbaf0bebc9d3c30bc016766"},
|
||||
{file = "coverage-7.13.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8842af7f175078456b8b17f1b73a0d16a65dcbdc653ecefeb00a56b3c8c298c4"},
|
||||
{file = "coverage-7.13.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:ccd7a6fca48ca9c131d9b0a2972a581e28b13416fc313fb98b6d24a03ce9a398"},
|
||||
{file = "coverage-7.13.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0403f647055de2609be776965108447deb8e384fe4a553c119e3ff6bfbab4784"},
|
||||
{file = "coverage-7.13.1-cp313-cp313-win32.whl", hash = "sha256:549d195116a1ba1e1ae2f5ca143f9777800f6636eab917d4f02b5310d6d73461"},
|
||||
{file = "coverage-7.13.1-cp313-cp313-win_amd64.whl", hash = "sha256:5899d28b5276f536fcf840b18b61a9fce23cc3aec1d114c44c07fe94ebeaa500"},
|
||||
{file = "coverage-7.13.1-cp313-cp313-win_arm64.whl", hash = "sha256:868a2fae76dfb06e87291bcbd4dcbcc778a8500510b618d50496e520bd94d9b9"},
|
||||
{file = "coverage-7.13.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:67170979de0dacac3f3097d02b0ad188d8edcea44ccc44aaa0550af49150c7dc"},
|
||||
{file = "coverage-7.13.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f80e2bb21bfab56ed7405c2d79d34b5dc0bc96c2c1d2a067b643a09fb756c43a"},
|
||||
{file = "coverage-7.13.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f83351e0f7dcdb14d7326c3d8d8c4e915fa685cbfdc6281f9470d97a04e9dfe4"},
|
||||
{file = "coverage-7.13.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb3f6562e89bad0110afbe64e485aac2462efdce6232cdec7862a095dc3412f6"},
|
||||
{file = "coverage-7.13.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77545b5dcda13b70f872c3b5974ac64c21d05e65b1590b441c8560115dc3a0d1"},
|
||||
{file = "coverage-7.13.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4d240d260a1aed814790bbe1f10a5ff31ce6c21bc78f0da4a1e8268d6c80dbd"},
|
||||
{file = "coverage-7.13.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d2287ac9360dec3837bfdad969963a5d073a09a85d898bd86bea82aa8876ef3c"},
|
||||
{file = "coverage-7.13.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:0d2c11f3ea4db66b5cbded23b20185c35066892c67d80ec4be4bab257b9ad1e0"},
|
||||
{file = "coverage-7.13.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:3fc6a169517ca0d7ca6846c3c5392ef2b9e38896f61d615cb75b9e7134d4ee1e"},
|
||||
{file = "coverage-7.13.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d10a2ed46386e850bb3de503a54f9fe8192e5917fcbb143bfef653a9355e9a53"},
|
||||
{file = "coverage-7.13.1-cp313-cp313t-win32.whl", hash = "sha256:75a6f4aa904301dab8022397a22c0039edc1f51e90b83dbd4464b8a38dc87842"},
|
||||
{file = "coverage-7.13.1-cp313-cp313t-win_amd64.whl", hash = "sha256:309ef5706e95e62578cda256b97f5e097916a2c26247c287bbe74794e7150df2"},
|
||||
{file = "coverage-7.13.1-cp313-cp313t-win_arm64.whl", hash = "sha256:92f980729e79b5d16d221038dbf2e8f9a9136afa072f9d5d6ed4cb984b126a09"},
|
||||
{file = "coverage-7.13.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:97ab3647280d458a1f9adb85244e81587505a43c0c7cff851f5116cd2814b894"},
|
||||
{file = "coverage-7.13.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8f572d989142e0908e6acf57ad1b9b86989ff057c006d13b76c146ec6a20216a"},
|
||||
{file = "coverage-7.13.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d72140ccf8a147e94274024ff6fd8fb7811354cf7ef88b1f0a988ebaa5bc774f"},
|
||||
{file = "coverage-7.13.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d3c9f051b028810f5a87c88e5d6e9af3c0ff32ef62763bf15d29f740453ca909"},
|
||||
{file = "coverage-7.13.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f398ba4df52d30b1763f62eed9de5620dcde96e6f491f4c62686736b155aa6e4"},
|
||||
{file = "coverage-7.13.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:132718176cc723026d201e347f800cd1a9e4b62ccd3f82476950834dad501c75"},
|
||||
{file = "coverage-7.13.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9e549d642426e3579b3f4b92d0431543b012dcb6e825c91619d4e93b7363c3f9"},
|
||||
{file = "coverage-7.13.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:90480b2134999301eea795b3a9dbf606c6fbab1b489150c501da84a959442465"},
|
||||
{file = "coverage-7.13.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e825dbb7f84dfa24663dd75835e7257f8882629fc11f03ecf77d84a75134b864"},
|
||||
{file = "coverage-7.13.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:623dcc6d7a7ba450bbdbeedbaa0c42b329bdae16491af2282f12a7e809be7eb9"},
|
||||
{file = "coverage-7.13.1-cp314-cp314-win32.whl", hash = "sha256:6e73ebb44dca5f708dc871fe0b90cf4cff1a13f9956f747cc87b535a840386f5"},
|
||||
{file = "coverage-7.13.1-cp314-cp314-win_amd64.whl", hash = "sha256:be753b225d159feb397bd0bf91ae86f689bad0da09d3b301478cd39b878ab31a"},
|
||||
{file = "coverage-7.13.1-cp314-cp314-win_arm64.whl", hash = "sha256:228b90f613b25ba0019361e4ab81520b343b622fc657daf7e501c4ed6a2366c0"},
|
||||
{file = "coverage-7.13.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:60cfb538fe9ef86e5b2ab0ca8fc8d62524777f6c611dcaf76dc16fbe9b8e698a"},
|
||||
{file = "coverage-7.13.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:57dfc8048c72ba48a8c45e188d811e5efd7e49b387effc8fb17e97936dde5bf6"},
|
||||
{file = "coverage-7.13.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3f2f725aa3e909b3c5fdb8192490bdd8e1495e85906af74fe6e34a2a77ba0673"},
|
||||
{file = "coverage-7.13.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ee68b21909686eeb21dfcba2c3b81fee70dcf38b140dcd5aa70680995fa3aa5"},
|
||||
{file = "coverage-7.13.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:724b1b270cb13ea2e6503476e34541a0b1f62280bc997eab443f87790202033d"},
|
||||
{file = "coverage-7.13.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:916abf1ac5cf7eb16bc540a5bf75c71c43a676f5c52fcb9fe75a2bd75fb944e8"},
|
||||
{file = "coverage-7.13.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:776483fd35b58d8afe3acbd9988d5de592ab6da2d2a865edfdbc9fdb43e7c486"},
|
||||
{file = "coverage-7.13.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b6f3b96617e9852703f5b633ea01315ca45c77e879584f283c44127f0f1ec564"},
|
||||
{file = "coverage-7.13.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:bd63e7b74661fed317212fab774e2a648bc4bb09b35f25474f8e3325d2945cd7"},
|
||||
{file = "coverage-7.13.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:933082f161bbb3e9f90d00990dc956120f608cdbcaeea15c4d897f56ef4fe416"},
|
||||
{file = "coverage-7.13.1-cp314-cp314t-win32.whl", hash = "sha256:18be793c4c87de2965e1c0f060f03d9e5aff66cfeae8e1dbe6e5b88056ec153f"},
|
||||
{file = "coverage-7.13.1-cp314-cp314t-win_amd64.whl", hash = "sha256:0e42e0ec0cd3e0d851cb3c91f770c9301f48647cb2877cb78f74bdaa07639a79"},
|
||||
{file = "coverage-7.13.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eaecf47ef10c72ece9a2a92118257da87e460e113b83cc0d2905cbbe931792b4"},
|
||||
{file = "coverage-7.13.1-py3-none-any.whl", hash = "sha256:2016745cb3ba554469d02819d78958b571792bb68e31302610e898f80dd3a573"},
|
||||
{file = "coverage-7.13.1.tar.gz", hash = "sha256:b7593fe7eb5feaa3fbb461ac79aac9f9fc0387a5ca8080b0c6fe2ca27b091afd"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
toml = ["tomli"]
|
||||
|
||||
[[package]]
|
||||
name = "distlib"
|
||||
version = "0.4.0"
|
||||
description = "Distribution utilities"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16"},
|
||||
{file = "distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "execnet"
|
||||
version = "2.1.2"
|
||||
@@ -367,25 +372,36 @@ testing = ["hatch", "pre-commit", "pytest", "tox"]
|
||||
|
||||
[[package]]
|
||||
name = "fastapi"
|
||||
version = "0.121.3"
|
||||
version = "0.127.1"
|
||||
description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
python-versions = ">=3.9"
|
||||
files = [
|
||||
{file = "fastapi-0.121.3-py3-none-any.whl", hash = "sha256:0c78fc87587fcd910ca1bbf5bc8ba37b80e119b388a7206b39f0ecc95ebf53e9"},
|
||||
{file = "fastapi-0.121.3.tar.gz", hash = "sha256:0055bc24fe53e56a40e9e0ad1ae2baa81622c406e548e501e717634e2dfbc40b"},
|
||||
{file = "fastapi-0.127.1-py3-none-any.whl", hash = "sha256:31d670a4f9373cc6d7994420f98e4dc46ea693145207abc39696746c83a44430"},
|
||||
{file = "fastapi-0.127.1.tar.gz", hash = "sha256:946a87ee5d931883b562b6bada787d6c8178becee2683cb3f9b980d593206359"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
annotated-doc = ">=0.0.2"
|
||||
pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0"
|
||||
pydantic = ">=2.7.0"
|
||||
starlette = ">=0.40.0,<0.51.0"
|
||||
typing-extensions = ">=4.8.0"
|
||||
|
||||
[package.extras]
|
||||
all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.8)", "httpx (>=0.23.0,<1.0.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=3.1.5)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"]
|
||||
standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.8)", "httpx (>=0.23.0,<1.0.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"]
|
||||
standard-no-fastapi-cloud-cli = ["email-validator (>=2.0.0)", "fastapi-cli[standard-no-fastapi-cloud-cli] (>=0.0.8)", "httpx (>=0.23.0,<1.0.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"]
|
||||
standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.8)", "httpx (>=0.23.0,<1.0.0)", "jinja2 (>=3.1.5)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"]
|
||||
standard-no-fastapi-cloud-cli = ["email-validator (>=2.0.0)", "fastapi-cli[standard-no-fastapi-cloud-cli] (>=0.0.8)", "httpx (>=0.23.0,<1.0.0)", "jinja2 (>=3.1.5)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "filelock"
|
||||
version = "3.20.1"
|
||||
description = "A platform independent file lock."
|
||||
optional = false
|
||||
python-versions = ">=3.10"
|
||||
files = [
|
||||
{file = "filelock-3.20.1-py3-none-any.whl", hash = "sha256:15d9e9a67306188a44baa72f569d2bfd803076269365fdea0934385da4dc361a"},
|
||||
{file = "filelock-3.20.1.tar.gz", hash = "sha256:b8360948b351b80f420878d8516519a2204b07aefcdcfd24912a5d33127f188c"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h11"
|
||||
@@ -421,13 +437,13 @@ trio = ["trio (>=0.22.0,<1.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "httpx"
|
||||
version = "0.27.2"
|
||||
version = "0.28.1"
|
||||
description = "The next generation HTTP client."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0"},
|
||||
{file = "httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2"},
|
||||
{file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"},
|
||||
{file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -435,7 +451,6 @@ anyio = "*"
|
||||
certifi = "*"
|
||||
httpcore = "==1.*"
|
||||
idna = "*"
|
||||
sniffio = "*"
|
||||
|
||||
[package.extras]
|
||||
brotli = ["brotli", "brotlicffi"]
|
||||
@@ -444,6 +459,20 @@ http2 = ["h2 (>=3,<5)"]
|
||||
socks = ["socksio (==1.*)"]
|
||||
zstd = ["zstandard (>=0.18.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "identify"
|
||||
version = "2.6.15"
|
||||
description = "File identification library for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
files = [
|
||||
{file = "identify-2.6.15-py2.py3-none-any.whl", hash = "sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757"},
|
||||
{file = "identify-2.6.15.tar.gz", hash = "sha256:e4f4864b96c6557ef2a1e1c951771838f4edc9df3a72ec7118b338801b11c7bf"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
license = ["ukkonen"]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.11"
|
||||
@@ -470,14 +499,48 @@ files = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mypy-extensions"
|
||||
version = "1.1.0"
|
||||
description = "Type system extensions for programs checked with the mypy type checker."
|
||||
name = "markdown-it-py"
|
||||
version = "4.0.0"
|
||||
description = "Python port of markdown-it. Markdown parsing, done right!"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
python-versions = ">=3.10"
|
||||
files = [
|
||||
{file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"},
|
||||
{file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"},
|
||||
{file = "markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147"},
|
||||
{file = "markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
mdurl = ">=0.1,<1.0"
|
||||
|
||||
[package.extras]
|
||||
benchmarking = ["psutil", "pytest", "pytest-benchmark"]
|
||||
compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "markdown-it-pyrs", "mistletoe (>=1.0,<2.0)", "mistune (>=3.0,<4.0)", "panflute (>=2.3,<3.0)"]
|
||||
linkify = ["linkify-it-py (>=1,<3)"]
|
||||
plugins = ["mdit-py-plugins (>=0.5.0)"]
|
||||
profiling = ["gprof2dot"]
|
||||
rtd = ["ipykernel", "jupyter_sphinx", "mdit-py-plugins (>=0.5.0)", "myst-parser", "pyyaml", "sphinx", "sphinx-book-theme (>=1.0,<2.0)", "sphinx-copybutton", "sphinx-design"]
|
||||
testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions", "requests"]
|
||||
|
||||
[[package]]
|
||||
name = "mdurl"
|
||||
version = "0.1.2"
|
||||
description = "Markdown URL utilities"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"},
|
||||
{file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nodeenv"
|
||||
version = "1.10.0"
|
||||
description = "Node.js virtual environment builder"
|
||||
optional = false
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
|
||||
files = [
|
||||
{file = "nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827"},
|
||||
{file = "nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -491,26 +554,15 @@ files = [
|
||||
{file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pathspec"
|
||||
version = "0.12.1"
|
||||
description = "Utility library for gitignore style pattern matching of file paths."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"},
|
||||
{file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "platformdirs"
|
||||
version = "4.5.0"
|
||||
version = "4.5.1"
|
||||
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
|
||||
optional = false
|
||||
python-versions = ">=3.10"
|
||||
files = [
|
||||
{file = "platformdirs-4.5.0-py3-none-any.whl", hash = "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3"},
|
||||
{file = "platformdirs-4.5.0.tar.gz", hash = "sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312"},
|
||||
{file = "platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31"},
|
||||
{file = "platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
@@ -533,6 +585,38 @@ files = [
|
||||
dev = ["pre-commit", "tox"]
|
||||
testing = ["coverage", "pytest", "pytest-benchmark"]
|
||||
|
||||
[[package]]
|
||||
name = "pre-commit"
|
||||
version = "4.5.1"
|
||||
description = "A framework for managing and maintaining multi-language pre-commit hooks."
|
||||
optional = false
|
||||
python-versions = ">=3.10"
|
||||
files = [
|
||||
{file = "pre_commit-4.5.1-py2.py3-none-any.whl", hash = "sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77"},
|
||||
{file = "pre_commit-4.5.1.tar.gz", hash = "sha256:eb545fcff725875197837263e977ea257a402056661f09dae08e4b149b030a61"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
cfgv = ">=2.0.0"
|
||||
identify = ">=1.0.0"
|
||||
nodeenv = ">=0.11.1"
|
||||
pyyaml = ">=5.1"
|
||||
virtualenv = ">=20.10.0"
|
||||
|
||||
[[package]]
|
||||
name = "prompt-toolkit"
|
||||
version = "3.0.52"
|
||||
description = "Library for building powerful interactive command lines in Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955"},
|
||||
{file = "prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
wcwidth = "*"
|
||||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
version = "2.12.5"
|
||||
@@ -687,6 +771,29 @@ files = [
|
||||
[package.dependencies]
|
||||
typing-extensions = ">=4.14.1"
|
||||
|
||||
[[package]]
|
||||
name = "pydantic-settings"
|
||||
version = "2.12.0"
|
||||
description = "Settings management using Pydantic"
|
||||
optional = false
|
||||
python-versions = ">=3.10"
|
||||
files = [
|
||||
{file = "pydantic_settings-2.12.0-py3-none-any.whl", hash = "sha256:fddb9fd99a5b18da837b29710391e945b1e30c135477f484084ee513adb93809"},
|
||||
{file = "pydantic_settings-2.12.0.tar.gz", hash = "sha256:005538ef951e3c2a68e1c08b292b5f2e71490def8589d4221b95dab00dafcfd0"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
pydantic = ">=2.7.0"
|
||||
python-dotenv = ">=0.21.0"
|
||||
typing-inspection = ">=0.4.0"
|
||||
|
||||
[package.extras]
|
||||
aws-secrets-manager = ["boto3 (>=1.35.0)", "boto3-stubs[secretsmanager]"]
|
||||
azure-key-vault = ["azure-identity (>=1.16.0)", "azure-keyvault-secrets (>=4.8.0)"]
|
||||
gcp-secret-manager = ["google-cloud-secret-manager (>=2.23.1)"]
|
||||
toml = ["tomli (>=2.0.1)"]
|
||||
yaml = ["pyyaml (>=6.0.1)"]
|
||||
|
||||
[[package]]
|
||||
name = "pygments"
|
||||
version = "2.19.2"
|
||||
@@ -793,18 +900,100 @@ files = [
|
||||
cli = ["click (>=5.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "pytokens"
|
||||
version = "0.3.0"
|
||||
description = "A Fast, spec compliant Python 3.14+ tokenizer that runs on older Pythons."
|
||||
name = "pyyaml"
|
||||
version = "6.0.3"
|
||||
description = "YAML parser and emitter for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "pytokens-0.3.0-py3-none-any.whl", hash = "sha256:95b2b5eaf832e469d141a378872480ede3f251a5a5041b8ec6e581d3ac71bbf3"},
|
||||
{file = "pytokens-0.3.0.tar.gz", hash = "sha256:2f932b14ed08de5fcf0b391ace2642f858f1394c0857202959000b68ed7a458a"},
|
||||
{file = "PyYAML-6.0.3-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f"},
|
||||
{file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4"},
|
||||
{file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3"},
|
||||
{file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6"},
|
||||
{file = "PyYAML-6.0.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369"},
|
||||
{file = "PyYAML-6.0.3-cp38-cp38-win32.whl", hash = "sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295"},
|
||||
{file = "PyYAML-6.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b"},
|
||||
{file = "pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b"},
|
||||
{file = "pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956"},
|
||||
{file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8"},
|
||||
{file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198"},
|
||||
{file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b"},
|
||||
{file = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0"},
|
||||
{file = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69"},
|
||||
{file = "pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e"},
|
||||
{file = "pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c"},
|
||||
{file = "pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e"},
|
||||
{file = "pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824"},
|
||||
{file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c"},
|
||||
{file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00"},
|
||||
{file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d"},
|
||||
{file = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a"},
|
||||
{file = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4"},
|
||||
{file = "pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b"},
|
||||
{file = "pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf"},
|
||||
{file = "pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196"},
|
||||
{file = "pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0"},
|
||||
{file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28"},
|
||||
{file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c"},
|
||||
{file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc"},
|
||||
{file = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e"},
|
||||
{file = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea"},
|
||||
{file = "pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5"},
|
||||
{file = "pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b"},
|
||||
{file = "pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd"},
|
||||
{file = "pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8"},
|
||||
{file = "pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1"},
|
||||
{file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c"},
|
||||
{file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5"},
|
||||
{file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6"},
|
||||
{file = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6"},
|
||||
{file = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be"},
|
||||
{file = "pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26"},
|
||||
{file = "pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c"},
|
||||
{file = "pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb"},
|
||||
{file = "pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac"},
|
||||
{file = "pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310"},
|
||||
{file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7"},
|
||||
{file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788"},
|
||||
{file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5"},
|
||||
{file = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764"},
|
||||
{file = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35"},
|
||||
{file = "pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac"},
|
||||
{file = "pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3"},
|
||||
{file = "pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3"},
|
||||
{file = "pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba"},
|
||||
{file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c"},
|
||||
{file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702"},
|
||||
{file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c"},
|
||||
{file = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065"},
|
||||
{file = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65"},
|
||||
{file = "pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9"},
|
||||
{file = "pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b"},
|
||||
{file = "pyyaml-6.0.3-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da"},
|
||||
{file = "pyyaml-6.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917"},
|
||||
{file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9"},
|
||||
{file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5"},
|
||||
{file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a"},
|
||||
{file = "pyyaml-6.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926"},
|
||||
{file = "pyyaml-6.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7"},
|
||||
{file = "pyyaml-6.0.3-cp39-cp39-win32.whl", hash = "sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0"},
|
||||
{file = "pyyaml-6.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007"},
|
||||
{file = "pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
dev = ["black", "build", "mypy", "pytest", "pytest-cov", "setuptools", "tox", "twine", "wheel"]
|
||||
[[package]]
|
||||
name = "questionary"
|
||||
version = "2.1.1"
|
||||
description = "Python library to build pretty command line user prompts ⭐️"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
files = [
|
||||
{file = "questionary-2.1.1-py3-none-any.whl", hash = "sha256:a51af13f345f1cdea62347589fbb6df3b290306ab8930713bfae4d475a7d4a59"},
|
||||
{file = "questionary-2.1.1.tar.gz", hash = "sha256:3d7e980292bb0107abaa79c68dd3eee3c561b83a0f89ae482860b181c8bd412d"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
prompt_toolkit = ">=2.0,<4.0"
|
||||
|
||||
[[package]]
|
||||
name = "requests"
|
||||
@@ -827,43 +1016,70 @@ urllib3 = ">=1.21.1,<3"
|
||||
socks = ["PySocks (>=1.5.6,!=1.5.7)"]
|
||||
use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
|
||||
|
||||
[[package]]
|
||||
name = "rich"
|
||||
version = "14.2.0"
|
||||
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
|
||||
optional = false
|
||||
python-versions = ">=3.8.0"
|
||||
files = [
|
||||
{file = "rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd"},
|
||||
{file = "rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
markdown-it-py = ">=2.2.0"
|
||||
pygments = ">=2.13.0,<3.0.0"
|
||||
|
||||
[package.extras]
|
||||
jupyter = ["ipywidgets (>=7.5.1,<9)"]
|
||||
|
||||
[[package]]
|
||||
name = "rich-click"
|
||||
version = "1.9.5"
|
||||
description = "Format click help output nicely with rich"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "rich_click-1.9.5-py3-none-any.whl", hash = "sha256:9b195721a773b1acf0e16ff9ec68cef1e7d237e53471e6e3f7ade462f86c403a"},
|
||||
{file = "rich_click-1.9.5.tar.gz", hash = "sha256:48120531493f1533828da80e13e839d471979ec8d7d0ca7b35f86a1379cc74b6"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
click = ">=8"
|
||||
colorama = {version = "*", markers = "platform_system == \"Windows\""}
|
||||
rich = ">=12"
|
||||
|
||||
[package.extras]
|
||||
dev = ["inline-snapshot (>=0.24)", "jsonschema (>=4)", "mypy (>=1.14.1)", "nodeenv (>=1.9.1)", "packaging (>=25)", "pre-commit (>=3.5)", "pytest (>=8.3.5)", "pytest-cov (>=5)", "rich-codex (>=1.2.11)", "ruff (>=0.12.4)", "typer (>=0.15)", "types-setuptools (>=75.8.0.20250110)"]
|
||||
docs = ["markdown-include (>=0.8.1)", "mike (>=2.1.3)", "mkdocs-github-admonitions-plugin (>=0.1.1)", "mkdocs-glightbox (>=0.4)", "mkdocs-include-markdown-plugin (>=7.1.7)", "mkdocs-material-extensions (>=1.3.1)", "mkdocs-material[imaging] (>=9.5.18,<9.6.0)", "mkdocs-redirects (>=1.2.2)", "mkdocs-rss-plugin (>=1.15)", "mkdocs[docs] (>=1.6.1)", "mkdocstrings[python] (>=0.26.1)", "rich-codex (>=1.2.11)", "typer (>=0.15)"]
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.14.7"
|
||||
version = "0.14.10"
|
||||
description = "An extremely fast Python linter and code formatter, written in Rust."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "ruff-0.14.7-py3-none-linux_armv6l.whl", hash = "sha256:b9d5cb5a176c7236892ad7224bc1e63902e4842c460a0b5210701b13e3de4fca"},
|
||||
{file = "ruff-0.14.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3f64fe375aefaf36ca7d7250292141e39b4cea8250427482ae779a2aa5d90015"},
|
||||
{file = "ruff-0.14.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:93e83bd3a9e1a3bda64cb771c0d47cda0e0d148165013ae2d3554d718632d554"},
|
||||
{file = "ruff-0.14.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3838948e3facc59a6070795de2ae16e5786861850f78d5914a03f12659e88f94"},
|
||||
{file = "ruff-0.14.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:24c8487194d38b6d71cd0fd17a5b6715cda29f59baca1defe1e3a03240f851d1"},
|
||||
{file = "ruff-0.14.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:79c73db6833f058a4be8ffe4a0913b6d4ad41f6324745179bd2aa09275b01d0b"},
|
||||
{file = "ruff-0.14.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:12eb7014fccff10fc62d15c79d8a6be4d0c2d60fe3f8e4d169a0d2def75f5dad"},
|
||||
{file = "ruff-0.14.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c623bbdc902de7ff715a93fa3bb377a4e42dd696937bf95669118773dbf0c50"},
|
||||
{file = "ruff-0.14.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f53accc02ed2d200fa621593cdb3c1ae06aa9b2c3cae70bc96f72f0000ae97a9"},
|
||||
{file = "ruff-0.14.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:281f0e61a23fcdcffca210591f0f53aafaa15f9025b5b3f9706879aaa8683bc4"},
|
||||
{file = "ruff-0.14.7-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:dbbaa5e14148965b91cb090236931182ee522a5fac9bc5575bafc5c07b9f9682"},
|
||||
{file = "ruff-0.14.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1464b6e54880c0fe2f2d6eaefb6db15373331414eddf89d6b903767ae2458143"},
|
||||
{file = "ruff-0.14.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f217ed871e4621ea6128460df57b19ce0580606c23aeab50f5de425d05226784"},
|
||||
{file = "ruff-0.14.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6be02e849440ed3602d2eb478ff7ff07d53e3758f7948a2a598829660988619e"},
|
||||
{file = "ruff-0.14.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:19a0f116ee5e2b468dfe80c41c84e2bbd6b74f7b719bee86c2ecde0a34563bcc"},
|
||||
{file = "ruff-0.14.7-py3-none-win32.whl", hash = "sha256:e33052c9199b347c8937937163b9b149ef6ab2e4bb37b042e593da2e6f6cccfa"},
|
||||
{file = "ruff-0.14.7-py3-none-win_amd64.whl", hash = "sha256:e17a20ad0d3fad47a326d773a042b924d3ac31c6ca6deb6c72e9e6b5f661a7c6"},
|
||||
{file = "ruff-0.14.7-py3-none-win_arm64.whl", hash = "sha256:be4d653d3bea1b19742fcc6502354e32f65cd61ff2fbdb365803ef2c2aec6228"},
|
||||
{file = "ruff-0.14.7.tar.gz", hash = "sha256:3417deb75d23bd14a722b57b0a1435561db65f0ad97435b4cf9f85ffcef34ae5"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sniffio"
|
||||
version = "1.3.1"
|
||||
description = "Sniff out which async library your code is running under"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"},
|
||||
{file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"},
|
||||
{file = "ruff-0.14.10-py3-none-linux_armv6l.whl", hash = "sha256:7a3ce585f2ade3e1f29ec1b92df13e3da262178df8c8bdf876f48fa0e8316c49"},
|
||||
{file = "ruff-0.14.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:674f9be9372907f7257c51f1d4fc902cb7cf014b9980152b802794317941f08f"},
|
||||
{file = "ruff-0.14.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d85713d522348837ef9df8efca33ccb8bd6fcfc86a2cde3ccb4bc9d28a18003d"},
|
||||
{file = "ruff-0.14.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6987ebe0501ae4f4308d7d24e2d0fe3d7a98430f5adfd0f1fead050a740a3a77"},
|
||||
{file = "ruff-0.14.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:16a01dfb7b9e4eee556fbfd5392806b1b8550c9b4a9f6acd3dbe6812b193c70a"},
|
||||
{file = "ruff-0.14.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7165d31a925b7a294465fa81be8c12a0e9b60fb02bf177e79067c867e71f8b1f"},
|
||||
{file = "ruff-0.14.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c561695675b972effb0c0a45db233f2c816ff3da8dcfbe7dfc7eed625f218935"},
|
||||
{file = "ruff-0.14.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4bb98fcbbc61725968893682fd4df8966a34611239c9fd07a1f6a07e7103d08e"},
|
||||
{file = "ruff-0.14.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f24b47993a9d8cb858429e97bdf8544c78029f09b520af615c1d261bf827001d"},
|
||||
{file = "ruff-0.14.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59aabd2e2c4fd614d2862e7939c34a532c04f1084476d6833dddef4afab87e9f"},
|
||||
{file = "ruff-0.14.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:213db2b2e44be8625002dbea33bb9c60c66ea2c07c084a00d55732689d697a7f"},
|
||||
{file = "ruff-0.14.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b914c40ab64865a17a9a5b67911d14df72346a634527240039eb3bd650e5979d"},
|
||||
{file = "ruff-0.14.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1484983559f026788e3a5c07c81ef7d1e97c1c78ed03041a18f75df104c45405"},
|
||||
{file = "ruff-0.14.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c70427132db492d25f982fffc8d6c7535cc2fd2c83fc8888f05caaa248521e60"},
|
||||
{file = "ruff-0.14.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5bcf45b681e9f1ee6445d317ce1fa9d6cba9a6049542d1c3d5b5958986be8830"},
|
||||
{file = "ruff-0.14.10-py3-none-win32.whl", hash = "sha256:104c49fc7ab73f3f3a758039adea978869a918f31b73280db175b43a2d9b51d6"},
|
||||
{file = "ruff-0.14.10-py3-none-win_amd64.whl", hash = "sha256:466297bd73638c6bdf06485683e812db1c00c7ac96d4ddd0294a338c62fdc154"},
|
||||
{file = "ruff-0.14.10-py3-none-win_arm64.whl", hash = "sha256:e51d046cf6dda98a4633b8a8a771451107413b0f07183b2bef03f075599e44e6"},
|
||||
{file = "ruff-0.14.10.tar.gz", hash = "sha256:9a2e830f075d1a42cd28420d7809ace390832a490ed0966fe373ba288e77aaf4"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -879,11 +1095,21 @@ files = [
|
||||
|
||||
[package.dependencies]
|
||||
anyio = ">=3.6.2,<5"
|
||||
typing-extensions = {version = ">=4.10.0", markers = "python_version < \"3.13\""}
|
||||
|
||||
[package.extras]
|
||||
full = ["httpx (>=0.27.0,<0.29.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.18)", "pyyaml"]
|
||||
|
||||
[[package]]
|
||||
name = "tomlkit"
|
||||
version = "0.13.3"
|
||||
description = "Style preserving TOML library"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "tomlkit-0.13.3-py3-none-any.whl", hash = "sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0"},
|
||||
{file = "tomlkit-0.13.3.tar.gz", hash = "sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.15.0"
|
||||
@@ -911,30 +1137,30 @@ typing-extensions = ">=4.12.0"
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "2.5.0"
|
||||
version = "2.6.2"
|
||||
description = "HTTP library with thread-safe connection pooling, file post, and more."
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
files = [
|
||||
{file = "urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc"},
|
||||
{file = "urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760"},
|
||||
{file = "urllib3-2.6.2-py3-none-any.whl", hash = "sha256:ec21cddfe7724fc7cb4ba4bea7aa8e2ef36f607a4bab81aa6ce42a13dc3f03dd"},
|
||||
{file = "urllib3-2.6.2.tar.gz", hash = "sha256:016f9c98bb7e98085cb2b4b17b87d2c702975664e4f060c6532e64d1c1a5e797"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"]
|
||||
brotli = ["brotli (>=1.2.0)", "brotlicffi (>=1.2.0.0)"]
|
||||
h2 = ["h2 (>=4,<5)"]
|
||||
socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
|
||||
zstd = ["zstandard (>=0.18.0)"]
|
||||
zstd = ["backports-zstd (>=1.0.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "uvicorn"
|
||||
version = "0.38.0"
|
||||
version = "0.40.0"
|
||||
description = "The lightning-fast ASGI server."
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
python-versions = ">=3.10"
|
||||
files = [
|
||||
{file = "uvicorn-0.38.0-py3-none-any.whl", hash = "sha256:48c0afd214ceb59340075b4a052ea1ee91c16fbc2a9b1469cca0e54566977b02"},
|
||||
{file = "uvicorn-0.38.0.tar.gz", hash = "sha256:fd97093bdd120a2609fc0d3afe931d4d4ad688b6e75f0f929fde1bc36fe0e91d"},
|
||||
{file = "uvicorn-0.40.0-py3-none-any.whl", hash = "sha256:c6c8f55bc8bf13eb6fa9ff87ad62308bbbc33d0b67f84293151efe87e0d5f2ee"},
|
||||
{file = "uvicorn-0.40.0.tar.gz", hash = "sha256:839676675e87e73694518b5574fd0f24c9d97b46bea16df7b8c05ea1a51071ea"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -944,7 +1170,52 @@ h11 = ">=0.8"
|
||||
[package.extras]
|
||||
standard = ["colorama (>=0.4)", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"]
|
||||
|
||||
[[package]]
|
||||
name = "virtualenv"
|
||||
version = "20.35.4"
|
||||
description = "Virtual Python Environment builder"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "virtualenv-20.35.4-py3-none-any.whl", hash = "sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b"},
|
||||
{file = "virtualenv-20.35.4.tar.gz", hash = "sha256:643d3914d73d3eeb0c552cbb12d7e82adf0e504dbf86a3182f8771a153a1971c"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
distlib = ">=0.3.7,<1"
|
||||
filelock = ">=3.12.2,<4"
|
||||
platformdirs = ">=3.9.1,<5"
|
||||
|
||||
[package.extras]
|
||||
docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"]
|
||||
test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"]
|
||||
|
||||
[[package]]
|
||||
name = "wcmatch"
|
||||
version = "10.1"
|
||||
description = "Wildcard/glob file name matcher."
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
files = [
|
||||
{file = "wcmatch-10.1-py3-none-any.whl", hash = "sha256:5848ace7dbb0476e5e55ab63c6bbd529745089343427caa5537f230cc01beb8a"},
|
||||
{file = "wcmatch-10.1.tar.gz", hash = "sha256:f11f94208c8c8484a16f4f48638a85d771d9513f4ab3f37595978801cb9465af"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
bracex = ">=2.1.1"
|
||||
|
||||
[[package]]
|
||||
name = "wcwidth"
|
||||
version = "0.2.14"
|
||||
description = "Measures the displayed width of unicode strings in a terminal"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
files = [
|
||||
{file = "wcwidth-0.2.14-py2.py3-none-any.whl", hash = "sha256:a7bb560c8aee30f9957e5f9895805edd20602f2d7f720186dfd906e82b4982e1"},
|
||||
{file = "wcwidth-0.2.14.tar.gz", hash = "sha256:4d478375d31bc5395a3c55c40ccdf3354688364cd61c4f6adacaa9215d0b3605"},
|
||||
]
|
||||
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.12"
|
||||
content-hash = "dd1f7cc9b08f7515824379744774caee93d0c793429d1d6d92776480b180415b"
|
||||
python-versions = "==3.14.2"
|
||||
content-hash = "ec920fd78ea55c063bf2e4696c328056b50d8d1694f057c2d455ca2619938aac"
|
||||
|
||||
+36
-27
@@ -1,26 +1,46 @@
|
||||
[tool.poetry]
|
||||
name = "agent-media"
|
||||
version = "0.1.0"
|
||||
name = "alfred"
|
||||
version = "0.1.7"
|
||||
description = "AI agent for managing a local media library"
|
||||
authors = ["Francwa <francois.hodiaumont@gmail.com>"]
|
||||
readme = "README.md"
|
||||
package-mode = false
|
||||
|
||||
[tool.alfred.settings]
|
||||
image_name = "alfred_media_organizer"
|
||||
librechat_version = "v0.8.1"
|
||||
rag_version = "v0.7.0"
|
||||
runner = "poetry"
|
||||
service_name = "alfred"
|
||||
|
||||
[tool.alfred.security]
|
||||
jwt_secret = "32:b64"
|
||||
jwt_refresh_secret = "32:b64"
|
||||
creds_key = "32:b64"
|
||||
creds_iv = "16:b64"
|
||||
meili_master_key = "32:b64"
|
||||
mongo_password = "16:hex"
|
||||
postgres_password = "16:hex"
|
||||
qbittorrent_password = "16:hex"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.12"
|
||||
python = "==3.14.2"
|
||||
python-dotenv = "^1.0.0"
|
||||
requests = "^2.32.5"
|
||||
fastapi = "^0.121.1"
|
||||
fastapi = "^0.127.1"
|
||||
pydantic = "^2.12.4"
|
||||
uvicorn = "^0.38.0"
|
||||
uvicorn = "^0.40.0"
|
||||
pytest-xdist = "^3.8.0"
|
||||
httpx = "^0.28.1"
|
||||
pydantic-settings = "^2.12.0"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
pytest = "^8.0.0"
|
||||
pytest-cov = "^4.1.0"
|
||||
pytest-asyncio = "^0.23.0"
|
||||
httpx = "^0.27.0"
|
||||
pytest-asyncio = "^1.0.0"
|
||||
ruff = "^0.14.7"
|
||||
black = "^25.11.0"
|
||||
pre-commit = "^4.5.1"
|
||||
bump-my-version = "^1.2.5"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
@@ -29,6 +49,8 @@ build-backend = "poetry.core.masonry.api"
|
||||
[tool.pytest.ini_options]
|
||||
# Chemins où pytest cherche les tests
|
||||
testpaths = ["tests"]
|
||||
# Ajouter le répertoire racine au PYTHONPATH pour les imports
|
||||
pythonpath = ["."]
|
||||
|
||||
# Patterns de fichiers/classes/fonctions à considérer comme tests
|
||||
python_files = ["test_*.py"] # Fichiers commençant par "test_"
|
||||
@@ -39,12 +61,13 @@ python_functions = ["test_*"] # Fonctions commençant par "test_"
|
||||
addopts = [
|
||||
"-v", # --verbose : affiche chaque test individuellement
|
||||
"--tb=short", # --traceback=short : tracebacks courts et lisibles
|
||||
"--cov=.", # --coverage : mesure le coverage de tout le projet (.)
|
||||
"--cov-report=term-missing", # Affiche les lignes manquantes dans le terminal
|
||||
"--cov-report=html", # Génère un rapport HTML dans htmlcov/
|
||||
"--cov-report=xml", # Génère un rapport XML (pour CI/CD)
|
||||
"--cov-fail-under=80", # Échoue si coverage < 80%
|
||||
#"--cov=.", # --coverage : mesure le coverage de tout le projet (.)
|
||||
#"--cov-report=term-missing", # Affiche les lignes manquantes dans le terminal
|
||||
#"--cov-report=html", # Génère un rapport HTML dans htmlcov/
|
||||
#"--cov-report=xml", # Génère un rapport XML (pour CI/CD)
|
||||
#"--cov-fail-under=80", # Échoue si coverage < 80%
|
||||
"-n=auto", # --numprocesses=auto : parallélise les tests (pytest-xdist)
|
||||
"--dist=loadscope", # Distribution strategy: group tests by module
|
||||
"--strict-markers", # Erreur si un marker non déclaré est utilisé
|
||||
"--disable-warnings", # Désactive l'affichage des warnings (sauf erreurs)
|
||||
]
|
||||
@@ -77,20 +100,6 @@ exclude_lines = [
|
||||
"if __name__ == .__main__.:",
|
||||
]
|
||||
|
||||
[tool.black]
|
||||
line-length = 88
|
||||
target-version = ['py312']
|
||||
include = '\.pyi?$'
|
||||
exclude = '''
|
||||
/(
|
||||
__pycache__
|
||||
| \.git
|
||||
| \.qodo
|
||||
| \.vscode
|
||||
| \.ruff_cache
|
||||
)/
|
||||
'''
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 88
|
||||
exclude = [
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:base",
|
||||
":disableRateLimiting",
|
||||
":semanticCommits"
|
||||
],
|
||||
"labels": ["dependencies", "renovate"],
|
||||
|
||||
"packageRules": [
|
||||
{
|
||||
"matchLanguages": ["python"],
|
||||
"matchUpdateTypes": ["patch"],
|
||||
"automerge": true,
|
||||
"groupName": "Python Security Patches"
|
||||
},
|
||||
{
|
||||
"matchPackageNames": ["python"],
|
||||
"matchUpdateTypes": ["major", "minor"],
|
||||
"dependencyDashboardApproval": true,
|
||||
"groupName": "Python Update(s) (Manual Action Required)"
|
||||
},
|
||||
{
|
||||
"matchPackageNames": ["getmeili/meilisearch"],
|
||||
"enabled": false
|
||||
}
|
||||
],
|
||||
|
||||
"regexManagers": [
|
||||
{
|
||||
"description": "Update Docker variables in the Makefile",
|
||||
"fileMatch": ["^Makefile$"],
|
||||
"matchStrings": [
|
||||
"# renovate: datasource=(?<datasource>.*?) depName=(?<depName>.*?)( versioning=(?<versioning>.*?))?\\s[A-Z_]+_VERSION [?:]?= (?<currentValue>.*)"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
import secrets
|
||||
from pathlib import Path
|
||||
|
||||
import tomllib
|
||||
from config_loader import load_build_config, write_env_make
|
||||
|
||||
|
||||
def generate_secret(rule: str) -> str:
|
||||
"""
|
||||
Generates a cryptographically secure secret based on a spec string.
|
||||
Example specs: '32:b64', '16:hex'.
|
||||
"""
|
||||
chunks: list[str] = rule.split(":")
|
||||
size: int = int(chunks[0])
|
||||
tech: str = chunks[1]
|
||||
|
||||
if tech == "b64":
|
||||
return secrets.token_urlsafe(size)
|
||||
elif tech == "hex":
|
||||
return secrets.token_hex(size)
|
||||
else:
|
||||
raise ValueError(f"Invalid security format: {tech}")
|
||||
|
||||
|
||||
def extract_python_version(version_string: str) -> tuple[str, str]:
|
||||
"""
|
||||
Extract Python version from poetry dependency string.
|
||||
Examples:
|
||||
"==3.14.2" -> ("3.14.2", "3.14")
|
||||
"^3.14.2" -> ("3.14.2", "3.14")
|
||||
"~3.14.2" -> ("3.14.2", "3.14")
|
||||
"3.14.2" -> ("3.14.2", "3.14")
|
||||
"""
|
||||
import re # noqa: PLC0415
|
||||
|
||||
# Remove poetry version operators (==, ^, ~, >=, etc.)
|
||||
clean_version = re.sub(r"^[=^~><]+", "", version_string.strip())
|
||||
|
||||
# Extract version parts
|
||||
parts = clean_version.split(".")
|
||||
|
||||
if len(parts) >= 2:
|
||||
full_version = clean_version
|
||||
short_version = f"{parts[0]}.{parts[1]}"
|
||||
return full_version, short_version
|
||||
else:
|
||||
raise ValueError(f"Invalid Python version format: {version_string}")
|
||||
|
||||
|
||||
# TODO: Refactor
|
||||
def bootstrap(): # noqa: PLR0912, PLR0915
|
||||
"""
|
||||
Initializes the .env file by merging .env.example with generated secrets
|
||||
and build variables from pyproject.toml.
|
||||
Also generates .env.make for Makefile.
|
||||
|
||||
ALWAYS preserves existing secrets!
|
||||
"""
|
||||
base_dir = Path(__file__).resolve().parent.parent
|
||||
env_path = base_dir / ".env"
|
||||
|
||||
example_path = base_dir / ".env.example"
|
||||
if not example_path.exists():
|
||||
print(f"❌ {example_path.name} not found.")
|
||||
return
|
||||
|
||||
toml_path = base_dir / "pyproject.toml"
|
||||
if not toml_path.exists():
|
||||
print(f"❌ {toml_path.name} not found.")
|
||||
return
|
||||
|
||||
# ALWAYS load existing .env if it exists
|
||||
existing_env = {}
|
||||
if env_path.exists():
|
||||
print("🔄 Reading existing .env...")
|
||||
with open(env_path) as f:
|
||||
for line in f:
|
||||
if "=" in line and not line.strip().startswith("#"):
|
||||
key, value = line.split("=", 1)
|
||||
existing_env[key.strip()] = value.strip()
|
||||
print(f" Found {len(existing_env)} existing keys")
|
||||
print("🔧 Updating .env file (keeping secrets)...")
|
||||
else:
|
||||
print("🔧 Initializing: Creating secure .env file...")
|
||||
|
||||
# Load data from pyproject.toml
|
||||
with open(toml_path, "rb") as f:
|
||||
data = tomllib.load(f)
|
||||
security_keys = data["tool"]["alfred"]["security"]
|
||||
settings_keys = data["tool"]["alfred"]["settings"]
|
||||
dependencies = data["tool"]["poetry"]["dependencies"]
|
||||
alfred_version = data["tool"]["poetry"]["version"]
|
||||
|
||||
# Normalize TOML keys to UPPER_CASE for .env format (done once)
|
||||
security_keys_upper = {k.upper(): v for k, v in security_keys.items()}
|
||||
settings_keys_upper = {k.upper(): v for k, v in settings_keys.items()}
|
||||
|
||||
# Extract Python version
|
||||
python_version_full, python_version_short = extract_python_version(
|
||||
dependencies["python"]
|
||||
)
|
||||
|
||||
# Read .env.example
|
||||
with open(example_path) as f:
|
||||
example_lines = f.readlines()
|
||||
|
||||
new_lines = []
|
||||
# Process each line from .env.example
|
||||
for raw_line in example_lines:
|
||||
line = raw_line.strip()
|
||||
|
||||
if line and not line.startswith("#") and "=" in line:
|
||||
key, value = line.split("=", 1)
|
||||
key = key.strip()
|
||||
|
||||
# Check if key exists in current .env (update mode)
|
||||
if key in existing_env:
|
||||
# Keep existing value for secrets
|
||||
if key in security_keys_upper:
|
||||
new_lines.append(f"{key}={existing_env[key]}\n")
|
||||
print(f" ↻ Kept existing {key}")
|
||||
# Update build vars from pyproject.toml
|
||||
elif key in settings_keys_upper:
|
||||
new_value = settings_keys_upper[key]
|
||||
if existing_env[key] != new_value:
|
||||
new_lines.append(f"{key}={new_value}\n")
|
||||
print(f" ↻ Updated {key}: {existing_env[key]} → {new_value}")
|
||||
else:
|
||||
new_lines.append(f"{key}={existing_env[key]}\n")
|
||||
print(f" ↻ Kept {key}={existing_env[key]}")
|
||||
# Update Python versions
|
||||
elif key == "PYTHON_VERSION":
|
||||
if existing_env[key] != python_version_full:
|
||||
new_lines.append(f"{key}={python_version_full}\n")
|
||||
print(
|
||||
f" ↻ Updated Python: {existing_env[key]} → {python_version_full}"
|
||||
)
|
||||
else:
|
||||
new_lines.append(f"{key}={existing_env[key]}\n")
|
||||
print(f" ↻ Kept Python: {existing_env[key]}")
|
||||
elif key == "PYTHON_VERSION_SHORT":
|
||||
if existing_env[key] != python_version_short:
|
||||
new_lines.append(f"{key}={python_version_short}\n")
|
||||
print(
|
||||
f" ↻ Updated Python (short): {existing_env[key]} → {python_version_short}"
|
||||
)
|
||||
else:
|
||||
new_lines.append(f"{key}={existing_env[key]}\n")
|
||||
print(f" ↻ Kept Python (short): {existing_env[key]}")
|
||||
elif key == "ALFRED_VERSION":
|
||||
if existing_env.get(key) != alfred_version:
|
||||
new_lines.append(f"{key}={alfred_version}\n")
|
||||
print(
|
||||
f" ↻ Updated Alfred version: {existing_env.get(key, 'N/A')} → {alfred_version}"
|
||||
)
|
||||
else:
|
||||
new_lines.append(f"{key}={alfred_version}\n")
|
||||
print(f" ↻ Kept Alfred version: {alfred_version}")
|
||||
# Keep other existing values
|
||||
else:
|
||||
new_lines.append(f"{key}={existing_env[key]}\n")
|
||||
# Key doesn't exist, generate/add it
|
||||
elif key in security_keys_upper:
|
||||
rule = security_keys_upper[key]
|
||||
secret = generate_secret(rule)
|
||||
new_lines.append(f"{key}={secret}\n")
|
||||
print(f" + Secret generated for {key} ({rule})")
|
||||
elif key in settings_keys_upper:
|
||||
value = settings_keys_upper[key]
|
||||
new_lines.append(f"{key}={value}\n")
|
||||
print(f" + Setting added: {key}={value}")
|
||||
elif key == "PYTHON_VERSION":
|
||||
new_lines.append(f"{key}={python_version_full}\n")
|
||||
print(f" + Python version: {python_version_full}")
|
||||
elif key == "PYTHON_VERSION_SHORT":
|
||||
new_lines.append(f"{key}={python_version_short}\n")
|
||||
print(f" + Python version (short): {python_version_short}")
|
||||
elif key == "ALFRED_VERSION":
|
||||
new_lines.append(f"{key}={alfred_version}\n")
|
||||
print(f" + Alfred version: {alfred_version}")
|
||||
else:
|
||||
new_lines.append(raw_line)
|
||||
else:
|
||||
# Keep comments and empty lines
|
||||
new_lines.append(raw_line)
|
||||
|
||||
# Compute database URIs from the generated values
|
||||
final_env = {}
|
||||
for line in new_lines:
|
||||
if "=" in line and not line.strip().startswith("#"):
|
||||
key, value = line.split("=", 1)
|
||||
final_env[key.strip()] = value.strip()
|
||||
|
||||
# Compute MONGO_URI
|
||||
if "MONGO_USER" in final_env and "MONGO_PASSWORD" in final_env:
|
||||
mongo_uri = (
|
||||
f"mongodb://{final_env.get('MONGO_USER', 'alfred')}:"
|
||||
f"{final_env.get('MONGO_PASSWORD', '')}@"
|
||||
f"{final_env.get('MONGO_HOST', 'mongodb')}:"
|
||||
f"{final_env.get('MONGO_PORT', '27017')}/"
|
||||
f"{final_env.get('MONGO_DB_NAME', 'alfred')}?authSource=admin"
|
||||
)
|
||||
# Update MONGO_URI in new_lines
|
||||
for i, line in enumerate(new_lines):
|
||||
if line.startswith("MONGO_URI="):
|
||||
new_lines[i] = f"MONGO_URI={mongo_uri}\n"
|
||||
print(" ✓ Computed MONGO_URI")
|
||||
break
|
||||
|
||||
# Compute POSTGRES_URI
|
||||
if "POSTGRES_USER" in final_env and "POSTGRES_PASSWORD" in final_env:
|
||||
postgres_uri = (
|
||||
f"postgresql://{final_env.get('POSTGRES_USER', 'alfred')}:"
|
||||
f"{final_env.get('POSTGRES_PASSWORD', '')}@"
|
||||
f"{final_env.get('POSTGRES_HOST', 'vectordb')}:"
|
||||
f"{final_env.get('POSTGRES_PORT', '5432')}/"
|
||||
f"{final_env.get('POSTGRES_DB_NAME', 'alfred')}"
|
||||
)
|
||||
# Update POSTGRES_URI in new_lines
|
||||
for i, line in enumerate(new_lines):
|
||||
if line.startswith("POSTGRES_URI="):
|
||||
new_lines[i] = f"POSTGRES_URI={postgres_uri}\n"
|
||||
print(" ✓ Computed POSTGRES_URI")
|
||||
break
|
||||
|
||||
# Write .env file
|
||||
with open(env_path, "w", encoding="utf-8") as f:
|
||||
f.writelines(new_lines)
|
||||
print(f"\n✅ {env_path.name} generated successfully.")
|
||||
|
||||
# Generate .env.make for Makefile using shared config loader
|
||||
config = load_build_config(base_dir)
|
||||
write_env_make(config, base_dir)
|
||||
print("✅ .env.make generated for Makefile.")
|
||||
print("\n⚠️ Reminder: Please manually add your API keys to the .env file.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
bootstrap()
|
||||
@@ -0,0 +1,89 @@
|
||||
"""Shared configuration loader for bootstrap and CI."""
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import NamedTuple
|
||||
|
||||
import tomllib
|
||||
|
||||
|
||||
class BuildConfig(NamedTuple):
|
||||
"""Build configuration extracted from pyproject.toml."""
|
||||
|
||||
alfred_version: str
|
||||
python_version: str
|
||||
python_version_short: str
|
||||
runner: str
|
||||
image_name: str
|
||||
service_name: str
|
||||
librechat_version: str
|
||||
rag_version: str
|
||||
|
||||
|
||||
def extract_python_version(version_string: str) -> tuple[str, str]:
|
||||
"""
|
||||
Extract Python version from poetry dependency string.
|
||||
Examples:
|
||||
"==3.14.2" -> ("3.14.2", "3.14")
|
||||
"^3.14.2" -> ("3.14.2", "3.14")
|
||||
"~3.14.2" -> ("3.14.2", "3.14")
|
||||
"3.14.2" -> ("3.14.2", "3.14")
|
||||
"""
|
||||
clean_version = re.sub(r"^[=^~><]+", "", version_string.strip())
|
||||
parts = clean_version.split(".")
|
||||
|
||||
if len(parts) >= 2:
|
||||
full_version = clean_version
|
||||
short_version = f"{parts[0]}.{parts[1]}"
|
||||
return full_version, short_version
|
||||
else:
|
||||
raise ValueError(f"Invalid Python version format: {version_string}")
|
||||
|
||||
|
||||
def load_build_config(base_dir: Path | None = None) -> BuildConfig:
|
||||
"""Load build configuration from pyproject.toml."""
|
||||
if base_dir is None:
|
||||
base_dir = Path(__file__).resolve().parent.parent
|
||||
|
||||
toml_path = base_dir / "pyproject.toml"
|
||||
if not toml_path.exists():
|
||||
raise FileNotFoundError(f"pyproject.toml not found: {toml_path}")
|
||||
|
||||
with open(toml_path, "rb") as f:
|
||||
data = tomllib.load(f)
|
||||
settings_keys = data["tool"]["alfred"]["settings"]
|
||||
dependencies = data["tool"]["poetry"]["dependencies"]
|
||||
alfred_version = data["tool"]["poetry"]["version"]
|
||||
|
||||
python_version_full, python_version_short = extract_python_version(
|
||||
dependencies["python"]
|
||||
)
|
||||
|
||||
return BuildConfig(
|
||||
alfred_version=alfred_version,
|
||||
python_version=python_version_full,
|
||||
python_version_short=python_version_short,
|
||||
runner=settings_keys["runner"],
|
||||
image_name=settings_keys["image_name"],
|
||||
service_name=settings_keys["service_name"],
|
||||
librechat_version=settings_keys["librechat_version"],
|
||||
rag_version=settings_keys["rag_version"],
|
||||
)
|
||||
|
||||
|
||||
def write_env_make(config: BuildConfig, base_dir: Path | None = None) -> None:
|
||||
"""Write .env.make file for Makefile."""
|
||||
if base_dir is None:
|
||||
base_dir = Path(__file__).resolve().parent.parent
|
||||
|
||||
env_make_path = base_dir / ".env.make"
|
||||
with open(env_make_path, "w", encoding="utf-8") as f:
|
||||
f.write("# Auto-generated from pyproject.toml\n")
|
||||
f.write(f"export ALFRED_VERSION={config.alfred_version}\n")
|
||||
f.write(f"export PYTHON_VERSION={config.python_version}\n")
|
||||
f.write(f"export PYTHON_VERSION_SHORT={config.python_version_short}\n")
|
||||
f.write(f"export RUNNER={config.runner}\n")
|
||||
f.write(f"export IMAGE_NAME={config.image_name}\n")
|
||||
f.write(f"export SERVICE_NAME={config.service_name}\n")
|
||||
f.write(f"export LIBRECHAT_VERSION={config.librechat_version}\n")
|
||||
f.write(f"export RAG_VERSION={config.rag_version}\n")
|
||||
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate .env.make for CI/CD without generating secrets."""
|
||||
|
||||
import sys
|
||||
|
||||
from config_loader import load_build_config, write_env_make
|
||||
|
||||
|
||||
def main():
|
||||
"""Generate .env.make from pyproject.toml."""
|
||||
try:
|
||||
config = load_build_config()
|
||||
write_env_make(config)
|
||||
print("✅ .env.make generated successfully.")
|
||||
return 0
|
||||
except Exception as e:
|
||||
print(f"❌ Failed to generate .env.make: {e}")
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
+22
-2
@@ -1,13 +1,24 @@
|
||||
"""Pytest configuration and shared fixtures."""
|
||||
|
||||
# TODO: Moved directory, should not be necessary anymore but need to check !!
|
||||
# Ajouter le dossier parent (brain) au PYTHONPATH
|
||||
# sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, Mock
|
||||
|
||||
import pytest
|
||||
|
||||
from infrastructure.persistence import Memory, set_memory
|
||||
from alfred.infrastructure.persistence import Memory, set_memory
|
||||
from alfred.settings import settings
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_settings():
|
||||
"""Create a mock Settings instance for testing."""
|
||||
return settings
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -18,6 +29,16 @@ def temp_dir():
|
||||
shutil.rmtree(dirpath)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_memory_storage_dir(monkeypatch):
|
||||
"""Override MEMORY_STORAGE_DIR for all tests to use a temp directory."""
|
||||
test_dir = tempfile.mkdtemp()
|
||||
monkeypatch.setenv("MEMORY_STORAGE_DIR", test_dir)
|
||||
yield
|
||||
# Cleanup
|
||||
shutil.rmtree(test_dir, ignore_errors=True)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def memory(temp_dir):
|
||||
"""Create a fresh Memory instance for testing."""
|
||||
@@ -248,7 +269,6 @@ def mock_deepseek():
|
||||
def test_something(mock_deepseek):
|
||||
# Your test code here
|
||||
"""
|
||||
import sys
|
||||
from unittest.mock import Mock
|
||||
|
||||
# Save the original module if it exists
|
||||
|
||||
+37
-35
@@ -2,31 +2,31 @@
|
||||
|
||||
from unittest.mock import Mock
|
||||
|
||||
from agent.agent import Agent
|
||||
from infrastructure.persistence import get_memory
|
||||
from alfred.agent.agent import Agent
|
||||
from alfred.infrastructure.persistence import get_memory
|
||||
|
||||
|
||||
class TestAgentInit:
|
||||
"""Tests for Agent initialization."""
|
||||
|
||||
def test_init(self, memory, mock_llm):
|
||||
def test_init(self, memory, mock_settings, mock_llm):
|
||||
"""Should initialize agent with LLM."""
|
||||
agent = Agent(llm=mock_llm)
|
||||
agent = Agent(settings=mock_settings, llm=mock_llm, max_tool_iterations=10)
|
||||
|
||||
assert agent.llm is mock_llm
|
||||
assert agent.tools is not None
|
||||
assert agent.prompt_builder is not None
|
||||
assert agent.max_tool_iterations == 5
|
||||
assert agent.max_tool_iterations == 10
|
||||
|
||||
def test_init_custom_iterations(self, memory, mock_llm):
|
||||
def test_init_custom_iterations(self, memory, mock_settings, mock_llm):
|
||||
"""Should accept custom max iterations."""
|
||||
agent = Agent(llm=mock_llm, max_tool_iterations=10)
|
||||
agent = Agent(settings=mock_settings, llm=mock_llm, max_tool_iterations=10)
|
||||
|
||||
assert agent.max_tool_iterations == 10
|
||||
|
||||
def test_tools_registered(self, memory, mock_llm):
|
||||
def test_tools_registered(self, memory, mock_settings, mock_llm):
|
||||
"""Should register all tools."""
|
||||
agent = Agent(llm=mock_llm)
|
||||
agent = Agent(settings=mock_settings, llm=mock_llm)
|
||||
|
||||
expected_tools = [
|
||||
"set_path_for_folder",
|
||||
@@ -46,9 +46,9 @@ class TestAgentInit:
|
||||
class TestExecuteToolCall:
|
||||
"""Tests for _execute_tool_call method."""
|
||||
|
||||
def test_execute_known_tool(self, memory, mock_llm, real_folder):
|
||||
def test_execute_known_tool(self, memory, mock_settings, mock_llm, real_folder):
|
||||
"""Should execute known tool."""
|
||||
agent = Agent(llm=mock_llm)
|
||||
agent = Agent(settings=mock_settings, llm=mock_llm)
|
||||
memory.ltm.set_config("download_folder", str(real_folder["downloads"]))
|
||||
|
||||
tool_call = {
|
||||
@@ -62,9 +62,9 @@ class TestExecuteToolCall:
|
||||
|
||||
assert result["status"] == "ok"
|
||||
|
||||
def test_execute_unknown_tool(self, memory, mock_llm):
|
||||
def test_execute_unknown_tool(self, memory, mock_settings, mock_llm):
|
||||
"""Should return error for unknown tool."""
|
||||
agent = Agent(llm=mock_llm)
|
||||
agent = Agent(settings=mock_settings, llm=mock_llm)
|
||||
|
||||
tool_call = {
|
||||
"id": "call_123",
|
||||
@@ -75,9 +75,9 @@ class TestExecuteToolCall:
|
||||
assert result["error"] == "unknown_tool"
|
||||
assert "available_tools" in result
|
||||
|
||||
def test_execute_with_bad_args(self, memory, mock_llm):
|
||||
def test_execute_with_bad_args(self, memory, mock_settings, mock_llm):
|
||||
"""Should return error for bad arguments."""
|
||||
agent = Agent(llm=mock_llm)
|
||||
agent = Agent(settings=mock_settings, llm=mock_llm)
|
||||
|
||||
tool_call = {
|
||||
"id": "call_123",
|
||||
@@ -87,9 +87,9 @@ class TestExecuteToolCall:
|
||||
|
||||
assert result["error"] == "bad_args"
|
||||
|
||||
def test_execute_tracks_errors(self, memory, mock_llm):
|
||||
def test_execute_tracks_errors(self, memory, mock_settings, mock_llm):
|
||||
"""Should track errors in episodic memory."""
|
||||
agent = Agent(llm=mock_llm)
|
||||
agent = Agent(settings=mock_settings, llm=mock_llm)
|
||||
|
||||
# Use invalid arguments to trigger a TypeError
|
||||
tool_call = {
|
||||
@@ -104,9 +104,9 @@ class TestExecuteToolCall:
|
||||
mem = get_memory()
|
||||
assert len(mem.episodic.recent_errors) > 0
|
||||
|
||||
def test_execute_with_invalid_json(self, memory, mock_llm):
|
||||
def test_execute_with_invalid_json(self, memory, mock_settings, mock_llm):
|
||||
"""Should handle invalid JSON arguments."""
|
||||
agent = Agent(llm=mock_llm)
|
||||
agent = Agent(settings=mock_settings, llm=mock_llm)
|
||||
|
||||
tool_call = {
|
||||
"id": "call_123",
|
||||
@@ -120,17 +120,17 @@ class TestExecuteToolCall:
|
||||
class TestStep:
|
||||
"""Tests for step method."""
|
||||
|
||||
def test_step_text_response(self, memory, mock_llm):
|
||||
def test_step_text_response(self, memory, mock_settings, mock_llm):
|
||||
"""Should return text response when no tool call."""
|
||||
agent = Agent(llm=mock_llm)
|
||||
agent = Agent(settings=mock_settings, llm=mock_llm)
|
||||
|
||||
response = agent.step("Hello")
|
||||
|
||||
assert response == "I found what you're looking for!"
|
||||
|
||||
def test_step_saves_to_history(self, memory, mock_llm):
|
||||
def test_step_saves_to_history(self, memory, mock_settings, mock_llm):
|
||||
"""Should save conversation to STM history."""
|
||||
agent = Agent(llm=mock_llm)
|
||||
agent = Agent(settings=mock_settings, llm=mock_llm)
|
||||
|
||||
agent.step("Hi there")
|
||||
|
||||
@@ -141,11 +141,13 @@ class TestStep:
|
||||
assert history[0]["content"] == "Hi there"
|
||||
assert history[1]["role"] == "assistant"
|
||||
|
||||
def test_step_with_tool_call(self, memory, mock_llm_with_tool_call, real_folder):
|
||||
def test_step_with_tool_call(
|
||||
self, memory, mock_settings, mock_llm_with_tool_call, real_folder
|
||||
):
|
||||
"""Should execute tool and continue."""
|
||||
memory.ltm.set_config("download_folder", str(real_folder["downloads"]))
|
||||
|
||||
agent = Agent(llm=mock_llm_with_tool_call)
|
||||
agent = Agent(settings=mock_settings, llm=mock_llm_with_tool_call)
|
||||
|
||||
response = agent.step("List my downloads")
|
||||
|
||||
@@ -157,7 +159,7 @@ class TestStep:
|
||||
assert first_call_args[1]["tools"] is not None, "Tools not passed to LLM!"
|
||||
assert len(first_call_args[1]["tools"]) > 0, "Tools list is empty!"
|
||||
|
||||
def test_step_max_iterations(self, memory, mock_llm):
|
||||
def test_step_max_iterations(self, memory, mock_settings, mock_llm):
|
||||
"""Should stop after max iterations."""
|
||||
call_count = [0]
|
||||
|
||||
@@ -185,15 +187,15 @@ class TestStep:
|
||||
return {"role": "assistant", "content": "I couldn't complete the task."}
|
||||
|
||||
mock_llm.complete = Mock(side_effect=mock_complete)
|
||||
agent = Agent(llm=mock_llm, max_tool_iterations=3)
|
||||
agent = Agent(settings=mock_settings, llm=mock_llm, max_tool_iterations=3)
|
||||
|
||||
agent.step("Do something")
|
||||
|
||||
assert call_count[0] == 4
|
||||
|
||||
def test_step_includes_history(self, memory_with_history, mock_llm):
|
||||
def test_step_includes_history(self, memory_with_history, mock_settings, mock_llm):
|
||||
"""Should include conversation history in prompt."""
|
||||
agent = Agent(llm=mock_llm)
|
||||
agent = Agent(settings=mock_settings, llm=mock_llm)
|
||||
|
||||
agent.step("New message")
|
||||
|
||||
@@ -201,10 +203,10 @@ class TestStep:
|
||||
messages_content = [m.get("content", "") for m in call_args]
|
||||
assert any("Hello" in str(c) for c in messages_content)
|
||||
|
||||
def test_step_includes_events(self, memory, mock_llm):
|
||||
def test_step_includes_events(self, memory, mock_settings, mock_llm):
|
||||
"""Should include unread events in prompt."""
|
||||
memory.episodic.add_background_event("download_complete", {"name": "Movie.mkv"})
|
||||
agent = Agent(llm=mock_llm)
|
||||
agent = Agent(settings=mock_settings, llm=mock_llm)
|
||||
|
||||
agent.step("What's new?")
|
||||
|
||||
@@ -212,9 +214,9 @@ class TestStep:
|
||||
messages_content = [m.get("content", "") for m in call_args]
|
||||
assert any("download" in str(c).lower() for c in messages_content)
|
||||
|
||||
def test_step_saves_ltm(self, memory, mock_llm, temp_dir):
|
||||
def test_step_saves_ltm(self, memory, mock_settings, mock_llm, temp_dir):
|
||||
"""Should save LTM after step."""
|
||||
agent = Agent(llm=mock_llm)
|
||||
agent = Agent(settings=mock_settings, llm=mock_llm)
|
||||
|
||||
agent.step("Hello")
|
||||
|
||||
@@ -225,7 +227,7 @@ class TestStep:
|
||||
class TestAgentIntegration:
|
||||
"""Integration tests for Agent."""
|
||||
|
||||
def test_multiple_tool_calls(self, memory, mock_llm, real_folder):
|
||||
def test_multiple_tool_calls(self, memory, mock_settings, mock_llm, real_folder):
|
||||
"""Should handle multiple tool calls in sequence."""
|
||||
memory.ltm.set_config("download_folder", str(real_folder["downloads"]))
|
||||
memory.ltm.set_config("movie_folder", str(real_folder["movies"]))
|
||||
@@ -276,7 +278,7 @@ class TestAgentIntegration:
|
||||
}
|
||||
|
||||
mock_llm.complete = Mock(side_effect=mock_complete)
|
||||
agent = Agent(llm=mock_llm)
|
||||
agent = Agent(settings=mock_settings, llm=mock_llm)
|
||||
|
||||
agent.step("List my downloads and movies")
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user